原文: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 invocation
var 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); // => true
this.myNumber = 20; // add 'myNumber' property to global object
return a + b;
}
// sum() is invoked as a function
// this in sum() is a global object (window)
sum(15, 16); // => 31
window.myNumber; // => 20
当 sum(15, 16) 被调用时,JavaScript 自动的把 this 设置为全局对象,在浏览器中即 window。
当 this 在函数作用域之外使用时(即顶级作用域:全局执行上下文),它是全局对象:
console.log(this === window); // => true
this.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 mode
console.log(this === undefined); // => true
return a * b;
}
// multiply() function invocation with strict mode enabled
// this in multiply() is undefined
multiply(2, 5); // => 10
当 multiply(2, 5) 被调用时,this 为 undefined。
严格模式不仅对当前作用域起作用,还对内部作用域起作用(所有内部声明的函数)。
function execute() {
'use strict'; // activate the strict mode
function concat(str1, str2) {
// the strict mode is enabled too
console.log(this === undefined); // => true
return str1 + str2;
}
// concat() is invoked as a function in strict mode
// this in concat() is undefined
concat('Hello', ' World!'); // => "Hello World!"
}
execute();
‘use strict’ 出现在 execute 函数体的顶部时,在它的作用域内就启用了严格模式。因为 concat 在 execute 作用域内被声明,它因此继承了严格模式。在调用 concat(‘Hello’, ‘ World!’) 时,this 等于 undefined。
单个 JavaScript 文件可能同时包含严格和非严格模式。因此,在单个脚本中的同一种调用类型可能有着不同的上下文行为:
function nonStrictSum(a, b) {
// non-strict mode
console.log(this === window); // => true
return a + b;
}
function strictSum(a, b) {
'use strict';
// strict mode is enabled
console.log(this === undefined); // => true
return a + b;
}
// nonStrictSum() is invoked as a function in non-strict mode
// this in nonStrictSum() is the window object
nonStrictSum(5, 6); // => 11
// strictSum() is invoked as a function in strict mode
// this in strictSum() is undefined
strictSum(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); // => true
function calculate() {
// this is window or undefined in strict mode
console.log(this === numbers); // => false
return 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); // => true
function calculate() {
console.log(this === numbers); // => true
return this.numberA + this.numberB;
}
// use .call() method to modify the context
return 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 method
helloFunction: 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 invocation
var obj = {};
obj.myFunction = function() {
return new Date().toString();
};
obj.myFunction(); // method invocation
var otherFunction = obj.myFunction;
otherFunction(); // function invocation
parseFloat('16.60'); // function invocation
isNaN(0); // function invocation
理解函数调用和方法调用之间的区别有助于正确识别上下文。
3.1. 方法调用的 this
方法调用中的 this 就是 拥有该方法的对象
对对象的方法调用时,this 就是对象本身:
让我们写一个对象,其中一个方法实现了自增:
var calc = {
num: 0,
increment: function() {
console.log(this === calc); // => true
this.num += 1;
return this.num;
}
};
// method invocation. this is calc
calc.increment(); // => 1
calc.increment(); // => 2
调用 calc.increment() 时 increment 函数的上下文即 calc 对象。因此,使用 this.num 会增加属性数值。
我们来看另一个例子。一个 JavaScript 对象从它的 原型 继承了一个方法。当继承的方法在对象上调用时,调用的上下文仍然是对象本身:
var myDog = Object.create({
sayName: function() {
console.log(this === myDog); // => true
return this.name;
}
});
myDog.name = 'Milo';
// method invocation. this is myDog
myDog.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); // => true
return this.name;
}
}
var earth = new Planet('Earth');
// method invocation. the context is earth
earth.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); // => false
console.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 mode
setTimeout(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); // => true
console.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 invocation
var france = new Country('France', false);
// Constructor invocation
var 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 invocation
var 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); // => true
this.property = 'Default Value';
}
// Constructor invocation
var fooInstance = new Foo();
fooInstance.property; // => 'Default Value'
new Foo() 调用了构造器,上下文是 fooInstance。Foo 内部对象进行了初始化:this.property 被赋了一个默认值。
当使用 class (ES2015)语法时也是一样,只是初始化发生在了 constructor 方法。
class Bar {
constructor() {
console.log(this instanceof Bar); // => true
this.property = 'Default Value';
}
}
// Constructor invocation
var 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; // => true
reg2 instanceof RegExp; // => true
reg1.source === reg2.source; // => true
当执行 new RegExp(‘\w+’) 和 RegExp(‘\w+’) 时,JavaScript会生成等价的正则表达式对象。
使用函数调用生成对象是一个潜在的问题(除了工厂模式),因为某些构造器可能在缺失 new 关键字时忽略初始化对象的逻辑。
下面的例子说明了这个问题:
function Vehicle(type, wheelsCount) {
this.type = type;
this.wheelsCount = wheelsCount;
return this;
}
// Function invocation
var car = Vehicle('Car', 4);
car.type; // => 'Car'
car.wheelsCount // => 4
car === 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 invocation
var car = new Vehicle('Car', 4);
car.type // => 'Car'
car.wheelsCount // => 4
car 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); // => 11
increment.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); // => true
return string + this.name;
}
// Indirect invocations
concatName.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); // => true
this.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 context
var double = multiply.bind(2);
// invoke the bound function
double(3); // => 6
double(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 function
var boundGetNumbers = numbers.getNumbers.bind(numbers);
boundGetNumbers(); // => [3, 5, 10]
// Extract method from object
var 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 invocation
one(); // => 1
// Use bound function with .apply() and .call()
one.call(2); // => 1
one.apply(2); // => 1
// Bind again
one.bind(2)(); // => 1
// Call the bound function as a constructor
new 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); // => true
setTimeout(()=> {
console.log(this === myPoint); // => true
console.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); // => true
return this;
};
console.log(getContext() === window); // => true
箭头函数绑定词法上下文仅一次并永久绑定。使用上下文改变方法(译者注:.call() 、.apply() 这些)也无法修改:
var numbers = [1, 2];
(function() {
var get = () => {
console.log(this === numbers); // => true
return this;
};
console.log(this === numbers); // => true
get(); // => [1, 2]
// Use arrow function with .apply() and .call()
get.call([0]); // => [1, 2]
get.apply([0]); // => [1, 2]
// Bind
get.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); // => true
return 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); // => true
return 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的知识并分享该帖子,你的同事会感激你。
记住,不要丢掉你的上下文 ;)