简析JavaScript中的this关键字

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

1. 谜之this

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

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

可是在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指代的对象和咱们预期的可能稍有不一样。express

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

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

在开始以前,咱们先熟悉几个概念:安全

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

文章目录:app

  1. 谜之thisdom

  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

相关文章
相关标签/搜索