JavaScript 核心概念与执行机制
JavaScript 核心概念与执行机制
JavaScript 是一门单线程、非阻塞、异步的脚本语言,理解其核心概念和执行机制对于编写高质量的代码至关重要。这些底层机制——执行上下文、作用域、闭包、原型链、this、事件循环——是几乎所有 JavaScript 面试的必考点,更是排查诡异 bug、写出高性能代码的根基。
为什么必须理解执行机制
很多开发者能写出运行的代码,却说不清"为什么这段代码输出 5 个 5 而不是 0 到 4","为什么箭头函数里 this 变了"。这就像会开车但不懂发动机,一旦抛锚就束手无策。理解执行机制能带来三个实质收益:
可以把 JavaScript 引擎的工作比作一个尽职的秘书:拿到代码后先"通读一遍"(编译阶段,登记所有声明),再"逐行办事"(执行阶段)。理解这个"先通读后执行"的模型,是理解一切的起点。
🔄 执行上下文
执行上下文(Execution Context)是 JavaScript 代码执行的环境,可以理解为代码运行时的"工作空间",里面存放着变量、函数、this 指向等信息。
执行上下文的类型:
执行上下文的创建过程:
执行上下文栈(调用栈):
// 调用栈演示:观察进出栈顺序
function first() {
console.log('进入 first');
second();
console.log('离开 first');
}
function second() {
console.log('进入 second');
third();
console.log('离开 second');
}
function third() {
console.log('执行 third');
}
first();
// 输出顺序:进入 first → 进入 second → 执行 third → 离开 second → 离开 first
// 栈变化:[global] → [global,first] → [global,first,second] → [global,first,second,third]
// → 逐层弹出回到 [global]💻 代码示例:执行上下文演示
// 全局执行上下文
var globalVar = 'global';
function outer() {
// outer 函数执行上下文
var outerVar = 'outer';
function inner() {
// inner 函数执行上下文
var innerVar = 'inner';
console.log(innerVar); // 'inner'
console.log(outerVar); // 'outer'
console.log(globalVar); // 'global'
}
inner();
}
outer();变量提升示例
变量提升(Hoisting)是"先通读后执行"模型的直接体现:引擎在编译阶段把声明"提"到作用域顶部登记,但赋值留在原地。
// 变量提升
console.log(a); // undefined,不是 ReferenceError
var a = 10;
console.log(b); // ReferenceError
let b = 20;
// 函数提升
console.log(foo); // 函数定义
foo(); // 'foo'
function foo() {
console.log('foo');
}
console.log(bar); // undefined
bar(); // TypeError: bar is not a function
var bar = function() {
console.log('bar');
};var、let、const 对比
| 特性 | var | let | const |
| --- | --- | --- | --- |
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 提升 | 提升并初始化为 undefined | 提升但不初始化(TDZ) | 提升但不初始化(TDZ) |
| 重复声明 | 允许 | 报错 | 报错 |
| 重新赋值 | 允许 | 允许 | 不允许 |
| 挂到 window | 会 | 不会 | 不会 |
现代开发建议:默认用 const,需要重新赋值时用 let,避免使用 var。
闭包
什么是闭包:
用一个类比:闭包就像一个背包。函数被创建时,把它出生环境里用得到的变量装进背包背走,无论走到哪里(在哪里被调用),都能从背包里取出这些变量。这就是为什么函数执行完,被闭包"背走"的变量不会被回收。
为什么重要: 闭包是 JavaScript 实现数据私有、模块化、函数式编程的基石。React 的 useState、防抖节流、柯里化、事件回调,背后全是闭包。
闭包的应用:
闭包的优缺点:
代码示例
闭包实现私有变量
function createCounter() {
let count = 0;
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount()); // 1
// count 变量无法直接访问,实现了私有变量闭包实现模块模式
const Module = (function() {
let privateVar = 'private';
function privateMethod() {
console.log('This is a private method');
}
return {
publicMethod() {
console.log('This is a public method');
privateMethod();
console.log(privateVar);
},
publicVar: 'public'
};
})();
Module.publicMethod();
// Module.privateMethod(); // ReferenceError
// Module.privateVar; // undefined闭包实现防抖
function debounce(func, wait) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
// 使用示例
const handleInput = debounce((e) => {
console.log('Input:', e.target.value);
}, 300);
document.getElementById('input').addEventListener('input', handleInput);闭包实现节流
function throttle(func, limit) {
let inThrottle;
return function(...args) {
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// 使用示例
const handleScroll = throttle(() => {
console.log('Scrolling...');
}, 100);
window.addEventListener('scroll', handleScroll);闭包实现函数只执行一次(once)
// 真实场景:确保初始化逻辑只跑一次(如只弹一次引导、只连一次数据库)
function once(fn) {
let called = false;
let result;
return function (...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}
const init = once(() => {
console.log('初始化,只会打印一次');
return { ready: true };
});
init(); // 打印
init(); // 不打印,直接返回缓存结果原型链
原型链的概念:
用类比理解:原型链就像找东西——先翻自己口袋(对象自身),没有就问爸爸(原型),爸爸没有再问爷爷(原型的原型),一直往上问到祖先(Object.prototype),再往上就是 null(无人可问,返回 undefined)。
原型链的工作原理:
代码示例
原型链示例
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
const person = new Person('Alice');
person.sayHello(); // 'Hello, my name is Alice'
console.log(person.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true原型继承
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
// 设置原型链
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log(`${this.name} is barking`);
};
const dog = new Dog('Buddy', 'Golden Retriever');
dog.eat(); // 'Buddy is eating'
dog.bark(); // 'Buddy is barking'ES6 类继承
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
console.log(`${this.name} is barking`);
}
}
const dog = new Dog('Buddy', 'Golden Retriever');
dog.eat(); // 'Buddy is eating'
dog.bark(); // 'Buddy is barking'instanceof 的原理
// instanceof 本质是检查右侧构造函数的 prototype 是否在左侧对象的原型链上
function myInstanceof(obj, Constructor) {
let proto = Object.getPrototypeOf(obj);
while (proto) {
if (proto === Constructor.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
console.log(myInstanceof(dog, Animal)); // true
console.log(myInstanceof(dog, Object)); // true
console.log(myInstanceof(dog, Array)); // falsethis 指向
this 是 JavaScript 最容易出错的概念之一。核心口诀:this 的值不看函数在哪里定义,只看函数怎么被调用(箭头函数除外)。
this 的绑定规则:
四种绑定规则对比
| 调用方式 | this 指向 | 示例 |
| --- | --- | --- |
| 独立调用 | 全局对象 / undefined(严格模式) | fn() |
| 方法调用 | 调用它的对象 | obj.fn() |
| call/apply/bind | 指定的对象 | fn.call(obj) |
| new 调用 | 新创建的实例 | new Fn() |
| 箭头函数 | 外层作用域的 this | () => this |
手写 call / apply / bind
// 手写 call:理解显式绑定的本质
Function.prototype.myCall = function (context, ...args) {
context = context || globalThis;
const key = Symbol('fn');
context[key] = this; // 把函数临时挂到 context 上
const result = context[key](...args); // 以方法形式调用,this 即 context
delete context[key];
return result;
};
// 手写 bind:返回绑定了 this 的新函数
Function.prototype.myBind = function (context, ...preArgs) {
const fn = this;
return function (...args) {
return fn.apply(context, [...preArgs, ...args]);
};
};
function greet(greeting) {
return `${greeting}, ${this.name}`;
}
console.log(greet.myCall({ name: 'Alice' }, 'Hi')); // 'Hi, Alice'
const boundGreet = greet.myBind({ name: 'Bob' });
console.log(boundGreet('Hello')); // 'Hello, Bob'深入理解
变量提升的详细机制
变量提升的本质:
暂时性死区(TDZ):
// TDZ 演示
{
// 这里是 x 的暂时性死区
// console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 10;
console.log(x); // 10,声明之后才可访问
}
// 函数声明优先于变量声明
console.log(typeof value); // 'function'
var value = 'hello';
function value() {}作用域链的查找过程
查找顺序:
词法作用域 vs 动态作用域:
// 词法作用域证明:value 取定义处的作用域,而非调用处
var value = 1;
function foo() {
console.log(value); // 永远是 1(定义时的外层作用域)
}
function bar() {
var value = 2;
foo(); // 仍然输出 1,而不是 2
}
bar();闭包的内存管理
闭包的内存占用:
避免内存泄漏的方法:
原型链的查找优化
原型链查找的性能:
优化策略:
this 绑定的优先级
绑定优先级(从高到低):
特殊情况:
事件循环的深入理解
事件循环的组成部分:
浏览器环境 vs Node.js 环境:
宏任务与微任务对比:
| 类型 | 包含的 API | 执行时机 |
| --- | --- | --- |
| 宏任务(macrotask) | setTimeout、setInterval、setImmediate、I/O、UI 渲染 | 每轮事件循环取一个执行 |
| 微任务(microtask) | Promise.then、async/await、queueMicrotask、process.nextTick、MutationObserver | 当前宏任务后、下一个宏任务前,清空全部 |
性能优化:
实战应用
模块化实现
// 使用闭包实现模块
const myModule = (function() {
// 私有变量和方法
let privateVar = 0;
function privateMethod() {
return privateVar * 2;
}
// 公共 API
return {
increment() {
privateVar++;
console.log('Incremented:', privateVar);
},
decrement() {
privateVar--;
console.log('Decremented:', privateVar);
},
getValue() {
return privateMethod();
}
};
})();
// 使用模块
myModule.increment(); // Incremented: 1
myModule.increment(); // Incremented: 2
console.log(myModule.getValue()); // 4
myModule.decrement(); // Decremented: 1函数柯里化
// 柯里化函数
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
}
};
}
// 使用柯里化
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1, 2, 3)); // 6
// 实际应用:创建可复用的函数
const multiply = (a, b) => a * b;
const double = curry(multiply)(2);
console.log(double(5)); // 10
console.log(double(10)); // 20高阶函数
// 高阶函数:接受函数作为参数或返回函数
function withLogging(fn) {
return function(...args) {
console.log('Calling function with args:', args);
const result = fn.apply(this, args);
console.log('Function returned:', result);
return result;
};
}
// 使用高阶函数
function add(a, b) {
return a + b;
}
const loggedAdd = withLogging(add);
loggedAdd(3, 4); // Calling function with args: [3, 4] // Function returned: 7
// 实际应用:权限检查
function withAuth(fn) {
return function(...args) {
if (!isAuthenticated()) {
throw new Error('Not authenticated');
}
return fn.apply(this, args);
};
}
function deleteUser(userId) {
// 删除用户逻辑
console.log('Deleting user:', userId);
}
const protectedDeleteUser = withAuth(deleteUser);性能优化示例
// 使用闭包缓存计算结果
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Cache hit:', key);
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
console.log('Cache miss:', key);
return result;
};
}
// 使用记忆化
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const memoizedFibonacci = memoize(fibonacci);
console.log(memoizedFibonacci(10)); // 大幅提升性能
// 使用节流优化滚动事件
function optimizedScrollHandler() {
// 节流后的处理逻辑
console.log('Optimized scroll handler');
}
const throttledScroll = throttle(optimizedScrollHandler, 100);
window.addEventListener('scroll', throttledScroll);记忆化的收益有多大?未优化的递归 fibonacci(40) 会产生约 3.3 亿次函数调用,耗时以秒计;加上记忆化后,每个 n 只算一次,调用次数降到线性级别(约 80 次),毫秒级返回。这就是空间换时间的经典应用。
常见坑与解决方案
循环中的闭包陷阱
这是最经典的面试题,也是真实开发中的高频 bug。
// 错误示例
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 输出 5, 5, 5, 5, 5
}, 100);
}
// 原因:var 是函数作用域,5 个回调共享同一个 i,等回调执行时 i 早已变成 5
// 解决方案1:使用 let(块级作用域,每次迭代都是新的 i)
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 输出 0, 1, 2, 3, 4
}, 100);
}
// 解决方案2:使用 IIFE 创建独立作用域
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 输出 0, 1, 2, 3, 4
}, 100);
})(i);
}this 绑定陷阱
// 陷阱1:方法中的箭头函数 this
const obj1 = {
name: 'Alice',
getName() {
console.log(this.name); // 正确,'Alice'
},
getArrowName: () => {
console.log(this.name); // 错误,this 指向外层(全局),undefined
}
};
obj1.getName(); // 'Alice'
obj1.getArrowName(); // undefined
// 陷阱2:隐式绑定丢失(回调中最常见)
const obj2 = {
name: 'Bob',
greet() { console.log(this.name); }
};
const fn = obj2.greet;
fn(); // undefined,this 丢失
setTimeout(obj2.greet, 100); // undefined,同样丢失
// 解决方案:bind 或箭头函数包裹
setTimeout(obj2.greet.bind(obj2), 100); // 'Bob'
setTimeout(() => obj2.greet(), 100); // 'Bob'
// 陷阱3:类事件处理中的 this
class Button {
constructor() {
this.count = 0;
this.button = document.createElement('button');
// 错误:this 指向 button 元素
// this.button.addEventListener('click', this.handleClick);
// 解决方案1:bind
this.button.addEventListener('click', this.handleClick.bind(this));
// 解决方案2:箭头函数
this.button.addEventListener('click', () => this.handleClick());
}
handleClick() {
this.count++;
console.log('Clicked:', this.count);
}
}异步陷阱
// 陷阱:期望同步执行
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// 输出:1, 3, 2(setTimeout 是宏任务,最后执行)
// 陷阱:async 函数里忘记 await
async function bad() {
const data = fetchData(); // 忘了 await,data 是 Promise 而非结果
console.log(data); // Promise {<pending>}
}
// 解决方案
async function good() {
const data = await fetchData();
console.log(data); // 真实数据
}
// 解决方案:用 async/await 确保顺序执行
async function sequentialLoop() {
for (let i = 0; i < 3; i++) {
await new Promise(resolve => {
setTimeout(() => {
console.log(i);
resolve();
}, 100);
});
}
}
sequentialLoop(); // 0, 1, 2(严格按顺序)常见坑速查表
| 坑 | 现象 | 根因 | 解法 |
| --- | --- | --- | --- |
| 循环闭包 | 输出全是最大值 | var 共享同一变量 | 用 let 或 IIFE |
| this 丢失 | this 变 undefined | 方法被赋值/作回调 | bind 或箭头函数 |
| 箭头函数当方法 | this 指向外层 | 箭头无自己的 this | 对象方法用普通函数 |
| TDZ | 声明前报错 | let/const 未初始化 | 先声明再使用 |
| 忘记 await | 拿到 Promise | 异步未等待 | 加 await |
代码示例
this 绑定规则
// 默认绑定
function foo() {
console.log(this); // 全局对象或 undefined(严格模式)
}
foo();
// 隐式绑定
const obj = {
name: 'Alice',
foo() {
console.log(this.name); // 'Alice'
}
};
obj.foo();
// 显式绑定
function greet() {
console.log(`Hello, ${this.name}`);
}
const person = { name: 'Bob' };
greet.call(person); // 'Hello, Bob'
greet.apply(person); // 'Hello, Bob'
const boundGreet = greet.bind(person);
boundGreet(); // 'Hello, Bob'
// new 绑定
function Person(name) {
this.name = name;
}
const p = new Person('Charlie');
console.log(p.name); // 'Charlie'
// 箭头函数
const obj2 = {
name: 'Alice',
foo() {
const arrow = () => {
console.log(this.name); // 'Alice',继承 foo 的 this
};
arrow();
}
};
obj2.foo();事件循环
事件循环的概念:
用类比理解:主线程是一个只有一个窗口的银行柜台(单线程)。同步任务是当场办理的业务;异步任务(如定时器、请求)是"取号排队",办完当前所有业务后再叫号。微任务是 VIP 队列,每办完一件普通业务就先清空所有 VIP。
执行过程:
宏任务和微任务:
代码示例
事件循环示例
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
// 输出顺序:1, 4, 3, 2
// 解析:同步先执行 1、4;微任务 3 在本轮宏任务后立即执行;宏任务 2 最后复杂事件循环
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => {
console.log('5');
}, 0);
});
console.log('6');
// 输出顺序:1, 6, 4, 2, 3, 5async/await 与事件循环
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end'); // await 后的代码相当于微任务
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
async1();
Promise.resolve().then(() => console.log('promise'));
console.log('script end');
// 输出:script start → async1 start → async2 → script end
// → async1 end → promise → setTimeout执行上下文与作用域链的深入剖析
前面介绍了执行上下文的基本概念,这里我们深入到引擎内部,理解变量对象(VO)、活动对象(AO)、全局对象(GO)以及作用域链(Scope Chain)是如何一步步构建起来的。这些是 ECMAScript 规范(ES3 时代)里最经典的模型,虽然 ES5 之后规范改用了"词法环境(Lexical Environment)+ 环境记录(Environment Record)"的新术语,但 VO/AO 模型对理解变量提升、闭包依然极具解释力。
变量对象 VO 与活动对象 AO
变量对象(Variable Object,VO) 是与执行上下文相关的数据作用域,存储了上下文中定义的变量和函数声明。在不同上下文中,VO 的具体表现不同:
VO/AO 的关键区别在于"激活时机": VO 在函数创建时理论上存在但不可访问,只有在函数被调用、进入执行上下文的那一刻,VO 才被激活成 AO,此时里面的属性才能被访问。这正是变量提升的底层解释。
函数上下文中 AO 的建立分两个阶段:
// 用一个函数说明 AO 的两个建立阶段
function build(a, b) {
var c = 10;
function d() {}
var e = function () {};
return a + b + c;
}
build(1, 2);
// 阶段一:进入上下文(编译/预处理),此时 AO 大致为:
// AO = {
// arguments: { 0: 1, 1: 2, length: 2 },
// a: 1, // 形参已赋值
// b: 2, // 形参已赋值
// c: undefined, // var 变量声明,值为 undefined
// d: <function d>, // 函数声明,整体提升
// e: undefined // 函数表达式,此刻只是 var 声明
// }
// 阶段二:执行代码,逐行赋值,AO 变为:
// AO = {
// arguments: { 0: 1, 1: 2, length: 2 },
// a: 1, b: 2,
// c: 10, // 执行到 var c = 10
// d: <function d>,
// e: <function> // 执行到 var e = function(){}
// }理解了这两个阶段,就能解释为什么函数声明可以在声明前调用,而 var 变量在赋值前只是 undefined。
VO 建立时的三条填充规则与优先级
进入执行上下文、建立 VO/AO 时,引擎按固定顺序填充属性,顺序决定了同名冲突时谁覆盖谁:
// 验证优先级:函数声明 > 变量声明
function conflict(x) {
console.log(typeof x); // 'function',不是 'number'
var x = 5; // var 声明不覆盖同名函数
function x() {} // 函数声明胜出
console.log(typeof x); // 'number',执行到 var x = 5 后才被赋值覆盖
}
conflict(1);
// 结论:
// 1) 进入上下文时,形参 x=1 → 被函数声明 x 覆盖 → var x 声明被忽略
// 2) 所以第一次 typeof 是 'function'
// 3) 执行阶段 var x = 5 生效,第二次 typeof 是 'number'作用域链的构建:[[Scopes]] 与 outer 引用
每个函数在被创建时,就会保存一个内部属性 `[[Scopes]]`(或规范里的 `[[Environment]]`),指向它定义时所处的所有父级作用域。当函数被调用、创建执行上下文时,把自己的 AO 放到 `[[Scopes]]` 前面,就形成了完整的作用域链:
Scope Chain = [ 当前AO ] + [[Scopes]](父级VO/AO...直到全局VO)// 作用域链的分层演示
var globalVar = 'G';
function outer() {
var outerVar = 'O';
function inner() {
var innerVar = 'I';
// inner 的作用域链:
// [ inner的AO {innerVar} ] → [ outer的AO {outerVar} ] → [ 全局VO {globalVar} ]
console.log(innerVar, outerVar, globalVar); // I O G
}
inner();
}
outer();
// 关键点:作用域链在【函数定义时】就已经通过 [[Scopes]] 确定了雏形,
// 与函数在哪里被【调用】无关,这就是词法作用域。词法作用域 vs 动态作用域
这是理解 JavaScript 作用域最核心的一对概念。JavaScript 采用词法作用域(也叫静态作用域):变量的作用域由代码书写的位置决定,在编译阶段就已确定。而动态作用域则由函数调用的位置(调用栈)决定,运行时才确定。
| 对比维度 | 词法作用域(JS 采用) | 动态作用域 |
| --- | --- | --- |
| 确定时机 | 代码编写/编译时 | 函数运行调用时 |
| 依据 | 函数定义的位置 | 函数调用的位置 |
| 查找方向 | 沿定义时的嵌套结构向外 | 沿调用栈向上 |
| 典型语言 | JavaScript、C、Java | Bash、早期 Lisp |
| 可预测性 | 高,静态分析即可确定 | 低,需追踪调用链 |
// 经典证明题:JS 是词法作用域
var value = 1;
function foo() {
console.log(value); // 取【定义 foo 时】的外层作用域,value = 1
}
function bar() {
var value = 2;
foo(); // 尽管在 bar 内部调用,foo 依然输出 1
}
bar(); // 输出 1(若是动态作用域会输出 2)
// 反证:如果 JavaScript 是动态作用域,foo 会顺着调用栈找到 bar 里的 value=2。
// 但事实是输出 1,证明 JS 是词法作用域。GO/AO 演进与 ES5 词法环境模型
ES5 之后,规范废弃了 VO/AO 的说法,改用更精确的词法环境(Lexical Environment)模型,但本质思路一致:
| 老模型(ES3) | 新模型(ES5+) | 说明 |
| --- | --- | --- |
| 全局对象 GO | 全局环境记录(对象式 + 声明式) | var/function 挂对象式,let/const/class 挂声明式 |
| 活动对象 AO | 函数环境记录 | 存储局部变量、arguments |
| 作用域链 | 环境记录的 outer 引用链 | 每个环境记录有 outer 指向父环境 |
| VO 激活 | 环境记录实例化 | 进入上下文时创建绑定 |
新模型有个重要细节:一个执行上下文其实包含两个环境组件——变量环境(VariableEnvironment) 存放 var 和 function 声明,词法环境(LexicalEnvironment) 存放 let、const、class 声明。这解释了为什么 let/const 有 TDZ 而 var 没有:它们被登记在不同的环境组件里,且初始化时机不同。
// 两个环境组件的差异
function demo() {
// VariableEnvironment: { a: undefined } ← var 立即初始化为 undefined
// LexicalEnvironment: { b: <uninitialized> } ← let 处于 TDZ,未初始化
console.log(a); // undefined(可访问)
// console.log(b); // ReferenceError(TDZ)
var a = 1;
let b = 2;
}
demo();真实案例:作用域链导致的性能问题
在深层嵌套中频繁访问全局变量,会让引擎每次都沿作用域链走到底,产生可测量的开销。经验数据:在 V8 中,访问局部变量比访问跨 5 层作用域链的变量快约 20%~30%(具体数字取决于引擎版本与 JIT 优化)。
// 反例:循环内反复沿作用域链访问全局/外层变量
function slow(arr) {
let result = 0;
for (let i = 0; i < arr.length; i++) {
// arr.length 每次都要沿作用域链找 arr,再取 length
result += arr[i] * window.devicePixelRatio; // 每次都查全局 window
}
return result;
}
// 优化:把跨作用域的值缓存到局部变量
function fast(arr) {
let result = 0;
const len = arr.length; // 缓存 length
const ratio = window.devicePixelRatio; // 缓存全局访问
for (let i = 0; i < len; i++) {
result += arr[i] * ratio;
}
return result;
}
// 在 100 万次循环下,fast 通常比 slow 快 15%~40%。常见坑与最佳实践
| 最佳实践 | 说明 |
| --- | --- |
| 优先局部变量 | 减少作用域链查找,提升性能与可读性 |
| 用块级作用域隔离 | let/const 天然块级,避免变量泄漏到外层 |
| 避免修改全局对象 | 减少副作用与命名冲突 |
| 理解两个环境组件 | 解释 var 与 let/const 行为差异的根本 |
小结
| 概念 | 本质 | 关键记忆点 |
| --- | --- | --- |
| VO/AO | 上下文的变量数据作用域 | AO 在进入函数上下文时激活 |
| GO | 全局变量对象 | 浏览器是 window,Node 是 global |
| 作用域链 | AO + 父级 VO/AO 链 | 定义时确定雏形,与调用无关 |
| 词法作用域 | 按书写位置定作用域 | JS 的核心,闭包的基础 |
| 变量环境/词法环境 | var 与 let/const 分开存 | 解释 TDZ 与提升差异 |
变量提升、暂时性死区与 let/const/var 的彻底对比
变量提升(Hoisting)是面试高频考点,但真正的难点在于把 var 的提升、let/const 的 TDZ、函数声明的整体提升三者放在一起理解。本节用大量可运行代码把每一种行为掰开揉碎。
提升的本质:声明前置,赋值留原地
"提升"并不是引擎真的把代码搬到顶部,而是编译阶段先把所有声明登记进对应的环境记录。可以想象成引擎"通读一遍先记名单,再逐行办事"。
// 你写的代码
console.log(a);
var a = 2;
function f() {}
// 引擎理解成(等价心智模型)
var a; // 声明登记,初始化为 undefined
function f() {} // 函数声明整体提升
console.log(a); // undefined
a = 2; // 赋值留在原地执行四种声明的提升行为对照
// 1) var:提升并初始化为 undefined
console.log(v); // undefined
var v = 1;
// 2) function 声明:整体提升,可提前调用
hoisted(); // 'ok'
function hoisted() { console.log('ok'); }
// 3) let / const:提升但不初始化,进入 TDZ
// console.log(l); // ReferenceError: Cannot access 'l' before initialization
let l = 2;
// 4) 函数表达式 / 箭头函数:只提升变量名,值仍是 undefined
// arrowFn(); // TypeError: arrowFn is not a function
var arrowFn = () => {};
// 5) class:提升但有 TDZ,行为类似 let
// new C(); // ReferenceError
class C {}暂时性死区 TDZ 深入
暂时性死区(Temporal Dead Zone,TDZ)指从块级作用域开始到 let/const 变量声明语句之间的区域,在这段区域里访问该变量会抛 `ReferenceError`。TDZ 的存在是为了让 const 的"不可变绑定"语义更严格,并帮助开发者尽早发现"声明前使用"的错误。
// TDZ 的边界演示
let x = 'outer';
{
// 从这里开始进入内层 x 的 TDZ
// console.log(x); // ReferenceError!即使外层有 x,也访问不了外层的
// typeof x; // 在 TDZ 内 typeof 也会报错(与未声明变量不同)
let x = 'inner'; // TDZ 到此结束
console.log(x); // 'inner'
}
// 对比:完全未声明的变量,typeof 安全返回 'undefined'
console.log(typeof neverDeclared); // 'undefined',不报错TDZ 有个反直觉的坑——在默认参数中也存在:
// 默认参数的 TDZ
function bad(a = b, b = 2) {
// 计算 a 的默认值时用到 b,但 b 还在 TDZ 中
return a + b;
}
// bad(); // ReferenceError: Cannot access 'b' before initialization
function good(a = 1, b = a + 1) {
// 计算 b 时 a 已初始化完成,合法
return a + b;
}
console.log(good()); // 3var 的函数作用域陷阱
var 只有函数作用域,没有块级作用域,这是无数 bug 的源头:
// var 在 if / for 块里"泄漏"到函数作用域
function leak() {
if (true) {
var secret = 42;
}
console.log(secret); // 42,var 无视块级作用域
}
leak();
// for 循环里的 var 泄漏
for (var i = 0; i < 3; i++) {}
console.log(i); // 3,循环结束后 i 依然存在
// let 则不会泄漏
for (let j = 0; j < 3; j++) {}
// console.log(j); // ReferenceError,j 只在循环块内有效let 在 for 循环中的"每轮新绑定"
let 在 for 循环里有个特殊机制:每次迭代都会创建一个全新的绑定,并把上一轮的值拷贝过来。这正是 let 能解决循环闭包问题的根本原因。
// let 每轮独立绑定
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0, 1, 2
}
// 引擎为每次迭代创建独立的 i:i0=0, i1=1, i2=2
// 三个闭包分别捕获了不同的绑定
// var 只有一个绑定,三个闭包共享
for (var k = 0; k < 3; k++) {
setTimeout(() => console.log(k), 0); // 3, 3, 3
}const 的"常量"到底锁住了什么
const 锁定的是变量绑定(binding),而不是值本身。对于对象/数组,引用不能变,但内部属性可以改。
const obj = { a: 1 };
obj.a = 2; // 合法,改的是属性
obj.b = 3; // 合法,加属性
// obj = {}; // TypeError: Assignment to constant variable.
const arr = [1, 2];
arr.push(3); // 合法,[1,2,3]
// arr = []; // TypeError
// 真正的不可变需要 Object.freeze(且只冻结一层,是浅冻结)
const frozen = Object.freeze({ a: 1, nested: { b: 2 } });
frozen.a = 99; // 静默失败(严格模式下报错)
frozen.nested.b = 99; // 生效!freeze 只冻结第一层
console.log(frozen.a, frozen.nested.b); // 1 99
// 深冻结需要递归
function deepFreeze(o) {
Object.keys(o).forEach((k) => {
if (typeof o[k] === 'object' && o[k] !== null) deepFreeze(o[k]);
});
return Object.freeze(o);
}var / let / const 全维度对比表
| 维度 | var | let | const |
| --- | --- | --- | --- |
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 提升 | 提升+初始化 undefined | 提升+TDZ | 提升+TDZ |
| 声明前访问 | undefined | ReferenceError | ReferenceError |
| 重复声明 | 允许 | 报错 | 报错 |
| 重新赋值 | 允许 | 允许 | 不允许 |
| 必须初始化 | 否 | 否 | 是(声明即赋值) |
| 挂到 window/global | 是 | 否 | 否 |
| for 循环每轮绑定 | 单一共享 | 每轮独立 | 不适用(不能自增) |
| typeof 声明前 | 'undefined' | 报错 | 报错 |
真实案例:老代码迁移 var 到 let 的意外
一个真实场景:团队把 var 批量替换为 let 后,某个依赖"var 提升 + 重复声明"的模块崩了。
// 老代码(依赖 var 可重复声明,且提升不报错)
function processLegacy(items) {
for (var i = 0; i < items.length; i++) { /* ... */ }
// 后面又来一段,再次 var i —— var 允许,静默复用
for (var i = 0; i < items.length; i++) { /* ... */ }
}
// 直接改成 let 会报 "Identifier 'i' has already been declared"
// 正确迁移:合并声明或改用不同变量名
function processFixed(items) {
for (let i = 0; i < items.length; i++) { /* 第一段 */ }
for (let i = 0; i < items.length; i++) { /* 第二段 */ }
// let 在不同的 for 块里各自独立,反而更安全
}最佳实践
| 实践 | 理由 |
| --- | --- |
| 默认用 const | 表达"不变"意图,减少意外重新赋值 |
| 需要重新赋值才用 let | 明确可变性 |
| 彻底弃用 var | 避免函数作用域泄漏与提升坑 |
| 变量声明尽量靠近使用处 | 缩小 TDZ 与作用域,提升可读性 |
| 对象常量配合 Object.freeze | 需要真正不可变时使用(注意浅冻结) |
小结
变量提升的三条铁律:函数声明整体提升且优先级最高;var 提升为 undefined;let/const/class 提升但有 TDZ。理解"两个环境组件 + TDZ"就能秒杀所有相关面试题。现代工程一律 const 优先、let 次之、彻底告别 var。
闭包的深入原理、陷阱与实战
闭包是 JavaScript 最强大也最容易被误解的特性。前面已经介绍了闭包的基本用法,本节从引擎实现的角度深挖闭包的原理,系统梳理经典陷阱、内存泄漏排查和性能考量。
闭包的引擎级定义
从规范角度,闭包 = 函数 + 该函数被创建时所处的词法环境。当一个内部函数被返回或以其他方式在其定义作用域之外被引用时,它依然保留着对外层环境记录的引用,导致这些环境记录无法被垃圾回收——这就是闭包。
// 闭包的最小模型
function makeAdder(x) {
// x 存活在 makeAdder 的环境记录中
return function (y) {
return x + y; // 内部函数引用了外层的 x → 形成闭包
};
}
const add5 = makeAdder(5);
const add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
// 关键:makeAdder(5) 执行完毕,但它的环境记录 {x:5} 没被回收,
// 因为 add5 还引用着它。add5 和 add10 各自持有独立的 x。用 Chrome DevTools 的 Sources 面板打断点,能在 Scope → Closure 里亲眼看到被闭包捕获的变量,这是验证闭包最直观的方式。
闭包只捕获"变量"而非"值"
一个高频误解:闭包捕获的是变量的引用,不是创建闭包那一刻的值快照。变量后续变化,闭包读到的也是新值。
function counter() {
let n = 0;
const read = () => n; // 捕获变量 n(引用)
const inc = () => { n++; };
return { read, inc };
}
const c = counter();
console.log(c.read()); // 0
c.inc();
c.inc();
console.log(c.read()); // 2 —— read 读到的是变化后的 n,证明捕获的是变量本身经典循环陷阱的三种解法与原理
循环 + 异步 + var 的组合是最经典的闭包陷阱。这里给出三种解法并解释各自原理。
// 陷阱:三个回调共享同一个 var i
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log('bug:', i), 100); // 3, 3, 3
}
// 解法 1:let 每轮新绑定(最推荐,ES6+)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log('let:', i), 100); // 0, 1, 2
}
// 解法 2:IIFE 立即执行函数,用参数固化当前值
for (var i = 0; i < 3; i++) {
((j) => {
setTimeout(() => console.log('iife:', j), 100); // 0, 1, 2
})(i);
}
// 解法 3:setTimeout 第三参数传值(较少用但有效)
for (var i = 0; i < 3; i++) {
setTimeout((j) => console.log('arg:', j), 100, i); // 0, 1, 2
}原理对比:let 靠每轮独立绑定;IIFE 靠函数调用创建新的执行上下文/环境记录,把 i 的当前值作为实参固化在参数 j 上;第三参数则由 setTimeout 在注册时立即求值并保存。
闭包实现柯里化与偏函数
柯里化(Currying)把多参函数转换为一系列单参函数,本质是层层闭包保存已收集的参数。
// 通用柯里化
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
// 参数不够,返回新函数继续收集(闭包保存已有 args)
return (...rest) => curried.apply(this, [...args, ...rest]);
};
}
const sum3 = (a, b, c) => a + b + c;
const cSum = curry(sum3);
console.log(cSum(1)(2)(3)); // 6
console.log(cSum(1, 2)(3)); // 6
console.log(cSum(1)(2, 3)); // 6
// 偏函数(Partial Application):固定部分参数
function partial(fn, ...preset) {
return (...later) => fn(...preset, ...later);
}
const log = (level, msg) => `[${level}] ${msg}`;
const errorLog = partial(log, 'ERROR');
console.log(errorLog('磁盘满了')); // [ERROR] 磁盘满了闭包实现真正的私有状态与模块
在 class 私有字段(#field)普及前,闭包是实现私有状态的唯一手段,至今仍广泛用于模块模式。
// 用闭包做带私有状态的计数器工厂
function createStore(initial) {
let state = initial; // 完全私有,外部无法直接触碰
const listeners = [];
return {
getState: () => state,
setState(next) {
state = typeof next === 'function' ? next(state) : next;
listeners.forEach((fn) => fn(state));
},
subscribe(fn) {
listeners.push(fn);
// 返回取消订阅函数,同样依赖闭包
return () => {
const idx = listeners.indexOf(fn);
if (idx > -1) listeners.splice(idx, 1);
};
},
};
}
const store = createStore(0);
const unsub = store.subscribe((s) => console.log('变化:', s));
store.setState((s) => s + 1); // 变化: 1
store.setState(10); // 变化: 10
unsub();
store.setState(20); // 无输出,已取消订阅
// 这正是 Redux/Zustand 等状态库的核心思路。闭包导致的内存泄漏场景与排查
闭包会延长变量生命周期,用不好就成内存泄漏。以下是四类高频泄漏场景。
// 场景 1:闭包意外持有大对象
function attach() {
const bigData = new Array(1000000).fill('x'); // 约占几 MB
return function () {
// 即使这里只用一个小值,只要引用了 bigData 所在作用域的任何东西,
// 整个环境记录(含 bigData)都可能被保留
return bigData.length;
};
}
// 修复:只闭包保留需要的最小数据
function attachFixed() {
const bigData = new Array(1000000).fill('x');
const len = bigData.length; // 只留下需要的
return () => len; // bigData 可被回收
}
// 场景 2:DOM 事件监听未移除,回调闭包持有 DOM
function bind() {
const el = document.getElementById('btn');
const handler = () => console.log(el.id);
el.addEventListener('click', handler);
// 泄漏:不 removeEventListener,el 与 handler 互相引用永不释放
return () => el.removeEventListener('click', handler); // 提供清理函数
}
// 场景 3:定时器未清理
function poll() {
const data = { huge: new Array(100000) };
const id = setInterval(() => console.log(data.huge.length), 1000);
return () => clearInterval(id); // 必须提供清理,否则 data 永不释放
}
// 场景 4:缓存无上限增长
const cache = new Map();
function memoLeak(key, val) {
cache.set(key, val); // 永远只增不减 → 内存持续上涨
}
// 修复:用 LRU 或 WeakMap(键为对象时自动回收)
const weakCache = new WeakMap();排查内存泄漏的标准流程:
闭包的性能考量
闭包本身开销很小,但需注意:
// 反例:每次渲染都新建闭包(React 中常见的重渲染诱因)
items.forEach((item) => {
el.addEventListener('click', () => handle(item)); // 每次都新建
});
// 优化:事件委托,只用一个闭包
el.addEventListener('click', (e) => {
const id = e.target.dataset.id;
if (id) handle(id);
});闭包对比表
| 用途 | 核心机制 | 典型场景 | 风险 |
| --- | --- | --- | --- |
| 私有变量 | 外层变量不暴露 | 计数器、状态库 | 引用未释放 |
| 模块化 | IIFE 返回公共 API | UMD、老式模块 | 全局污染(若忘记 IIFE) |
| 柯里化/偏函数 | 分层保存参数 | 函数式编程 | 过度嵌套难调试 |
| 防抖节流 | 保存 timer/标志 | 输入、滚动优化 | 忘清定时器泄漏 |
| 记忆化 | 保存 cache | 昂贵计算缓存 | 缓存无上限膨胀 |
最佳实践与小结
闭包是 JavaScript 函数式能力的根基,理解"函数 + 词法环境"这一定义,就能同时解释私有状态、模块化、柯里化和内存泄漏,做到既会用又用得安全。
原型与原型链的系统全解
原型(prototype)是 JavaScript 面向对象的基石。本节系统梳理 new 的执行步骤、instanceof 原理、`__proto__`/`prototype`/`constructor` 三者关系、Object.create、六种继承方式对比、ES6 class 与原型的关系,以及 Symbol、getter/setter。
new 操作符的四个步骤
理解 new 到底做了什么,是理解构造函数与原型的关键。new 一个构造函数分四步:
// 手写 new,还原 new 的四步
function myNew(Constructor, ...args) {
// 步骤 1:创建一个空对象,其原型指向构造函数的 prototype
const obj = Object.create(Constructor.prototype);
// 步骤 2:以新对象为 this 执行构造函数
const result = Constructor.apply(obj, args);
// 步骤 3:如果构造函数返回了一个对象,则用它;否则用新建的 obj
// 步骤 4:返回对象
return result !== null && (typeof result === 'object' || typeof result === 'function')
? result
: obj;
}
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.hello = function () {
return `我是 ${this.name},${this.age} 岁`;
};
const p1 = myNew(Person, 'Alice', 30);
const p2 = new Person('Bob', 25);
console.log(p1.hello()); // 我是 Alice,30 岁
console.log(p2.hello()); // 我是 Bob,25 岁
console.log(p1 instanceof Person); // true第三步的返回值判断很关键:构造函数若显式 return 一个对象,new 的结果就是那个对象;若 return 原始值或不 return,则返回新建的实例。
function Weird() {
this.a = 1;
return { b: 2 }; // 返回对象,覆盖了 this
}
console.log(new Weird()); // { b: 2 },而不是 { a: 1 }
function Normal() {
this.a = 1;
return 42; // 返回原始值,被忽略
}
console.log(new Normal()); // { a: 1 }__proto__、prototype、constructor 三角关系
这三者的关系是原型链的核心,一图胜千言,用代码验证:
function Foo() {}
const f = new Foo();
// prototype:函数特有的属性,指向"实例的原型对象"
console.log(typeof Foo.prototype); // 'object'
// __proto__:每个对象都有,指向"自己的原型"(即创建它的构造函数的 prototype)
console.log(f.__proto__ === Foo.prototype); // true
// constructor:原型对象上的属性,指回构造函数
console.log(Foo.prototype.constructor === Foo); // true
console.log(f.constructor === Foo); // true(f 自身没有,沿原型链找到)
// 完整链条
console.log(f.__proto__ === Foo.prototype); // true
console.log(Foo.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true 顶端
// 函数也是对象,函数的 __proto__ 指向 Function.prototype
console.log(Foo.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype);// true一个容易绕晕的经典结论:`Function.__proto__ === Function.prototype` 为 true(Function 由自身构造),且 `Object instanceof Function` 和 `Function instanceof Object` 都为 true。
console.log(Function.__proto__ === Function.prototype); // true
console.log(Object instanceof Function); // true(Object 是构造函数,由 Function 造)
console.log(Function instanceof Object); // true(Function 最终原型是 Object.prototype)推荐用标准 API 而非 __proto__
`__proto__` 是浏览器早期私有实现,虽被 ES6 纳入规范附录,但生产代码应使用标准方法:
const obj = {};
// 读取原型
Object.getPrototypeOf(obj); // 推荐,替代 obj.__proto__
// 设置原型(性能差,慎用)
Object.setPrototypeOf(obj, proto); // 替代 obj.__proto__ = proto
// 创建时指定原型
const child = Object.create(proto); // 推荐
// 创建无原型对象(纯字典,防原型污染)
const dict = Object.create(null);
console.log(dict.toString); // undefined,没有从 Object.prototype 继承任何东西instanceof 的原理与手写
instanceof 检查右侧构造函数的 prototype 是否出现在左侧对象的原型链上:
function myInstanceof(obj, Ctor) {
if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
return false; // 原始值直接 false
}
let proto = Object.getPrototypeOf(obj);
const target = Ctor.prototype;
while (proto !== null) {
if (proto === target) return true;
proto = Object.getPrototypeOf(proto); // 逐级向上
}
return false;
}
class A {}
class B extends A {}
const b = new B();
console.log(myInstanceof(b, B)); // true
console.log(myInstanceof(b, A)); // true(继承链上)
console.log(myInstanceof(b, Object)); // true
console.log(myInstanceof(b, Array)); // falseinstanceof 的局限:跨 iframe/window 时构造函数不同会失效;对原始值无效。更稳妥的类型判断常用 `Object.prototype.toString.call`:
const type = (v) => Object.prototype.toString.call(v).slice(8, -1);
console.log(type([])); // 'Array'
console.log(type(null)); // 'Null'
console.log(type(/x/)); // 'RegExp'
console.log(type(new Date()));// 'Date'六种继承方式对比
JavaScript 在 ES6 class 之前有多种继承实现,各有优劣。逐一给出代码。
// 1) 原型链继承:子类原型指向父类实例
function Parent1() { this.list = [1, 2]; }
function Child1() {}
Child1.prototype = new Parent1();
// 缺点:引用类型属性被所有实例共享;无法向父类传参
const a1 = new Child1(), b1 = new Child1();
a1.list.push(3);
console.log(b1.list); // [1,2,3] 被污染
// 2) 借用构造函数(经典继承):子类中调用父类构造
function Parent2(name) { this.name = name; this.list = [1, 2]; }
function Child2(name) { Parent2.call(this, name); }
// 优点:解决共享问题、可传参;缺点:父类原型方法无法继承
const a2 = new Child2('x');
a2.list.push(3);
console.log(new Child2('y').list); // [1,2] 不受影响
// 3) 组合继承(最常用的经典方案):构造函数 + 原型链
function Parent3(name) { this.name = name; this.list = [1, 2]; }
Parent3.prototype.say = function () { return this.name; };
function Child3(name, age) {
Parent3.call(this, name); // 第二次调用父构造
this.age = age;
}
Child3.prototype = new Parent3(); // 第一次调用父构造(缺点:调了两次)
Child3.prototype.constructor = Child3;
// 4) 原型式继承:Object.create 的原理
function objCreate(o) {
function F() {}
F.prototype = o;
return new F();
}
// 5) 寄生式继承:在原型式基础上增强对象
function parasitic(o) {
const clone = Object.create(o);
clone.extra = function () { return 'enhanced'; };
return clone;
}
// 6) 寄生组合继承(最理想方案,class 底层就是它)
function inherit(Child, Parent) {
// 不调用父构造,只继承原型,避免组合继承调两次的缺点
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
}
function Parent6(name) { this.name = name; }
Parent6.prototype.say = function () { return this.name; };
function Child6(name, age) {
Parent6.call(this, name);
this.age = age;
}
inherit(Child6, Parent6);
const c6 = new Child6('Alice', 30);
console.log(c6.say(), c6.age); // Alice 30| 继承方式 | 能否传参 | 引用属性共享问题 | 继承原型方法 | 父构造调用次数 | 推荐度 |
| --- | --- | --- | --- | --- | --- |
| 原型链继承 | 否 | 有(被污染) | 能 | 1 | 低 |
| 借用构造函数 | 能 | 无 | 不能 | 1 | 中 |
| 组合继承 | 能 | 无 | 能 | 2(冗余) | 高 |
| 原型式继承 | 否 | 有 | 能 | 0 | 中 |
| 寄生式继承 | 否 | 有 | 能 | 0 | 中 |
| 寄生组合继承 | 能 | 无 | 能 | 1 | 最高 |
ES6 class 与原型的关系
class 只是语法糖,底层依然是原型和寄生组合继承。用代码揭示:
class Animal {
constructor(name) { this.name = name; }
eat() { return `${this.name} 在吃`; }
static create(name) { return new Animal(name); } // 静态方法挂在类本身
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 相当于 Animal.call(this, name)
this.breed = breed;
}
bark() { return `${this.name} 汪汪`; }
}
const d = new Dog('旺财', '柴犬');
// 验证底层就是原型链
console.log(d.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(typeof Dog); // 'function',class 本质是函数
// 实例方法挂在 prototype 上,不可枚举
console.log(Object.getOwnPropertyNames(Dog.prototype)); // ['constructor','bark']
// class 内部默认严格模式;类声明不提升(有 TDZ)class 与函数构造的关键差异:
| 特性 | ES6 class | function 构造函数 |
| --- | --- | --- |
| 提升 | 有 TDZ,不可提前用 | 可提前调用(函数声明) |
| 严格模式 | 内部强制严格模式 | 取决于外部 |
| 方法可枚举 | 不可枚举 | 可枚举 |
| 必须 new 调用 | 是(直接调用报错) | 否 |
| super | 支持 | 需手动 Parent.call |
Symbol 与原型/属性
Symbol 是 ES6 引入的第 7 种原始类型,值唯一,常用于定义不冲突的属性键和"知名 Symbol"来定制对象行为。
// Symbol 作为唯一属性键
const id = Symbol('id');
const user = { [id]: 123, name: 'Alice' };
console.log(user[id]); // 123
console.log(Object.keys(user)); // ['name'],Symbol 键不被常规枚举
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id)]
// 知名 Symbol 定制行为
class Range {
constructor(start, end) { this.start = start; this.end = end; }
// 定制 instanceof
static [Symbol.hasInstance](inst) {
return typeof inst === 'number';
}
// 定制迭代
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i++) yield i;
}
}
console.log(5 instanceof Range); // true,被 Symbol.hasInstance 改写
console.log([...new Range(1, 3)]); // [1, 2, 3]
// Symbol.toPrimitive 定制类型转换
const money = {
amount: 100,
[Symbol.toPrimitive](hint) {
if (hint === 'number') return this.amount;
if (hint === 'string') return `¥${this.amount}`;
return this.amount;
},
};
console.log(+money); // 100
console.log(`${money}`); // ¥100getter/setter 与属性描述符
访问器属性(getter/setter)让属性访问变成函数调用,是响应式框架(如 Vue2)的核心。
const person = {
firstName: '张',
lastName: '三',
get fullName() {
return this.firstName + this.lastName;
},
set fullName(value) {
[this.firstName, this.lastName] = [value[0], value.slice(1)];
},
};
console.log(person.fullName); // 张三
person.fullName = '李四';
console.log(person.firstName, person.lastName); // 李 四
// 用 Object.defineProperty 精确控制属性描述符
const obj = {};
let internal = 0;
Object.defineProperty(obj, 'count', {
get() { return internal; },
set(v) { console.log('拦截到赋值:', v); internal = v; },
enumerable: true,
configurable: true,
});
obj.count = 5; // 拦截到赋值: 5
console.log(obj.count); // 5
// 这正是 Vue2 响应式的原理;Vue3 改用 Proxy 解决了 defineProperty 无法监听新增属性、数组索引等局限。小结
| 概念 | 一句话本质 | 关键点 |
| --- | --- | --- |
| new 四步 | 建对象、连原型、执行、返回 | 返回对象会覆盖 this |
| prototype | 函数指向实例原型的属性 | 实例方法挂这里 |
| __proto__ | 对象指向自己原型 | 用 getPrototypeOf 替代 |
| constructor | 原型指回构造函数 | 重写原型要修回它 |
| instanceof | 检查原型链上是否有某 prototype | 跨 iframe 会失效 |
| 寄生组合继承 | 最优继承,class 底层实现 | 只调一次父构造 |
| Symbol | 唯一值,定制对象行为 | 知名 Symbol 是元编程入口 |
| getter/setter | 属性访问变函数调用 | Vue2 响应式基础 |
this 指向的完整解析
this 是 JavaScript 最让人头疼的概念,因为它的值取决于函数被调用的方式而非定义的位置。本节系统讲清五种绑定规则、优先级,手写 call/apply/bind,剖析箭头函数,并给出 this 丢失的诊断与修复。
五种绑定规则逐一拆解
规则一:默认绑定(独立调用)
function show() {
console.log(this);
}
show();
// 非严格模式:this 指向全局对象(浏览器 window / Node global)
// 严格模式 'use strict':this 是 undefined规则二:隐式绑定(作为对象方法调用)
const obj = {
name: 'Alice',
greet() { return this.name; },
};
console.log(obj.greet()); // 'Alice',this 指向调用它的 obj
// 只看"调用那一刻点号前面是谁",多层嵌套只认最后一层
const outer = { name: 'O', inner: { name: 'I', greet() { return this.name; } } };
console.log(outer.inner.greet()); // 'I'规则三:显式绑定(call/apply/bind)
function intro(city, job) {
return `${this.name},来自${city},职业${job}`;
}
const person = { name: 'Bob' };
console.log(intro.call(person, '北京', '工程师')); // 参数逐个传
console.log(intro.apply(person, ['上海', '设计师'])); // 参数用数组
const bound = intro.bind(person, '广州'); // 返回新函数,可预置参数
console.log(bound('产品经理'));规则四:new 绑定
function Person(name) {
this.name = name; // this 指向 new 出来的新实例
}
const p = new Person('Charlie');
console.log(p.name); // 'Charlie'规则五:箭头函数(词法 this)
const obj = {
name: 'Alice',
regular() {
// 普通函数里的箭头,this 继承 regular 的 this(即 obj)
const arrow = () => this.name;
return arrow();
},
};
console.log(obj.regular()); // 'Alice'
// 箭头函数没有自己的 this,call/apply/bind 都改不了它五种绑定的优先级
当多条规则同时可能适用时,优先级从高到低为:new 绑定 > 显式绑定(bind/call/apply)> 隐式绑定 > 默认绑定。箭头函数是特例,它在定义时就锁定 this,不参与上面的比较。
// 验证:显式绑定 > 隐式绑定
function f() { return this.tag; }
const objA = { tag: 'A', f };
const objB = { tag: 'B' };
console.log(objA.f.call(objB)); // 'B',call 赢过隐式
// 验证:new > 显式(bind)
function Foo(v) { this.v = v; }
const bound = Foo.bind({ v: 'bound' });
const inst = new bound(42);
console.log(inst.v); // 42,new 赢过 bind
// 验证:箭头函数无视一切
const arrow = () => this;
console.log(arrow.call({ any: 1 }) === arrow()); // true,call 无效手写 call / apply / bind
彻底理解显式绑定,最好的方式是自己实现一遍。
// 手写 call
Function.prototype.myCall = function (context, ...args) {
// context 为 null/undefined 时指向全局对象
context = context == null ? globalThis : Object(context);
const key = Symbol('fn'); // 用 Symbol 避免覆盖已有属性
context[key] = this; // this 是被调用的函数
const result = context[key](...args); // 以方法形式调用,this 自然指向 context
delete context[key]; // 清理临时属性
return result;
};
// 手写 apply(区别仅在参数是数组)
Function.prototype.myApply = function (context, args) {
context = context == null ? globalThis : Object(context);
const key = Symbol('fn');
context[key] = this;
const result = Array.isArray(args) ? context[key](...args) : context[key]();
delete context[key];
return result;
};
// 手写 bind(要处理 new 调用的情况)
Function.prototype.myBind = function (context, ...preArgs) {
const fn = this;
function bound(...laterArgs) {
// 若 bound 被 new 调用,this 是新实例,应忽略绑定的 context
const isNew = this instanceof bound;
return fn.apply(isNew ? this : context, [...preArgs, ...laterArgs]);
}
// 维持原型链,使 new bound() 的实例能访问原函数原型上的方法
if (fn.prototype) {
bound.prototype = Object.create(fn.prototype);
}
return bound;
};
// 验证
function greet(greeting, punc) {
return `${greeting}, ${this.name}${punc}`;
}
console.log(greet.myCall({ name: 'Alice' }, 'Hi', '!')); // Hi, Alice!
console.log(greet.myApply({ name: 'Bob' }, ['Hello', '.'])); // Hello, Bob.
const bg = greet.myBind({ name: 'Carol' }, 'Hey');
console.log(bg('~')); // Hey, Carol~this 丢失的四大场景与修复
const obj = {
name: 'Alice',
greet() { return this.name; },
};
// 场景 1:方法赋值给变量后独立调用
const g = obj.greet;
console.log(g()); // undefined(默认绑定)
console.log(g.bind(obj)()); // 'Alice' 修复
// 场景 2:作为回调传入(setTimeout / 数组方法 / 事件)
setTimeout(obj.greet, 0); // undefined
setTimeout(() => obj.greet(), 0); // 'Alice' 修复:箭头包裹
setTimeout(obj.greet.bind(obj), 0); // 'Alice' 修复:bind
[1].forEach(function () { /* this 默认 undefined */ });
[1].forEach(function () { /* ... */ }, obj); // forEach 支持第二参数指定 this
[1].forEach(() => obj.greet()); // 箭头继承外层 this
// 场景 3:类方法作为事件处理器
class Counter {
count = 0;
// 方案 A:类字段 + 箭头函数,定义即绑定 this
handleClick = () => { this.count++; };
// 方案 B:构造函数里 bind
constructor() {
this.handleClickB = this.handleClickB.bind(this);
}
handleClickB() { this.count++; }
}
// 场景 4:嵌套普通函数导致 this 丢失
const timer = {
seconds: 0,
start() {
// setInterval(function () { this.seconds++; }, 1000); // this 错误
setInterval(() => { this.seconds++; }, 1000); // 箭头继承 start 的 this
},
};箭头函数不能做的事
// 1) 不能作构造函数
const Arrow = () => {};
// new Arrow(); // TypeError: Arrow is not a constructor
// 2) 没有自己的 arguments
const fn = () => {
// console.log(arguments); // ReferenceError(或取到外层的)
};
const rest = (...args) => args; // 用剩余参数替代
console.log(rest(1, 2, 3)); // [1,2,3]
// 3) 不适合做对象方法(this 指向外层而非对象)
const bad = {
name: 'X',
greet: () => this.name, // this 是外层(模块顶层多为 undefined)
};
// 4) 不能用作 generator(没有 yield)this 绑定规则对比表
| 调用方式 | this 指向 | 优先级 | 示例 |
| --- | --- | --- | --- |
| 默认(独立调用) | 全局对象 / undefined | 最低 | fn() |
| 隐式(方法调用) | 点号前的对象 | 低 | obj.fn() |
| 显式(call/apply/bind) | 指定对象 | 高 | fn.call(o) |
| new 绑定 | 新实例 | 最高 | new Fn() |
| 箭头函数 | 定义时外层 this | 不参与 | () => this |
小结
判断 this 的口诀:先看是不是箭头函数(是则取外层);否则看是不是 new(是则新实例);再看有没有 call/apply/bind(有则指定对象);再看有没有对象调用(有则该对象);都没有就是默认绑定(全局或 undefined)。掌握这个决策顺序,任何 this 题都能秒答。
事件循环的深入:浏览器与 Node.js
事件循环(Event Loop)是 JavaScript 单线程实现非阻塞异步的核心机制。前面讲过基本模型,本节深入到宏任务/微任务的完整清单、浏览器与 Node.js 的差异、requestAnimationFrame 的时机,并用多道经典输出题彻底吃透执行顺序。
宏任务与微任务的完整清单
任务分两大类,执行时机截然不同:每执行完一个宏任务,就会清空所有微任务队列,然后才取下一个宏任务。
| 分类 | 具体 API(浏览器) | 具体 API(Node.js) |
| --- | --- | --- |
| 宏任务(macrotask) | setTimeout、setInterval、MessageChannel、UI 渲染、I/O、用户交互事件 | setTimeout、setInterval、setImmediate、I/O |
| 微任务(microtask) | Promise.then/catch/finally、queueMicrotask、MutationObserver、await 之后 | Promise 回调、queueMicrotask、process.nextTick |
关键规则:微任务优先级高于宏任务;process.nextTick 优先级又高于普通微任务(Promise)。
浏览器事件循环的完整流程
一轮事件循环(tick):
1. 从宏任务队列取【一个】最老的宏任务执行
2. 执行过程中产生的微任务进入微任务队列
3. 宏任务执行完,清空【所有】微任务(包括微任务里又产生的微任务)
4. 需要则执行渲染(requestAnimationFrame → 样式计算 → 布局 → 绘制)
5. 回到步骤 1经典输出题逐题精讲
// 题 1:基础宏微任务
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 输出:1 4 3 2
// 解析:同步 1、4 → 清空微任务 3 → 下一个宏任务 2// 题 2:微任务里再生微任务,宏任务里生微任务
console.log('start');
setTimeout(() => {
console.log('timeout');
Promise.resolve().then(() => console.log('promise in timeout'));
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
Promise.resolve().then(() => console.log('promise2'));
});
console.log('end');
// 输出:start end promise1 promise2 timeout "promise in timeout"
// 解析:
// 同步:start、end
// 清空微任务:promise1 → 执行时又产生 promise2,继续清空 → promise2
// 下一个宏任务:timeout,产生新微任务
// 该宏任务后清空微任务:promise in timeout// 题 3:async/await 的拆解
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end'); // 相当于 async2().then(() => {...}),是微任务
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
async1();
new Promise((resolve) => {
console.log('promise executor'); // 同步执行!
resolve();
}).then(() => console.log('promise then'));
console.log('script end');
// 输出:
// script start
// async1 start
// async2
// promise executor
// script end
// async1 end
// promise then
// setTimeout
// 要点:Promise 的 executor 是同步执行的;await 后面的代码进微任务队列// 题 4:await 后跟 Promise 与直接值的差异(现代规范已优化)
async function f() {
console.log('a');
await Promise.resolve();
console.log('b'); // 微任务
}
f();
Promise.resolve().then(() => console.log('c'));
console.log('d');
// 输出:a d b c
// 现代 V8(TC39 优化后)await Promise.resolve() 只多一个微任务 tickrequestAnimationFrame 的时机
requestAnimationFrame(rAF)既不是宏任务也不是普通微任务,它在每次重绘之前、布局之前执行,与显示器刷新率同步(通常 60Hz,约每 16.7ms 一次)。
// rAF vs setTimeout 的执行时机
console.log('sync');
setTimeout(() => console.log('timeout'), 0);
requestAnimationFrame(() => console.log('raf'));
Promise.resolve().then(() => console.log('microtask'));
// 典型输出:sync microtask raf timeout(或 timeout 与 raf 顺序视浏览器帧时机)
// 要点:微任务先于 rAF;rAF 在渲染前、通常先于 0ms 的 setTimeout
// 用 rAF 做流畅动画(比 setTimeout 更稳,掉帧时自动跳过)
function animate(el) {
let start = null;
function step(timestamp) {
if (!start) start = timestamp;
const progress = timestamp - start;
el.style.transform = `translateX(${Math.min(progress / 10, 200)}px)`;
if (progress < 2000) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}Node.js 事件循环的六个阶段
Node.js 基于 libuv,事件循环分为六个阶段,每个阶段有自己的宏任务队列,阶段之间会清空微任务(Promise 与 process.nextTick)。
| 阶段 | 处理内容 |
| --- | --- |
| timers | 执行到期的 setTimeout / setInterval 回调 |
| pending callbacks | 执行延迟到下一轮的 I/O 回调 |
| idle, prepare | 内部使用 |
| poll | 获取新 I/O 事件,执行 I/O 回调(可能阻塞等待) |
| check | 执行 setImmediate 回调 |
| close callbacks | 执行 close 事件回调(如 socket.on('close')) |
// Node 中 setTimeout 与 setImmediate 的顺序(主模块中不确定)
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// 主模块里顺序不确定,取决于进程启动耗时
// 但在 I/O 回调内部,setImmediate 一定先于 setTimeout
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// 输出稳定:immediate 先,因为 poll 阶段结束直接进 check 阶段
});process.nextTick 与微任务优先级
在 Node.js 中,process.nextTick 的回调在当前操作完成后、事件循环继续之前立即执行,优先级高于 Promise 微任务。
// Node.js 优先级演示
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
console.log('sync');
// 输出:sync → nextTick → promise → timeout/immediate
// nextTick 队列优先于 Promise 微任务队列注意 nextTick 的坑:递归调用 process.nextTick 会让事件循环"饿死",永远进不到下一个阶段,I/O 被完全阻塞。
浏览器 vs Node.js 差异总结
| 维度 | 浏览器 | Node.js |
| --- | --- | --- |
| 微任务队列 | 单一微任务队列 | nextTick 队列 + Promise 队列 |
| 微任务清空时机 | 每个宏任务后 | 每个阶段之间 |
| 特有 API | requestAnimationFrame、MutationObserver | setImmediate、process.nextTick |
| 定时器精度 | 嵌套 5 层后最小 4ms | 最小 1ms |
| 阶段划分 | 无显式阶段 | 六个明确阶段 |
常见坑与最佳实践
// 时间切片:把 10 万条数据分批渲染,避免长任务卡顿
function renderInChunks(list, chunkSize = 500) {
let i = 0;
function work() {
const end = Math.min(i + chunkSize, list.length);
for (; i < end; i++) { /* 渲染 list[i] */ }
if (i < list.length) {
// 让出主线程给浏览器渲染,避免一次性阻塞
requestAnimationFrame(work);
}
}
work();
}小结
| 概念 | 关键点 |
| --- | --- |
| 微任务优先 | 每个宏任务后清空所有微任务 |
| Promise executor | 同步执行,then 回调才是微任务 |
| await 后代码 | 等价于 then 回调,是微任务 |
| requestAnimationFrame | 渲染前执行,与刷新率同步 |
| Node 六阶段 | timers/poll/check 等,阶段间清微任务 |
| process.nextTick | Node 中优先级最高,慎防饿死 |
内存管理与垃圾回收
JavaScript 是自动内存管理语言,开发者不手动分配和释放内存,但理解垃圾回收(Garbage Collection,GC)机制能帮你避免内存泄漏、写出更省内存的代码。本节讲清引用计数、标记清除、V8 分代 GC,以及内存泄漏的定位方法。
内存生命周期
任何语言的内存都经历三个阶段:分配 → 使用 → 释放。JavaScript 的分配和释放大多是自动的。
// 分配:声明变量、创建对象/函数时引擎自动分配内存
const num = 123; // 栈上分配原始值
const obj = { a: 1 }; // 堆上分配对象,栈上存引用
const arr = new Array(1000); // 堆上分配数组
const fn = function () {}; // 堆上分配函数对象
// 使用:读写这些值
obj.a = 2;
// 释放:当没有任何引用指向对象时,GC 回收它
// obj = null; // 解除引用,使原对象可被回收原始类型(number、string、boolean 等)通常存在栈中,引用类型(对象、数组、函数)存在堆中,栈里只放指向堆的引用地址。
引用计数算法
早期 GC 策略:记录每个对象被引用的次数,计数为 0 时回收。缺点是无法处理循环引用。
// 引用计数的致命缺陷:循环引用
function leak() {
const a = {};
const b = {};
a.ref = b; // b 被引用 +1
b.ref = a; // a 被引用 +1
// 函数结束后,a 和 b 都不再被外部引用,但它们互相引用,
// 引用计数永远不为 0 → 在纯引用计数下永远不会被回收(泄漏)
}
leak();
// 现代浏览器早已改用标记清除,不受此问题影响。标记清除算法
现代 JavaScript 引擎(V8 等)主流采用标记清除(Mark-and-Sweep)。它从一组"根"(全局对象、当前调用栈上的变量等)出发,标记所有可达对象,未被标记的即为垃圾,予以清除。
标记清除三步:
1. 标记(Mark):从根出发,遍历所有可达对象并打标记
2. 清除(Sweep):遍历堆,回收所有未被标记的对象
3. 整理(Compact,可选):移动存活对象,压缩碎片空间因为判断标准是"从根是否可达",循环引用只要整体从根不可达,就会被一起回收,彻底解决了引用计数的缺陷。
V8 的分代式垃圾回收
V8 基于"大多数对象很快死亡(弱代假说)",把堆分为新生代和老生代,采用不同策略,大幅提升效率。
| 分代 | 大小(约) | 特点 | 回收算法 |
| --- | --- | --- | --- |
| 新生代(Young) | 1~8 MB | 存放新对象,回收频繁 | Scavenge(复制算法,From/To 空间) |
| 老生代(Old) | 数百 MB~上 GB | 存活久的对象 | 标记清除 + 标记整理 |
新生代 Scavenge:把新生代分成 From 和 To 两半,只用一半。GC 时把 From 中存活对象复制到 To,然后交换角色,清空原 From。对象经历两次 Scavenge 仍存活则"晋升"到老生代。这种复制算法速度快但浪费一半空间,适合小而多的新生代。
老生代:对象多、存活久,用标记清除回收垃圾,再用标记整理消除内存碎片。V8 还引入了增量标记(把标记拆成小段穿插执行)、惰性清除、并发/并行回收,尽量减少 GC 造成的主线程停顿(Stop-The-World)。
// 观察内存(Node.js)
const used = process.memoryUsage();
console.log({
rss: (used.rss / 1024 / 1024).toFixed(2) + ' MB', // 常驻内存
heapTotal: (used.heapTotal / 1024 / 1024).toFixed(2) + ' MB', // V8 堆总量
heapUsed: (used.heapUsed / 1024 / 1024).toFixed(2) + ' MB', // 已用堆
external: (used.external / 1024 / 1024).toFixed(2) + ' MB', // C++ 对象
});
// 浏览器可用 performance.memory(非标准)观察 usedJSHeapSize常见内存泄漏场景
// 泄漏 1:意外的全局变量
function leak1() {
leaked = 'I am global'; // 忘写 var/let/const,挂到了全局,永不回收
}
// 防御:文件顶部加 'use strict',未声明赋值会报错
// 泄漏 2:被遗忘的定时器
let data = { huge: new Array(100000) };
const timerId = setInterval(() => {
console.log(data.huge.length); // 闭包持有 data,只要定时器在就不回收
}, 1000);
// 修复:clearInterval(timerId); data = null;
// 泄漏 3:脱离 DOM 的元素引用
const cache = {};
function keepRef() {
const el = document.getElementById('big-table');
cache.el = el; // JS 里存了引用
}
// 即使从 DOM 移除了该元素,cache.el 仍持有它 → DOM 内存泄漏
// 修复:cache.el = null;
// 泄漏 4:闭包无意持有大对象(见闭包章节)
// 泄漏 5:事件监听未移除
function bindLeak(node) {
const handler = () => {};
node.addEventListener('click', handler);
// 未 removeEventListener,node 被移除后监听器仍引用它
}WeakMap / WeakSet 避免泄漏
WeakMap 和 WeakSet 对键持弱引用,当键对象没有其他引用时会被自动回收,非常适合做"附加数据"缓存。
// 用 WeakMap 给 DOM 节点关联数据,节点移除后数据自动回收
const nodeData = new WeakMap();
function setData(node, data) {
nodeData.set(node, data); // node 被 GC 后,这条记录自动消失
}
function getData(node) {
return nodeData.get(node);
}
// 对比普通 Map:Map 强引用 key,node 移除后记录仍在 → 泄漏
// WeakMap 实现真正私有属性
const _private = new WeakMap();
class Account {
constructor(balance) {
_private.set(this, { balance });
}
getBalance() {
return _private.get(this).balance;
}
}
const acc = new Account(100);
console.log(acc.getBalance()); // 100,balance 外部无法直接访问内存泄漏定位流程
减少内存占用的实践
| 实践 | 说明 |
| --- | --- |
| 及时解除引用 | 大对象用完置 null,尤其是缓存和闭包 |
| 用 WeakMap/WeakSet | 关联数据随对象自动回收 |
| 清理定时器与监听 | 组件卸载时 clearInterval/removeEventListener |
| 避免全局变量膨胀 | 严格模式 + 模块化隔离 |
| 分片处理大数据 | 避免一次性构造超大数组/字符串 |
| 对象池复用 | 高频创建销毁的小对象可复用,减轻 GC 压力 |
小结
| 概念 | 关键点 |
| --- | --- |
| 引用计数 | 简单但无法处理循环引用 |
| 标记清除 | 从根判断可达性,现代主流 |
| 新生代 Scavenge | 复制算法,快,晋升机制 |
| 老生代 | 标记清除+整理,增量/并发优化 |
| WeakMap/WeakSet | 弱引用,自动回收,防泄漏 |
| 泄漏定位 | 快照对比 + Retainers 保留链 |
类型系统与类型转换
JavaScript 是动态弱类型语言,类型转换规则是无数"诡异 bug"的来源。本节讲清 7 种原始类型、包装对象、显式与隐式类型转换规则(含 == 强制转换表)、typeof/instanceof 的适用边界,以及深浅拷贝的实现。
七种原始类型与一种引用类型
// 7 种原始类型(primitive)
const a = 42; // number
const b = 42n; // bigint(ES2020,大整数)
const c = 'hi'; // string
const d = true; // boolean
const e = undefined; // undefined
const f = null; // null
const g = Symbol('id'); // symbol
// 引用类型:object(数组、函数、Date、RegExp 等都是 object)
const obj = {};
const arr = [];
const fn = () => {};原始类型的值不可变(immutable),存储在栈中,按值传递;引用类型可变,存储在堆中,按引用传递(传的是地址)。
// 按值 vs 按引用
let x = 1;
let y = x; // 拷贝值
y = 2;
console.log(x); // 1,互不影响
let o1 = { n: 1 };
let o2 = o1; // 拷贝的是引用地址
o2.n = 2;
console.log(o1.n); // 2,指向同一对象typeof 的返回值与坑
console.log(typeof 42); // 'number'
console.log(typeof 42n); // 'bigint'
console.log(typeof 'hi'); // 'string'
console.log(typeof true); // 'boolean'
console.log(typeof undefined); // 'undefined'
console.log(typeof Symbol()); // 'symbol'
console.log(typeof {}); // 'object'
console.log(typeof []); // 'object'(数组也是 object!)
console.log(typeof function(){});// 'function'(函数特殊)
console.log(typeof null); // 'object'(历史遗留 bug!)typeof null === 'object' 是 JavaScript 诞生之初的 bug,因兼容性无法修复。判断数组要用 `Array.isArray`,判断 null 直接 `=== null`,精确判断类型用 `Object.prototype.toString.call`。
包装对象
原始类型没有属性和方法,但我们能写 `'hi'.length`。这是因为引擎在访问时临时创建了包装对象(String、Number、Boolean),用完即销毁。
const str = 'hello';
console.log(str.length); // 5
console.log(str.toUpperCase()); // 'HELLO'
// 引擎背后做了:new String('hello').length,取完值再丢弃包装对象
// 证据:给原始值加属性无效(包装对象转瞬即逝)
str.custom = 123;
console.log(str.custom); // undefined,每次访问都是新的临时包装对象
// 不要显式 new 包装对象,会得到 object 而非原始值
const bad = new String('x');
console.log(typeof bad); // 'object'
console.log(bad == 'x'); // true(值相等)
console.log(bad === 'x'); // false(类型不同)显式类型转换
// 转数字
Number('123'); // 123
Number('12px'); // NaN
Number(''); // 0
Number(true); // 1
Number(null); // 0
Number(undefined);// NaN
Number([]); // 0
Number([5]); // 5
Number([1, 2]); // NaN
parseInt('12px'); // 12(宽松,从左解析到非法字符)
parseFloat('3.14abc'); // 3.14
// 转字符串
String(123); // '123'
String(null); // 'null'
String(undefined);// 'undefined'
String([1, 2]); // '1,2'
String({}); // '[object Object]'
(123).toString(); // '123'
// 转布尔(记住 falsy 值即可)
Boolean(0); // false
Boolean(''); // false
Boolean(NaN); // false
Boolean(null); // false
Boolean(undefined);// false
Boolean(false); // false
// 以上 6 个(及 0n)是全部 falsy 值,其余全部 truthy
Boolean('0'); // true(非空字符串)
Boolean([]); // true(空数组也是 truthy!)
Boolean({}); // true隐式类型转换与 == 强制转换
隐式转换发生在运算符两侧类型不一致时。最复杂的是 `==` 的强制转换(coercion)。
== 的转换规则(简化版):
console.log(1 == '1'); // true('1' → 1)
console.log(true == 1); // true(true → 1)
console.log(true == '1'); // true(true → 1,'1' → 1)
console.log(null == undefined);// true(特殊规定)
console.log(null == 0); // false(null 只等于 undefined)
console.log(undefined == 0); // false
console.log(NaN == NaN); // false(NaN 谁都不等)
console.log([] == false); // true([] → '' → 0,false → 0)
console.log([] == ![]); // true(![] 是 false → 0,[] → 0)
console.log('' == 0); // true('' → 0)
console.log([1] == 1); // true([1] → '1' → 1)
console.log({} == {}); // false(不同引用)经典陷阱 `[] == ![]` 解析:`![]` 先算,`[]` 是 truthy 所以 `![]` 为 false;然后 `[] == false`,false 转 0,`[]` 转原始值为 `''` 再转 0,`0 == 0` 为 true。
== 强制转换速查表(部分组合):
| 左 | 右 | 结果 | 转换过程 |
| --- | --- | --- | --- |
| 1 | '1' | true | 字符串转数字 |
| 0 | '' | true | 空串转 0 |
| 0 | '0' | true | '0' 转 0 |
| false | 0 | true | false 转 0 |
| null | undefined | true | 特殊规定 |
| null | 0 | false | null 不转数字比较 |
| NaN | NaN | false | NaN 恒不等 |
| [] | false | true | []→''→0,false→0 |
| [] | ![] | true | ![]→false→0,[]→0 |
结论:除非明确需要类型转换,永远用 === 严格相等,避开 == 的所有坑。
对象转原始值的顺序
对象参与运算时会转原始值,规则是先调 `Symbol.toPrimitive`(若有),否则按 hint 顺序调 valueOf 和 toString。
const obj = {
valueOf() { console.log('valueOf'); return 10; },
toString() { console.log('toString'); return 'str'; },
};
console.log(obj + 1); // hint 'default':先 valueOf → 11
console.log(`${obj}`); // hint 'string':先 toString → 'str'
console.log(obj * 2); // hint 'number':先 valueOf → 20
// 数组的 toString 是 join(',')
console.log([1, 2, 3] + ''); // '1,2,3'
console.log({} + ''); // '[object Object]'深拷贝与浅拷贝
浅拷贝只复制第一层,嵌套对象仍共享引用;深拷贝递归复制所有层级。
// 浅拷贝
const obj = { a: 1, nested: { b: 2 } };
const shallow1 = { ...obj }; // 展开运算符
const shallow2 = Object.assign({}, obj);
shallow1.nested.b = 99;
console.log(obj.nested.b); // 99,嵌套对象被共享(浅拷贝的局限)
// 深拷贝方案 1:structuredClone(现代浏览器/Node 17+ 内置,推荐)
const deep1 = structuredClone(obj);
deep1.nested.b = 100;
console.log(obj.nested.b); // 99,不受影响
// 支持 Date、RegExp、Map、Set、循环引用;不支持函数、Symbol 键、DOM
// 深拷贝方案 2:JSON(简单但有局限)
const deep2 = JSON.parse(JSON.stringify(obj));
// 局限:丢失 undefined、函数、Symbol;Date 变字符串;不支持循环引用(报错);BigInt 报错
// 深拷贝方案 3:手写递归(处理循环引用)
function deepClone(target, map = new WeakMap()) {
if (target === null || typeof target !== 'object') return target;
if (target instanceof Date) return new Date(target);
if (target instanceof RegExp) return new RegExp(target);
if (map.has(target)) return map.get(target); // 处理循环引用
const clone = Array.isArray(target) ? [] : {};
map.set(target, clone);
Reflect.ownKeys(target).forEach((key) => {
clone[key] = deepClone(target[key], map);
});
return clone;
}
const a = { x: 1 };
a.self = a; // 循环引用
const cloned = deepClone(a);
console.log(cloned.self === cloned); // true,循环引用被正确处理| 拷贝方式 | 层级 | 函数 | 循环引用 | Date/RegExp | 性能 |
| --- | --- | --- | --- | --- | --- |
| 展开/Object.assign | 浅 | 保留 | 保留 | 保留 | 最快 |
| JSON 方法 | 深 | 丢失 | 报错 | 变字符串 | 中 |
| structuredClone | 深 | 报错 | 支持 | 支持 | 快 |
| 手写递归 | 深 | 可保留 | 可处理 | 可处理 | 视实现 |
小结
| 概念 | 关键点 |
| --- | --- |
| 7 种原始类型 | number/string/boolean/undefined/null/symbol/bigint |
| typeof null | 返回 'object',历史 bug |
| 包装对象 | 原始值临时借用方法,转瞬即逝 |
| == 强制转换 | 规则复杂,一律用 === |
| falsy 值 | 0/''/NaN/null/undefined/false/0n |
| 深浅拷贝 | 浅拷贝共享嵌套引用,深拷贝优先 structuredClone |
异步编程的演进:从回调到 async/await
JavaScript 的异步方案经历了回调函数 → Promise → Generator → async/await 的演进。本节按这条脉络逐一剖析,手写符合 Promise/A+ 规范的关键片段,并给出并发控制的实战代码。
回调函数与回调地狱
最早的异步用回调函数,多层嵌套导致"回调地狱"(Callback Hell),代码横向发展、难以维护、错误处理繁琐。
// 回调地狱:层层嵌套,俗称"厄运金字塔"
getUser(userId, (err, user) => {
if (err) return handle(err);
getOrders(user.id, (err, orders) => {
if (err) return handle(err);
getOrderDetail(orders[0].id, (err, detail) => {
if (err) return handle(err);
getShipping(detail.shipId, (err, shipping) => {
if (err) return handle(err);
console.log(shipping); // 嵌套四层,每层都要重复错误处理
});
});
});
});回调的三大问题:嵌套过深难读;错误处理靠约定(error-first callback),每层都要写;控制反转(把回调交给第三方,无法保证只调用一次、调用时机、异常吞没)。
Promise 解决了什么
Promise 是对异步结果的封装,有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。状态一旦从 pending 变为 fulfilled 或 rejected 就不可再变(immutable)。它用链式调用把嵌套变成扁平。
// Promise 链式改写回调地狱
getUser(userId)
.then((user) => getOrders(user.id))
.then((orders) => getOrderDetail(orders[0].id))
.then((detail) => getShipping(detail.shipId))
.then((shipping) => console.log(shipping))
.catch((err) => handle(err)); // 统一错误处理,任一环节出错都进这里// Promise 基本用法
const p = new Promise((resolve, reject) => {
// executor 同步执行
setTimeout(() => {
const ok = Math.random() > 0.5;
ok ? resolve('成功数据') : reject(new Error('失败'));
}, 100);
});
p.then(
(data) => console.log('成功:', data),
(err) => console.log('失败:', err.message)
).finally(() => console.log('无论成败都执行'));手写符合 Promise/A+ 规范的核心片段
理解 Promise 最好的方式是自己实现。下面是一个覆盖 A+ 关键点(状态机、then 异步、值穿透、链式返回新 Promise)的实现。
class MyPromise {
static PENDING = 'pending';
static FULFILLED = 'fulfilled';
static REJECTED = 'rejected';
constructor(executor) {
this.status = MyPromise.PENDING;
this.value = undefined;
this.reason = undefined;
this.onFulfilledCbs = []; // 缓存 pending 期间注册的回调
this.onRejectedCbs = [];
const resolve = (value) => {
if (this.status === MyPromise.PENDING) {
this.status = MyPromise.FULFILLED;
this.value = value;
this.onFulfilledCbs.forEach((fn) => fn());
}
};
const reject = (reason) => {
if (this.status === MyPromise.PENDING) {
this.status = MyPromise.REJECTED;
this.reason = reason;
this.onRejectedCbs.forEach((fn) => fn());
}
};
try {
executor(resolve, reject); // executor 同步执行
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
// 值穿透:非函数则透传
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (v) => v;
onRejected = typeof onRejected === 'function' ? onRejected : (e) => { throw e; };
// then 必须返回新 Promise 以支持链式
const promise2 = new MyPromise((resolve, reject) => {
const handleFulfilled = () => {
// 用微任务模拟异步(规范要求 then 回调异步执行)
queueMicrotask(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
});
};
const handleRejected = () => {
queueMicrotask(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
});
};
if (this.status === MyPromise.FULFILLED) handleFulfilled();
else if (this.status === MyPromise.REJECTED) handleRejected();
else {
// pending:缓存回调,等状态改变再执行
this.onFulfilledCbs.push(handleFulfilled);
this.onRejectedCbs.push(handleRejected);
}
});
return promise2;
}
catch(onRejected) {
return this.then(null, onRejected);
}
}
// 处理 then 返回值可能是 thenable/Promise 的情况(A+ 核心)
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('循环引用'));
}
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
let called = false;
try {
const then = x.then;
if (typeof then === 'function') {
then.call(
x,
(y) => { if (!called) { called = true; resolvePromise(promise2, y, resolve, reject); } },
(r) => { if (!called) { called = true; reject(r); } }
);
} else {
resolve(x);
}
} catch (err) {
if (!called) { called = true; reject(err); }
}
} else {
resolve(x);
}
}
// 验证
new MyPromise((resolve) => setTimeout(() => resolve(1), 50))
.then((v) => { console.log(v); return v + 1; })
.then((v) => console.log(v)); // 依次输出 1、2Promise 的静态组合方法
const p1 = Promise.resolve(1);
const p2 = new Promise((r) => setTimeout(() => r(2), 100));
const p3 = Promise.reject(new Error('boom'));
// all:全部成功才成功,一个失败即失败(短路)
Promise.all([p1, p2]).then((vals) => console.log(vals)); // [1, 2]
// allSettled:等所有完成,返回每个的状态(不短路)
Promise.allSettled([p1, p3]).then((results) => console.log(results));
// [{status:'fulfilled',value:1}, {status:'rejected',reason:Error}]
// race:第一个敲定(无论成败)的结果
Promise.race([p2, Promise.resolve('fast')]).then((v) => console.log(v)); // 'fast'
// any:第一个成功的结果,全部失败才失败(AggregateError)
Promise.any([p3, p1]).then((v) => console.log(v)); // 1| 方法 | 成功条件 | 失败条件 | 返回 |
| --- | --- | --- | --- |
| all | 全部成功 | 任一失败(短路) | 结果数组 |
| allSettled | 总是敲定 | 从不失败 | 状态对象数组 |
| race | 第一个敲定成功 | 第一个敲定失败 | 单个结果 |
| any | 任一成功 | 全部失败 | 单个结果 |
Generator:可暂停的函数
Generator(生成器)通过 `function*` 和 `yield` 实现函数的暂停与恢复,是 async/await 的底层基础。
function* gen() {
const a = yield 1; // 在 yield 处暂停,next(x) 传入的 x 成为 a
const b = yield a + 1;
return b;
}
const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.next(10)); // a=10,{ value: 11, done: false }
console.log(g.next(20)); // b=20,{ value: 20, done: true }
// 用 Generator 手动管理异步流程(async/await 的雏形)
function run(genFn) {
const it = genFn();
function step(nextVal) {
const { value, done } = it.next(nextVal);
if (done) return Promise.resolve(value);
return Promise.resolve(value).then(step); // 自动驱动
}
return step();
}
run(function* () {
const a = yield Promise.resolve(1);
const b = yield Promise.resolve(a + 1);
console.log(a, b); // 1 2
});async/await:异步的终极形态
async/await 是 Generator + Promise 的语法糖,让异步代码写起来像同步一样直观。
// async 函数总是返回 Promise;await 等待 Promise 敲定
async function loadData(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const detail = await getOrderDetail(orders[0].id);
return detail; // 被包装成 resolved Promise
} catch (err) {
// 用同步的 try/catch 捕获所有 await 的错误
console.error('出错:', err);
throw err;
}
}并发控制实战
// 陷阱:串行 await 让本可并发的请求变慢
async function slow() {
const a = await fetch('/a'); // 等 a 完成
const b = await fetch('/b'); // 才开始 b(总耗时 = a + b)
return [a, b];
}
// 优化:并发发起,用 Promise.all 等待(总耗时 = max(a, b))
async function fast() {
const [a, b] = await Promise.all([fetch('/a'), fetch('/b')]);
return [a, b];
}
// 限制并发数:有 1000 个任务但最多同时跑 5 个
async function runWithLimit(tasks, limit = 5) {
const results = [];
const executing = new Set();
for (const [i, task] of tasks.entries()) {
const p = Promise.resolve().then(() => task());
results[i] = p;
executing.add(p);
const clean = () => executing.delete(p);
p.then(clean, clean);
if (executing.size >= limit) {
await Promise.race(executing); // 等任一完成腾出名额
}
}
return Promise.all(results);
}
// 用法:控制并发请求,避免瞬间打爆服务器
const urls = Array.from({ length: 100 }, (_, i) => `/api/item/${i}`);
runWithLimit(urls.map((u) => () => fetch(u)), 5)
.then(() => console.log('全部完成,最多同时 5 个请求'));// 带重试与超时的健壮请求封装
async function fetchWithRetry(url, { retries = 3, timeout = 5000 } = {}) {
for (let i = 0; i <= retries; i++) {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
const res = await fetch(url, { signal: controller.signal });
clearTimeout(timer);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
if (i === retries) throw err; // 重试用尽,抛出
// 指数退避:200ms、400ms、800ms...
await new Promise((r) => setTimeout(r, 200 * 2 ** i));
}
}
}异步方案演进对比
| 方案 | 可读性 | 错误处理 | 组合能力 | 出现时间 |
| --- | --- | --- | --- | --- |
| 回调函数 | 差(嵌套地狱) | error-first,繁琐 | 弱 | ES3 时代 |
| Promise | 中(链式扁平) | .catch 统一 | 强(all/race 等) | ES6 |
| Generator | 中(需驱动器) | try/catch | 中 | ES6 |
| async/await | 好(近似同步) | try/catch,直观 | 强(配合 Promise) | ES2017 |
常见坑与最佳实践
// forEach 不等待异步(坑)
[1, 2, 3].forEach(async (n) => { await task(n); });
console.log('这行会先于所有 task 完成打印'); // forEach 不等 await
// 正确:需要顺序用 for...of,需要并发用 map + Promise.all
for (const n of [1, 2, 3]) { await task(n); } // 顺序
await Promise.all([1, 2, 3].map((n) => task(n))); // 并发小结
| 概念 | 关键点 |
| --- | --- |
| 回调地狱 | 嵌套深、错误处理繁、控制反转 |
| Promise | 三状态不可逆,链式扁平化 |
| Promise/A+ | then 异步、返回新 Promise、值穿透 |
| 组合方法 | all/allSettled/race/any 各有语义 |
| Generator | 可暂停函数,async 的底层 |
| async/await | 语法糖,同步写法,try/catch 处理错误 |
| 并发控制 | Promise.all 并发,限流用 race 腾名额 |
综合实战:贯通所有核心机制的输出题
真正检验对执行机制的理解,是把作用域、闭包、this、原型、事件循环揉在一起的综合题。本节精选若干"面试杀手题",逐行拆解,帮你把前面所有知识串成一张网。
综合题一:闭包 + 循环 + 事件循环
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log('A', i), 0);
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log('B', j), 0);
}
// 输出:A 3 / A 3 / A 3 / B 0 / B 1 / B 2
// 解析:
// var i 是函数作用域,3 个回调共享同一 i,等宏任务执行时 i 已是 3
// let j 每轮新绑定,3 个回调各自捕获 0/1/2
// 两组都是宏任务,按注册顺序执行综合题二:this + 原型 + 隐式绑定丢失
const obj = {
val: 'obj',
getVal() { return this.val; },
getValArrow: () => this.val,
};
const fn = obj.getVal;
console.log(obj.getVal()); // 'obj'(隐式绑定)
console.log(fn()); // undefined(默认绑定,this 丢失)
console.log(fn.call(obj)); // 'obj'(显式绑定)
console.log(obj.getValArrow()); // undefined(箭头 this 是模块顶层)
// 原型链上的方法同样遵循调用规则
function Widget(val) { this.val = val; }
Widget.prototype.getVal = function () { return this.val; };
const w = new Widget('w');
const extracted = w.getVal;
console.log(w.getVal()); // 'w'
console.log(extracted()); // undefined,方法脱离对象即丢 this综合题三:微任务、宏任务与 async 的交织
console.log('1 script start');
setTimeout(() => console.log('2 setTimeout'), 0);
async function asyncFn() {
console.log('3 async start');
await null; // await 非 Promise 也会产生微任务切点
console.log('4 async after await');
}
asyncFn();
new Promise((resolve) => {
console.log('5 promise executor');
resolve();
}).then(() => console.log('6 promise then'));
console.log('7 script end');
// 输出顺序:
// 1 script start
// 3 async start
// 5 promise executor
// 7 script end
// 4 async after await
// 6 promise then
// 2 setTimeout
// 解析:同步部分 1、3、5、7;微任务队列 4、6 依注册顺序;宏任务 2 最后综合题四:变量提升 + 函数作用域
var name = '全局';
function show() {
console.log(name); // undefined,不是 '全局'
var name = '局部'; // 提升:函数内 var name 声明被提到顶部
console.log(name); // '局部'
}
show();
// 解析:函数内有 var name 声明,整个函数作用域内 name 都指向局部变量
// 第一次 console 时局部 name 已声明但未赋值,为 undefined(遮蔽了全局)综合题五:立即执行 + 闭包缓存
const funcs = [];
(function () {
for (let i = 0; i < 3; i++) {
funcs.push(() => i);
}
})();
console.log(funcs.map((f) => f())); // [0, 1, 2]
// 若把 let 换成 var,则输出 [3, 3, 3]
// 因为 IIFE 只创建一个函数作用域,var i 被三个闭包共享综合题六:类型转换的连环坑
console.log([] + []); // ''(两个空数组转空串拼接)
console.log([] + {}); // '[object Object]'
console.log(1 + '2' + 3); // '123'(遇字符串转拼接)
console.log(1 + 2 + '3'); // '33'(先算 1+2=3 再拼)
console.log('5' - 2); // 3(减法强制转数字)
console.log('5' * '2'); // 10(乘法转数字)
console.log(true + true); // 2(true 转 1)
console.log([1, 2] + [3]); // '1,23'(数组转 '1,2' 和 '3' 拼接)
console.log(+''); // 0(一元加转数字)
console.log(+[]); // 0
console.log(+{}); // NaN自测清单
用这份清单快速自检对核心机制的掌握程度:
| 能否解释 | 涉及机制 |
| --- | --- |
| 为什么循环里 setTimeout 输出全是最大值 | 闭包 + var 作用域 + 事件循环 |
| 为什么方法赋值给变量后 this 变了 | 隐式绑定丢失 |
| 为什么 typeof null 是 'object' | 历史遗留 bug |
| 为什么 [] == ![] 是 true | == 强制转换 |
| async 函数里 await 之后的代码何时执行 | 微任务机制 |
| 为什么闭包会导致内存泄漏 | 环境记录被引用不回收 |
| new 一个构造函数发生了什么 | new 四步 + 原型链 |
| let 和 var 在 for 循环里的区别 | 块级作用域 + 每轮绑定 |
能流畅回答以上全部问题,说明你已经真正掌握了 JavaScript 的执行机制,从"会写"进阶到了"理解"。
核心概念总结
| 概念 | 一句话本质 | 关键点 |
| --- | --- | --- |
| 执行上下文 | 代码运行的工作空间 | 创建 VO、作用域链、this |
| 调用栈 | 管理上下文的 LIFO 栈 | 后进先出,过深会溢出 |
| 变量提升 | 声明提前登记,赋值留原地 | var 提升为 undefined,let/const 有 TDZ |
| 闭包 | 函数背走出生环境的变量 | 数据私有、模块化,注意内存泄漏 |
| 原型链 | 逐级向上查找属性 | 顶端是 Object.prototype,尽头 null |
| this | 看调用方式而非定义位置 | new > 显式 > 隐式 > 默认,箭头继承外层 |
| 事件循环 | 单线程调度异步的机制 | 同步 → 清空微任务 → 一个宏任务 → 循环 |
最佳实践
掌握这些底层机制,你就从"会用 JavaScript"进阶到了"理解 JavaScript",无论是排查疑难 bug、优化性能,还是阅读框架源码,都会游刃有余。基础越扎实,走得越远。