JavaScript中this关键字的优雅解释
译者:道奇 作者:Dmitri Pavlutin 原文: Gentle Explanation of this Keyword in JavaScript
1.神秘的this
this关键字对于许多刚开始学JavaScript的人来说就是迷一样的存在,它的功能虽然很强大,但是需要花很大的力气才能弄懂它。
像Java, PHP或者其他标准的后台开发语言中,this被看作是类方法中当前对象的实例:不多不少,大部分情况下,它不能在方法之外使用,这样的规定让this不至于混乱。
但在JavaScript里情况就不一样了:this是函数的当前执行上下文,并且有4种函数调用类型:
- 函数调用:
alert('Hello World!') - 方法调用:
console.log('Hello World!') - 构造函数调用:
new RegExp('\\d') - 间接调用:
alert.call(undefined, 'Hello World!')
每种调用类型以自己的方式定义了上下文,所以this的行为就和开发者们预期的有点不太一样。
此外,严格模式也会影响执行的上下文。
理解this关键字的关键在于需要对函数调用有清晰的理解并且知道它是怎样影响上下文的。
这篇文章主要聚焦于调用的解释,函数调用对this有怎样的影响,并演示了识别上下文的常见陷阱。
开始之前,先熟悉一下几个术语:
- 调用(
Invocation),函数的调用就是执行函数主体代码,或者就是简单的函数调用。例如,parseInt函数调用是parseInt('15') - 上下文(
Context),调用的上下文是函数体内this的值。例如,map是map.set('key','value')这个调用的上下文。 - 域(
Scope),函数的域是函数体中可访问的变量、对象、函数的集合。
2.函数调用
当一个表达式计算为函数对象后紧跟开始圆括号(, 以逗号分隔的参数表达式列表和结束),就是函数调用,例如parseInt('18')。
函数调用表达式不可以是属性访问器obj.myFunc(),因为它会创建一个方法调用。例如[1,5].join(',')不是函数调用,而是方法调用。记住这个区别很重要。
简单的函数调用例子:
function hello(name) {
return 'Hello ' + name + '!';
}
// 函数调用
const message = hello('World');
console.log(message); // => 'Hello World!'
hello('world')是个函数调用:hello表达式相当于是个函数对象,后跟一对包含'world'参数的圆括号。
更复杂的例子就是IIFE(立即调用函数表达式):
const message = (function(name) {
return 'Hello ' + name + '!';
})('World');
console.log(message) // => 'Hello World!'
IIFE也是函数调用:第一对圆括号(function(name) {...})相当于是个函数对象,后跟后跟一对包含'world'参数的圆括号:('world')。
2.1. 函数调用中的this
this在函数调用中是个全局对象
全局对象的值由执行环境决定。在浏览器中,它是window对象。
在函数调用中,执行上下文是全局对象。
看一下下面函数中的上下文:
function sum(a, b) {
console.log(this === window); // => true
this.myNumber = 20; // 向全局对象中添加'myNumber'属性
return a + b;
}
// sum()作为函数被调用
// sum()中的this是全局对象(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!'
<!-- 在html文件中 -->
<script type="text/javascript">
console.log(this === window); // => true
</script>
2.2 严格模式下,函数调用中的this
"严格模式下,函数调用中的this等于undefined"
严格模式是从ECMAScript 5.1版本开始引入的,它限制了JavaScript一些的多变性,但提供了更好的安全性和更强的错误检查。
在函数体的头部放上指令'use strict'就可以启动严格模式。
一旦启动,严格模式就会影响执行上下文,常规函数调用中的this就会变成undefined,执行上下文就不是全局对象了,对比上面的2.1的情况。
严格模式下函数调用的例子:
function multiply(a, b) {
'use strict'; // 开启严格模式
console.log(this === undefined); // => true
return a * b;
}
// multiply() 开启严格模式下的函数调用
// multiply()中的this等于undefined
multiply(2, 5); // => 10
当multiply(2, 5)作为函数被调用,this等于undefined。
严格模式不仅在当前作用域下有效,在作用域内部也有效(所有内部声明的函数):
function execute() {
'use strict'; // 激活严格模式
function concat(str1, str2) {
// 严格模式也会开启
console.log(this === undefined); // => true
return str1 + str2;
}
// 严格模式下,concat()作为函数被调用
// concat()内的this等于undefined
concat('Hello', ' World!'); // => "Hello World!"
}
execute();
'use strict'放在执行主体的头部,可以开启所在域的严格模式。因为concat是在执行域内申明的,它就继承了严格模式。调用concat('Hello', ' World!')时this的值会变成undefined。
一个JavaScript文件可能同时包含严格和非严格模式,在同一调用类型的单个脚本中可以有不同的上下文行为:
function nonStrictSum(a, b) {
// 非严格模式
console.log(this === window); // => true
return a + b;
}
function strictSum(a, b) {
'use strict';
// 开启严格模式
console.log(this === undefined); // => true
return a + b;
}
// 非严格模式下,nonStrictSum()作为函数被调用
// nonStrictSum()里的this是window对象
nonStrictSum(5, 6); // => 11
// 严格模式下,strictSum()作为函数被调用
// strictSum()内的this是undefined
strictSum(8, 12); // => 20
2.3 陷阱:内部函数中的this
⚠️ 函数调用中常见的一个陷阱就是认为this在函数内部和函数外部是一样的。
👍 正确的是内部函数的上下文仅仅依赖于它的调用类型,而不是外部函数的上下文。
想将this设置成所需的值,需要通过间接调用(使用.call()或.apply(),看第5节)或创建绑定函数(使用.bind(),看第6节)来修改内部函数的上下文。
下面的例子是计算两个数值的和:
const numbers = {
numberA: 5,
numberB: 10,
sum: function() {
console.log(this === numbers); // => true
function calculate() {
// 在严格模式下,this是window或undefined
console.log(this === numbers); // => false
return this.numberA + this.numberB;
}
return calculate();
}
};
numbers.sum(); // => 在严格模式下是NaN或抛出类型异常的错误
⚠️ numbers.sum()是对象上的方法调用(看第3节),所以sum中的上下文是numbers对象。calculate函数定义在sum中,所以你可能会认为calculate()中的this也是指向numbers对象的。
然而calculate()是函数调用(而非方法调用),它会将this设置为全局对象window(2.1的例子)或在严格模式下的undefined(2.2的例子)。尽管numbers对象是外部函数sum的上下文,但是在calculate()内部它是没有任何影响的。
numbers.sum() 的调用结果是NaN(或者抛出TypeError错误:在严格模式下,不能读取undefined的'numberA'属性),很明显不是预期结果5 + 10 = 15,这些都是因为calculate没有正确的被调用。
👍 要解决这个问题,就要使calculate函数执行的上下文和sum的一样,这样就可以访问到numberA和numberB属性了。
一种解决方案是通过调用calculate.call(this)(函数的间接调用,看第5节)手动将calculate的上下文改成需要的上下文。
const numbers = {
numberA: 5,
numberB: 10,
sum: function() {
console.log(this === numbers); // => true
function calculate() {
console.log(this === numbers); // => true
return this.numberA + this.numberB;
}
// 使用.call()方法修改上下文
return calculate.call(this);
}
};
numbers.sum(); // => 15
calculate.call(this)正常执行calculate函数,但是另外会将上下文改成特定值,特定值以参数有形式传入了。
现在this.numberA + this.numberB等于numbers.numberA + numbers.numberB,函数返回预期结果5 + 10 = 15。
另外一种解决方法会更好一点,那就是使用箭头函数:
const numbers = {
numberA: 5,
numberB: 10,
sum: function() {
console.log(this === numbers); // => true
const calculate = () => {
console.log(this === numbers); // => true
return this.numberA + this.numberB;
}
return calculate();
}
};
numbers.sum(); // => 15
箭头函数从词法上绑定this,或者简单的使用sum()方法的this值。
3. 方法调用(invocation)
方法是存储在对象属性里的函数。例如:
const myObject = {
// helloFunction是个方法
helloFunction: function() {
return 'Hello World!';
}
};
const message = myObject.helloFunction();
helloFunction是myObject的方法,可以通过属性访问的方式访问这个方法:myObject.helloFunction。
当表达式是属性访问的形式,并且该表达式是函数对象后紧跟开始圆括号(, 以逗号分隔的参数表达式列表和结束),方法调用(invocation)就会执行。
回忆一下前面的例子,myObject.helloFunction()是myObject对象上helloFunction的方法调用。方法调用(call)还有:[1, 2].join(',')或/\s/.test('beautiful world')。
区分函数调用(看第二节)和方法调用是很重要的,它们是不同的类型,主要的区别在于方法调用需要通过属性访问器的形式调用函数(obj.myFunc()或者obj['myFunc']()),而函数调用就不需要(myFunc))。
['Hello', 'World'].join(', '); //方法调用
({ ten: function() { return 10; } }).ten(); // 方法调用
const obj = {};
obj.myFunction = function() {
return new Date().toString();
};
obj.myFunction(); // 方法调用
const otherFunction = obj.myFunction;
otherFunction(); // 函数调用
parseFloat('16.60'); // 函数调用
isNaN(0); // 函数调用
理解函数调用和方法调用的区别可以帮助我们正确的识别上下文。
3. 方法调用中的this
"this是方法调用中方法所属的对象"
当调用对象上的方法时,this是对象本身。
创建一个对象,让其包括一个数字递增的函数:
const calc = {
num: 0,
increment: function() {
console.log(this === calc); // => true
this.num += 1;
return this.num;
}
};
// 函数调用. this是calc
calc.increment(); // => 1
calc.increment(); // => 2
调用calc.increment(),使得increment函数的上下文指向calc对象,所以使用this.num递增num属性是正常工作的。
看另外一种情况,JavaScript对象是从它的prototype中继承方法的,当继承的方法在对象上被调用,调用的上下文仍然是对象本身:
const myDog = Object.create({
sayName: function() {
console.log(this === myDog); // => true
return this.name;
}
});
myDog.name = 'Milo';
// 函数调用. this是myDog
myDog.sayName(); // => 'Milo'
Object.create()创建一个新的对象myDog并且从第一个参数设置它的原型,myDog对象继承了sayName方法。
当myDog.sayName()执行的时候,myDog是调用的上下文。
在ECMAScript 2015的类语法中,方法调用的上下文就是实例本身:
class Planet {
constructor(name) {
this.name = name;
}
getName() {
console.log(this === earth); // => true
return this.name;
}
}
var earth = new Planet('Earth');
// 方法调用. 上下文是earth
earth.getName(); // => 'Earth'
3.2陷阱:将方法和它的对象分离
⚠��� 方法可以从对象中抽取出来成为独立的变量const alone = myObj.myMethod。当函数单独被调用时,就脱离了原来的对象alone,你可能会觉得this是在定义方法的对象myObject上。
👍 正确的是如果方法没有通过对象调用,会执行函数调用,也就是说,在严格模式下this是全局对象window或undefined(看2.1和2.2)。
绑定函数const alone = myObj.myMethod.bind(myObj)(使用.bind(), 看第6节)可以修正上下文,使它依旧拥有这个方法。
下面的例子定义了Animal构造函数并且创建了它的实例:myCat,然后1秒后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');
}
}
const myCat = new Animal('Cat', 4);
// logs "The undefined has undefined legs"
// 或者在严格模式下抛出类型异常的错误
setTimeout(myCat.logInfo, 1000);
⚠️ 你可能会认为setTimeout会调用myCat.logInfo(),这样记录的就是myCat对象的信息。
很不幸,当作为参数:setTimout(myCat.logInfo)传入时,方法和它的对象分离了,下列情况相同:
setTimout(myCat.logInfo);
// 等价于:
const 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');
};
}
const myCat = new Animal('Cat', 4);
// logs "The Cat has 4 legs"
setTimeout(myCat.logInfo.bind(myCat), 1000);
myCat.logInfo.bind(myCat)返回一个新的函数,它的执行方式与logInfo完全相同,但是它的this是myCat,即使在函数调用中也是如此。
另外一种选择是将logInfo()函数定义成箭头函数,可以从词法上绑定this:
function Animal(type, legs) {
this.type = type;
this.legs = legs;
this.logInfo = () => {
console.log(this === myCat); // => true
console.log('The ' + this.type + ' has ' + this.legs + ' legs');
};
}
const myCat = new Animal('Cat', 4);
// logs "The Cat has 4 legs"
setTimeout(myCat.logInfo, 1000);
4.构造函数调用
构造函数调用是在new关键字后面接一个表达式时执行的,该表达式是函数对象后紧跟开始圆括号(, 以逗号分隔的参数表达式列表和结束)。
例如: new Anumal('cat', 4), new RegExp('\\d')
下面的例子申明了一个函数Country,然后用构造函数的方式调用它:
function Country(name, traveled) {
this.name = name ? name : 'United Kingdom';
this.traveled = Boolean(traveled); // 转成布尔值
}
Country.prototype.travel = function() {
this.traveled = true;
};
//构造函数调用
const france = new Country('France', false);
// Constructor invocation
const unitedKingdom = new Country;
france.travel(); // Travel to France
new Country('France', false)是Country函数的构造函数调用,该调用创建了一个新对象,该对象有个'France'的name属性。
如果调用无参构造函数,那圆括号可以省略:new Country。
从ECMAScript 2015开始,JavaScript允许使用class语法定义构造函数:
class City {
constructor(name, traveled) {
this.name = name;
this.traveled = false;
}
travel() {
this.traveled = true;
}
}
// 构造函数调用
const 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';
}
// 构造函数调用
const fooInstance = new Foo();
fooInstance.property; // => 'Default Value'
new Foo()创建了上下文是fooInstance的构造函数调用,对象在Foo中进行初始化:给this.property设置了默认值。
同样的场景发生成使用class语法的时候,在构造函数方法中只有初始化语句:
class Bar {
constructor() {
console.log(this instanceof Bar); // => true
this.property = 'Default Value';
}
}
// 构造函数
const 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
const 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,你可能会认为Vehicle('Car', 4)在创建和初始化新对象时实现的很好。
但是this在函数调用中是window对象(看2.1节),因此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;
}
// 构造函数调用
const car = new Vehicle('Car', 4);
car.type // => 'Car'
car.wheelsCount // => 4
car instanceof Vehicle // => true
// 函数调用.抛出错误.
const brokenCar = Vehicle('Broken Car', 3);
new Vehicle('Car', 4)执行没有出错:创建了新对象并且完成初始化,因为new关键字目前是在进行构造函数调用。
为了确保执行上下文是对的对象类型,在构造函数调中增加了一步验证:this instanceof Vehicle,如果this不是Vehicle类型,就会抛出一个错误。任何时候不用new执行Vehicle('Broken Car', 3)就会抛出异常错误: Error: Incorrect invocation。
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;
}
// 间接调用
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
// 间接调用,调用父构造函数.
Runner.call(this, name);
this.countLegs = countLegs;
}
const myRabbit = new Rabbit('White Rabbit', 4);
myRabbit; // { name: 'White Rabbit', countLegs: 4 }
Rabbit中的Runner.call(this, name)创建了父函数的间接调用来初始化对象。
6. 绑定函数
绑定函数是连接对象的函数,通常使用.bind()方法从原始函数创建。原始函数和绑定函数共享同样的代码和作用域,但是执行时的上下文是不同的。
myFunc.bind(thisArg[, arg1[, arg2[, ...]]])方法将第一个参数thisArg用作调用时绑定函数的上下文,可选的参数列表 arg1, arg2, ...作为参数传入被调函数,返回一个绑定了thisArg的新函数。
下面的代码创建了一个绑定函数并进行调用:
function multiply(number) {
'use strict';
return this * number;
}
// 创建带上下文的绑定函数
const double = multiply.bind(2);
// 调用绑定函数
double(3); // => 6
double(10); // => 20
multiply.bind(2)返回一个新的函数对象double,这个对象绑定了数字2,multiply和double有同样的代码和作用域。
对比.apply()和.call()方法(看第5节)以正确的方式调用函数,.bind()方法只返回新的函数,这个函数应该在之后使用预设的this值进行调用。
6.1 绑定函数中的this
"当调用绑定函数时,this是.bind()的第一个参数"
.bind()的作用就是创建一个新函数,调用该函数会将上下文作为第一个参数传入.bind(),这是一种强大的技术,它允许创建包含预定义this值的函数。
看一下怎样配置绑定函数的this:
const numbers = {
array: [3, 5, 10],
getNumbers: function() {
return this.array;
}
};
// 创建绑定函数
const boundGetNumbers = numbers.getNumbers.bind(numbers);
boundGetNumbers(); // => [3, 5, 10]
// 从对象中提取方法
const simpleGetNumbers = numbers.getNumbers;
simpleGetNumbers(); // => 严格模式下undefined或抛出错误
numbers.getNumbers.bind(numbers)返回绑定了numbers对象的函数boundGetNumbers,boundGetNumbers()将numbers作为this进行调用并返回正确的数组对象。
函数numbers.getNumbers可以提取出来直接赋值给simpleGetNumbers,之后的函数调用simpleGetNumbers()内this的值(严格模式下是windw或undefiend),而不是numbers对象(看3.2节的陷阱),这种情况下simpleGetNumbers()不会正确返回数组。
6.2稳固上下文绑定
.bind()创造了一个永久性的上下文链接并且会一直保留它,在对不同上下文使.call()或.apply()时,绑定函数是不能改变它的链接上下文,就算重新绑定也不会有用。
只有绑定函数的构造函数调用可以改变已经绑定的上下文,但通常你不会这样做(构造函数调用必须使用常规的非绑定函数)。
下面的例子创建了一个绑定函数,然后试图改变已经预定义的上文:
function getThis() {
'use strict';
return this;
}
const one = getThis.bind(1);
// 绑定函数调用
one(); // => 1
// Use bound function with .apply() and .call()
one.call(2); // => 1
one.apply(2); // => 1
// 再次绑定
one.bind(2)(); // => 1
// 以构造函数的方式调用绑定函数
new one(); // => Object
只有new one()改变了绑定函数的上下文,其他调用类型全部得到的是this等于1。
7. 箭头函数
箭头函数是设计用于以简短的形式声明方法的并在词法上绑定上下文。
可以这样使用:
onst hello = (name) => {
return 'Hello ' + name;
};
hello('World'); // => 'Hello World'
// 只保留偶数
[1, 2, 5, 6].filter(item => item % 2 === 0); // => [2, 6]
箭头函数语法比较简单,不需要使用啰嗦的的关键字function,当箭头函数只有一个语句,你甚至可以省略return关键字。
箭头函数是匿名的,这意味着name属性是空字符串'',这种情况,它就没有词法上的函数名(这对递归、分离事件处理程序有用)。
同样的,与常规函数相反,它没有提供arguments对象,ES2015可以使用rest参数修复缺失的arguments:
const 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,换句话说,箭头函数从词法上绑定了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);
}
}
const myPoint = new Point(95, 165);
myPoint.log();
setTimeout调用了和log()方法有同样上下文(myPoint对象)的箭头函数,正如所看到的,箭头函数“继承”了函数定义所在的上下文。
这个例子中的常规函数会创建自己的上下文(严格模式下是window或undefined),所以为了要使相同的代码与函数表达式一起正常运行,有必要手动绑定上下文:setTimeout(function() {...}.bind(this)),这个显示很啰嗦,使用箭头函数就会更简短。
如果箭头函数定义在顶层域上(在任何函数外面),上下文经常是全局对象(浏览器中是window):
const getContext = () => {
console.log(this === window); // => true
return this;
};
console.log(getContext() === window); // => true
箭头函数与词法上下文绑定一次就永久有效,就算使用修改上下文的方法也不能修改this。
const numbers = [1, 2];
(function() {
const 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);
不管箭头函数get是怎样调用的,它总是保留着词法上下文numbers,通过其他上下文get.call([0])或get.apply([0])进行间接调用,重新绑定get.bind([0])()也不会有影响。
箭头函数不能用于构造函数,使用构造函数new get()来调用会抛出错误:类型异常:get不是构造函数。
7.2 陷进:使用箭头函数定义函数
你可能需要在对象上使用箭头函数来声明方法,很好:箭头函数的声明比函数表达式简短多了:用(param) => {...}代替function(param) {..}。
下面的例子在类Person上使用箭头函数定义了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';
};
const walkPeriod = new Period(2, 30);
walkPeriod.format(); // => 'undefined hours and undefined minutes'
因为format是定义在全局上文(顶层域)中的箭头函数,它内部的this是window对象。
就算format是在对象walkPeriod.format()上以方法执行的,window依然作为调用的上下文。因为箭头函数有个不会因为调用类型不同而改变的静态上下文。
方法返回'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';
};
const 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这个问题时,会让你免于头痛。
转载自:https://juejin.cn/post/6844903989784346637