簡析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

相關文章
相關標籤/搜索