简析JavaScript中的this关键字

旧城冷巷雨未停 提交于 2019-12-02 10:14:50

本文为译文,文章有点长,但是仔细通篇阅读下来,关于this的识别问题基本就搞定了。由于译者水平有限,文中有纰漏之处,还请读者多多指正。下面看正文吧:

1. 谜之this

在很长一段时间内,this关键字都让我感到迷惑,相信很多JavaScript的初学者也是一样。this是JavaScript中很强大的一个特点,但是想搞懂它,你必须得花点时间。

对于像Java、PHP这样的标准语言来说,this在类方法中指代的就是调用这个方法的实例。一般来说,this不能在方法外使用,如此简单的规则不会让人迷惑。

但是在JavaScript中情况就有些不同了:this指的是当前函数的执行上下文。在JavaScript中,函数有4种调用类型:

  • 函数调用(function invocation):alert('Hello World')
  • 方法调用(method invocation):console.log('Hello World')
  • 构造调用(constructor invocation):new RegExp(\\d)
  • 间接调用(indirect invocation):alert.call(undefined, 'Hello World')

每种调用类型都有自己定义执行上下文的方式,所以this指代的对象和我们预期的可能稍有不同。

此外严格模式也会影响执行上下文。

理解this的关键在于要对函数的调用类型以及函数调用类型如何影响执行上下文有一个清晰的认识。本篇文章的目的就是解释函数调用的类型、函数调用的类型如何影响this的取值以及演示辨认执行上下文时的常见误区。

在开始之前,我们先熟悉几个概念:

  • 函数调用:直接调用函数,如parseInt的函数调用为parseInt('15')
  • 执行上下文:函数体内this的值,如map.set('key', 'value')set方法的执行上下文是mapset函数体中this指的就是map
  • 函数作用域:函数体内所有可以使用的变量、对象和函数的集合

文章目录:

  1. 谜之this

  2. 函数调用

    2.1 函数调用中的this
    
    2.2 严格模式时函数调用中的this
    
    2.3 误区:内部函数中的this
    
  3. 方法调用

    3.1 方法调用中的this
    
    3.2 误区:从对象提取的方法
    
  4. 构造调用

    4.1 构造调用中的this
    
    4.2 误区:忽略了new
    
  5. 间接调用

    5.1 间接调用中的this
    
  6. 绑定函数

    6.1 绑定函数中的this
    
    6.2 紧密的上下文绑定
    
  7. 箭头函数

    7.1 箭头函数中的this
    
    7.2 使用箭头函数定义方法
    
  8. 结论

2. 函数调用

函数名后面加上一对小括号,括号里可以填写参数,这就是函数调用,如parseInt('18')

函数调用不能写为属性访问的方式,如obj.myFunc()。属性访问的方式称为方法调用,如[1, 5].join(',')不是函数调用,而是方法调用。记住这个区别很重要。

下面是函数调用的简单示例:

function hello(name) {  
  return 'Hello ' + name + '!';
}
// 函数调用
var message = hello('World');  
console.log(message); // => 'Hello World!'  

hello('World')是函数调用,hello函数名后面紧跟了一对小括号,'World'是参数。

下面是一个更高级的示例——立即执行函数(IIFE,immediately-invoked function expression):

var message = (function(name) {  
  return 'Hello ' + name + '!';
})('World');
console.log(message) // => 'Hello World!' 

IIFE也是函数调用,第一个小括号内是函数定义,紧跟的一个小括号是调用,'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

严格模式时,函数调用中的thisundefined

严格模式是从ECMAScript 5.1时被引入的,它是JavaScript的一种限制模式,更安全,并且提供了更强大的错误检查机制。

在函数体的上方添加'use strict'就启用了严格模式。

严格模式一旦被启用,它就会影响执行上下文,使this在函数调用中为undefined

严格模式时函数调用示例:

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)被调用时,thisundefined

严格模式不仅在当前作用域生效,而且在内部的作用域(在函数内部定义的函数)也生效:

function execute() {  
  'use strict'; // 启用严格模式    
  function concat(str1, str2) {
    // 在这里严格模式也生效
    console.log(this === undefined); // => true
    return str1 + str2;
  }
  // concat()在严格模式中进行函数调用
  // this在concat()里为undefined
  concat('Hello', ' World!'); // => "Hello World!"
}
execute();  

