我们在JavaScript执行上下文中提到,在JavaScript中,this
值指的是当前执行上下文的上下文对象,每个执行上下文都会绑定一个上下文对象。执行上下文是运行时创建的,可见this
的值也是运行时绑定的,函数的调用方式决定了this
的值。JavaScript规定,一旦this
值被绑定,不能在运行时被更改。松散模式和严格模式下this
绑定有不同表现,松散模式下this
值必须是一个对象,在严格模式下this
值才可能被绑定为基础数据类型值。
什么决定了this
绑定的动态性
在JavaScript中,一个函数也是一个对象。作为对象,函数能够作为值进行传递,可以赋值给另一个对象的属性,可以直接定义,也可以定义为另一个对象的属性。当一个函数是一个对象的属性的时候,我们也称它为对象的方法。对象的方法和函数都是一类对象(它们都是构造函数Function
创建的对象)。由于函数的动态性,this
作为执行上下文对象也具备跟随函数调用方式变化而变化的动态性。简单来说,函数中this
的值不取决于函数在哪里定义,取决于函数在哪里执行。同样的JavaScript在松散模式和严格模式下不同的运行表现,也决定了this
绑定的动态性。在松散模式下为了保证this
的值总是一个对象,在某些情况下this
的值会发生静默转换。
在全局环境下,JavaScript引擎维护一个特殊的并唯一的
this
值——globalThis
,在不同的运行环境下,globalThis
有不同的别名。
请看如下代码在浏览器环境中运行的结果:
// 在网页浏览器中
function printThisFunc() {
console.log(this);
}
const obj1 = {
name: 'obj1',
func: printThisFunc,
printThisMethod: function () {
console.log(this);
}
};
const printThisMethod = obj1.printThisMethod;
printThisFunc(); // 输出: window 对象
obj1.func(); // 输出: obj1 对象
obj1.printThisMethod(); // 输出: obj1 对象
printThisMethod(); // 输出: window 对象
在示例代码中,我们定义了两个函数对象,分别是全局函数printThisFunc
和obj1
对象的方法printThisMethod
。当printThisFunc
在全局调用时,输出全局this对象window
,当将它赋值给obj1.func
并通过obj1.func()
调用时,输出的this
值是obj1
对象;当直接调用obj1.printThisMethod
对象时,输出的this
值是obj1
对象,而当把obj1.printThisMethod
先取出到全局环境再进行调用时,输出全局this对象window
。
扩展:globalThis
提供了一个标准的方式来获取不同环境下的全局this
对象(也就是全局对象自身)。它确保可以在有无窗口的各种环境下正常工作。在 Web 中,可以通过window
、self
或者frames
取到全局对象,但是在 Web Workers 中,只有self
可以。在Node.js中,它们都无法获取,必须使用global
。
this
绑定的静默转换
在松散模式下,JavaScript引擎会总是确保this
值是一个对象,在绑定this
遇到非对象值时,会发生如下转换:
- 如果一个函数被调用时
this
被设置为undefined
或null
,this
会被替换为globalThis
。 - 如果函数被调用时
this
被设置为一个原始值,this
会被替换为原始值的包装对象。
松散模式下,函数上下文中进行this
绑定时,不论是默认绑定、隐式绑定、显示绑定,还是new
绑定,这个静默转换总会发生。需要注意的是,在构造函数调用中,这种静默转换总会发生,哪怕是在严格模式中。
示例代码如下:
function getThis() {
return this;
}
const one = 1;
console.log(one instanceof Number); // 输出:false
console.log(getThis.call(null)); // 输出 window 对象
console.log(getThis.call(1) instanceof Number); // 输出:true
console.log(getThis.call(1).valueOf()); // 输出:1
这种自动转化为对象的过程不仅是一种性能上的损耗,同时在浏览器中暴露出全局对象也会成为安全隐患,因为全局对象提供了访问那些所谓安全的JavaScript环境必须限制的功能的途径。
禁用this
绑定默认转换
在严格模式下通过this
传递给一个函数的值不会被强制转换为一个对象。所以对于一个开启严格模式的函数,指定的this
不再被封装为对象,而且如果没有指定this
的话它值是undefined
:
"use strict";
function getThis() {
return this;
}
console.log(getThis()); // 输出:undefined
console.log(getThis.call(1) === 1); // 输出:true
console.log(getThis.apply(null)); // 输出:null
console.log(getThis.call(undefined)); // 输出:undefined
console.log(getThis.bind(true)() === true); // 输出:true
函数上下文中this
的绑定
函数的调用情况不同,this
绑定不同。函数上下文中this
绑定的规则分为四种,分别是默认绑定、隐式绑定、显示绑定、new
调用绑定。
- 默认绑定:函数被独立调用时绑定。
- 隐式绑定:函数通过对象调用时绑定。
- 显示绑定:通过
call()
、apply()
、bind()
方法指定函数运行上下文的绑定。 new
绑定:函数作为构造函数时的绑定。
箭头函数作为一种特殊的函数,它的函数作用域中没有基于箭头函数自身的this
,也不会产生this
绑定。当上述四类this
绑定情况发生冲突时,JavaScript表现的绑定的优先级如下:
new
绑定 > 显示绑定 > 隐式绑定 > 默认绑定
下面我们分别介绍函数调用中this
绑定的具体情况。
1. 独立函数调用
不论函数定义在哪里,对它进行独立调用时会进行默认绑定,非严格模式下this
指向window
,严格模式下this
指向undefined
。符合静默转换规则。
function getThis() {
return this;
}
function getStrickThis() {
"use strict";
return this;
}
const obj = {
getThisMethod: function () {
return this;
}
}
const getThisMethod = obj.getThisMethod;
console.log(getThis()); // 输出 window 对象
console.log(getThisMethod()); // 输出 window 对象
console.log(getStrickThis()); // 输出:undefined
2. 作为对象方法调用
不论函数定义在哪里,函数作为对象方法调用的候,会进行隐式绑定,将当前对象作为上下文对象,this
指向当前对象。
function getThis() {
return this;
}
const obj1 = {
name: 'obj1',
getThisFunc: getThis,
getThisMethod: function() {
return this;
}
};
const obj2 = {
name: 'obj2',
getThisFunc: getThis
};
console.log(obj1.getThisFunc()); // 输出 obj1 对象
console.log(obj1.getThisMethod()); // 输出 obj1 对象
console.log(obj2.getThisFunc()); // 输出 obj2 对象
3. 使用call()
、apply()
、bind()
调用函数
使用call()
、apply()
调用函数时可以为指定函数执行上下文中this
的绑定值。使用bind()
可以生成一个新函数,并且指定生成的函数执行上下文中this
的绑定值,并且指定的this
的绑定值不能再次使用bind()
进行覆盖。这类this
绑定为显示绑定。其中,bind()
指定的this
绑定不仅不可被bind()
自身覆盖,也不可被call()
、apply()
调用覆盖。当然,call()
、apply()
、bind()
的作用并不仅仅是为了改变函数运行时的上下文对象。
示例代码如下:
function getThis() {
return this;
}
const obj1 = {
name: 'obj1',
getThisMethod: function() {
return this;
}
};
const obj2 = {
name: 'obj2'
};
console.log(getThis.call(obj1)); // 输出 obj1 对象
console.log(getThis.call(obj2)); // 输出 obj2 对象
console.log(getThis.apply(obj1)); // 输出 obj1 对象
// 显示绑定优先级大于隐式绑定
console.log(obj1.getThisMethod.call(obj2)); // 输出 obj2 对象
console.log(getThis.bind(obj1)()); // 输出 obj1 对象
// 重复绑定无效
console.log(getThis.bind(obj1).bind(obj2)()); // 输出 obj1 对象
obj1.getThisMethod.bind(obj2).call(obj1); // 输出 obj2 对象
call()
、apply()
的不同点在于为函数指定传参的形式不同,开发人员可以根据需要选用任一方法指定this
绑定。
4. 回调函数调用
当一个函数作为回调函数传递时,this
的值取决于如何调用回调,这由API的实现者决定。当你定义一个函数作为回调函数传递给其他调用者调用时,这点要特别注意,函数中的this
不是你能确定的,如果需要你定义的回调函数在确定的上下文对象上执行,你可以使用bind()
进行硬绑定。
在JavaScript社区中,回调函数通常以undefined
作为this
的值被调用(直接调用,而不附加到任何对象上),这意味着如果函数是在非严格模式,this
的值会是全局对象(globalThis
)。这在迭代数组方法、Promise()
构造函数等例子中都是适用的。迭代数组方法示例如下:
function logThis() {
console.log(this);
}
function logStrickThis() {
"use strict";
console.log(this);
}
[1, 2, 3].forEach(logThis); // 输出三次 window 对象
[1, 2, 3].forEach(logStrickThis); // 输出三次 undefined
5. 箭头函数调用
箭头函数内没有独立的this
绑定,箭头函数中的this
对象保留了闭合词法上下文的this
值。换句话说,箭头函数在其周围的作用域上创建一个this
值的闭包,这意味着箭头函数的行为就像它们是“自动绑定”的。无论如何调用,this
都绑定到函数创建时的值。
const obj = {
name: 'obj',
getThisGetter: function() {
const getThis = () => this;
return getThis;
},
};
const getThis = obj.getThisGetter();
const _this = getThis();
console.log(_this); // 输出 obj 对象
此外,当使用call()
、apply()
、bind()
调用箭头函数时,显示指定的this
值不生效。
6. 构造函数调用
当一个函数被用作构造函数(使用new
关键字)时,无论构造函数是在哪个对象上被访问的,其this
都会被绑定到正在构造的新对象上。除非构造函数返回另一个非原始值,不然this
的值会成为new
表达式的值。这种绑定被称为new
绑定。它的优先级最高且不可被覆盖。
function getThis() {
return this;
}
function Person(name) {
this.name = name;
}
const context = { contextTag: 'context' };
const getThisWithContext = getThis.bind(context);
const PersonCopy = Person.bind();
const person = new PersonCopy('Guang');
console.log(getThisWithContext()); // 输出 context 对象
console.log(person); // 输出 person 对象,person 对象仅包含 name 属性,name 属性值为"Guang"
7. super
关键字调用函数
super
关键字用于访问对象字面量或类的原型([[Prototype]])上的属性,或调用父类的构造函数。ECMAScript 6推出类(class
)声明,同时推出了一系列类继承相关的关键字,super
就是其一。当一个函数以super.method()
的形式被调用时,method函数内的this
与super.method()
调用周围的this
值相同,通常不等于super
所指向的对象。这是因为super.method
和上面的对象成员访问不同——它是有关类继承机制中的特殊语法,有不同的绑定规则。
类上下文中this
的绑定
目前,对于JavaScript引擎来说,类(class
)的本质还是函数作为构造函数,所以类上下文中的this
绑定很多内容属于new
绑定。同时,类的功能越来越丰富,特别是类的静态初始化块的出现,让我们有必要将类上下文分成两个上下文去理解:实例上下文和静态上下文。两个上下文分别维护两个this
绑定。
- 实例上下文:包括构造函数、方法、字段初始化器。
- 静态上下文:包括静态方法、静态字段初始化器、静态初始化块。
JavaScript规定类的调用只能使用new
调用,实例上下文中的this
和普通函数作为构造函数调用时的绑定行为一样:this
值是正在创建的新实例。类方法的行为像对象字面量中的方法——this
值是方法被访问的对象。如果方法没有转移到另一个对象,this
通常是类的一个实例。实例字段是在this
被设置为正在构造的实例的情况下被初始化的,所以,字段初始化器中this
绑定到当前构造的实例,字段初始化器中箭头函数中的this
绑定到当前实例。
静态方法和静态字段时类本身的属性,它们需要通过类来访问,此时,this
绑定就是类(或子类)自身的值。静态初始化块也是在this
绑定为当前类自身的情况下进行求值的。静态字段是在this
被设置为当前类的情况下被初始化的,所以,静态字段初始化器中this
绑定到当前类自身,静态字段初始化器中箭头函数中的this
绑定到当前类自身。
验证代码:
class C {
instanceField = this;
static staticField = this;
}
const c = new C();
console.log(c.instanceField === c); // 输出:true
console.log(C.staticField === C); // 输出:true
派生类构造函数
派生类与基类共用一个实例上下文的this
绑定,同时和基类有着各自的静态上下文this
绑定。派生类和基类共用的this
绑定是由基类创建的,派生类构造函数没有初始的this
绑定。派生类构造函数中,调用super()
会调用基类的构造函数,基类在构造函数中创建一个this
绑定,基本上和this = new Base()
的效果类似(Base
是基类)。这也是为什么派生类构造函数中super()
语句有下面两条规则:
- 在调用
super()
之前引用this
将抛出错误。 - 派生类在调用
super()
之前不能有返回,除非构造函数返回一个对象(这样this
值就会被覆盖)或者类根本没有构造函数。
验证代码如下:
class Base {}
class Good extends Base {}
class AlsoGood extends Base {
constructor() {
return { a: 5 };
}
}
class Bad extends Base {
constructor() {}
}
new Good();
new AlsoGood();
new Bad(); // 报错: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
全局上下文中的this
的绑定
在全局执行上下文中(在任何函数或类之外;可能在全局范围内定义的块或箭头函数内部),this
值取决于脚本运行的执行上下文。像回调函数一样,this
值由运行时环境(调用者)确定。
代码示例如下:
// 在网页浏览器中,window 对象也是全局对象:
console.log(this === window); // 输出:true
this.b = "MDN";
console.log(window.b); // 输出:"MDN"
console.log(b); // 输出:"MDN"
在脚本的顶层,无论是否在严格模式下,this
会指向globalThis
。如果源代码作为模块加载(对于 HTML,这意味着在 <script> 标签中添加 type="module"
),在顶层,this
总是undefined
。
如果源代码使用eval()
执行,this
与直接调用eval()
的闭合上下文相同,或者与间接调用eval
的globalThis
(就像它在单独的全局脚本中运行一样)相同。代码示例如下:
function test() {
// 直接调用 eval
console.log(eval("this") === this);
// 间接调用 eval,非严格模式
console.log(eval?.("this") === globalThis);
// 间接调用 eval,严格模式
console.log(eval?.("'use strict'; this") === globalThis);
}
test.call({ name: "obj" }); // 输出 3 个 "true"
注意:某些源代码虽然看起来像全局作用域,但在执行时实际上被包装在一个函数中。例如,Node.js CommonJS 模块被包装在一个函数中,并且this
值设置为module.exports
。事件处理器属性执行时,this
设置为它们附加到的元素。
两个特殊情况
1. 对象字面量上的this
对象字面量不创建this
作用域——只有在对象内定义的函数(方法)才会这样做。在对象字面量中使用this
会从周围的作用域继承值。
示例代码如下:
const obj1 = {
name: 'obj1',
_self: this
};
function getObj(name) {
const obj = {
name: name,
_self: this
};
return obj;
}
const obj2 = getObj.bind(obj1)('obj2');
console.log(obj1._self); // 输出 window 对象
console.log(obj2._self); // 输出 obj1 对象
2. getter或setter中的this
在getter和setter中,this
是基于访问属性的对象,而不是定义属性的对象。用作getter或setter的函数会将其this
绑定到正在设置或获取属性的对象。
代码示例:
function sum() {
return this.a + this.b + this.c;
}
const obj = {
a: 1,
b: 2,
c: 3,
get average() {
return (this.a + this.b + this.c) / 3;
},
};
Object.defineProperty(obj, "sum", {
get: sum,
enumerable: true,
configurable: true,
});
console.log( obj.sum); // 输出:6
console.log(obj.average); // 输出:2
this
动态性的好处
首先,作为上下文对象,this
的实现是面向对象编程范式的基本要素,this
绑定的存在提高了JavaScript语言的表达能力。其次,this
绑定的灵活,将函数或方法与其定义者解耦,让函数更加容易复用或被对象共享,能够支持跨上下文的函数或方法的共享。再者,函数在作为回调函数进行调用时,函数的调用环境可以很方便地增强回调函数的表达,使回调函数的表达更直观。比如DOM 事件处理函数中this
自动指向触发元素。另一方面,JavaScript原型机制也依赖于this
动态绑定的特性。
this
的值不是拥有此函数作为自己属性的对象,而是用于调用此函数的对象。这是JavaScript原型机制良好工作地基础。看如下代码:
const nameKeeperPrototype = {
sayName: function() {
return 'My name is: ' + this.name;
}
};
const person = {
name: 'Guang'
};
Object.setPrototypeOf(person, nameKeeperPrototype);
console.log(person.sayName()); // 输出 "My name is: Guang"
sayName()
虽然定义在nameKeeperPrototype
对象上,但是被person
对象调用时,this
指向person
对象。
this
动态性带来的问题
将this
设计为动态的虽然具有许多优点,但也带来了一些潜在的缺点和挑战。首先,this
的动态性增加了理解和调试的困难程度,尤其是在大型应用中,this
的指向可能不易追踪,开发者需要注意在不同上下文中如何使用this
。其次,在一些异步操作或回调函数中,this
的指向可能会丢失,导致程序出现难以追踪的错误。在Promise
和async/await
语法出现之前,回调地狱是每个前端开发人员的噩梦。还比如,在事件处理或异步回调中,this
可能不再指向预期的对象。再者,this
默认绑定到全局的行为增加了全局环境被污染的可能性,开发人员很容易在无意间把值更新到了全局对象上。另一方面,this
的动态性不利于单元测试,编写单元测试时,可能需要更复杂的模拟和设置。特别是在测试事件处理程序、回调函数等异步操作时,this
的值很难预先确定,可能需要使用如call()
、apply()
、bind()
等方法显式设置this
的值,这增加了测试的复杂性。
下面代码演示了回调函数丢失this
:
function Timer() {
this.time = 0;
setInterval(function() {
this.time++; // 这里的 `this` 不再指向 Timer 实例
console.log(this.time); // 可能输出 NaN 或 undefined
}, 1000);
}
const timer = new Timer();
为了避免this
动态性带来的这些问题,开发人员需要大量的使用箭头函数,或者利用call()
、apply()
、bind()
显示绑定this
的值,这不仅增加了项目的维护复杂度,也给程序性能带来了不好的影响。