原文:Gentle explanation of ‘this’ keyword in JavaScript 链接:https://dmitripavlutin.com/gentle-explanation-of-this-in-javascript/ 翻译:Robin
1.神秘的 this
很久以来,this 对于我和一些初级开发者来说都很神秘。它的特性强大,需要努力去理解。
对于拥有 Java、PHP及其他标准语言背景的人来说,this 被看作是类方法中的当前对象的实例:不多不少。通常,它不会在方法外使用,这样也不会造成一些困惑。
在 JavaScript 中,情况会有所不同:this 是一个函数当前的执行上下文。JavaScript 有 4 种函数调用类型:
- 函数调用:alert(‘Hello World!’)
- 方法调用:console.log(‘Hello World!’)
- 构造器调用:new RegExp(‘\d’)
- 间接调用:alert.call(undefined, ‘Hello World!’)
每种调用方式决定了上下文,因此 this 会和开发者预想的有所出入。

此外,严格模式 也会影响执行上下文。
理解 this 关键字的重点是对函数调用以及上下文影响要有清晰的认识。
本文聚焦于调用分析,函数调用对于 this 的影响以及判断上下文的一些常见陷阱解释。
在这之前,让我们熟悉几个术语:
- 函数 调用,函数体的代码执行,或者说简单的调用函数。类如 parseInt 函数的调用 parseInt(‘15’)。
- 调用的 上下文 就是函数体内的 this 值。例如 map.set(‘key’, ‘value’) 的调用的上下文就是 map。
- 函数 作用域 就是函数体内可访问的变量、对象和函数集合。
目录:
- 神秘的 this
- 函数调用
- 函数调用的 this
- 严格模式下函数调用的 this
- 陷阱:内部函数的 this
- 方法调用
- 方法调用的 this
- 陷阱:方法与对象分离
- 构造器调用
- 构造器调用的 this
- 陷阱:忘记关键字 new
- 间接调用
- 间接调用的 this
- 绑定函数
- 绑定函数的 this
- 紧上下文绑定
- 箭头函数
- 箭头函数的 this
- 陷阱:使用箭头函数定义方法
- 总结
2.函数调用
当一个表达式赋值了一个函数对象,后面跟着圆括号 (,被分号隔开的参数以及一个圆括号 ),函数调用就被执行。例如 parseInt(‘18’)。
函数调用表达式不是 obj.myFunc() 这样的一个 属性访问器,后者会产生 方法调用。记住这个区别十分重要。
函数调用的简单例子:
function hello(name) {return 'Hello ' + name + '!';}// Function invocationvar message = hello('World');console.log(message); // => 'Hello World!'
hello(‘World’) 是一个函数调用:hello 表达式被赋值一个函数对象,紧接着一对圆括号和 ‘World’ 参数。
更高级的例子是 IIFE (立即执行函数表达式)。
var message = (function(name) {return 'Hello ' + name + '!';})('World');console.log(message) // => 'Hello World!'
IIFE 也是一个函数调用:(function(name) {…}) 的第一对圆括号是一个函数对象赋值表达式,紧跟着包着 ‘World’ 参数的圆括号。
2.1. 函数调用的 this
函数调用的 this 是 全局对象
全局对象由执行环境决定。在浏览器中是 window 对象。