'use strict'声明在excute函数体的顶部以便在该函数作用域内启用严格模式。因为concat被声明在excute的作用域内,所以它继承了excute的严格模式,于是concat的函数调用时,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()在非严格模式下进行函数调用
// this在nonStrictSum()中为window对象
nonStrictSum(5, 6); // => 11  
// strictSum()在严格模式下进行函数调用
// this在strictSum()中为undefined
strictSum(8, 12); // => 20  

2.3 误区:内部函数中的this

函数调用一个常见的误区是认为内部函数和外部函数中的this是相同的。

其实内部函数的上下文只依赖函数的调用类型,而不是外部函数的上下文。

如果要指定this的值,我们可以通过间接调用(使用.call().apply())的方式改变内部函数的上下文或者创建一个绑定函数(使用.bind())。

下面是一个计算两个数和的例子:

var 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或抛出TypeError错误(严格模式)

numbers.sum()是对象上的方法调用,所以sum里的上下文是numbers对象。calculate函数定义在sum内部,所以你可能认为在calculate()this也是numbers对象。

然而calculate()是一个函数调用,而不是方法调用,所以它的this为全局对象window或在严格模式时为undefined,尽管外部函数sum的上下文是numbers对象。

numbers.sum()的结果是NaN或在严格模式时抛出一个错误:TypeError: Cannot read property 'numberA' of undefined,因为calculate()this为全局对象window或在严格模式时为undefinedwindow上并没有numberAnumberB

为了解决这个问题,calculate在执行时必须和sum有相同的上下文,以便使用numbersAnumbersB属性。

一个解决方案是通过calculate.call(this)(函数的间接调用)手动改变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;
    }
    // 使用.call()方法修改上下文
    return calculate.call(this);
  }
};
numbers.sum(); // => 15  

calculate.call(this)还是像通常一样执行calculate函数,只不过它的上下文被修改为了传递的第一个参数。现在this.numbersA + this.numbersB就等同于numbers.numbersA + numbers.numbersB,这样就可以得到正确的结果了:5 + 5 = 15

3. 方法调用

方法是存储在对象属性上的函数。例如:

var myObject = {  
  // helloFunction是一个方法
  helloFunction: function() {
    return 'Hello World!';
  }
};
var message = myObject.helloFunction();  

helloFunctionmyObject的一个方法,可以使用属性访问符获取该方法:myObject.helloFunction

属性访问后面加上一对小括号,括号内可以传递参数,这就是方法调用。

还是上面的这个例子,myObject.helloFunction()myObjecthelloFunction的方法调用。下面这些也是方法调用:[1, 2].join(',')/\s/.test('beautiful world')

区分函数调用和方法调用是很重要的,因为它们是不同的调用类型。它们主要的区别是方法调用需要属性访问符(obj.myFunc()obj['myFunc']()),而函数调用则不需要(myFunc())。

下面这些调用示例演示了如何区分它们:

['Hello', 'World'].join(', '); // 方法调用
({ ten: function() { return 10; } }).ten(); // 方法调用
var obj = {};  
obj.myFunction = function() {  
  return new Date().toString();
};
obj.myFunction(); // 方法调用

var otherFunction = obj.myFunction;  
otherFunction();     // 函数调用  
parseFloat('16.60'); // 函数调用  
isNaN(0);            // 函数调用

理解了函数调用和方法调用的不同可以帮助我们正确地识别上下文。

3.1 方法调用中的this

方法调用中的this是该方法的所有者。

当在一个对象上调用方法时,this指的就是该对象。

下面我们创建一个包含自增方法的对象:

var 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的自增。

我们再看一个例子,一个对象从它的原型上继承了一个方法,当继承来的方法在该对象上调用时,上下文仍然是该对象:

var myDog = Object.create({  
  sayName: function() {
    console.log(this === myDog); // => true
    return this.name;
  }
});
myDog.name = 'Milo';  
// 方法调用。this是myDog
myDog.sayName(); // => 'Milo'  

Object.create()创建了原对象的一个子对象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');  
// 方法调用。上下文是earth
earth.getName(); // => 'Earth'  

3.2 误区:从对象提取的方法

对象的方法可以被提取到一个单独的变量中:var alone = myObj.myMethod。当一个方法从对象上分离,单独被调用时:alone(),你或许会认为this还是该对象。

但实际上,一个方法如果不通过对象而直接调用,它就是一个函数调用:this是全局对象window或在严格模式中为undefined

创建一个绑定函数var alone = myObj.myMethod.bind(myObj)(使用.bind())可以固定上下文,使上下文始终为该方法的所有者。

