在JavaScript的世界里,变量定义看似简单却暗藏玄机。很多开发者工作多年仍会在面试中被var/let/const的区别难住。让我们从底层原理出发,彻底搞懂这三者的差异。
var的设计源于JavaScript早期,它采用函数级作用域。这意味着在函数内用var定义的变量,在整个函数范围内都有效。而let/const采用块级作用域(block scope),只在大括号{}内有效。
javascript复制function varTest() {
var x = 1;
if (true) {
var x = 2; // 同一个变量!
console.log(x); // 2
}
console.log(x); // 2
}
function letTest() {
let x = 1;
if (true) {
let x = 2; // 不同的变量
console.log(x); // 2
}
console.log(x); // 1
}
变量提升(hoisting)是另一个关键差异。var声明的变量会被提升到函数顶部(仅声明,不提升赋值),而let/const存在暂时性死区(TDZ),在声明前访问会报错。
很多初学者误以为const定义的变量完全不可变。实际上,const保证的是变量指向的内存地址不变。对于基本类型(number, string等),值就保存在地址中,所以表现为不可变。但对于引用类型(object, array等),const只能保证你不重新赋值,但可以修改对象内部属性。
javascript复制const obj = { name: 'John' };
obj.name = 'Mike'; // ✅ 允许
obj = { name: 'Jane' }; // ❌ 报错
const arr = [1, 2];
arr.push(3); // ✅ 允许
arr = [4, 5]; // ❌ 报错
重要提示:在严格模式('use strict')下,未声明的变量赋值会报错,这能避免意外创建全局变量。
基本类型(Primitive types)直接存储在栈内存中,包括:
引用类型(Reference types)存储在堆内存中,变量保存的是内存地址:
javascript复制let a = 10;
let b = a; // 值拷贝
b = 20;
console.log(a); // 10 (不受影响)
let obj1 = { count: 1 };
let obj2 = obj1; // 地址拷贝
obj2.count = 2;
console.log(obj1.count); // 2 (同步变化)
由于存储机制不同,比较运算时:
javascript复制1 === 1 // true
'hello' === 'hello' // true
[] === [] // false (不同地址)
{} === {} // false
let arr = [];
let arrCopy = arr;
arr === arrCopy // true (相同地址)
需要完全独立拷贝对象时,常用方法有:
javascript复制// 浅拷贝
const shallowCopy = {...obj};
// 深拷贝(简单版)
const deepCopy = JSON.parse(JSON.stringify(obj));
// 深拷贝(完整版)
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
const result = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = deepClone(obj[key]);
}
}
return result;
}
注意:JSON方法会丢失函数、undefined和循环引用,复杂对象建议使用lodash的_.cloneDeep()
slice(start, end)用于截取数组片段而不修改原数组:
javascript复制const fruits = ['apple', 'banana', 'orange', 'grape'];
// 基本用法
fruits.slice(1, 3); // ['banana', 'orange']
// 省略end
fruits.slice(1); // ['banana', 'orange', 'grape']
// 负数索引
fruits.slice(-3, -1); // ['banana', 'orange']
// 原数组不变
console.log(fruits); // ['apple', 'banana', 'orange', 'grape']
splice(start, deleteCount, ...items)可以:
javascript复制const numbers = [1, 2, 3, 4, 5];
// 删除
numbers.splice(1, 2); // 返回 [2, 3]
// numbers现在是 [1, 4, 5]
// 添加
numbers.splice(1, 0, 'a', 'b'); // 返回 []
// numbers现在是 [1, 'a', 'b', 4, 5]
// 替换
numbers.splice(2, 2, 'x', 'y'); // 返回 ['b', 4]
// numbers现在是 [1, 'a', 'x', 'y', 5]
forEach是最简单的遍历方法,适合只需要遍历不需要返回值的场景:
javascript复制const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
];
users.forEach((user, index) => {
console.log(`${index + 1}. ${user.name}`);
});
// 输出:
// 1. Alice
// 2. Bob
// 3. Charlie
注意:forEach无法用break中断,需要中断时改用for...of循环
map将数组元素映射为新值,返回新数组:
javascript复制const numbers = [1, 2, 3];
const squares = numbers.map(n => n * n); // [1, 4, 9]
// 处理对象数组
const userNames = users.map(user => user.name); // ['Alice', 'Bob', 'Charlie']
filter返回满足条件的元素组成的新数组:
javascript复制const evenNumbers = numbers.filter(n => n % 2 === 0); // [2]
// 复杂条件筛选
const activeUsers = users.filter(user => !user.isDisabled);
reduce是最强大的数组方法,可以实现各种聚合操作:
javascript复制// 数组求和
const sum = numbers.reduce((acc, cur) => acc + cur, 0);
// 对象统计
const fruitBasket = ['apple', 'banana', 'apple', 'orange'];
const count = fruitBasket.reduce((acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
}, {}); // { apple: 2, banana: 1, orange: 1 }
// 多维数组扁平化
const deepArray = [1, [2, [3, [4]]]];
const flatten = deepArray.reduce((acc, val) =>
acc.concat(Array.isArray(val) ? flatten(val) : val), []
); // [1, 2, 3, 4]
javascript复制const str = 'Hello World';
str.substr(1, 4); // 'ello'
str.substring(1, 4); // 'ell'
javascript复制'Hello'.substr(-3, 2); // 'll' (倒数第3个开始取2个)
'Hello'.substring(-3, 2); // 'He' (相当于substring(0, 2))
'Hello'.substring(3, 1); // 'el' (自动交换为1,3)
ES6引入的字符串方法更直观:
javascript复制// 获取部分字符串
'Hello'.slice(1, 3); // 'el' (类似substring但支持负数)
// 判断包含关系
'Hello'.includes('ell'); // true
// 重复字符串
'abc'.repeat(3); // 'abcabcabc'
javascript复制const person = {
name: 'John',
greet: function(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`;
}
};
// call - 参数逐个传递
person.greet.call({ name: 'Alice' }, 'Hello', '!'); // "Hello, Alice!"
// apply - 参数数组传递
person.greet.apply({ name: 'Bob' }, ['Hi', '?']); // "Hi, Bob?"
// bind - 返回绑定函数
const greetJane = person.greet.bind({ name: 'Jane' });
greetJane('Hey', '...'); // "Hey, Jane..."
借用数组方法处理类数组对象:
javascript复制// 类数组对象
const arrayLike = { 0: 'a', 1: 'b', length: 2 };
// 使用数组方法
Array.prototype.push.call(arrayLike, 'c'); // {0: 'a', 1: 'b', 2: 'c', length: 3}
// 转换真正数组
const realArray = Array.prototype.slice.call(arrayLike);
实现继承:
javascript复制function Parent(name) {
this.name = name;
}
function Child(name, age) {
Parent.call(this, name); // 调用父类构造函数
this.age = age;
}
性能优化:
javascript复制// 缓存绑定函数提高性能
const query = document.querySelector.bind(document);
const header = query('#header');
javascript复制const fetchUser = id => fetch(`/users/${id}`);
const fetchPosts = id => fetch(`/users/${id}/posts`);
Promise.all([
fetchUser(1),
fetchPosts(1)
])
.then(([user, posts]) => {
console.log('User:', user);
console.log('Posts:', posts);
})
.catch(error => {
console.error('One of the requests failed', error);
});
javascript复制const timeout = ms => new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
Promise.race([
fetch('/api/data'),
timeout(5000)
])
.then(data => {
console.log('Data received:', data);
})
.catch(error => {
console.error('Error:', error.message);
});
Promise.allSettled(ES2020):
javascript复制Promise.allSettled([
Promise.resolve('success'),
Promise.reject('error')
]).then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Success:', result.value);
} else {
console.log('Failed:', result.reason);
}
});
});
自定义批处理函数:
javascript复制function batchPromises(promises, batchSize = 5) {
const results = [];
return promises.reduce((chain, promise, index) => {
return chain.then(() => promise)
.then(result => results[index] = result)
.catch(error => results[index] = { error });
}, Promise.resolve())
.then(() => results);
}
javascript复制const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
if (obj.hasOwnProperty(key)) { // 避免原型链属性
console.log(`${key}: ${obj[key]}`);
}
}
// 输出:
// a: 1
// b: 2
// c: 3
javascript复制// 数组遍历
const arr = ['a', 'b', 'c'];
for (const item of arr) {
console.log(item);
}
// 字符串遍历
for (const char of 'hello') {
console.log(char);
}
// Map遍历
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) {
console.log(`${key}: ${value}`);
}
javascript复制// 带索引的for...of
for (const [index, value] of arr.entries()) {
console.log(index, value);
}
javascript复制const uniqueNumbers = new Set([1, 2, 2, 3, 3, 3]);
console.log([...uniqueNumbers]); // [1, 2, 3]
// 常用操作
uniqueNumbers.add(4).add(5);
console.log(uniqueNumbers.has(4)); // true
uniqueNumbers.delete(1);
console.log(uniqueNumbers.size); // 4
// 数组去重
const arr = [1, 2, 2, 3];
const uniqueArr = [...new Set(arr)]; // [1, 2, 3]
javascript复制const map = new Map();
// 键可以是任意类型
map.set({ id: 1 }, 'value1');
map.set(1, 'value2');
// 保持插入顺序
map.forEach((value, key) => {
console.log(key, value);
});
// 与对象互转
const obj = Object.fromEntries(map.entries());
const newMap = new Map(Object.entries(obj));
WeakSet和WeakMap的键必须是对象,且不计入引用计数,适合做临时缓存:
javascript复制// 跟踪已处理对象
const processed = new WeakSet();
function processObj(obj) {
if (processed.has(obj)) return;
processed.add(obj);
// 处理逻辑...
}
// 私有数据存储
const privateData = new WeakMap();
class Person {
constructor(name) {
privateData.set(this, { name });
}
getName() {
return privateData.get(this).name;
}
}
| 需求 | 字符串方法 | 数组方法 |
|---|---|---|
| 转为数组 | split(分隔符) | - |
| 转为字符串 | - | join(分隔符) |
| 字符/元素访问 | charAt(index) | [index] |
| 需求 | 字符串方法 | 数组方法 |
|---|---|---|
| 包含检测 | includes(str) | includes(item) |
| 查找索引 | indexOf(str) | indexOf(item) |
| 条件查找 | - | find/findIndex |
| 正则匹配 | match(regex) | - |
| 需求 | 字符串方法 | 数组方法 |
|---|---|---|
| 拼接 | concat(str) | concat(arr) |
| 截取 | slice/substr | slice/splice |
| 填充 | padStart/padEnd | fill |
| 大小写转换 | toUpperCase/LowerCase | - |
JavaScript运行时包含:
javascript复制console.log('1'); // 同步
setTimeout(() => console.log('2'), 0); // 宏任务
Promise.resolve().then(() => console.log('3')); // 微任务
console.log('4'); // 同步
// 输出顺序:1, 4, 3, 2
回调地狱:
javascript复制getData(function(a) {
getMoreData(a, function(b) {
getMoreData(b, function(c) {
// 深度嵌套...
});
});
});
Promise链:
javascript复制getData()
.then(a => getMoreData(a))
.then(b => getMoreData(b))
.then(c => {
// 扁平结构
});
Async/Await:
javascript复制async function process() {
const a = await getData();
const b = await getMoreData(a);
const c = await getMoreData(b);
return c;
}
Promise链错误处理:
javascript复制fetchData()
.then(processData)
.catch(handleError)
.finally(cleanup);
Async/Await错误处理:
javascript复制async function fetchWithRetry(url, retries = 3) {
try {
const response = await fetch(url);
return await response.json();
} catch (err) {
if (retries <= 0) throw err;
await new Promise(resolve => setTimeout(resolve, 1000));
return fetchWithRetry(url, retries - 1);
}
}
javascript复制// 不好:多次重排
for (let i = 0; i < 100; i++) {
document.body.innerHTML += `<div>${i}</div>`;
}
// 好:使用文档片段
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = i;
fragment.appendChild(div);
}
document.body.appendChild(fragment);
javascript复制// 不好:每个元素绑定事件
document.querySelectorAll('.item').forEach(item => {
item.addEventListener('click', handleClick);
});
// 好:事件委托
document.querySelector('.container').addEventListener('click', event => {
if (event.target.matches('.item')) {
handleClick(event);
}
});
javascript复制// 定时器清理
const timer = setInterval(() => {}, 1000);
// 不再需要时
clearInterval(timer);
// 事件监听器移除
element.addEventListener('click', handler);
// 不再需要时
element.removeEventListener('click', handler);
// 大对象释放
let largeData = new Array(1000000).fill({});
// 不再需要时
largeData = null;
javascript复制// 对象解构
const { name, age, ...rest } = user;
// 数组解构
const [first, second, ...others] = arr;
// 函数参数解构
function greet({ name, age = 18 }) {
console.log(`Hello ${name}, age ${age}`);
}
// 变量交换
let a = 1, b = 2;
[a, b] = [b, a];
javascript复制// 数组合并
const combined = [...arr1, ...arr2];
// 对象合并
const merged = { ...obj1, ...obj2 };
// 函数参数展开
Math.max(...numbers);
// 浅拷贝
const arrCopy = [...arr];
const objCopy = { ...obj };
javascript复制// 可选链
const street = user?.address?.street;
// 空值合并
const count = inventory?.count ?? 0;
// 函数调用可选链
obj.method?.();
javascript复制// math.js
export const add = (a, b) => a + b;
export const PI = 3.14159;
// app.js
import { add, PI } from './math.js';
console.log(add(2, 3));
javascript复制// 按需加载
button.addEventListener('click', async () => {
const module = await import('./dialog.js');
module.openDialog();
});
// 预加载
<link rel="modulepreload" href="critical-module.js">
javascript复制// a.js
import { b } from './b.js';
export const a = 'A' + b;
// b.js
import { a } from './a.js';
export const b = 'B' + a; // 报错
// 解决方案:函数导出
// a.js
export function getA() { return 'A' + getB(); }
// b.js
export function getB() { return 'B' + getA(); }
javascript复制// 表格展示
console.table(users);
// 分组日志
console.group('User Details');
console.log('Name:', user.name);
console.log('Age:', user.age);
console.groupEnd();
// 性能测量
console.time('fetch');
await fetchData();
console.timeEnd('fetch');
javascript复制// 条件断点
function processItem(item) {
if (item.value > 100) { // 在此行设置条件断点: item.value > 100
// ...
}
}
// 日志断点
// 在代码行设置断点,右键选择"Log Message"替代暂停
javascript复制// 使用Jest测试框架
describe('math functions', () => {
test('adds 1 + 2 to equal 3', () => {
expect(add(1, 2)).toBe(3);
});
test('async fetch data', async () => {
await expect(fetchData()).resolves.toMatchObject({
status: 200
});
});
});
javascript复制// 不安全
element.innerHTML = userInput;
// 安全
element.textContent = userInput;
// 必须使用HTML时
function sanitize(html) {
const div = document.createElement('div');
div.textContent = html;
return div.innerHTML;
}
javascript复制// 服务端生成token
const csrfToken = generateToken();
// 前端发送请求时携带
fetch('/api', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken
},
// ...
});
javascript复制// 不安全
const data = eval('(' + jsonString + ')');
// 安全
const data = JSON.parse(jsonString);
// 带reviver函数
const data = JSON.parse(jsonString, (key, value) => {
if (key === 'date') return new Date(value);
return value;
});
javascript复制class Logger {
constructor() {
if (!Logger.instance) {
this.logs = [];
Logger.instance = this;
}
return Logger.instance;
}
log(message) {
this.logs.push(message);
console.log(message);
}
}
const logger1 = new Logger();
const logger2 = new Logger();
console.log(logger1 === logger2); // true
javascript复制class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
(this.events[event] || (this.events[event] = [])).push(listener);
}
emit(event, ...args) {
(this.events[event] || []).forEach(listener => listener(...args));
}
}
const emitter = new EventEmitter();
emitter.on('data', data => console.log('Data:', data));
emitter.emit('data', { id: 1 }); // 输出: Data: {id: 1}
javascript复制const strategies = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b
};
function calculate(strategy, a, b) {
return strategies[strategy](a, b);
}
calculate('add', 2, 3); // 5
calculate('multiply', 2, 3); // 6
javascript复制// localStorage - 长期存储
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme');
// sessionStorage - 会话级存储
sessionStorage.setItem('token', 'abc123');
// 存储事件监听
window.addEventListener('storage', event => {
console.log(`${event.key} changed from ${event.oldValue} to ${event.newValue}`);
});
javascript复制// main.js
const worker = new Worker('worker.js');
worker.postMessage({ data: [1, 2, 3] });
worker.onmessage = event => {
console.log('Result:', event.data);
};
// worker.js
self.onmessage = event => {
const result = event.data.data.map(x => x * 2);
self.postMessage(result);
};
javascript复制navigator.geolocation.getCurrentPosition(
position => {
console.log('Latitude:', position.coords.latitude);
console.log('Longitude:', position.coords.longitude);
},
error => {
console.error('Error:', error.message);
},
{ enableHighAccuracy: true, timeout: 5000 }
);
javascript复制// .eslintrc.js
module.exports = {
env: {
browser: true,
es2021: true
},
extends: ['eslint:recommended', 'plugin:prettier/recommended'],
rules: {
'no-console': 'warn',
'no-unused-vars': 'error',
'prefer-const': 'error'
}
};
javascript复制// webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist')
},
optimization: {
splitChunks: {
chunks: 'all'
}
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: ['babel-loader']
}
]
}
};
javascript复制// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
targets: '> 0.25%, not dead',
useBuiltIns: 'usage',
corejs: 3
}]
],
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-transform-runtime'
]
};
javascript复制// 类装饰器
@log
class MyClass {
@readonly
value = 42;
@debounce(100)
handleClick() {
// ...
}
}
function log(target) {
console.log(`Class ${target.name} defined`);
}
function readonly(target, name, descriptor) {
descriptor.writable = false;
return descriptor;
}
function debounce(ms) {
return function(target, name, descriptor) {
const original = descriptor.value;
let timer;
descriptor.value = function(...args) {
clearTimeout(timer);
timer = setTimeout(() => original.apply(this, args), ms);
};
return descriptor;
};
}
javascript复制// 提案阶段
const result = value
|> double
|> addTen
|> square;
function double(x) { return x * 2; }
function addTen(x) { return x + 10; }
function square(x) { return x * x; }
javascript复制// 提案阶段
const record = #{
name: 'John',
age: 30
};
const tuple = #[1, 2, 3];
// 不可变
record.age = 31; // 报错
tuple.push(4); // 报错