引言
JavaScript 是一门既简单又复杂的语言。说它简单,是因为短短几行代码就能写出可运行的交互页面;说它复杂,是因为作用域(Scope)、闭包(Closure)、this 绑定和原型链(Prototype Chain) 这四个核心概念相互交织,构成了语言最深层的运行机制。
理解这四个概念,是从「会用 JavaScript」迈向「懂 JavaScript」的必经之路。无论你是准备面试、阅读源码,还是写出更健壮的生产代码,这篇文章都将为你提供透彻的讲解和完整的可运行示例。
1. 执行上下文与调用栈
1.1 什么是执行上下文
执行上下文(Execution Context)是 JavaScript 引擎在执行代码时创建的内部数据结构,它记录了代码运行所需的全部环境信息。通俗地说,执行上下文就是代码的「运行环境」。
JavaScript 中有三种执行上下文:
| 上下文类型 | 说明 |
|---|---|
| 全局执行上下文 | 代码首次运行时创建,每个程序只有一个 |
| 函数执行上下文 | 每次调用函数时创建,可能有多个 |
| eval 执行上下文 | eval 函数内部代码的上下文(不推荐使用) |
// 全局执行上下文 — 程序启动时创建
const globalVar = '我是全局变量';
function outer() {
// 调用 outer() 时创建 outer 的函数执行上下文
const outerVar = '我是 outer 的变量';
function inner() {
// 调用 inner() 时创建 inner 的函数执行上下文
const innerVar = '我是 inner 的变量';
console.log(innerVar); // '我是 inner 的变量'
console.log(outerVar); // '我是 outer 的变量' — 通过作用域链访问
console.log(globalVar); // '我是全局变量' — 通过作用域链访问
}
inner();
}
outer();1.2 创建阶段 vs 执行阶段
每个执行上下文经历两个阶段:
① 创建阶段(Creation Phase):
- 创建变量对象(Variable Object, VO) / 活动对象(Activation Object, AO)
- 建立作用域链(Scope Chain)
- 确定 this 指向
② 执行阶段(Execution Phase):
- 逐行执行代码
- 变量赋值、函数调用
console.log(a); // undefined(不是 ReferenceError!)
var a = 10;
console.log(a); // 10
// 上述代码在引擎眼中等同于此:
// 创建阶段:
// VO = { a: undefined }
// 执行阶段:
// console.log(a); → undefined
// a = 10;
// console.log(a); → 101.3 变量对象与活动对象
- 变量对象(VO):全局上下文中,VO 就是全局对象(浏览器中的
window,Node.js 中的global) - 活动对象(AO):函数上下文中,VO 被称为 AO,包含
arguments对象和所有形参
function greet(name) {
// 创建阶段的 AO:
// AO = {
// arguments: { 0: 'Alice', length: 1 },
// name: 'Alice',
// greeting: undefined // var 声明提升
// }
var greeting = 'Hello, ' + name;
return greeting;
}
greet('Alice'); // 'Hello, Alice'1.4 调用栈机制
JavaScript 是单线程语言,使用调用栈(Call Stack) 来管理函数调用。每当调用一个函数,引擎就将该函数的执行上下文压入栈顶;函数返回时,从栈顶弹出。
function multiply(a, b) {
return a * b;
}
function square(n) {
return multiply(n, n);
}
function main() {
const result = square(5);
console.log(result); // 25
}
main();
// 调用栈变化过程:
// 1. [全局上下文]
// 2. [全局上下文, main 上下文]
// 3. [全局上下文, main 上下文, square 上下文]
// 4. [全局上下文, main 上下文, square 上下文, multiply 上下文]
// 5. [全局上下文, main 上下文, square 上下文] ← multiply 返回
// 6. [全局上下文, main 上下文] ← square 返回
// 7. [全局上下文] ← main 返回1.5 栈溢出与尾调用优化
栈溢出(Stack Overflow): 当递归调用过深,调用栈超过浏览器限制时抛出 Maximum call stack size exceeded。
// 危险:无终止条件的递归 → 栈溢出
function infiniteRecursion() {
return infiniteRecursion();
}
// infiniteRecursion(); // RangeError: Maximum call stack size exceeded
// 安全:有终止条件的递归
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
console.log(factorial(5)); // 120尾调用优化(Tail Call Optimization, TCO): 如果函数的最后一步是调用另一个函数(且不依赖当前栈的变量),引擎可以复用当前栈帧,避免栈增长。ES6 规范中规定了尾调用优化,但并非所有浏览器都实现了它。
// 非尾调用:需要在乘法后保留栈帧
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // 乘法在递归返回后执行
}
// 尾调用优化版本
function factorialTail(n, acc = 1) {
if (n <= 1) return acc;
return factorialTail(n - 1, n * acc); // 最后一步仅是函数调用
}
console.log(factorialTail(5)); // 1202. 作用域(Scope)
2.1 作用域的类型
作用域决定了变量和函数的可访问范围。JavaScript 有三种主要作用域:
全局作用域
在任何函数或块之外声明的变量属于全局作用域,程序任何位置都可访问。
// 全局作用域
const globalVar = '我在任何地方都能被访问';
function test() {
console.log(globalVar); // '我在任何地方都能被访问'
}
test();
console.log(globalVar); // '我在任何地方都能被访问'函数作用域
使用 var 或在函数内声明的变量,只在函数内部可访问。
function funcScope() {
var message = '我只能在这个函数内部访问';
console.log(message); // 正常
}
funcScope();
// console.log(message); // ReferenceError: message is not defined块级作用域(ES6)
let 和 const 将变量限制在最近的 {} 块内(如 if、for、while)。
{
let blockVar = '我只能在这个块内访问';
const alsoBlock = '我也是';
var notBlocked = '我逃出了这个块';
}
// console.log(blockVar); // ReferenceError
// console.log(alsoBlock); // ReferenceError
console.log(notBlocked); // '我逃出了这个块' — var 无视块作用域
if (true) {
let x = 10;
const y = 20;
var z = 30;
}
console.log(z); // 30
// console.log(x); // ReferenceError
// console.log(y); // ReferenceError2.2 词法作用域(静态作用域)
JavaScript 采用词法作用域(Lexical Scoping),也叫静态作用域。变量的作用域在编写代码时就已确定,而非在运行时。内部函数可以访问外部函数中声明的变量。
const outerVar = '全局';
function outer() {
const outerVar = '外部';
function inner() {
const innerVar = '内部';
console.log(outerVar); // '外部' — 依据定义位置,而非调用位置
console.log(globalVar); // '全局'
}
inner();
}
outer();理解词法作用域的关键:函数的作用域在定义时决定,不在调用时决定。
const value = '全局';
function foo() {
console.log(value); // 输出什么?
}
function bar() {
const value = 'bar 内部';
foo(); // 调用 foo,但 foo 的作用域在定义时就固定了!
}
bar(); // 输出 '全局',不是 'bar 内部'2.3 作用域链
当访问一个变量时,JavaScript 引擎会从当前作用域开始查找,逐级向上直到全局作用域。这个查找路径就是作用域链(Scope Chain)。
const global = 'GLOBAL';
function outer() {
const outerVar = 'OUTER';
function middle() {
const middleVar = 'MIDDLE';
function inner() {
const innerVar = 'INNER';
// 作用域链:inner → middle → outer → 全局
console.log(innerVar); // 'INNER' — 当前作用域
console.log(middleVar); // 'MIDDLE' — 上一级作用域
console.log(outerVar); // 'OUTER' — 上两级作用域
console.log(global); // 'GLOBAL' — 全局作用域
}
inner();
}
middle();
}
outer();作用域链的遮蔽效应: 如果内层和外层有同名变量,内层变量会「遮蔽」外层变量。
let name = '全局';
function showName() {
let name = '局部';
console.log(name); // '局部' — 局部变量遮蔽了全局变量
}
showName();
console.log(name); // '全局' — 全局变量不受影响2.4 var vs let/const 全面对比
| 特性 | var |
let / const |
|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 |
| 提升 | 声明提升,初始值 undefined |
声明提升,但存在 TDZ |
| 重复声明 | 允许 | 不允许 |
| 全局声明挂载到 window | 是 | 否 |
| 能否在声明前访问 | 能(值为 undefined) |
不能(ReferenceError) |
// 重复声明
var a = 1;
var a = 2; // ✅ 允许
console.log(a); // 2
// let b = 1;
// let b = 2; // ❌ SyntaxError: Identifier 'b' has already been declared
// 全局挂载
var globalVar = 'hello';
console.log(window.globalVar); // 'hello'(浏览器环境)
const globalConst = 'world';
console.log(window.globalConst); // undefined2.5 模块作用域
ES6 模块(import / export)中,每个文件都有自己独立的模块作用域。模块顶层声明的变量不会泄露到全局。
// 📁 module-a.js
export const shared = '共享变量';
const privateVar = '仅模块内部可见'; // 不导出,外部无法访问
// 📁 module-b.js
import { shared } from './module-a.js';
console.log(shared); // '共享变量'
// console.log(privateVar); // ReferenceError: privateVar is not defined3. 变量提升(Hoisting)
3.1 var 声明提升
var 声明的变量会被提升到当前函数(或全局)作用域的顶部,初始值为 undefined。
console.log(age); // undefined(不是 ReferenceError。)
var age = 25;
console.log(age); // 25
// 引擎实际执行的等价代码:
// var age;
// console.log(age); // undefined
// age = 25;
// console.log(age); // 253.2 function 声明提升
函数声明会被完整提升——函数名和函数体都会提升到作用域顶部。这意味着你可以在函数定义之前调用它。
sayHello(); // 'Hello!' — 函数声明被完整提升
function sayHello() {
console.log('Hello!');
}对比:函数声明 vs 函数表达式
// 函数声明 — 完整提升 ✅
foo(); // '我是函数声明'
function foo() {
console.log('我是函数声明');
}
// 函数表达式 var — 只有变量提升,函数体不提升 ❌
// bar(); // TypeError: bar is not a function
var bar = function() {
console.log('我是函数表达式');
};
bar(); // '我是函数表达式' ✅ (赋值之后)
// 函数表达式 let/const — TDZ 阻止访问 ❌
// baz(); // ReferenceError: Cannot access 'baz' before initialization
const baz = function() {
console.log('我是 const 函数表达式');
};
baz(); // 正常 ✅3.3 let/const 与暂时性死区(TDZ)
let 和 const 也会被提升,但在声明之前访问它们会抛出 ReferenceError。从作用域开始到声明之间的区域称为暂时性死区(Temporal Dead Zone, TDZ)。
{
// TDZ 开始 — x 已存在于块作用域,但不可访问
// console.log(x); // ReferenceError: Cannot access 'x' before initialization
const y = 10; // TDZ 结束 — 从此处开始可以访问
console.log(y); // 10
let x = 5;
// 还有一点:typeof 在 TDZ 中也会抛出错误
// console.log(typeof x); // 正常,此时 x 已初始化
}
// 有趣的现象:typeof 对未声明的变量是安全的,但对 TDZ 中的变量不安全
// console.log(typeof notDeclared); // 'undefined' — 安全
// console.log(typeof tdzVar); // ReferenceError — ❌
// let tdzVar = 'hi';经典面试题:
var a = 1;
function test() {
console.log(a); // undefined(不是 1,也不是 ReferenceError)
var a = 2;
console.log(a); // 2
}
test();
// 等价于:
// function test() {
// var a; // 提升到函数顶部,遮蔽外部 a
// console.log(a); // undefined
// a = 2;
// console.log(a); // 2
// }4. 闭包(Closure)
4.1 闭包的定义与形成条件
闭包(Closure) 是指一个函数能够「记住」并访问其词法作用域中的变量,即使该函数在其词法作用域之外执行。
形成闭包需要满足三个条件:
- 存在函数嵌套(内层函数引用外层函数的变量)
- 内层函数引用了外层函数的变量
- 内层函数在外层函数执行完毕之后仍可被调用
function createCounter() {
let count = 0; // 外层函数的变量
// 内层函数形成了一个闭包
return function() {
count++; // 引用外层变量
return count;
};
}
const counter = createCounter();
// 此时 createCounter 已执行完毕,但 count 仍然存在
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 34.2 常见闭包场景
场景一:计数器
function makeCounter(initial = 0) {
let count = initial;
return {
increment: () => ++count,
decrement: () => --count,
reset: () => { count = initial; return count; },
getValue: () => count
};
}
const myCounter = makeCounter(10);
console.log(myCounter.increment()); // 11
console.log(myCounter.increment()); // 12
console.log(myCounter.decrement()); // 11
console.log(myCounter.reset()); // 10场景二:私有变量
JavaScript 没有真正的「私有字段」(ES2022 之前),闭包是实现私有变量的经典方式。
function createPerson(name, age) {
// name 和 age 是「私有」变量
let _name = name;
let _age = age;
return {
getName: () => _name,
setName: (newName) => { _name = newName; },
getAge: () => _age,
setAge: (newAge) => {
if (newAge > 0 && newAge < 150) {
_age = newAge;
} else {
console.error('无效年龄');
}
}
};
}
const person = createPerson('张三', 30);
console.log(person.getName()); // '张三'
person.setAge(31);
console.log(person.getAge()); // 31
// 无法直接访问 _name 或 _age
// console.log(person._name); // undefined场景三:柯里化(Currying)
柯里化将多参数函数转换为一系列单参数函数,闭包是其实现基础。
// 普通版本
function add(a, b, c) {
return a + b + c;
}
// 柯里化版本
function curryAdd(a) {
return function(b) {
return function(c) {
return a + b + c; // a 和 b 通过闭包保留
};
};
}
console.log(add(1, 2, 3)); // 6
console.log(curryAdd(1)(2)(3)); // 6
// 实用例子:预配置函数
function multiply(x) {
return function(y) {
return x * y;
};
}
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15场景四:事件监听中的闭包
function setupButtons() {
const buttons = document.querySelectorAll('button');
// 错误方式:点击所有按钮都打印 3
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log('Button ' + i + ' clicked'); // 总是 3
});
}
// 正确方式(IIFE 闭包)
for (var i = 0; i < buttons.length; i++) {
(function(index) {
buttons[index].addEventListener('click', function() {
console.log('Button ' + index + ' clicked');
});
})(i);
}
// 更佳方式:使用 let
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log('Button ' + i + ' clicked');
});
}
}4.3 经典问题:for 循环 + var 的闭包陷阱
这是面试中最经典的闭包问题:
// 问题:打印出来全是 3
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 3, 3, 3
}, 100);
}原因: var i 是函数作用域,循环结束后 i 已经变为 3。所有 setTimeout 回调共享同一个 i 变量。
四种解决方案:
// 方案 1:IIFE(立即执行函数)— 为每次循环创建独立作用域
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 0, 1, 2
}, 100);
})(i);
}
// 方案 2:let — 为每次迭代创建独立的块级作用域
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 0, 1, 2
}, 100);
}
// 方案 3:setTimeout 第三个参数(将参数传递给回调函数)
for (var i = 0; i < 3; i++) {
setTimeout(function(j) {
console.log(j); // 0, 1, 2
}, 100, i);
}
// 方案 4:用 bind 创建新函数
for (var i = 0; i < 3; i++) {
setTimeout(console.log.bind(console, i), 100); // 0, 1, 2
}4.4 闭包内存管理
闭包会导致外部函数的变量不会被垃圾回收(因为内层函数仍然持有引用),如果不加控制可能造成内存泄漏。
function createLargeArray() {
const large = new Array(1000000).fill('数据');
return {
getData: function() {
return large; // large 数组被闭包引用,无法被回收
},
getLength: function() {
return large.length; // 同样持有引用
}
};
}
const obj = createLargeArray();
// large 数组会一直存在于内存中
console.log(obj.getLength()); // 1000000
// 当不再需要时,释放引用
// obj = null; // 解除对返回对象的引用 → large 可被回收最佳实践:
- 闭包只引用必要的变量,不要无意识地捕获整个作用域
- 闭包不再使用时,主动解除引用(设为
null) - 在现代浏览器中,引擎已经优化了闭包的内存管理,但大型数据仍需注意
4.5 实际应用:防抖与节流
防抖(Debounce)
防抖将多次高频触发合并为最后一次执行。闭包用于保存 timer 变量。
function debounce(fn, delay = 300) {
let timer = null; // 闭包:保存定时器 ID
return function(...args) {
const context = this;
// 每次调用都清除上一次的定时器
if (timer) clearTimeout(timer);
// 重新设置定时器
timer = setTimeout(() => {
fn.apply(context, args);
timer = null;
}, delay);
};
}
// 使用示例
function search(query) {
console.log('搜索:', query);
}
const debouncedSearch = debounce(search, 500);
debouncedSearch('J'); // 定时器重置
debouncedSearch('Ja'); // 定时器重置
debouncedSearch('Jav'); // 定时器重置
debouncedSearch('Java'); // 定时器重置
// 只有最后一次调用在 500ms 后真正执行节流(Throttle)
节流确保函数在指定时间间隔内最多执行一次。闭包保存 lastTime 或 timer。
function throttle(fn, interval = 300) {
let lastTime = 0; // 闭包:保存上次执行时间
return function(...args) {
const context = this;
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(context, args);
}
};
}
// 使用示例
function handleScroll() {
console.log('滚动时间:', Date.now());
}
const throttledScroll = throttle(handleScroll, 1000);
// 触发滚动时,每秒最多执行一次
window.addEventListener('scroll', throttledScroll);模块模式(Module Pattern)
在 ES6 模块出现之前,闭包是实现模块化的标准方式。
const Calculator = (function() {
// 私有变量
let lastResult = 0;
// 私有函数
function validate(num) {
if (typeof num !== 'number') {
throw new Error('参数必须是数字');
}
}
// 公有 API
return {
add(a, b) {
validate(a);
validate(b);
lastResult = a + b;
return lastResult;
},
subtract(a, b) {
validate(a);
validate(b);
lastResult = a - b;
return lastResult;
},
getLastResult() {
return lastResult;
}
};
})();
console.log(Calculator.add(5, 3)); // 8
console.log(Calculator.subtract(10, 4)); // 6
console.log(Calculator.getLastResult()); // 6
// console.log(Calculator.lastResult); // undefined — 私有变量5. this 关键字
this 是 JavaScript 中最令人困惑的概念之一。它的指向不是在定义时确定的,而是在函数调用时根据调用方式动态确定(箭头函数除外)。
5.1 默认绑定
非严格模式: this 指向全局对象(浏览器中的 window,Node.js 中的 global)。
严格模式: this 为 undefined。
function showThis() {
console.log(this);
}
showThis(); // 浏览器: window | Node: global
// 严格模式
function showThisStrict() {
'use strict';
console.log(this);
}
showThisStrict(); // undefined5.2 隐式绑定
当函数作为对象的方法被调用时,this 指向调用该方法的对象。
const user = {
name: '张三',
greet() {
console.log(`你好,我是 ${this.name}`);
}
};
user.greet(); // '你好,我是 张三' — this → user
// 但注意:方法赋给变量后再调用,隐式绑定会丢失
const greetFn = user.greet;
greetFn(); // '你好,我是 undefined' — this → window/undefined链式调用时的 this:
const obj = {
a: 1,
b: {
a: 2,
getA() {
console.log(this.a);
}
}
};
obj.b.getA(); // 2 — this → obj.b(点号左侧的最后一个对象)5.3 显式绑定:call、apply、bind
我们可以用 call、apply 和 bind 手动指定函数的 this 指向。
function introduce(greeting, punctuation) {
console.log(`${greeting},我是 ${this.name}${punctuation}`);
}
const person1 = { name: 'Alice' };
const person2 = { name: 'Bob' };
// call:参数列表形式
introduce.call(person1, '你好', '!'); // '你好,我是 Alice!'
// apply:参数数组形式
introduce.apply(person2, ['Hello', '!']); // 'Hello,我是 Bob!'
// bind:返回新函数,this 永久绑定
const boundIntroduce = introduce.bind(person1, '嗨');
boundIntroduce('~'); // '嗨,我是 Alice~'bind 的「永久绑定」特性:
const obj1 = { x: 1 };
const obj2 = { x: 2 };
function getX() {
console.log(this.x);
}
const bound = getX.bind(obj1);
bound(); // 1
// 即使再次 bind 或 call,也无法改变 this
bound.call(obj2); // 1 — 还是 1!bind 的 this 不可覆盖
bound.bind(obj2)(); // 1
// 只有 new 可以覆盖 bind 的 this(构造函数场景)call/apply 的实际应用:借用方法
// 借用数组方法操作类数组对象
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
const realArray = Array.prototype.slice.call(arrayLike);
console.log(realArray); // ['a', 'b', 'c']
// 借用 Object.prototype.toString 判断类型
function getType(value) {
return Object.prototype.toString.call(value);
}
console.log(getType([])); // '[object Array]'
console.log(getType({})); // '[object Object]'
console.log(getType('')); // '[object String]'
console.log(getType(null)); // '[object Null]'5.4 new 绑定
使用 new 关键字调用构造函数时,this 指向新创建的对象实例。
function Person(name, age) {
// 创建新对象 const this = {};(引擎自动完成)
this.name = name;
this.age = age;
// return this;(引擎自动完成)
}
const alice = new Person('Alice', 25);
console.log(alice.name); // 'Alice'
console.log(alice.age); // 25
// 如果没有 new,this 会指向全局对象(非严格模式)或 undefined(严格模式)
const bob = Person('Bob', 30); // 忘记 new
console.log(bob); // undefined — 函数没有 returnnew 操作符的内部机制:
function myNew(Constructor, ...args) {
// 1. 创建一个新对象,原型指向构造函数的 prototype
const obj = Object.create(Constructor.prototype);
// 2. 执行构造函数,this 指向新对象
const result = Constructor.apply(obj, args);
// 3. 如果构造函数返回了对象,返回该对象;否则返回新创建的对象
return (typeof result === 'object' && result !== null) ? result : obj;
}
function Car(brand) {
this.brand = brand;
}
const myCar = myNew(Car, 'Tesla');
console.log(myCar.brand); // 'Tesla'
console.log(myCar instanceof Car); // true5.5 箭头函数:词法 this
箭头函数没有自己的 this,它的 this 继承自定义时的外层作用域(词法作用域)。箭头函数的 this 是静态的,在定义时就已确定,且无法通过 call、apply、bind 修改。
const obj = {
name: '箭头函数测试',
regularFunc: function() {
console.log('普通函数:', this.name);
const arrow = () => {
console.log('箭头函数:', this.name);
};
arrow();
},
arrowFunc: () => {
console.log('对象方法箭头函数:', this.name);
}
};
obj.regularFunc();
// 普通函数: 箭头函数测试
// 箭头函数: 箭头函数测试 — 继承了 regularFunc 的 this
obj.arrowFunc();
// 对象方法箭头函数: undefined — this 指向外层(全局),不是 obj箭头函数的 this 不可被修改:
const obj1 = { name: 'obj1' };
const obj2 = { name: 'obj2' };
const arrow = () => {
console.log(this.name);
};
// 定义时 this 指向全局(name 为 undefined)
arrow.call(obj1); // undefined
arrow.apply(obj2); // undefined
arrow.bind(obj1)(); // undefined
// 这与普通函数形成鲜明对比
function normal() {
console.log(this.name);
}
normal.call(obj1); // 'obj1'箭头函数的典型应用场景:
// 1. 定时器和事件回调中保持 this
function Timer() {
this.seconds = 0;
// 普通函数:this 指向 window/undefined
// setInterval(function() {
// this.seconds++; // ❌
// }, 1000);
// 箭头函数:this 继承自 Timer 实例
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
const timer = new Timer();
// 2. 回调函数中避免 that/self 临时变量
function Counter() {
const self = this; // 老方法
self.count = 0;
return {
increment() {
// 老方式
// setInterval(function() {
// self.count++;
// }, 1000);
// 箭头函数方式
setInterval(() => {
this.count++; // this → Counter 实例
}, 1000);
}
};
}5.6 this 绑定优先级
当多种绑定规则同时适用时,优先级从高到低为:
new 绑定 > 显式绑定(bind/call/apply) > 隐式绑定(对象方法) > 默认绑定(全局/undefined)
// 验证优先级
function foo() {
console.log(this.name);
}
const obj1 = { name: 'obj1' };
const obj2 = { name: 'obj2' };
// 隐式 vs 显式:显式更高
foo.bind(obj1).call(obj2); // 'obj1' — bind 优先级高于 call
// 标准验证
const boundFoo = foo.bind(obj1);
boundFoo(); // 'obj1' — 显式绑定
// new vs bind:new 更高
function Bar(name) {
this.name = name;
}
const BoundBar = Bar.bind({ name: 'bind 对象' });
const instance = new BoundBar('实例对象');
console.log(instance.name); // '实例对象' — new 覆盖了 bind 的 this
// 箭头函数例外:箭头函数优先级无意义(它没有自己的 this)5.7 绑定丢失的常见场景
const user = {
name: '张三',
greet() {
console.log(`你好,我是 ${this.name}`);
}
};
// 场景 1:方法赋值给变量
const fn = user.greet;
fn(); // '你好,我是 undefined' — this 丢失
// 场景 2:作为回调传入
setTimeout(user.greet, 100); // '你好,我是 undefined'
// 场景 3:作为事件处理器
// element.onclick = user.greet; // 点击时 this → element
// 解决方式:
// 1. 用 bind 绑定
const boundGreet = user.greet.bind(user);
boundGreet(); // '你好,我是 张三'
setTimeout(user.greet.bind(user), 100); // '你好,我是 张三'
// 2. 用箭头函数包裹
setTimeout(() => user.greet(), 100); // '你好,我是 张三'6. 原型与原型链
6.1 proto 与 prototype 的关系
这是 JavaScript 中最容易混淆的一对概念:
| 属性 | 属于谁 | 指向谁 |
|---|---|---|
prototype |
函数(构造函数) | 该函数的「原型对象」 |
__proto__ |
所有对象(包括函数) | 该对象的「构造函数的 prototype」 |
// 构造函数
function Animal(name) {
this.name = name;
}
// 往构造函数的 prototype 上添加方法
Animal.prototype.sayName = function() {
console.log(`我是 ${this.name}`);
};
const dog = new Animal('旺财');
// 关系图:
// dog.__proto__ === Animal.prototype // true
// Animal.prototype.constructor === Animal // true
// Animal.__proto__ === Function.prototype // true(函数是 Function 的实例)
console.log(dog.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.constructor === Animal); // true
console.log(Animal.__proto__ === Function.prototype); // true图解原型关系:
Animal (构造函数)
├── Animal.prototype ──────────→ { constructor: Animal, sayName: fn, __proto__: Object.prototype }
└── Animal.__proto__ ──────────→ Function.prototype
dog (实例)
└── dog.__proto__ ─────────────→ Animal.prototype
└── Animal.prototype.__proto__ ──→ Object.prototype
└── Object.prototype.__proto__ ──→ null6.2 原型链查找机制
当访问对象的属性或方法时,JavaScript 引擎按以下顺序查找:
- 对象自身(自有属性)
对象.__proto__(构造函数的prototype)对象.__proto__.__proto__(上一级原型)- ... 直到
Object.prototype.__proto__为null(查找结束)
const grandParent = { inherited: '来自祖父' };
const parent = Object.create(grandParent);
parent.own = '来自父';
const child = Object.create(parent);
child.own = '来自子';
console.log(child.own); // '来自子' — 自有属性
console.log(child.parentOwn); // undefined — 不存在
console.log(child.inherited); // '来自祖父' — 沿着原型链找到
// 原型链可视化
// child → parent → grandParent → Object.prototype → null完整原型链示例:
const arr = [1, 2, 3];
// arr 的原型链
console.log(arr.__proto__ === Array.prototype); // true
console.log(Array.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
// 因此 arr 可以访问:
arr.push(4); // Array.prototype 的方法
arr.toString(); // Object.prototype 的方法(被 Array 重写)
arr.hasOwnProperty(0); // Object.prototype 的方法6.3 instanceof 原理
instanceof 运算符检查构造函数的 prototype 属性是否出现在实例的原型链上。
function MyClass() {}
const obj = new MyClass();
console.log(obj instanceof MyClass); // true
console.log(obj instanceof Object); // true
console.log(obj instanceof Array); // false
// 手动实现 instanceof
function myInstanceof(instance, constructor) {
let proto = Object.getPrototypeOf(instance);
const prototype = constructor.prototype;
while (proto !== null) {
if (proto === prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
console.log(myInstanceof(obj, MyClass)); // true
console.log(myInstanceof(obj, Object)); // true
// 一个有趣的例子
const str = 'hello';
console.log(str instanceof String); // false — 原始类型不是对象
const strObj = new String('hello');
console.log(strObj instanceof String); // true6.4 hasOwnProperty vs in 操作符
| 操作 | 检查范围 |
|---|---|
hasOwnProperty |
仅检查对象自身的属性 |
in 操作符 |
检查对象自身 + 原型链上的属性 |
function Parent() {
this.parentProp = '父属性';
}
Parent.prototype.sharedMethod = function() {
console.log('共享方法');
};
function Child() {
Parent.call(this);
this.childProp = '子属性';
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
const child = new Child();
console.log(child.hasOwnProperty('childProp')); // true — 自有属性
console.log(child.hasOwnProperty('parentProp')); // true — Parent.call 添加的,也是自有
console.log(child.hasOwnProperty('sharedMethod')); // false — 原型上的方法
console.log('childProp' in child); // true
console.log('parentProp' in child); // true
console.log('sharedMethod' in child); // true — in 操作符会沿原型链查找
// 安全的 hasOwnProperty 调用(防止对象重写该方法)
const obj = { hasOwnProperty: null };
// obj.hasOwnProperty('xxx'); // TypeError
console.log(Object.prototype.hasOwnProperty.call(obj, 'xxx')); // false ✅6.5 原型继承的实现
原型继承有多种实现方式,以下是几种常见模式:
原型链继承
function Parent(name) {
this.name = name;
this.hobbies = ['读书', '跑步'];
}
Parent.prototype.sayHello = function() {
console.log(`你好,我是 ${this.name}`);
};
function Child(name, age) {
this.age = age;
}
// 子类原型指向父类实例
Child.prototype = new Parent();
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(`我 ${this.age} 岁`);
};
const child1 = new Child('小明', 10);
const child2 = new Child('小红', 12);
// ⚠️ 问题:引用类型属性被所有实例共享
child1.hobbies.push('画画');
console.log(child2.hobbies); // ['读书', '跑步', '画画'] — 被意外修改了!构造函数继承(经典继承)
function Parent(name) {
this.name = name;
this.hobbies = ['读书', '跑步'];
}
function Child(name, age) {
// 在子类构造函数中调用父类构造函数
Parent.call(this, name);
this.age = age;
}
const child1 = new Child('小明', 10);
const child2 = new Child('小红', 12);
child1.hobbies.push('画画');
console.log(child1.hobbies); // ['读书', '跑步', '画画']
console.log(child2.hobbies); // ['读书', '跑步'] — 互不影响 ✅
// ⚠️ 问题:无法继承父类原型上的方法
// child1.sayHello(); // TypeError: child1.sayHello is not a function组合继承(最常用)
function Parent(name) {
this.name = name;
this.hobbies = ['读书', '跑步'];
}
Parent.prototype.sayHello = function() {
console.log(`你好,我是 ${this.name}`);
};
function Child(name, age) {
Parent.call(this, name); // 第二次调用 Parent(实例属性)
this.age = age;
}
Child.prototype = new Parent(); // 第一次调用 Parent(原型方法)
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(`我 ${this.age} 岁`);
};
const child = new Child('小明', 10);
child.sayHello(); // '你好,我是 小明' ✅
child.hobbies.push('画画');
// ⚠️ 小缺点:Parent 构造函数被调用了两次寄生组合继承(最优方案)
function Parent(name) {
this.name = name;
this.hobbies = ['读书', '跑步'];
}
Parent.prototype.sayHello = function() {
console.log(`你好,我是 ${this.name}`);
};
function Child(name, age) {
Parent.call(this, name); // 只调用一次 Parent
this.age = age;
}
// 关键:使用 Object.create 避免调用父类构造函数
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(`我 ${this.age} 岁`);
};
const child = new Child('小明', 10);
console.log(child instanceof Child); // true
console.log(child instanceof Parent); // true
child.sayHello(); // '你好,我是 小明'6.6 ES6 class 语法糖
ES6 的 class 本质上是原型继承的语法糖,底层机制完全相同。
class Parent {
constructor(name) {
this.name = name;
this.hobbies = ['读书', '跑步'];
}
// 实例方法 → 等同于 Parent.prototype.sayHello
sayHello() {
console.log(`你好,我是 ${this.name}`);
}
// 静态方法 → 等同于 Parent.createAnonymous
static createAnonymous() {
return new Parent('匿名');
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类构造函数
this.age = age;
}
sayAge() {
console.log(`我 ${this.age} 岁`);
}
// 重写父类方法
sayHello() {
super.sayHello(); // 调用父类方法
console.log(`我今年 ${this.age} 岁了`);
}
}
const child = new Child('小明', 10);
// 验证底层仍然是原型链
console.log(child.__proto__ === Child.prototype); // true
console.log(Child.prototype.__proto__ === Parent.prototype); // true
console.log(Parent.prototype.__proto__ === Object.prototype); // true
child.sayHello();
// '你好,我是 小明'
// '我今年 10 岁了'
const anonymous = Parent.createAnonymous();
console.log(anonymous.name); // '匿名'class 与构造函数的对比:
// class 写法
class Rectangle {
constructor(w, h) {
this.w = w;
this.h = h;
}
area() {
return this.w * this.h;
}
}
// 等价的构造函数写法
function RectangleConstructor(w, h) {
this.w = w;
this.h = h;
}
RectangleConstructor.prototype.area = function() {
return this.w * this.h;
};
// 用法完全一致
const r1 = new Rectangle(3, 4);
const r2 = new RectangleConstructor(3, 4);
console.log(r1.area()); // 12
console.log(r2.area()); // 12class 的独有特性(语法糖之外的增强):
class Example {
// 1. 类体中的代码自动在严格模式下运行
// 2. 类声明不会被提升
// 3. 必须用 new 调用,否则报错
constructor() {}
}
// Example(); // TypeError: Class constructor Example cannot be invoked without 'new'
// 4. 类方法不可枚举
console.log(Object.keys(Example.prototype)); // [](普通构造函数的方法是可枚举的)总结:四大核心概念的关系
这四个概念并非孤立存在,它们交织形成了 JavaScript 的运行时骨架:
- 执行上下文 是代码的运行环境,作用域决定了变量在该环境中如何组织与访问
- 作用域链 让闭包成为可能——函数可以「记住」创建时的作用域,即使在该作用域之外执行
- this 绑定 提供了动态的上下文切换,而箭头函数用词法
this弥合了闭包与this之间的鸿沟 - 原型链 是 JavaScript 的继承机制,所有对象通过
__proto__连接成链,实现了属性查找和方法共享
一句话总结:
执行上下文管理运行环境,作用域与闭包控制变量的可访问性和生命周期,this 决定函数执行时的上下文对象,原型链是对象间的继承机制。四者共同构成了 JavaScript 语言的核心运行模型。
掌握了这四个概念,你就能真正理解 JavaScript 代码「为什么这样运行」,而不仅仅是「这样能运行」。
本文发布于 2024-08-31,更新于 2024-08-31。