在函数调用中,执行上下文就是全局对象。
让我们看看下面这个函数的上下文:
function sum(a, b) {console.log(this === window); // => truethis.myNumber = 20; // add 'myNumber' property to global objectreturn a + b;}// sum() is invoked as a function// this in sum() is a global object (window)sum(15, 16); // => 31window.myNumber; // => 20
当 sum(15, 16) 被调用时,JavaScript 自动的把 this 设置为全局对象,在浏览器中即 window。
当 this 在函数作用域之外使用时(即顶级作用域:全局执行上下文),它是全局对象:
console.log(this === window); // => truethis.myString = 'Hello World!';console.log(window.myString); // => 'Hello World!'
<!-- In an html file --><script type="text/javascript">console.log(this === window); // => true</script>
2.2. 严格模式下函数调用的 this
严格模式下函数调用的 this 为 undefined
严格模式是在 ECMAScript 5.1 被引入的。它提供了更好的安全性和错误检查。
启用严格模式,需将 ‘use strict’ 放在函数体顶部。
一旦启用,严格模式会影响执行上下文,在普通函数调用里 this 为 undefined。在 2.1 的例子中,执行上下文 不 再是全局对象。

严格模式下函数执行的例子:
function multiply(a, b) {'use strict'; // enable the strict modeconsole.log(this === undefined); // => truereturn a * b;}// multiply() function invocation with strict mode enabled// this in multiply() is undefinedmultiply(2, 5); // => 10
当 multiply(2, 5) 被调用时,this 为 undefined。
严格模式不仅对当前作用域起作用,还对内部作用域起作用(所有内部声明的函数)。
function execute() {'use strict'; // activate the strict modefunction concat(str1, str2) {// the strict mode is enabled tooconsole.log(this === undefined); // => truereturn str1 + str2;}// concat() is invoked as a function in strict mode// this in concat() is undefinedconcat('Hello', ' World!'); // => "Hello World!"}execute();
‘use strict’ 出现在 execute 函数体的顶部时,在它的作用域内就启用了严格模式。因为 concat 在 execute 作用域内被声明,它因此继承了严格模式。在调用 concat(‘Hello’, ‘ World!’) 时,this 等于 undefined。
单个 JavaScript 文件可能同时包含严格和非严格模式。因此,在单个脚本中的同一种调用类型可能有着不同的上下文行为:
function nonStrictSum(a, b) {// non-strict modeconsole.log(this === window); // => truereturn a + b;}function strictSum(a, b) {'use strict';// strict mode is enabledconsole.log(this === undefined); // => truereturn a + b;}// nonStrictSum() is invoked as a function in non-strict mode// this in nonStrictSum() is the window objectnonStrictSum(5, 6); // => 11// strictSum() is invoked as a function in strict mode// this in strictSum() is undefinedstrictSum(8, 12); // => 20
2.3. 陷阱:内部函数的 this
函数调用的常见陷阱是认为内部函数的 this 和外部函数一样。
准确来说内部函数的上下文只依赖于调用(类型),而不是外部函数的上下文。
想要期望的 this,需要使用间接调用(使用 .call() 或者 .apply(),见第 5 节)或者绑定函数(使用 .bind(),见第 6 节)去改变内部函数的上下文。
下面是一个计算两个数之和的例子:
var numbers = {numberA: 5,numberB: 10,sum: function() {console.log(this === numbers); // => truefunction calculate() {// this is window or undefined in strict modeconsole.log(this === numbers); // => falsereturn this.numberA + this.numberB;}return calculate();}};numbers.sum(); // => NaN or throws TypeError in strict mode
numbers.sum() 是对对象的方法调用(见第 3 节),sum 的上下文就是 numbers 对象。calculate 定义在 sum 中,因此,你可能会认为 calculate() 的 this 也是 numbers。
然而,calculate() 是函数调用(并非方法调用)并且它的 this 是全局对象 window(示例2.1.)或者严格模式下的 undefined (示例2.2.)。即使外部函数 sum 的上下文是 numbers 对象,也没有影响到这里。
numbers.sum() 的调用结果是 NaN 或者在严格模式下抛出错误 TypeError: Cannot read property ‘numberA’ of undefined。总之不是所期待的结果 5 + 10 = 15,全因为 calculate 没有正确调用。
为了解决这个问题,calculate 函数应该和 sum 函数那样在相同的上下文中执行,这样才能访问 numberA 和 numberB 属性。
一个解决方案是通过调用 calculate.call(this) (函数的间接调用,见第 5 节)去手动的改变 calculate 的上下文。
var numbers = {numberA: 5,numberB: 10,sum: function() {console.log(this === numbers); // => truefunction calculate() {console.log(this === numbers); // => truereturn this.numberA + this.numberB;}// use .call() method to modify the contextreturn calculate.call(this);}};numbers.sum(); // => 15
calculate.call(this) 除了照常执行 calculate 函数,还会将第一个参数作为上下文的值。现在,this.numberA + this.numberB 等价于 numbers.numberA + numbers.numberB。函数将返回希望的结果 5 + 10 = 15。
3.方法调用
方法 就是保存在一个对象中的函数属性。例如:
var myObject = {// helloFunction is a methodhelloFunction: function() {return 'Hello World!';}};var message = myObject.helloFunction();
helloFunction 是 myObject 的方法。使用属性访问器获取方法:myObject.helloFunction。
当使用函数对象属性访问器后跟圆括号 (,一组逗号分隔的参数和圆括号 ) 的这种赋值表达式时,就是方法调用。
回想下前面的例子,myObject.helloFunction() 就是对 myObject 对象的 helloFunction 的方法调用。[1, 2].join(‘,’) 和 /\s/.test(‘beautiful world’) 也是方法调用。
区分 函数调用(见第 2 节)和 方法调用 很重要,因为他们是不同的类型。主要不同是方法调用需要以属性访问器的形式去调用函数(obj.myFunc() 或者 obj‘myFunc’),然而函数调用不需要(myFunc())。
下面的例子展示了如何区分这些类型:
['Hello', 'World'].join(', '); // method invocation({ ten: function() { return 10; } }).ten(); // method invocationvar obj = {};obj.myFunction = function() {return new Date().toString();};obj.myFunction(); // method invocationvar otherFunction = obj.myFunction;otherFunction(); // function invocationparseFloat('16.60'); // function invocationisNaN(0); // function invocation
理解函数调用和方法调用之间的区别有助于正确识别上下文。
3.1. 方法调用的 this
方法调用中的 this 就是 拥有该方法的对象
对对象的方法调用时,this 就是对象本身:

让我们写一个对象,其中一个方法实现了自增:
var calc = {num: 0,increment: function() {console.log(this === calc); // => truethis.num += 1;return this.num;}};// method invocation. this is calccalc.increment(); // => 1calc.increment(); // => 2
调用 calc.increment() 时 increment 函数的上下文即 calc 对象。因此,使用 this.num 会增加属性数值。
我们来看另一个例子。一个 JavaScript 对象从它的 原型 继承了一个方法。当继承的方法在对象上调用时,调用的上下文仍然是对象本身:
var myDog = Object.create({sayName: function() {console.log(this === myDog); // => truereturn this.name;}});myDog.name = 'Milo';// method invocation. this is myDogmyDog.sayName(); // => 'Milo'
Object.create() 生成了一个新的对象 myDog 并设置了原型。 myDog 对象继承了 sayName 方法。
当 myDog.sayName() 执行时,myDog 就是调用上下文。
在 ECMAscript 6 的 class 语法中,方法调用上下文同样是实例本身:
class Planet {constructor(name) {this.name = name;}getName() {console.log(this === earth); // => truereturn this.name;}}var earth = new Planet('Earth');// method invocation. the context is earthearth.getName(); // => 'Earth'
3.2. 陷阱:方法与对象分离
对象的方法可以抽离出来赋给独立的变量 var alone = myObj.myMethod。当方法被单独调用时,即使用 alone() 将从原对象分离,你可能认为 this 是定义该方法的对象。
准确来说,不使用对象的方法调用,此时发生了函数调用:这里的 this 是全局对象 window 或者严格模式下为 undefined(见 2.1 和 2.2)。
创建绑定函数 var alone = myObj.myMethod.bind(myObj)(使用 .bind(),见第 6 节)修复上下文,变为拥有该方法的对象。
下面的例子创建了 Animal 构造器和一个实例—— myCat。使用 setTimeout() 在一秒后打印 myCat 对象信息:
function Animal(type, legs) {this.type = type;this.legs = legs;this.logInfo = function() {console.log(this === myCat); // => falseconsole.log('The ' + this.type + ' has ' + this.legs + ' legs');}}var myCat = new Animal('Cat', 4);// logs "The undefined has undefined legs"// or throws a TypeError in strict modesetTimeout(myCat.logInfo, 1000);
你可能认为 setTimout 会调用 myCat.logInfo(),将会打印 myCat 对象的信息。
不幸的是在 setTimout(myCat.logInfo) 中当参数被传递时同对象分离了。下面的语句是等价的:
setTimout(myCat.logInfo);// is equivalent to:var extractedLogInfo = myCat.logInfo;setTimout(extractedLogInfo);
当 logInfo 作为函数被调用时,this 就是全局对象或者 undefined(而不是 myCat 对象)。因此对象信息没有正确打印。
一个函数使用 .bind() (见第 6 节)方法进行绑定。如果分离的方法绑定了 myCat 对象,上下文问题迎刃而解:
function Animal(type, legs) {this.type = type;this.legs = legs;this.logInfo = function() {console.log(this === myCat); // => trueconsole.log('The ' + this.type + ' has ' + this.legs + ' legs');};}var myCat = new Animal('Cat', 4);// logs "The Cat has 4 legs"setTimeout(myCat.logInfo.bind(myCat), 1000);
myCat.logInfo.bind(myCat) 返回一个执行与 logInfo 一致的新函数,this 为 myCat,即使执行的是函数调用。
4. 构造器调用
使用 new 关键字,后跟一个函数对象赋值表达式,圆括号 (,逗号分隔的一组参数和圆括号 ),此时发生了 构造器调用。例如: new RegExp(‘\d’)。
该例子声明了函数 Country,然后作为构造器调用:
function Country(name, traveled) {this.name = name ? name : 'United Kingdom';this.traveled = Boolean(traveled); // transform to a boolean}Country.prototype.travel = function() {this.traveled = true;};// Constructor invocationvar france = new Country('France', false);// Constructor invocationvar unitedKingdom = new Country;france.travel(); // Travel to France
new Country(‘France’, false) 是 Country 函数的构造器调用。执行的结果就是生成一个新的对象,name 属性是 ‘France’。调用无参的构造器,圆括号可以省略: new Country。
从 ECMAScript 2015 开始,JavaScript 允许使用 class 语法定义构造器:
class City {constructor(name, traveled) {this.name = name;this.traveled = false;}travel() {this.traveled = true;}}// Constructor invocationvar paris = new City('Paris', false);paris.travel();
new City(‘Paris’) 是构造器调用。对象的初始化由类中特殊的方法:constructor 去处理,this 就是新创建的对象。
构造器调用创建一个空的新对象,并继承了构造器原型的属性。构造器方法的作用就是初始化对象。你可能已经知道,这种调用类型的上下文就是生成的实例。这是下一个章节的主题。
当属性访问器 myObject.myFunction 冠以 new 关键字,JavaScript会执行构造器调用,并非方法调用。例如 new myObject.myFunction():首先使用属性访问器 extractedFunction = myObject.myFunction 将函数分离,然后作为构造器进行调用来创建新的对象:new extractedFunction()。
4.1. 构造器调用的 this
构造器调用的 this 就是新创建的对象
构造器调用的上下文就是新创建的对象。使用构造器函数的参数来初始化对象,设置属性的初始值,还有事件句柄等。

让我们检验下面例子的上下文:
function Foo () {console.log(this instanceof Foo); // => truethis.property = 'Default Value';}// Constructor invocationvar fooInstance = new Foo();fooInstance.property; // => 'Default Value'
new Foo() 调用了构造器,上下文是 fooInstance。Foo 内部对象进行了初始化:this.property 被赋了一个默认值。
当使用 class (ES2015)语法时也是一样,只是初始化发生在了 constructor 方法。
class Bar {constructor() {console.log(this instanceof Bar); // => truethis.property = 'Default Value';}}// Constructor invocationvar barInstance = new Bar();barInstance.property; // => 'Default Value'
new Bar() 执行时,JavaScript 创建了一个空对象并生成构造器方法的上下文。现在你可以用 this 关键字向对象中添加属性:this.property = ‘Default Value’。
4.2. 陷阱:忘记关键字 new
某些JavaScript函数进行构造器或者函数调用时都会生成实例。例如 RegExp:
var reg1 = new RegExp('\\w+');var reg2 = RegExp('\\w+');reg1 instanceof RegExp; // => truereg2 instanceof RegExp; // => truereg1.source === reg2.source; // => true
当执行 new RegExp(‘\w+’) 和 RegExp(‘\w+’) 时,JavaScript会生成等价的正则表达式对象。
使用函数调用生成对象是一个潜在的问题(除了工厂模式),因为某些构造器可能在缺失 new 关键字时忽略初始化对象的逻辑。
下面的例子说明了这个问题:
function Vehicle(type, wheelsCount) {this.type = type;this.wheelsCount = wheelsCount;return this;}// Function invocationvar car = Vehicle('Car', 4);car.type; // => 'Car'car.wheelsCount // => 4car === window // => true
Vehicle 是一个函数来为上下文对象设置 type 和 wheelsCount。执行 Vehicle(‘Car’, 4) 返回 car 对象,其中: car.type 为 ‘Car’ 且 car.wheelsCount 为 4。你可能认为该段代码会创建并初始化一个新对象。
然而,在这个函数调用(见2.1.)中 this 就是 window 对象,Vehicle(‘Car’, 4) 错误的为 window 对象设置了属性。新的对象并没有生成。
确保使用 new 操作符以期构造器调用被执行:
function Vehicle(type, wheelsCount) {if (!(this instanceof Vehicle)) {throw Error('Error: Incorrect invocation');}this.type = type;this.wheelsCount = wheelsCount;return this;}// Constructor invocationvar car = new Vehicle('Car', 4);car.type // => 'Car'car.wheelsCount // => 4car instanceof Vehicle // => true// Function invocation. Generates an error.var brokenCar = Vehicle('Broken Car', 3);
new Vehicle(‘Car’, 4) 正确执行:新的对象被创建和初始化,因为 new 关键字出现在了构造器调用之前。
5. 间接调用
当一个函数使用 myFun.call() 或者 myFun.apply() 方法执行时就是间接调用。
JavaScript中函数是头等对象,这意味着函数是对象。对象的类型是 Function。
函数对象的方法列表里 .call() 和 .apply() 用来给调用函数时配置一个上下文:
- .call(thisArg[, arg1[, arg2[, …]]]) 方法接受第一个参数 thisArg 作为调用上下文以及一个参数列表 arg1, arg2, … 传递给被调用的函数作为参数。
- .apply(thisArg, [arg1, arg2, …]) 方法接受第一个参数 thisArg 作为调用上下文以及一个参数数组 [arg1, arg2, …] 传递给被调用的函数作为参数。
下面是个间接调用的示例:
function increment(number) {return ++number;}increment.call(undefined, 10); // => 11increment.apply(undefined, [10]); // => 11
increment.call() 和 increment.apply() 都调用了自增函数,参数为 10。
主要不同是,.call() 接受一个参数列表,例如 myFun.call(thisValue, ‘val1’, ‘val2’)。而 .apply() 接受一个参数数组,即 myFunc.apply(thisValue, [‘val1’, ‘val2’])。
5.1. 间接调用的 this
间接调用中的 this 就是 .call() 或者 .apply() 的第一个参数。
很明显 this 在间接调用中就是 .call() 或者 .apply() 传递的一个参数。

下面是一个间接调用上下文的例子:
var rabbit = { name: 'White Rabbit' };function concatName(string) {console.log(this === rabbit); // => truereturn string + this.name;}// Indirect invocationsconcatName.call(rabbit, 'Hello '); // => 'Hello White Rabbit'concatName.apply(rabbit, ['Bye ']); // => 'Bye White Rabbit'
当函数执行需要指定上下文时非常有用。可以解决函数调用时 this 总为 window 或者严格模式下 undefined (见2.3.)的上下文问题。可以用来实现对象的方法调用(见之前的代码示例)。
另一个例子是在 ES5 中调用双亲的构造器生成类的继承关系:
function Runner(name) {console.log(this instanceof Rabbit); // => truethis.name = name;}function Rabbit(name, countLegs) {console.log(this instanceof Rabbit); // => true// Indirect invocation. Call parent constructor.Runner.call(this, name);this.countLegs = countLegs;}var myRabbit = new Rabbit('White Rabbit', 4);myRabbit; // { name: 'White Rabbit', countLegs: 4 }
Rabbit 中 Runner.call(this, name) 对双亲函数进行了间接调用来初始化对象。
6. 绑定函数
绑定函数是连接一个对象的函数。常常由原函数调用 .bind() 方法生成。原函数和绑定函数有着相同的代码和作用域,但是在执行时有着不同的上下文。
.bind(thisArg[, arg1[, arg2[, …]]]) 方法接受第一个参数 thisArg 作为调用时绑定函数的上下文以及可选的参数列表 arg1, arg2, … 传递给调用的函数作为参数。并返回一个绑定了 thisArg 的函数。
下面的代码创建了一个绑定函数并调用:
function multiply(number) {'use strict';return this * number;}// create a bound function with contextvar double = multiply.bind(2);// invoke the bound functiondouble(3); // => 6double(10); // => 20
multiply.bind(2) 返回了一个新函数对象 double,并绑定了数字 2。multiply 和 double 有相同的代码和作用域。
和 .apply() 和 .call() (见第 5 节)会立即调用不同,.bind() 方法仅会返回一个新函数,并在之后的调用中使用预先配置的 this。
6.1. 绑定函数的 this
当调用绑定函数时 this 就是 .bind() 的第一个参数
.bind() 的作用是生成新函数,并在之后的调用中使用第一个参数作为上下文。这允许使用一个预知的 this 值来生成函数。

让我们看下如何配置绑定函数的 this :
var numbers = {array: [3, 5, 10],getNumbers: function() {return this.array;}};// Create a bound functionvar boundGetNumbers = numbers.getNumbers.bind(numbers);boundGetNumbers(); // => [3, 5, 10]// Extract method from objectvar simpleGetNumbers = numbers.getNumbers;simpleGetNumbers(); // => undefined or throws an error in strict mode
numbers.getNumbers.bind(numbers) 返回了一个绑定 numbers 对象的函数 boundGetNumbers。boundGetNumbers() 调用时 this 就是 numbers 并返回了正确的数组对象。
numbers.getNumbers 没有使用绑定然后抽离并赋给 simpleGetNumbers。调用 simpleGetNumbers,this 为 window 或者 undefined,并不是 numbers 对象(见3.2. 陷阱)。simpleGetNumbers() 没有返回正确的数组。
6.2. 紧上下文绑定
.bind() 生成 不变的上下文链接 并永久保持。绑定函数不运行改变已链接的上下文,即使你调用 .call() 或者 .apply() 应用一个不同的上下文,甚至是重新绑定(译者注:调用bind())。
只有绑定函数的构造器调用可以改变,然而这种方式并不被推荐(使用构造器调用,而非绑定函数)。
下面的例子中创建了一个绑定函数,然后尝试更改已经预定义的上下文:
function getThis() {'use strict';return this;}var one = getThis.bind(1);// Bound function invocationone(); // => 1// Use bound function with .apply() and .call()one.call(2); // => 1one.apply(2); // => 1// Bind againone.bind(2)(); // => 1// Call the bound function as a constructornew one(); // => Object
只有 new one() 改变了绑定函数的上下文,其他调用类型的 this 仍等于 1。
7. 箭头函数
箭头函数 的目的是以更简短的方式声明函数并在词法上绑定上下文。
用法:
var hello = (name) => {return 'Hello ' + name;};hello('World'); // => 'Hello World'// Keep only even numbers[1, 2, 5, 6].filter(item => item % 2 === 0); // => [2, 6]
箭头函数有更轻量的语法,省略了关键字 function。当函数只有一个语句时,你甚至可以省略 return。
箭头函数是匿名的,这意味着(译者注:函数的) name 属性为空字符串 ‘’。这种情况下它没有函数名(这对于递归很有用,分离了事件句柄)。
同时,与常规函数不同,它不提供 arguments 对象。然而在 ES2015 中使用 剩余参数 可以解决这个问题。
var sumArguments = (...args) => {console.log(typeof arguments); // => 'undefined'return args.reduce((result, item) => result + item);};sumArguments.name // => ''sumArguments(5, 5, 6); // => 16
7.1. 箭头函数的 this
this 是定义箭头函数的 封闭上下文
箭头函数不会创建自己的执行上下文,this 来自定义箭头函数的外部函数。

下面例子展示了上下文的透明性:
class Point {constructor(x, y) {this.x = x;this.y = y;}log() {console.log(this === myPoint); // => truesetTimeout(()=> {console.log(this === myPoint); // => trueconsole.log(this.x + ':' + this.y); // => '95:165'}, 1000);}}var myPoint = new Point(95, 165);myPoint.log();
setTimeout 调用箭头函数,并和 log() 方法一样使用相同的上下文(myPoint 对象)。可以看到,箭头函数 ‘继承’ 了定义它的函数的上下文。
在这个例子中如果用常规函数则会创建自己的上下文(window 或者 undefined)。因此想要相同的代码正确运行有必要绑定上下文:setTimeout(function() {…}.bind(this))。这不太简洁,使用箭头函数则非常清晰简单。
如果箭头函数定义在顶级作用域(在任何函数之外),上下文永远是全局对象(在浏览器中就是 window):
var getContext = () => {console.log(this === window); // => truereturn this;};console.log(getContext() === window); // => true
箭头函数绑定词法上下文仅一次并永久绑定。使用上下文改变方法(译者注:.call() 、.apply() 这些)也无法修改:
var numbers = [1, 2];(function() {var get = () => {console.log(this === numbers); // => truereturn this;};console.log(this === numbers); // => trueget(); // => [1, 2]// Use arrow function with .apply() and .call()get.call([0]); // => [1, 2]get.apply([0]); // => [1, 2]// Bindget.bind([0])(); // => [1, 2]}).call(numbers);
使用 .call(numbers) 对函数表达式间接调用,调用的 this 是 numbers。箭头函数 get 的 this 也是 numbers,因为它使用词法上下文。
无论 get 如何调用,箭头函数永远保持初始的上下文 numbers。get.call([0]) 和 . get.apply([0]) 使用其他的上下文进行间接调用,以及 get.bind([0])() 进行重绑定也无效。
箭头函数不能用作构造器。如果使用 new get() 进行构造器调用,JavaScript 会抛出错误:TypeError: get is not a constructor。
7.2. 陷阱:使用箭头函数定义方法
你可能想要使用箭头函数来为对象声明方法。当然可以:使用 (param) => {…} 而非 function(param) {..},相对于函数表达式会非常简短。
下面的例子中我们对 Period 类使用箭头函数定义 format() 方法:
function Period (hours, minutes) {this.hours = hours;this.minutes = minutes;}Period.prototype.format = () => {console.log(this === window); // => truereturn this.hours + ' hours and ' + this.minutes + ' minutes';};var walkPeriod = new Period(2, 30);walkPeriod.format(); // => 'undefined hours and undefined minutes'
因为 format 是箭头函数并在全局上下文(顶级作用域)中定义,那么 this 就是 window 对象。
即使 format 在对象中作为方法执行:walkPeriod.format() ,window 仍是调用的上下文。因为箭头函数有一个静态上下文在不同的调用类型中不会改变。this 就是 window,因此 this.hours 和 this.minutes 都是 undefined。方法会返回:‘undefined hours and undefined minutes’,这与你预期的结果不一样。
函数表达式可以解决这个问题,因为常规函数的上下文依赖于调用(译者注:类型):
function Period (hours, minutes) {this.hours = hours;this.minutes = minutes;}Period.prototype.format = function() {console.log(this === walkPeriod); // => truereturn this.hours + ' hours and ' + this.minutes + ' minutes';};var walkPeriod = new Period(2, 30);walkPeriod.format(); // => '2 hours and 30 minutes'
walkPeriod.format() 是一个对象的方法调用(见 3.1.),上下文是 walkPeriod 对象。this.hours 等于 2 ,this.minutes 等于 30,因此方法返回正确的结果:‘2 hours and 30 minutes’。
8. 总结
因为函数调用(译者注:类型)对于 this 有很大的影响,从现在开始 不要 问自己:
this 从哪里来?
而是 要问自己:
函数是 如何调用 的?
对于箭头函数要问自己:
箭头函数在 定义处 的 this 是什么?
处理 this 时思路正确才不会令你头疼。
如果你对上下文陷阱的例子感兴趣或者刚刚碰到了这些困难,在下面进行评论让我们一起讨论!
传播关于JavaScript的知识并分享该帖子,你的同事会感激你。
记住,不要丢掉你的上下文 ;)
