引子:一个看似简单的问题
var bar = {
myName: "time.geekbang.com",
printName: function() {
console.log(this.myName);
}
}
当你写下:
bar.printName(); // 输出 "time.geekbang.com"
一切如你所愿。但如果你这样写:
let _printName = bar.printName;
_printName(); // 输出什么?
答案是:undefined(或全局变量的值)!
为什么?明明是同一个函数,只是换了个方式调用,结果却天差地别?
这正是 JavaScript 中 this 最令人费解的地方——它不是由函数定义的位置决定的,而是由函数被谁调用、以何种方式调用决定的。
这种设计,既体现了 JavaScript 的灵活性,也埋下了无数初学者的“坑”。
一、自由变量 vs this:两个世界的规则
要真正理解 this,必须先厘清 JavaScript 中两种完全不同的变量查找机制。
自由变量:词法作用域说了算
JavaScript 是一门词法作用域(Lexical Scope)语言。这意味着:
- 函数内部使用的变量,如果既不是参数也不是局部变量,就会沿着声明时的作用域链向上查找。
- 这个过程发生在编译阶段(更准确地说,是在函数创建时确定),是静态的、确定的。
比如:
let myName = '极客邦';
function foo() {
let myName = '极客时间';
return function inner() {
console.log(myName); // 输出 '极客时间'
};
}
foo()(); // '极客时间'
这里 myName 是自由变量(free variable)——它在 inner 函数中使用,但既不是参数也不是局部变量。它的值由函数在哪里写(即词法位置)决定,而不是在哪里执行。
这就是所谓的“闭包”:函数记住了它被创建时的环境。
this:动态绑定,运行时才揭晓
而 this 完全不同!
this不属于词法作用域。- 它的值完全取决于函数如何被调用,是在执行阶段动态确定的。
- 换句话说:谁调用了这个函数,
this就指向谁。
这正是 JavaScript 设计中最“反直觉”但也最灵活的地方。
📌 关键区别:
- 自由变量 → 看“出生地”(词法位置)
this→ 看“工作单位”(调用方式)
二、this 的五种常见命运
JavaScript 中 this 的指向并非随机,而是有明确规则。以下是五种最常见的场景。
1. 作为对象方法调用 → this 指向该对象
bar.printName(); // this === bar
这是最符合“面向对象直觉”的情况。当你通过对象访问一个函数并调用它,JavaScript 会自动将 this 绑定到该对象。
✅ 设计初衷:让函数能访问所属对象的属性和方法。
2. 作为普通函数调用 → 非严格模式下 this 指向 window
let _printName = bar.printName;
_printName(); // this === window(浏览器中)
⚠️ 问题来了:window 上并没有 myName 属性(尤其是用 let 声明时),所以 this.myName 是 undefined。
历史包袱:
JavaScript 诞生于1995年,当时没有类(class),也没有模块系统。Brendan Eich 在设计时认为:“既然函数可以作为方法使用,那它总得有个this吧?”于是他“偷懒”地让所有未绑定的this默认指向全局对象(浏览器中是window,Node.js 中是global)。
这一设计导致了严重的全局污染问题——var声明的变量会自动挂载到window上,使得this在普通函数调用时可能意外访问到全局变量。
3. 严格模式下 → this 为 undefined
'use strict';
_printName(); // TypeError: Cannot read property 'myName' of undefined
ES5 引入了严格模式(strict mode),其中一项重要改进就是:在普通函数调用中,this 不再默认指向全局对象,而是 undefined。
✅ 这是更安全的设计:不让错误静默发生,直接报错,迫使开发者显式处理 this 的绑定问题。
4. 使用 call / apply / bind → 手动指定 this
_printName.call(bar); // 输出 "time.geekbang.com"
_printName.apply(bar);
const bound = _printName.bind(bar);
bound();
这是 JavaScript 提供的“救赎机制”——你可以强行告诉函数:“你属于谁”。
call和apply立即执行函数,并传入this;bind返回一个新函数,永久绑定this。
这在事件处理、回调函数、高阶函数中非常常用。
5. 构造函数中 → this 指向新创建的实例
function Person(name) {
this.name = name;
}
const p = new Person('Alice'); // this === p
当使用 new 调用函数时,JavaScript 会:
- 创建一个新对象;
- 将
this绑定到该对象; - 执行函数体;
- 返回该对象(除非显式返回其他对象)。
这也是早期实现“类”的方式之一。
💡 注意:如果构造函数中忘记写
new,this会指向全局对象(非严格模式下),造成灾难性后果!
三、var vs let:为何影响 this.myName?
你可能注意到:
var myName = '全局'; // window.myName = '全局'
let myName = '全局'; // window.myName === undefined
var声明的变量会挂载到全局对象(如window)上;let/const声明的变量存在于词法环境中,不会污染全局对象。
所以当 this 指向 window 时:
- 用
var:this.myName能拿到值; - 用
let:this.myName是undefined。
这进一步说明:this 和变量作用域是两套独立系统。
技术细节:
var变量在全局作用域中会被提升并绑定到全局对象;let/const存在于“全局词法环境”中,但不成为全局对象的属性。
因此,window.myName对let声明的变量始终是undefined。
四、执行上下文视角:this 从何而来?
每次函数调用,JavaScript 引擎都会创建一个执行上下文(Execution Context),包含以下核心组件:
| 组件 | 作用 |
|---|---|
| 词法环境(Lexical Environment) | 存放 let/const/函数参数等,支持块级作用域 |
| 变量环境(Variable Environment) | 存放 var 声明(用于变量提升) |
| This 绑定(This Binding) | 关键! 根据调用方式决定 this 的值 |
| Outer 引用 | 构建作用域链,用于自由变量查找 |
重点:
this绑定与词法环境完全无关!它不参与自由变量查找,也不受函数定义位置影响。它只关心:此刻是谁在调用我?
这种分离设计,使得 JavaScript 既能支持函数式编程(依赖词法作用域),又能支持面向对象编程(依赖 this 动态绑定)。
五、箭头函数:this 的“叛逆者”
ES6 引入了箭头函数(Arrow Function),它有一个颠覆性的特性:
箭头函数没有自己的
this,它继承自外层词法作用域。
var bar = {
myName: 'time.geekbang.com',
printName: () => {
console.log(this.myName); // this 来自外层(通常是 window 或 undefined)
}
};
在这个例子中,printName 是箭头函数,它的 this 不是指向 bar,而是指向定义时的外层作用域(比如全局)。
✅ 适用场景:
- 回调函数(如
setTimeout、数组方法); - 避免
this丢失。
❌ 不适用场景:
- 对象方法(因为无法访问对象自身);
- 构造函数(箭头函数不能用
new调用)。
💡 记住:箭头函数的
this是“静态”的,由词法作用域决定,而非调用方式。
六、如何避免 this 的陷阱?实战建议
1. 显式绑定 this
const print = bar.printName.bind(bar);
print(); // 安全!
在传递方法引用时,务必使用 bind 固定 this。
2. 使用严格模式
'use strict';
让未绑定的 this 直接报错,而不是静默失败。
3. 优先使用 class(现代语法)
class Bar {
constructor() {
this.myName = 'time.geekbang.com';
}
printName() {
console.log(this.myName);
}
}
虽然底层仍是函数,但语义更清晰,减少误用。
4. 谨慎使用箭头函数作为方法
不要为了“避免 this 问题”而滥用箭头函数做对象方法,否则会失去访问对象自身的能力。
5. 理解调用栈
调试时,问自己:“这个函数是怎么被调用的?”
obj.method()→this = objmethod()→this = window(或undefined)method.call(ctx)→this = ctx
七、结语:this 是 JavaScript 的“双刃剑”
this 的设计,反映了 JavaScript 作为一门“脚本语言”的实用主义哲学:
- 灵活性:同一段代码可以在不同上下文中复用;
- 动态性:无需提前声明类型或绑定关系;
- 代价:不确定性,容易出错。
理解 this 的关键在于记住一句话:
“
this不是你写的,而是别人怎么叫你的。”
就像一个人,在公司是“张经理”,在家是“爸爸”,在朋友面前是“老张”——身份随场景变化。this 也是如此。
掌握它,你就能驾驭 JavaScript 的动态之美;忽视它,你将陷入无尽的 undefined 之中。
附:代码行为验证
// 全局
let myName = '极客邦';
var bar = {
myName: "time.geekbang.com",
printName: function() {
console.log(myName); // 自由变量 → '极客邦'(因为函数在全局定义)
console.log(bar.myName); // 显式访问 → "time.geekbang.com"
console.log(this); // 调用方式决定
console.log(this.myName); // 若 this=window,且 myName 用 let 声明 → undefined
}
}
function foo() {
let myName = '极客时间';
return bar.printName; // 返回函数引用,不改变其 this 绑定
}
var _printName = foo();
_printName(); // this → window → this.myName = undefined(let 不挂载到 window)
bar.printName(); // this → bar → this.myName = "time.geekbang.com"
完美印证了 this 的动态绑定本质,以及 var/let 对全局对象的影响。
愿你在 JavaScript 的世界里,不再被
this迷失方向。