下面的例子声明了一个Animal构造函数,接着创建了它的一个实例——myCat,然后通过setTimeout()在1秒钟后打印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);  
// 打印结果"The undefined has undefined legs"
// 或者在严格模式中抛出一个TypeError错误
setTimeout(myCat.logInfo, 1000); 

你可能会认为setTimeout()会执行myCat.logInfo()那样就会打印myCat的信息了。

但当作为参数传递的时候,方法是从对象提取出来的,这等同于下面的例子:

setTimout(myCat.logInfo);  
// 等同于:
var extractedLogInfo = myCat.logInfo;  
setTimout(extractedLogInfo); 

当提取出的logInfo被作为函数调用时,this是全局对象或在严格模式中为undefined(而不是myCat对象),所以不能打印出对象的信息。

一个函数可以使用.bind()方法绑定一个对象,如果被提取的方法绑定了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);  
// 打印"The Cat has 4 legs"
setTimeout(myCat.logInfo.bind(myCat), 1000);  

myCat.logInfo.bind(myCat)返回了一个等同于logInfo的新函数,但是新函数的thismyCat。即使是进行函数调用,它的this也是myCat

4. 构造调用

new关键字跟上函数名,再加上一对小括号就是构造调用,括号内同样可以传递参数,例如:new RegExp('\\d')

下面这个例子声明了一个Country函数,然后作为构造函数调用:

function Country(name, traveled) {  
   this.name = name ? name : 'United Kingdom';
   this.traveled = Boolean(traveled); // 转换为booleanl类型
}
Country.prototype.travel = function() {  
  this.traveled = true;
};
// 构造调用
var france = new Country('France', false);  
// 构造调用
var unitedKingdom = new Country;

france.travel(); // 到法国旅游 

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;
  }
}
// 构造调用
var paris = new City('Paris', false);  
paris.travel(); 

new City('Paris')是构造调用。对象是通过class中声明的一个特殊方法:constructor初始化的,constructor中的this为新创建的对象。

构造调用创建了一个从构造函数原型继承了属性的新的空对象,constructor的作用是初始化这个新对象。你可能已经知道,构造调用的上下文为新创建的对象,这是下一章的主题。

当属性访问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';
}
// 构造调用
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';
  }
}
// 构造调用
var barInstance = new Bar();  
barInstance.property; // => 'Default Value'

new Bar()一执行,JavaScript就会创建一个空对象,然后设置constructor方法的上下文为该对象,于是就可以使用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;
}
// 函数调用
var car = Vehicle('Car', 4);  
car.type;       // => 'Car'  
car.wheelsCount // => 4  
car === window  // => true  

Vehicle是给上下文对象设置typewheelsCount属性的函数。当执行Vehicle('Car', 4)是,返回了一个对象car,并且它的属性也是正确的:car.type'Car'car.wheelsCount4。你可能认为这不是也很好地创建并初始化了一个新对象嘛。

然而,在函数调用中thiswindow对象,结果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;
}
// 构造调用
var car = new Vehicle('Car', 4);  
car.type               // => 'Car'  
car.wheelsCount        // => 4  
car instanceof Vehicle // => true

// 函数调用。抛出一个错误
var brokenCar = Vehicle('Broken Car', 3);  

如上面代码所示,new Vehicle('Car', 4)很好的起作用了:使用了new关键字,一个新对象被创建并初始化了。

这个例子在构造函数中添加了一个校验this instanceof Vehicle,以保证执行上下文是正确的对象类型,如果this不是Vehicle类型,就报错。这样无论什么时候,执行Vehicle('Broken Car', 3)都会报错:Error: Incorrect invocation,可以确保必须使用new

5. 间接调用

使用myFun.call()myFun.apply()方法调用函数是间接调用。

在JavaScript中,函数本身就是对象,它的类型是Function

函数上的.call.apply()可以用来指定调用函数时的上下文:

  • .call(thisArg[, arg1[, arg2[, ...]])接收的第一个参数thisArg作为执行上下文,后面的arg1arg2、...作为实际的参数。
  • .apply(thisArg, [arg1, arg2, ...])接收的第一个参数作为执行上下文,后面的数组作为实际的参数。

下面的示例演示了间接调用:

function increment(number) {  
  return ++number;  
}
increment.call(undefined, 10);    // => 11 
increment.apply(undefined, [10]); // => 11 

increment.call()increment.apply()都是接收10作为参数执行increment函数。

.call().apply()的不同在于.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'  

当一个函数需要使用指定的上下文执行时,间接调用就很有用了。例如,可以解决函数调用的上下文总是windowundefined(严格模式)的问题,可以用来模拟方法调用(见前面的示例)。

另一个很实用的例子是在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;
}
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的新函数。

