JavaScript 中 this 的设计与指向规则
JavaScript 中的 this 是语言中最具争议且易引发困惑的核心机制之一。它不同于其他编程语言的静态绑定,而是由函数调用方式动态决定。这种设计源于 JavaScript 的历史演进:早期版本旨在支持面向对象编程(OOP),却缺乏内置类语法,因此引入 this 来模拟对象方法的上下文。然而,这种灵活性也带来了复杂性和潜在陷阱。本文将结合作用域、执行上下文及实际代码示例,系统剖析 this 的工作原理,帮助开发者避开常见误区,并在现代 JavaScript 中高效应用。
基础概念:自由变量查找与作用域链
在探讨 this 之前,先厘清变量查找机制,这有助于突出 this 的独特性。
编译阶段与作用域链
JavaScript 虽为解释型语言,却具备编译阶段。在此阶段,引擎进行词法分析并确立作用域。作用域定义变量的可见性和访问边界,主要包括全局作用域、函数作用域及 ES6 引入的块级作用域(let/const)。
- Lexical Scope(词法作用域):变量查找基于代码书写位置(静态确定),而非运行时动态调整。这意味着作用域在编译时即固定。
- 作用域链:函数内部引用非局部变量或参数时,该变量称为自由变量。引擎沿作用域链向上搜索:从当前函数作用域起始,逐层向外,直至全局作用域。
示例代码:
var bar = {
myName: "time.geekbang.com",
printName: function() {
console.log(myName); // 自由变量 myName,沿作用域链向上查找
console.log(bar.myName);
console.log(this.myName);
}
};
function foo() {
let myName = '极客时间';
return bar.printName;
}
let myName = '极客邦';
let _printName = foo();
_printName(); // 输出全局 myName: '极客邦'
bar.printName(); // 同上
在此,printName 中的 myName 为自由变量,其查找路径依函数定义位置(全局作用域)而定,最终指向全局 myName = '极客邦'。注意,var 声明的变量会挂载至全局对象(如浏览器 window),易导致污染。ES6 的 let 和 const 则避免此问题,不挂载全局。
严格模式('use strict')进一步优化:在非严格模式,未声明变量隐式全局化;严格模式下,直接抛错。
this 的设计:历史遗留与权宜之计
JavaScript 作者 Brendan Eich 旨在让函数灵活充当对象方法,故引入 this 以引用“当前对象”。理想中,对象方法内 this 应指向对象,便于属性访问(如 this.myName)。
然而,this 的指向非词法作用域决定(编译时静态),而依执行阶段调用方式动态绑定。这与变量查找形成鲜明对比,是设计例外。原因在于 JS 函数为一等公民,可随意传递。若函数脱离对象独立执行,this 需动态适应。
- 设计缺陷:早期忽略普通函数调用时的
this,默认指向全局对象(浏览器 window)。这放大全局污染,尤其结合var。 - 严格模式补救:普通函数
this为undefined,防止意外全局修改。 - 核心原则:
this作为指针,指向函数调用者。调用者决定指向。
此设计赋予 JS 极致灵活性,却常引发 bug。以下通过执行上下文剖析其规则。
执行上下文:this 的动态绑定环境
执行上下文(Execution Context)是引擎管理代码的环境,包含变量环境、词法环境及 this 绑定。函数调用创建新上下文,this 据调用形式绑定。
- 全局上下文:
this指向全局对象。 - 函数上下文:
this依调用决定。
以下详解 this 指向的五种常见场景,并配代码示例。
1. 对象方法调用:指向对象
函数作为对象属性调用时,this 指向该对象。这是 OOP 核心用法。
示例:
let bar = {
myName: "极客邦",
foo: function() {
this.myName = "极客时间"; // 修改 bar.myName
}
};
bar.foo();
console.log(bar); // { myName: "极客时间", foo: [Function: foo] }
调用者为 bar,故 this 指向之。
若提取函数独立调用,则变:
var myObj = {
name: "极客时间",
showThis: function() {
console.log(this);
}
};
var foo = myObj.showThis;
foo(); // this 为 window(非严格)或 undefined(严格)
2. 普通函数调用:指向全局或 undefined
独立调用函数,this 默认全局对象;严格模式下为 undefined。
示例:
function foo() {
console.log(this); // window(非严格)
}
foo(); // 隐式 window.foo()
易致污染:修改 this 即改全局。
3. call/apply/bind 显式绑定:指向指定对象
这些方法手动设定 this,适用于方法借用或上下文切换。
call(obj, arg1...)/apply(obj, [args]):立即调用,this为 obj。bind(obj):返回绑定函数。
示例:
function foo() {
this.myName = "极客时间";
}
let bar = { myName: "极客邦" };
foo.apply(bar);
console.log(bar); // { myName: "极客时间" }
4. 构造函数调用:指向新实例
new 关键字时,this 指向新建对象。
示例:
function CreateObj() {
this.name = "极客时间";
}
var myObj = new CreateObj();
console.log(myObj); // { name: "极客时间" }
内部:创建空对象、绑定 this、设原型、返回。
5. 事件处理函数:指向绑定元素
DOM 事件中,this 指向触发元素。
示例:
<a href="#" id="link">点击我</a>
<script>
document.getElementById('link').addEventListener('click', function() {
console.log(this); // <a id="link" ...>
});
</script>
利于事件委托。
实际应用:如何可靠获取 time.geekbang.com
针对开例,若需在可能独立调用的函数中访问 bar.myName = "time.geekbang.com":
- 优先
this.myName:对象方法中可靠。 - 箭头函数(ES6):不绑定自身
this,继承外层。 - 最佳实践:严格模式 + let/const 防污染;class 与箭头简化 OOP。
this 体现了 JS 的动态本质,却源于早期妥协。它非静态词法绑定,而是运行时调用决定,常令初学者困惑。关键:聚焦调用者,结合上下文分析。