🌟深入理解 JavaScript 中的 this:从设计缺陷到最佳实践
在 JavaScript 的世界里,this 是一个既基础又容易让人困惑的概念。它不像其他语言那样遵循固定的规则,而是由函数的调用方式决定——这一设计被不少人认为是 JavaScript 早期的一个“历史包袱”。本文将结合你提供的学习笔记和代码示例,系统梳理 this 的行为机制、常见陷阱以及如何优雅地应对。
🔍 一、自由变量与词法作用域(Lexical Scope)
在深入 this 之前,我们先明确一个关键概念:自由变量的查找依赖于词法作用域。
- 词法作用域(Lexical Scope) :指的是变量的作用域是在代码编写时就确定的,而不是运行时。
- 自由变量:在函数内部使用,但既不是参数也不是局部变量,而是来自外层作用域的变量。
let myName = "极客邦";
function foo() {
console.log(myName); // 自由变量,从外层作用域查找
}
这种查找机制是静态的,编译阶段就决定了变量的查找路径(通过作用域链)。而 this 却是个例外!
💡 关键区别:
- 变量查找 → 编译期(词法作用域)
this指向 → 运行期(调用方式决定)
⚠️ 二、JavaScript 中 this 的“不良设计”
正如笔记中所说,this 的设计确实存在争议:
“JS 做了一个不好的设计:
this由函数的调用方式决定。”
为什么说这是个问题?
早期 JavaScript 没有 class,为了模拟面向对象编程(OOP),开发者依赖函数和 this 来访问对象属性。理想情况下,方法内部的 this 应该自动指向所属对象。但现实是:
- 如果把方法赋值给一个变量再调用,
this就会丢失! - 在非严格模式下,
this默认指向全局对象(如浏览器中的window),导致意外污染。
示例分析
来看你提供的代码片段:
var bar = {
myName: "time.geekbang.com",
printName: function () {
console.log(myName); // ❌ 自由变量,查全局 → "极客邦"
console.log(bar.myName); // ✅ 显式访问对象属性
console.log(this); // 取决于调用方式!
console.log(this.myName); // 可能 undefined!
}
};
var myName = "极客邦"; // 挂载到 window.myName
var _printName = foo(); // foo 返回 bar.printName 函数引用
_printName(); // 普通函数调用!
当 _printName() 被当作普通函数调用时:
this指向window(非严格模式)this.myName实际是window.myName→ 输出"极客邦"- 而
myName作为自由变量,也从全局作用域找到"极客邦"
结果:你本想打印 "time.geekbang.com",却得到了 "极客邦"!😱
📌 这就是
this丢失的经典场景。
🧠 三、this 的五种绑定规则
JavaScript 中 this 的指向不是在函数定义时确定的,而是在函数被调用时根据调用方式动态决定的。为了系统掌握 this,我们必须理解它的五种绑定规则,并牢记它们的优先级顺序:
优先级从高到低:
1. new 绑定 → 2. 显式绑定 → 3. 隐式绑定 → 4. 默认绑定 → 5. 箭头函数(无独立 this)
下面我们逐一详解,并配以可运行的完整代码。
1️⃣ new 绑定(构造函数调用)
当函数通过 new 关键字调用时,会创建一个新对象,并将 this 绑定到这个新对象上。
function User(name) {
this.name = name;
this.greet = function() {
console.log(`Hello, I'm ${this.name}`);
};
}
const alice = new User("Alice");
alice.greet(); // 输出: Hello, I'm Alice
console.log(alice instanceof User); // true
✅ 关键点:
-
new调用时,JS 引擎会:- 创建一个空对象;
- 将该对象的原型链接到构造函数的
prototype; - 将
this指向这个新对象; - 执行构造函数体;
- 如果构造函数没有显式返回对象,则自动返回
this。
-
此时
this永远指向新创建的实例。
2️⃣ 显式绑定(call / apply / bind)
开发者可以主动指定 this 的值,使用 Function.prototype 上的方法:
const person = {
name: "Bob"
};
function introduce() {
console.log(`My name is ${this.name}`);
}
// call:传入参数列表
introduce.call(person); // 输出: My name is Bob
// apply:传入参数数组
introduce.apply(person); // 同样输出: My name is Bob
// bind:返回一个新函数,this 被永久绑定
const boundIntroduce = introduce.bind(person);
boundIntroduce(); // 输出: My name is Bob
✅ 关键点:
call和apply立即执行函数;bind不执行,而是返回一个this已绑定的新函数(常用于事件回调、setTimeout 等场景);- 显式绑定的优先级高于隐式和默认绑定。
💡 示例:修复
this丢失问题const print = bar.printName.bind(bar); print(); // 即使作为普通函数调用,this 仍指向 bar
3️⃣ 隐式绑定(作为对象方法调用)
当函数作为对象的属性(方法)被调用时,this 指向该对象。
const car = {
brand: "Tesla",
start: function() {
console.log(`${this.brand} is starting...`);
}
};
car.start(); // 输出: Tesla is starting...
⚠️ 但要注意“隐式丢失”问题:
const startCar = car.start; // 仅复制函数引用
startCar(); // ❌ this 指向全局(非严格模式)或 undefined(严格模式)
// 输出: undefined is starting... (严格模式下报错)
✅ 关键点:
- 只有直接通过对象调用(
obj.method())才算隐式绑定; - 一旦函数被赋值给变量、作为参数传递,就变成普通函数调用,触发默认绑定。
4️⃣ 默认绑定(普通函数调用)
当函数独立调用(不通过对象、不使用 new、未显式绑定),则触发默认绑定。
var globalName = "Global"; // 注意:var 会挂载到 window
function sayName() {
console.log(this.globalName);
}
sayName(); // 非严格模式 → 输出: Global(this === window)
但在严格模式下:
'use strict';
function sayNameStrict() {
console.log(this); // undefined
// console.log(this.globalName); // TypeError!
}
sayNameStrict(); // 报错:Cannot read property 'globalName' of undefined
✅ 关键点:
- 非严格模式:
this → 全局对象(浏览器中是 window) - 严格模式:
this → undefined - 这也是为什么推荐始终使用
'use strict'—— 避免意外污染全局。
5️⃣ 箭头函数(没有自己的 this)
箭头函数不绑定自己的 this,而是继承外层作用域的 this 值(词法 this)。
const team = {
members: ["Alice", "Bob"],
leader: "Carol",
report: function() {
// 普通函数:this 指向 team
this.members.forEach(function(member) {
// ❌ 普通回调函数:this 指向全局(非严格模式)
console.log(`${member} reports to ${this.leader}`); // this.leader → undefined
});
},
reportFixed: function() {
// ✅ 使用箭头函数:this 继承自 reportFixed 的 this(即 team)
this.members.forEach((member) => {
console.log(`${member} reports to ${this.leader}`); // 正确输出
});
}
};
team.report(); // 输出: Alice reports to undefined
team.reportFixed(); // 输出: Alice reports to Carol, Bob reports to Carol
✅ 关键点:
- 箭头函数的
this在定义时就确定了,无法被call/apply/bind改变; - 适合用在回调函数、定时器、数组方法等需要保留外层
this的场景; - 不要在对象方法或构造函数中使用箭头函数(会失去动态
this能力)。
🔁 优先级总结(实战验证)
function foo() {
console.log(this.name);
}
const obj1 = { name: "Obj1", fn: foo };
const obj2 = { name: "Obj2" };
// 测试:显式绑定 vs 隐式绑定
obj1.fn.call(obj2); // 输出: Obj2 → 显式绑定优先级更高!
// 测试:new 绑定最高
function Bar() {
this.name = "New Instance";
}
Bar.prototype.fn = foo;
const instance = new Bar();
instance.fn(); // 输出: New Instance(隐式绑定)
instance.fn.call(obj2); // 输出: Obj2(显式绑定)
new (instance.fn.bind(obj2))(); // 即使 bind 了,new 仍创建新对象 → 输出: undefined(因为 bind 后的函数没有设置 this.name)
📌 记住口诀:
“New 最强,Call/Apply/Bind 紧随其后,对象方法第三,普通函数垫底,箭头函数另辟蹊径。”
通过以上五种绑定规则的完整剖析,相信你已经能够自信地判断任何场景下 this 的指向了!接下来,只需在实践中多加注意调用方式,就能彻底告别 this 的困惑 😊
🛡️ 四、如何避免 this 陷阱?
✅ 使用严格模式
'use strict';
// 普通函数调用时 this 为 undefined,避免意外污染
✅ 避免将方法赋值给变量
如果必须传递方法,使用 .bind():
const safePrint = bar.printName.bind(bar);
safePrint(); // this 正确指向 bar
✅ 用箭头函数处理回调(谨慎!)
箭头函数没有自己的 this,适合用在不需要动态 this 的场景:
class Timer {
constructor() {
this.seconds = 0;
setInterval(() => {
this.seconds++; // ✅ 箭头函数继承 class 的 this
}, 1000);
}
}
⚠️ 但不要在对象方法中滥用箭头函数,否则会失去
this指向对象的能力!
✅ 优先使用 let/const 而非 var
var声明的变量会挂载到window,加剧this混乱let/const不会污染全局对象,更安全
🎯 五、回到最初的问题:如何拿到 time.geekbang.com?
你问:“如果要拿到 time.geekbang.com 怎么办?”
正确做法:
// 方案1:始终通过对象调用
bar.printName(); // this → bar → 输出正确
// 方案2:显式绑定
_printName.call(bar);
// 方案3:在函数内部避免依赖 this,直接用 bar.myName(不推荐,耦合强)
// 方案4:改写为箭头函数?不行!因为箭头函数无法通过 this 访问 bar
✨ 最佳实践:确保方法总是以
obj.method()形式调用,或使用.bind()固定上下文。
🧩 六、执行上下文与 this
每次函数调用都会创建一个执行上下文(Execution Context) ,包含:
- 变量环境(Variable Environment)
- 词法环境(Lexical Environment)
- this 绑定
而 this 的值就是在进入执行上下文时,根据调用点(call-site) 动态确定的,与函数定义位置无关。
🔍 小技巧:判断
this时,永远问自己——“这个函数是谁调用的? ”
🎉 结语
this 虽然源于早期设计的妥协,但只要掌握其动态绑定机制和五种规则,就能化繁为简。记住:
🔸 变量查找看定义(词法作用域)
🔸this指向看调用(动态绑定)
配合严格模式、bind、箭头函数等现代工具,你完全可以写出清晰、健壮的代码。随着 ES6+ 类语法的普及,this 的使用也更加规范。
希望这篇博客帮你彻底理清 this 的来龙去脉!🚀