本文爲譯文,文章有點長,可是仔細通篇閱讀下來,關於this
的識別問題基本就搞定了。因爲譯者水平有限,文中有紕漏之處,還請讀者多多指正。下面看正文吧:javascript
在很長一段時間內,this
關鍵字都讓我感到迷惑,相信不少JavaScript的初學者也是同樣。this
是JavaScript中很強大的一個特色,可是想搞懂它,你必須得花點時間。html
對於像Java、PHP這樣的標準語言來講,this
在類方法中指代的就是調用這個方法的實例。通常來講,this
不能在方法外使用,如此簡單的規則不會讓人迷惑。java
可是在JavaScript中狀況就有些不一樣了:this
指的是當前函數的執行上下文。在JavaScript中,函數有4種調用類型:正則表達式
alert('Hello World')
console.log('Hello World')
new RegExp(\\d)
alert.call(undefined, 'Hello World')
每種調用類型都有本身定義執行上下文的方式,因此this
指代的對象和咱們預期的可能稍有不一樣。express
此外嚴格模式也會影響執行上下文。數組
理解this
的關鍵在於要對函數的調用類型以及函數調用類型如何影響執行上下文有一個清晰的認識。本篇文章的目的就是解釋函數調用的類型、函數調用的類型如何影響this
的取值以及演示辨認執行上下文時的常見誤區。瀏覽器
在開始以前,咱們先熟悉幾個概念:安全
parseInt
的函數調用爲parseInt('15')
map.set('key', 'value')
, set
方法的執行上下文是map
,set
函數體中this
指的就是map
文章目錄:app
謎之thisdom
函數調用
2.1 函數調用中的this 2.2 嚴格模式時函數調用中的this 2.3 誤區:內部函數中的this
方法調用
3.1 方法調用中的this 3.2 誤區:從對象提取的方法
構造調用
4.1 構造調用中的this 4.2 誤區:忽略了new
間接調用
5.1 間接調用中的this
綁定函數
6.1 綁定函數中的this 6.2 緊密的上下文綁定
箭頭函數
7.1 箭頭函數中的this 7.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'
是參數。
函數調用中的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>
嚴格模式時,函數調用中的
this
爲undefined
嚴格模式是從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)
被調用時,this
爲undefined
。
嚴格模式不只在當前做用域生效,並且在內部的做用域(在函數內部定義的函數)也生效:
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
函數調用一個常見的誤區是認爲內部函數和外部函數中的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
或在嚴格模式時爲undefined
,window
上並無numberA
和numberB
。
爲了解決這個問題,calculate
在執行時必須和sum
有相同的上下文,以便使用numbersA
和numbersB
屬性。
一個解決方案是經過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
。
方法是存儲在對象屬性上的函數。例如:
var myObject = { // helloFunction是一個方法 helloFunction: function() { return 'Hello World!'; } }; var message = myObject.helloFunction();
helloFunction
是myObject
的一個方法,可使用屬性訪問符獲取該方法:myObject.helloFunction
。
屬性訪問後面加上一對小括號,括號內能夠傳遞參數,這就是方法調用。
仍是上面的這個例子,myObject.helloFunction()
是myObject
上helloFunction
的方法調用。下面這些也是方法調用:[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); // 函數調用
理解了函數調用和方法調用的不一樣能夠幫助咱們正確地識別上下文。
方法調用中的
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'
對象的方法能夠被提取到一個單獨的變量中: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
的新函數,可是新函數的this
是myCat
。即便是進行函數調用,它的this
也是myCat
。
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()
。
構造調用中的
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'
。
有些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
是給上下文對象設置type
和wheelsCount
屬性的函數。當執行Vehicle('Car', 4)
是,返回了一個對象car
,而且它的屬性也是正確的:car.type
是'Car'
、car.wheelsCount
是4
。你可能認爲這不是也很好地建立並初始化了一個新對象嘛。
然而,在函數調用中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; } // 構造調用 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
。
使用myFun.call()
或myFun.apply()
方法調用函數是間接調用。
在JavaScript中,函數自己就是對象,它的類型是Function
。
函數上的.call
和.apply()
能夠用來指定調用函數時的上下文:
.call(thisArg[, arg1[, arg2[, ...]])
接收的第一個參數thisArg
做爲執行上下文,後面的arg1
、arg2
、...做爲實際的參數。.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'])
。
間接調用中的
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'
當一個函數須要使用指定的上下文執行時,間接調用就頗有用了。例如,能夠解決函數調用的上下文老是window
或undefined
(嚴格模式)的問題,能夠用來模擬方法調用(見前面的示例)。
另外一個很實用的例子是在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)
來初始化新建立的對象。
一個函數綁定了某個對象稱爲綁定函數。一般它是經過調用原函數的.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
做爲上下文,但multity
和doubly
仍然具備相同的函數體和做用域。
與.apply()
和.call()
當即執行一個函數相反,.bind()
方法只是返回了一個新函數。接下來這個新函數被調用時,它的this
是以前.bind()
的第一個參數。
綁定函數的
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()
,this
爲window
或undefined
(嚴格模式),而不是numbers
對象,因而simpleGetNumbers()
無法正確返回一個數組。
.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
。
箭頭函數被設計用來採用簡短的形式聲明一個函數,並能在詞法上綁定上下文。
下面是箭頭函數的簡單形式:
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
箭頭函數中的
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
,因而內部的箭頭函數get
的this
也成了numbers
。
接着咱們看到,不管以什麼樣的方式調用get
,它始終保持初始化時的上下文numbers
。採用get.call([0])
或get.apply([0])
的形式進行間接調用,或者採用get.bind([0])()
的方式從新綁定再調用都不會影響。
須要注意的是,箭頭函數不能做爲構造函數。若是以構造函數的形式調用new get()
,JavaScript或拋出一個錯誤:TypeError: get is not a constructor
。
你也許想用箭頭函數定義對象上的方法。憑心而論:與函數表達式相比,它的語法很是簡短,如(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()
方法調用,上下文爲walkPeriod
對象。因而this.hours
爲2
,this.minutes
爲30
,因此該方法返回了正確的結果:'2 hours and 30 minutes'
。
由於函數的調用方式是this
的來源,因此從如今起,不要再問:
this
來自哪兒?
而是要問:
函數是如何被調用的?
對於箭頭函數,應該問:
箭頭函數定義在哪兒?
這纔是處理this
問題的正確思路,它能夠確保你不會再頭疼於this
的辨認了。
若是你還有辨認上下文的誤區示例,或恰好遇到了一個比較難的案例,能夠在下方留言,咱們一塊兒來討論一下!
傳播JavaScript知識,分享本篇文章吧,你的同事會感激你的。
說了這麼多,因此,不要再把你的上下文弄丟了 :)