每当提到 JavaScript 的 this
关键字,许多开发者都会感到头痛。它似乎总是以出乎意料的方式工作,让人捉摸不透。但事实上,一旦你理解了 this
的行为,一切都会变得清晰起来。
this
的基本概念
this
在 JavaScript 中是一个特殊的关键字,它代表的是函数执行上下文中的“当前对象”。然而,this
的指向并不是固定的,它取决于函数是如何被调用的。这一点在 ECMAScript 规范中有详细的定义:
tc39.es/ecma262/#se…
并且,如果你不了解它的运作机制,有时就会出现一些你认为的莫名其妙的现象。
那么,这个 this
到底说了什么内容呢?
说起来也简单,this
的指向会根据函数的调用方式不同而变化。 对于一些常见的函数调用模式,我们可以总结如下:
全局作用域中的
this
当在全局作用域中调用一个函数时,
this
通常指向全局对象(在浏览器中是window
,在 Node.js 中是global
)。作为普通函数调用时的
this
当一个函数被直接调用时(不是作为一个对象的方法),
this
通常指向全局对象(非严格模式下)或者undefined
(严格模式下)。作为对象方法调用时的
this
当一个函数作为对象的一个方法被调用时,
this
通常指向那个对象。构造函数中的
this
使用
new
关键字调用构造函数时,this
指向新创建的对象实例。事件处理程序中的
this
当一个函数作为事件处理程序被调用时,
this
通常指向触发事件的元素。箭头函数中的
this
箭头函数不会绑定自己的
this
,而是继承自外围函数的作用域中的this
。使用
.call
,.apply
,.bind
改变this
可以使用这些方法显式地设置函数执行时的
this
值。
为了描述this 的不同绑定方式,程序员们达成了共识,给this的绑定也分为了一下几类
社区里广泛使用的术语
默认绑定
当函数独立调用时,this指向全局对象(在浏览器环境中为window对象)。
示例:
复制 function sayName() { console.log(this.name); } var name = '全局名称'; sayName(); // 输出:全局名称
注意:当这里是严格模式的时候,会访问undefined导致TypeError
隐式绑定
当函数作为对象的方法调用时,this指向该对象。
var person = { name: '张三', sayName: function() { console.log(this.name); } }; person.sayName(); // 输出:张三
函数调用中的this
当一个函数作为普通函数调用时,this
的行为取决于上下文:
非严格模式下,
this
指向全局对象。严格模式下,
this
为undefined
。
function greet() { console.log(this); } greet(); // 输出: window (非严格模式) 或 undefined (严格模式)
箭头函数中的this
箭头函数不绑定自己的this
。它们继承外部函数或全局作用域中的this
。
const person = { name: 'Alice', greet: () => { console.log('Hello, ' + this.name); } }; person.greet(); // 输出: Hello, undefined (因为这里的this是全局的)
new绑定this
在构造函数中,this
指向新创建的对象实例。
function Person(name) { this.name = name; } const alice = new Person('Alice'); console.log(alice.name); // 输出: Alice
显式绑定
通过call、apply和bind方法,可以显式指定函数的this指向。
示例:
function sayName(age) { console.log(this.name + ',年龄:' + age); } var person = { name: '李四' }; sayName.call(person, 25); // 输出:李四,年龄:25
案例
来吧,让我们从最简单的案例开始看。
function sayHello() { console.log(this); } sayHello(); // 输出全局对象(非严格模式下)或者 undefined(严格模式下)
请仔细阅读上面的代码,然后你认为 sayHello
函数中的 this
是什么?
要回答这个问题,我们先要了解全局作用域的this
在全局作用域(非严格模式下),this
指向全局对象。在浏览器环境中,这通常指的是window
对象。
console.log(this); // 输出: window
在严格模式下(use strict
), 全局作用域中的this
将被设置为undefined
。
'use strict'; console.log(this); // 输出: undefined
很明显, 这里是普通函数的调用,且不在严格模式下,所以this指向全局。我们在浏览器环境中运行得到的答案就是window,在node环境指向的是globalThis
接下来我们来看一个稍微复杂一点的例子:
const obj = { name: 'Alice', greet: function() { console.log(`Hello, my name is ${this.name}`); } }; obj.greet(); // 输出 "Hello, my name is Alice"
请仔细阅读上面的代码,然后你认为 greet
函数中的 this
是什么?
相信你能够很自信的回答这个问题,greet
函数中的 this
指向 obj
对象。
这个答案是正确的,但如果我追问你是怎么得到这个答案的,我猜不了解 this
行为的你可能会说,因为它是作为一个对象的方法被调用的,所以 this
指向 obj
。
这里的情况是函数作为对象的方法被调用,因此遵循第3条规则。所以答案是'Hello, my name is Alice'
接下来我们继续增加复杂度:
const obj = { name: 'Alice', greet: function() { console.log(`Hello, my name is ${this.name}`); } }; const greet = obj.greet; greet();
这里我们将 greet
函数赋值给了一个新的变量,并直接调用它。你认为 greet
函数中的 this
是什么? 它依然是作为一个普通函数被调用了,遵循第二条,所以如果在浏览器环境下打印的仍是windows,但在node环境中会是globalThis
我们再来看看使用 new
关键字调用构造函数的情况:
function Person(name) { this.name = name; this.sayHello = function() { console.log(`Hello, my name is ${this.name}`); }; } const alice = new Person('Alice'); alice.sayHello(); // 输出 "Hello, my name is Alice"
这里我们使用 new
关键字创建了一个新的 Person
实例。你认为 sayHello
函数中的 this
是什么? sayHello
函数中的 this
指向 alice
对象。
这里的情况是函数作为对象的方法被调用,因此遵循第3条规则。此外,alice
对象是在构造函数中创建的,因此遵循第4条规则。
接下来我们看看箭头函数中的 this
指向:
const obj = { name: 'Alice', greet: () => { console.log(`Hello, my name is ${this.name}`); } }; obj.greet(); // 输出 "Hello, my name is undefined"
这里我们使用了箭头函数。你认为 greet
函数中的 this
是什么?
greet
函数中的 this
指向全局对象(非严格模式下)或者 undefined
(严格模式下)。
这里的情况是箭头函数,因此遵循第6条规则,它继承了外围函数的作用域中的 this
。
我们再看一个例子
// 定义一个对象 const user = { name: 'Alice', logName: function() { console.log('logName:', this.name); // 1 const innerFunc1 = () => { console.log('innerFunc1:', this.name); // 2 }; const innerFunc2 = () => { console.log('innerFunc2:', this.name); // 3 const innerFunc3 = () => { console.log('innerFunc3:', this.name); // 4 setTimeout(() => { console.log('setTimeout:', this.name); // 5 }, 1000); }; innerFunc3(); }; innerFunc1(); innerFunc2(); } }; // 调用对象的方法 user.logName(); // 在全局作用域中定义一个箭头函数 const globalArrowFunc = () => { console.log('globalArrowFunc:', this); // 6 }; globalArrowFunc();
先别看下面,你能独立分析出这里所有日志的内容吗?
分析
logName: 在对象方法
logName
内部,this
指向user
对象,因此this.name
输出Alice
。innerFunc1: 箭头函数
innerFunc1
继承了logName
的this
,因此this.name
输出Alice
。innerFunc2: 同样,箭头函数
innerFunc2
继承了logName
的this
,因此this.name
输出Alice
。innerFunc3: 箭头函数
innerFunc3
也继承了logName
的this
,因此this.name
输出Alice
。setTimeout: 即使在
setTimeout
中定义了一个箭头函数,它依然继承了logName
的this
,因此this.name
输出Alice
。globalArrowFunc: 在全局作用域中定义的箭头函数,
this
指向全局对象,因此this
输出全局对象。
你都分析对了吗?
this指向的优先级
当多种绑定规则同时存在时,它们的优先级顺序为:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定。
this指向问题到这里就大致讲完了,感谢大家的阅读
结论
理解this
的指向对于避免常见的JavaScript错误至关重要。记住,this
的值是在运行时由调用上下文决定的,而箭头函数则会继承其封闭作用域的this
。
在开发过程中,确保你清楚每个函数调用时this
的预期值,并使用适当的工具和技术来控制或修改它的行为。希望这篇文章对你有所帮助。