下面的代码创建了一个绑定函数,然后调用了它:

function multiply(number) {  
  'use strict';
  return this * number;
}
// 创建一个指定上下文的绑定函数
var double = multiply.bind(2);  
// 调用绑定函数
double(3);  // => 6  
double(10); // => 20  

multipty.bind(2)返回了一个新的函数对象double,它绑定了2作为上下文,但multitydoubly仍然具有相同的函数体和作用域。

.apply().call()立即执行一个函数相反,.bind()方法只是返回了一个新函数。接下来这个新函数被调用时,它的this是之前.bind()的第一个参数。

6.1 绑定函数中的this

绑定函数的this是之前.bind()的第一个参数。

.bind()的作用是创建一个采用第一个参数作为上下文的新函数。这是一个很强大的特点,它可以预先定义this的值。

下面我们看一下如何配置绑定函数的this

var numbers = {  
  array: [3, 5, 10],
  getNumbers: function() {
    return this.array;    
  }
};
// 创建绑定函数
var boundGetNumbers = numbers.getNumbers.bind(numbers);  
boundGetNumbers(); // => [3, 5, 10]  
// 从对象中提取方法
var simpleGetNumbers = numbers.getNumbers;  
simpleGetNumbers(); // => undefined或在严格模式时报错 

numbers.getNumbers.bind(numbers)返回了绑定numbers对象的函数boundGetNumbers,然后调用boundGetNumbers()this就是numbers对象,然后返回了正确的数组对象。

numbers.getNumbers没有使用绑定方法被提取到了变量simpleGetNumbers,接下来的函数调用simpleGetNumbers()thiswindowundefined(严格模式),而不是numbers对象,于是simpleGetNumbers()没法正确返回一个数组。

6.2 紧密的上下文绑定

.bind()创建了一个紧密的上下文绑定,使用.call().apply()也不能改变已经绑定的上下文,即使是重新绑定也不能改变。

但是构造调用可以改变绑定函数的上下文,然而不推荐这样做,构造调用主要是用来调用常规函数的,不是绑定函数,同时如果这样绑定函数也就没有意义了。

下面的示例创建了一个绑定函数,然后试图改变预定义的上下文:

function getThis() {  
  'use strict';
  return this;
}
var one = getThis.bind(1);  
// 绑定函数调用
one(); // => 1  
// 使用.call()和.apply()调用绑定函数
one.call(2);  // => 1  
one.apply(2); // => 1  
// 重新绑定
one.bind(2)(); // => 1  
// 使用构造调用的形式调用绑定函数
new one(); // => Object  

如代码所示,只有new one()能改变绑定函数的上下文,其他类型的调用this总为1

7. 箭头函数

箭头函数被设计用来采用简短的形式声明一个函数,并能在词法上绑定上下文。

下面是箭头函数的简单形式:

var 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参数:

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  

箭头函数会永久地绑定词法上的上下文,就算使用可以修改上下的方法也不能改变它:

var numbers = [1, 2];  
(function() {  
  var get = () => {
    console.log(this === numbers); // => true
    return this;
  };
  console.log(this === numbers); // => true
  get(); // => [1, 2]
  // 使用.apply()和.call()调用箭头函数
  get.call([0]);  // => [1, 2]
  get.apply([0]); // => [1, 2]
  // 绑定
  get.bind([0])(); // => [1, 2]
}).call(numbers);

在上面的代码中,一个函数采用.call(numbers)进行了间接调用,使this的值为numbers,于是内部的箭头函数getthis也成了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。因为箭头函数的上下文为静态上下文,不会随着调用类型的改变而改变。

thiswindow,所以this.hoursthis.minutesundefined,于是方法就返回:'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()方法调用,上下文为walkPeriod对象。于是this.hours2this.minutes30,所以该方法返回了正确的结果:'2 hours and 30 minutes'

8. 结论

因为函数的调用方式是this的来源,所以从现在起,不要再问:

this来自哪儿?

而是要问:

函数是如何被调用的?

对于箭头函数,应该问:

箭头函数定义在哪儿?

这才是处理this问题的正确思路,它可以确保你不会再头疼于this的辨认了。

如果你还有辨认上下文的误区示例,或刚好遇到了一个比较难的案例,可以在下方留言,我们一起来讨论一下!

传播JavaScript知识,分享本篇文章吧,你的同事会感激你的。

说了这么多,所以,不要再把你的上下文弄丢了 :)

本文译自Gentle explanation of 'this' keyword in JavaScript

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!