1.概念
在JavaScript中定踱,this
是指當(dāng)前函數(shù)中正在執(zhí)行的上下文環(huán)境开泽,因?yàn)檫@門(mén)語(yǔ)言擁有四種不同的函數(shù)調(diào)用類(lèi)型:
- 函數(shù)調(diào)用
alert('hello world!')
- 方法調(diào)用
console.log('hello world')
- 構(gòu)造函數(shù)調(diào)用
new RegExp('\\d')
- 間接調(diào)用
alert.call(undefined, 'hello world')
在以上每一項(xiàng)的調(diào)用上,他們都擁有獨(dú)立的上下文環(huán)境,就會(huì)造成 this
所指意義有所差別。此外,嚴(yán)格模式也會(huì)對(duì)執(zhí)行環(huán)境造成影響院崇。
理解 this
關(guān)鍵字的關(guān)鍵在于理解各種不用的函數(shù)調(diào)用以及他是如何給影響上下文環(huán)境的。
這篇文章旨在解釋不用環(huán)境下的函數(shù)調(diào)用會(huì)怎么影響 this
以及判斷上下文環(huán)境時(shí)會(huì)產(chǎn)生的一些常見(jiàn)陷阱袍祖。
在開(kāi)始描述之前底瓣,先熟悉一下一些術(shù)語(yǔ):
-
調(diào)用 是執(zhí)行當(dāng)前函數(shù)主體的代碼,即調(diào)用一個(gè)函數(shù)蕉陋。例:
parseInt
函數(shù)的調(diào)用為parseInt(15)
-
上下文環(huán)境 是方法調(diào)用中
this
所代表的值 - 作用域 是一系列方法內(nèi)可調(diào)用的變量捐凭,對(duì)象,方法組成的集合
2.函數(shù)調(diào)用
函數(shù)調(diào)用 代表了該函數(shù)接收以成對(duì)的引號(hào)包含凳鬓,用逗號(hào)分隔的不同參數(shù)組成的表達(dá)式茁肠。舉例: parseInt('18')
。這個(gè)表達(dá)式不能是屬性訪(fǎng)問(wèn)如 myObject.myFunction
這個(gè)會(huì)造成方法調(diào)用缩举。[1, 5].join(',')
同樣是方法調(diào)用而不是一個(gè)函數(shù)調(diào)用垦梆。
函數(shù)調(diào)用的一個(gè)簡(jiǎn)單例子:
function hello(name) {
return 'Hello' + name + '!';
}
// 函數(shù)調(diào)用
var message = hello('World!');
console.log(message); // => 'Hello World!'
hello('World')
是一個(gè)函數(shù)調(diào)用:hello
表達(dá)式代表了一個(gè)函數(shù)對(duì)象,接受了用成對(duì)引號(hào)包含的 World
參數(shù)仅孩。
高級(jí)一點(diǎn)的例子托猩,立即執(zhí)行函數(shù) IIFE(immediately-invoked function expression):
var message = (function(name) {
return 'Hello ' + name + '!';
})('World');
console.log(message); // => 'Hello World!'
2.1. 函數(shù)調(diào)用中的 this
this
is the global object in a function invocation
全局對(duì)象取決于當(dāng)前執(zhí)行環(huán)境,在瀏覽器中辽慕,全局對(duì)象即 window
京腥。
在函數(shù)調(diào)用中,上下文執(zhí)行環(huán)境是全局對(duì)象溅蛉,可以在以下函數(shù)中驗(yàn)證上下文:
function sum(a, b) {
console.log(this === window); // => true
this.myNumber = 20; // 在全局對(duì)象中添加 'myNumber' 屬性
return a + b;
}
// sum() 為函數(shù)調(diào)用
// this 在 sum() 中是全局對(duì)象(window)
sum(15, 16); // => 31
window.myNumber; // => 20
當(dāng) sum(15, 16)
被調(diào)用時(shí)绞旅,JavaScript 自動(dòng)將 this
設(shè)置為全局對(duì)象,即 window
温艇。
當(dāng) this
在任何函數(shù)作用域以外調(diào)用時(shí)(最外層作用域:全局執(zhí)行上下文環(huán)境)因悲,也會(huì)涉及到全局對(duì)象。
console.log(this === window); // => true
this.myString = 'Hello World!';
console.log(window.myString); // => 'Hello World!'
2.2. 嚴(yán)格模式下勺爱,函數(shù)調(diào)用中的 this
this
is undefined in a function invocation in strict mode
嚴(yán)格模式由 ECMAScript 5.1 引進(jìn)晃琳,用來(lái)限制 JavaScript 的一些異常處理,提供更好的安全性和更強(qiáng)壯的錯(cuò)誤檢查機(jī)制琐鲁。使用嚴(yán)格模式卫旱,只需要將 'use strict'
置于函數(shù)體的頂部。這樣就可以將上下文環(huán)境中的 this
轉(zhuǎn)為 undefined
围段。這樣執(zhí)行上下文環(huán)境不再是全局對(duì)象顾翼,與非嚴(yán)格模式剛好相反。
在嚴(yán)格模式下執(zhí)行函數(shù)的一個(gè)例子:
function multiply(a, b) {
'use strict'; // 開(kāi)啟嚴(yán)格模式
console.log(this === undefined); // => true
return a * b;
}
// 嚴(yán)格模式下的函數(shù)調(diào)用 multiply()
// this 在 multiply() 中為 undefined
multiply(2, 5); // => 10
當(dāng) multiply(2, 5)
執(zhí)行時(shí)奈泪,這個(gè)函數(shù)中的 this
是 undefined
适贸。
嚴(yán)格模式不僅在當(dāng)前作用域起到作用灸芳,它還會(huì)影響內(nèi)部作用域,即內(nèi)部聲明的一切內(nèi)部函數(shù)的作用域拜姿。
function execute() {
'use strict'; // 開(kāi)啟嚴(yán)格模式
function concat(str1, str2) {
// 內(nèi)部函數(shù)也是嚴(yán)格模式
console.log(this === undefined); // => true
return str1 + str2;
}
// 在嚴(yán)格模式下調(diào)用concat()
// this 在 concat() 下是 undefined
concat('hello', 'world'); // => 'hello world'
}
execute();
一個(gè)簡(jiǎn)單的 JavaScript 文件可能同時(shí)包含嚴(yán)格模式和非嚴(yán)格模式烙样,所以在用一種類(lèi)型調(diào)用中,可能也會(huì)有不同的上下文差異蕊肥。
function nonStrictSum(a, b) {
// 非嚴(yán)格模式
console.log(this === window); // => true
return a + b;
}
function strictSum(a, b) {
'use strict';
// 嚴(yán)格模式
console.log(this === undefined); // => true
return a + b;
}
// noStrictSum() 在非嚴(yán)格模式下調(diào)用
// this 在 nonStrictSum() 中是 window 對(duì)象
nonStrictSum(5, 6) // => 11
// strictSum() 在嚴(yán)格模式下被調(diào)用
// this 在strictSum() 中是 undefined
strictSum(8, 12); // => 20
2.3 陷阱: this
在內(nèi)部函數(shù)中
一個(gè)常見(jiàn)的陷阱是理所應(yīng)當(dāng)?shù)恼J(rèn)為函數(shù)調(diào)用的谒获,內(nèi)部函數(shù)中 this
等同于它的外部函數(shù)中的 this
。
正確的理解是內(nèi)部函數(shù)的上下文環(huán)境取決于調(diào)用環(huán)境壁却,而不是外部函數(shù)的上下文環(huán)境批狱。
為了獲取到所期望的 this
,應(yīng)該利用間接調(diào)用修改內(nèi)部函數(shù)的上下文環(huán)境展东,如使用 .call()
或者 apply()
或者創(chuàng)建一個(gè)綁定函數(shù) .bind()
精耐。
下面的例子表示計(jì)算兩個(gè)數(shù)之和:
var numbers = {
numberA: 5,
numberB: 10,
sum: function () {
console.log(this === numbers); // => true
function calculate() {
// 嚴(yán)格模式下,this 是 window or undefined
console.log(this === numbers); // => false
return this.numberA + this.numberB;
}
return calculate();
}
};
numbers.sum(); // => 嚴(yán)格模式下琅锻,結(jié)果為 NaN 或者 throws TypeError
numbers.sum()
是對(duì)象內(nèi)的一個(gè)方法調(diào)用卦停,因此 sum
的上下文是 numbers
對(duì)象,而 calculate
函數(shù)定義在 sum
函數(shù)內(nèi)恼蓬,所以會(huì)誤以為在 calculate
內(nèi) this
也指向的是 numbers
惊完。
然后 calculate()
在函數(shù)調(diào)用(而不是作為方法調(diào)用)時(shí),此時(shí)的 this
指向的是全局對(duì)象 window
或者在嚴(yán)格模式下指向的是 undefined
处硬,即使外部函數(shù) sum
擁有 numbers
對(duì)象作為上下文環(huán)境小槐,它也沒(méi)有辦法影響到內(nèi)部的 this
。
numbers.sum()
調(diào)用的結(jié)果是 NaN 或者在嚴(yán)格模式下直接拋出錯(cuò)誤 TypeError:Cannot read property 'numberA' of undefined
荷辕,而絕非期待的結(jié)果 5 + 10 = 15
凿跳,造成這樣的原因是 calculate
并沒(méi)有正確的被調(diào)用。
為了解決這個(gè)問(wèn)題疮方,正確的做法是使 calculate
函數(shù)被調(diào)用時(shí)的上下文同 sum
調(diào)用時(shí)一樣控嗜,為了得到屬性 numberA
和 numberB
,其中一種方法是使用 .call()
方法骡显。
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() 方法修改上下文環(huán)境
return calculate.call(this);
}
};
numbers.sum(); // => 15
calculate.call(this)
同樣執(zhí)行了 calculate
函數(shù)疆栏,但是格外的添加了 this
作為第一個(gè)參數(shù),修改上下文執(zhí)行環(huán)境惫谤。此時(shí)的 this.numberA + this.numberB
等同于 numbers.numberA + numbers.numberB
壁顶,其最終的結(jié)果就會(huì)如期盼的一樣為 5 + 10 = 15
。
3.方法調(diào)用
方法是作為一個(gè)對(duì)象屬性存儲(chǔ)的函數(shù)溜歪,舉個(gè)例子:
var myObject = {
// helloFunction 是對(duì)象中的方法
helloFunction: function() {
return 'hello world';
}
};
var message = myObject.helloFunction();
helloFunction
是屬于 myObject
的一個(gè)方法若专,調(diào)用這個(gè)方法可以使用屬性訪(fǎng)問(wèn)的方式 myObject.helloFunction
。
方法調(diào)用表現(xiàn)為對(duì)象屬性訪(fǎng)問(wèn)的形式蝴猪,支持傳入用成對(duì)引號(hào)包裹起來(lái)的一系列參數(shù)。上個(gè)例子中,myObject.helloFunction()
其實(shí)就是對(duì)象 myObject
上對(duì)屬性 helloFunction
的方法調(diào)用启搂。同樣 [1, 2].join(',')
和 /\s/.test('beautiful world')
都是方法調(diào)用。
區(qū)分函數(shù)調(diào)用和方法調(diào)用是非常重要的,它們是不同類(lèi)型的調(diào)用方式动壤。主要的差別在于方法調(diào)用為訪(fǎng)問(wèn)屬性的形式萝喘,如:.functionProperty()
或者 'functionProperty',而函數(shù)調(diào)用為
()```琼懊。
['hello', 'world'].join(','); // 方法調(diào)用
({ten: function() {return 10;} }).ten(); // 方法調(diào)用
var obj = {};
obj.myFunction = function() {
return new Date().toString();
};
obj.myFunction(); // 方法調(diào)用
var otherFunction = obj.myFunction;
otherFunction(); // 函數(shù)調(diào)用
parseFloat('16.60'); // 函數(shù)調(diào)用
isNaN(0); // 函數(shù)調(diào)用
3.1. 方法調(diào)用中的 this
this
is the object that owns the method in a method invocation
當(dāng)在一個(gè)對(duì)象里調(diào)用方法時(shí)阁簸,this
代表的是對(duì)象它自身。讓我們創(chuàng)建一個(gè)對(duì)象哼丈,其包含一個(gè)可以遞增屬性的方法启妹。
var calc = {
num: 0,
increment: function() {
console.log(this === calc); // => true
this.num += 1;
return this.num;
}
};
// 方法調(diào)用,this 指向 calc
calc.increment(); // => 1
calc.increment(); // => 2
calc.increment()
調(diào)用意味著上下文執(zhí)行環(huán)境在 calc
對(duì)象里醉旦,因此使用 this.num
遞增 num
這個(gè)屬性的可行的饶米。
一個(gè) JavaScript 對(duì)象繼承方法來(lái)自于它自身的屬性。當(dāng)一個(gè)被繼承方法在對(duì)象中調(diào)用時(shí)车胡,上下文執(zhí)行環(huán)境同樣是對(duì)象本身檬输。
var myDog = Object.create({
sayName: function() {
console.log(this === myDog); // => true
return this.name;
}
});
myDog.name = 'Milo';
// 方法調(diào)用,this 指向 myDog
myDog.sayname(); // => 'Milo';
Object.create()
創(chuàng)建了一個(gè)新的對(duì)象 myDog
并且設(shè)置了屬性匈棘,myDog
對(duì)象繼承了 myName
方法丧慈。當(dāng) myDog.sayName()
被執(zhí)行時(shí),上下文執(zhí)行環(huán)境指向 myDog
主卫。
在ECMAScript 5 的 class
語(yǔ)法中逃默,方法調(diào)用指的是實(shí)力本身。
class Planet {
constructor(name) {
this.name = name;
}
getName() {
console.log(this === earth); // => true
return this.name;
}
}
var earth = new Planet('Earth');
// 方法調(diào)用簇搅,上下文為 earth
earth.getName(); // => 'Earth'
3.2. 陷阱:方法會(huì)分離它自身的對(duì)象
一個(gè)對(duì)象中的方法可能會(huì)被提起抽離成一個(gè)變量完域。當(dāng)使用這個(gè)變量調(diào)用方法時(shí),開(kāi)發(fā)者可能會(huì)誤認(rèn)為 this
指向的還是定義該方法時(shí)的對(duì)象瘩将。
如果方法調(diào)用不依靠對(duì)象筒主,那么就是一個(gè)函數(shù)調(diào)用,即 this
指向全局對(duì)象 object
或者在嚴(yán)格模式下為 undefined
鸟蟹。創(chuàng)建函數(shù)綁定可以修復(fù)上下文乌妙,使該方法被正確對(duì)象調(diào)用。
下面的例子創(chuàng)建了構(gòu)造器函數(shù) Animal
并且創(chuàng)建了一個(gè)實(shí)例 myCat
建钥,在 setTimeout()
定時(shí)器 1s 后打印 myCat
對(duì)象信息
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"
// 或者在嚴(yán)格模式下拋出錯(cuò)誤 TypeError
setTimeout(myCat.logInfo, 1000);
開(kāi)發(fā)者可能認(rèn)為在 setTimeout
下調(diào)用 myCat.logInfo()
會(huì)打印出 myCat
對(duì)象的信息藤韵。但實(shí)際上這個(gè)方法被分離了出來(lái)作為了參數(shù)傳入函數(shù)內(nèi) setTimeout(myCat.logInfo)
,然后 1s 后會(huì)發(fā)生函數(shù)調(diào)用熊经。當(dāng) logInfo
被作為函數(shù)調(diào)用時(shí)泽艘,this
指向全局對(duì)象 window
或者在嚴(yán)格模式下為 undefined
欲险,因?yàn)閷?duì)象信息沒(méi)有正確地被打印。
方法綁定可以使用 .bind()
方法匹涮。如果被分離的方法綁定了 myCat
對(duì)象天试,那么上下文問(wèn)題就可以被解決了:
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 Car has 4 legs"
setTimeout(myCat.logInfo.bind(myCat), 1000);
此時(shí), myCat.logInfo.bind(myCat)
返回的新函數(shù)調(diào)用里的 this
指向了 myCat
然低。
4. 構(gòu)造函數(shù)調(diào)用
構(gòu)造函數(shù)調(diào)用使用 new
關(guān)鍵詞喜每,后面跟隨可帶參數(shù)的對(duì)象表達(dá)式,例: new RegExp('\\d')
雳攘。
以下的例子聲明了一個(gè)構(gòu)造函數(shù) Country
带兜,并調(diào)用。
function Country(name, traveled) {
this.name = name ? this.name : 'United Kingdom';
this.traveled = Boolean(traveled); // 轉(zhuǎn)換為 boolean 值
}
Country.prototype.travel = function() {
this.traveled = true;
};
// 構(gòu)造函數(shù)調(diào)用
var france = new Country('France', false);
// 構(gòu)造函數(shù)調(diào)用
var unitedKingdom = new Country;
france.travel(); // Travel to France
new City('Paris')
是一個(gè)構(gòu)造器調(diào)用吨灭,這個(gè)對(duì)象初始化使用了類(lèi)中特殊的方法 constructor
刚照,其中的 this
指向的是新創(chuàng)建的對(duì)象。
構(gòu)造器調(diào)用創(chuàng)建了一個(gè)空的新對(duì)象喧兄,從構(gòu)造器的原型中繼承屬性无畔。這個(gè)構(gòu)造器函數(shù)的意義在于初始化對(duì)象,因此這個(gè)類(lèi)型的函數(shù)調(diào)用創(chuàng)建實(shí)例吠冤。
當(dāng)一個(gè)屬性訪(fǎng)問(wèn) myObject.myFunction
前擁有 new
關(guān)鍵詞檩互,那么 JavaScript 會(huì)執(zhí)行構(gòu)造器調(diào)用而不是方法調(diào)用。舉個(gè)例子: new myObject.myFunction()
意味著首先這個(gè)函數(shù)會(huì)解析為一個(gè)屬性訪(fǎng)問(wèn)函數(shù) extractedFunction = myObject.myFunction
咨演,然后用構(gòu)造器創(chuàng)建一個(gè)新對(duì)象 new extractedFunction
闸昨。
4.1. 在構(gòu)造函數(shù)調(diào)用中的 this
this
is the newly created object in a constructor invocation
構(gòu)造器調(diào)用的環(huán)境是新創(chuàng)建的對(duì)象。通過(guò)傳遞構(gòu)造函數(shù)參數(shù)來(lái)初始化新建的對(duì)象薄风,添加屬性初始化值以及事件處理器饵较。
讓我們來(lái)驗(yàn)證以下這個(gè)例子的上下文環(huán)境:
function Foo() {
console.log(this instancof Foo); // => true
this.property = 'Default Value';
}
// 構(gòu)造函數(shù)調(diào)用
var fooInstance = new Foo();
fooInstance.property; // => 'Default Value'
new Foo()
建立構(gòu)造器調(diào)用,它的上下文環(huán)境為 fooInstance
遭赂,在 Foo
對(duì)象中初始化了 this.property
這個(gè)屬性并賦予初始值循诉。
在使用 class
語(yǔ)法時(shí)也是同樣的情況(在 ES6 中),初始化只發(fā)生在它的 constructor
方法中撇他。
class Bar {
constructor() {
console.log(this instanceof Bar); // => true
this.property = 'Default Value';
}
}
// 構(gòu)造函數(shù)調(diào)用
var barInstance = new Bar();
barInstance.property; // => 'Default Value'
當(dāng)執(zhí)行 new Bar()
時(shí)茄猫,JavaScript 創(chuàng)建了一個(gè)空對(duì)象并且它的上下文環(huán)境為 constructor
方法,因此添加屬性方法是使用 this
關(guān)鍵詞:this.property = 'Default Value'
困肩。
4.2. 陷阱:忘記添加 new
關(guān)鍵詞
一些 JavaScript 函數(shù)創(chuàng)建實(shí)例划纽,不僅僅可以使用構(gòu)造器的形式調(diào)用也可以利用函數(shù)調(diào)用,下面是一個(gè) RegExp
的例子:
var reg1 = new RegExp('\\w+');
var reg2 = RegExp('\\w+');
reg1 instanceof RegExp; // => true
reg2 instanceof RegExp; // => true
reg1.source === reg2.source; // => true
當(dāng)執(zhí)行 new RegExp('\\w+')
和 RegExp('\\w+')
時(shí)锌畸,JavaScript 創(chuàng)建了兩個(gè)相等的普通表達(dá)式對(duì)象勇劣。
但是使用函數(shù)調(diào)用創(chuàng)建對(duì)象會(huì)產(chǎn)生潛在的問(wèn)題(包括工廠(chǎng)模式),當(dāng)失去了 new
關(guān)鍵詞,一些構(gòu)造器會(huì)取消初始化對(duì)象比默。
以下例子描述了這個(gè)問(wèn)題:
function Vehicle(type, wheelsCount) {
this.type = type;
this.wheelsCount = wheelsCount;
return this;
}
// 函數(shù)調(diào)用
var car = Vehicle('Car', 4);
car.type; // => 'Car'
car.wheelsCount; // => 4
car === window // =>true
Vehicle
是一個(gè)在對(duì)象上設(shè)置了 type
和 wheelsCount
屬性的函數(shù)幻捏。
當(dāng)執(zhí)行了 Vehicle('Car', 4)
時(shí),會(huì)返回對(duì)象 car
命咐,它擁有正確的屬性值:
car.type
指向 Car
篡九,car.wheelsCount
指向 4
,開(kāi)發(fā)者誤以為這樣創(chuàng)建初始化對(duì)象沒(méi)有什么問(wèn)題醋奠。
然而榛臼,當(dāng)前執(zhí)行的是函數(shù)調(diào)用,因此 this
指向的是 window
對(duì)象钝域,所以它設(shè)置的屬性其實(shí)是掛在 window
對(duì)象上的讽坏,這樣是完全錯(cuò)誤的锭魔,它并沒(méi)有創(chuàng)建一個(gè)新對(duì)象例证。
應(yīng)該正確的執(zhí)行方式是使用 new
關(guān)鍵詞來(lái)保證構(gòu)造器被正確調(diào)用:
function Vehicle(type, wheelsCount) {
if (!(this instance of Vehicle)) {
throw Error('Error: Incorrect invocation');
}
this.type = type;
this.wheelsCount = wheelsCount;
return this;
}
// 構(gòu)造函數(shù)調(diào)用
var car = new Vehicle('Car', 4);
car.type; // => 'Car'
car.wheelsCount; // => 4
car instanceof Vehicle // => true
// 函數(shù)調(diào)用,會(huì)報(bào)錯(cuò)
var brokenCat = Vehicle('Broken Car', 3);
new Vehicle('Car', 4)
可以正確運(yùn)行:一個(gè)新的對(duì)象被創(chuàng)建和初始化迷捧,因?yàn)?new
關(guān)系詞代表了當(dāng)前為構(gòu)造器調(diào)用织咧。
在構(gòu)造器函數(shù)中添加驗(yàn)證:this instanceof Vehicle
,可以保證當(dāng)前的執(zhí)行上下文是正確的對(duì)象類(lèi)型漠秋。如果this
不是指向 Vehicle
笙蒙,那么就存在錯(cuò)誤。如果 Vehicle('Cat', 4)
表達(dá)式?jīng)]有 new
關(guān)鍵詞而被執(zhí)行庆锦,就會(huì)拋出錯(cuò)誤:Error:Incorrect invocation
捅位。
5. 間接調(diào)用
間接調(diào)用表現(xiàn)為當(dāng)一個(gè)函數(shù)使用了 .call()
或者 .apply()
方法。
在 JavaScript 中搂抒,函數(shù)為一等對(duì)象艇搀,這意味著函數(shù)是一個(gè)對(duì)象,對(duì)象類(lèi)型即為 Function
求晶。
在函數(shù)的一系列方法中焰雕, .call()
和 .apply()
被用來(lái)配置當(dāng)前調(diào)用的上下文環(huán)境。
方法 .call(thisArg[, arg1[, arg2, [, ...]]])
接受第一個(gè)參數(shù) thisArg
作為執(zhí)行的上下文環(huán)境芳杏,以及一系列參數(shù) arg1, arg2, ...
作為函數(shù)的傳參被調(diào)用矩屁。
并且,方法 .apply(thisArg, [args])
接收 thisArg
作為上下文環(huán)境爵赵,剩下的參數(shù)可以用類(lèi)數(shù)組對(duì)象 [args]
傳遞吝秕。
間接調(diào)用的例子:
function increment(number) {
return ++ number;
}
increment.call(undefined, 10); // => 11
increment.apply(undefined, [10]); // => 11
increment.call()
和 increment.apply()
同時(shí)傳遞了參數(shù) 10
調(diào)用 increment
函數(shù)。
兩個(gè)方法最主要的區(qū)別為 .call()
接收一組參數(shù)空幻,如 myFunction.call(thisValue, 'value1', 'value2')
郭膛,而 .apply()
接收一串參數(shù)作為類(lèi)數(shù)組對(duì)象傳遞,如 myFunction.apply(thisValue, ['value1', 'value2']);
氛悬。
5.1.間接調(diào)用中的 this
this
is the first argument of.call()
or.apply()
in an indirect invocation
很明顯则剃,在間接調(diào)用中耘柱,this
指向的是 .call()
和 .apply()
傳遞的第一個(gè)參數(shù)。
var rabbit = { name: 'white Rabbit' };
function concatName(string) {
console.log(this === rabbit);
return string + this.name;
}
// 間接調(diào)用
concatName.call(rabbit, 'Hello '); // => 'Hello White Rabbit'
concatName.apply(rabbit, ['Bye ']); // => 'Bye white Rabbit'
當(dāng)函數(shù)執(zhí)行需要特別指定上下文時(shí)棍现,間接調(diào)用非常有用调煎,它可以解決函數(shù)調(diào)用中的上下文問(wèn)題(this
指向 window
或者嚴(yán)格模式下指向 undefined
),同時(shí)也可以用來(lái)模擬方法調(diào)用對(duì)象己肮。
另一個(gè)實(shí)踐例子為士袄,在 ES5 中的類(lèi)繼承中,調(diào)用父級(jí)構(gòu)造器谎僻。
function Runner(name) {
console.log(this instanceof Rabbit); // => true
this.name = name;
}
function Rabbit(name, countLegs) {
console.log(this instanceof Rabbit); // => true
// 間接調(diào)用娄柳,調(diào)用了父級(jí)構(gòu)造器
Runner.call(this, name);
this.countLegs = countLegs;
}
var myRabbit = new Rabbit('white Rabbit', 4);
myRabbit; // { name: 'white Rabbit', countLegs: 4 }
Runner.call(this, name)
在 Rabbit
里間接調(diào)用了父級(jí)方法初始化對(duì)象。
6. 綁定函數(shù)調(diào)用
綁定函數(shù)調(diào)用是將函數(shù)綁定一個(gè)對(duì)象艘绍,它是一個(gè)原始函數(shù)使用了 .bind()
方法赤拒。原始綁定函數(shù)共享相同的代碼和作用域,但是在執(zhí)行時(shí)擁有不同的上下文環(huán)境诱鞠。
方法 .bind(thisArg[, arg1[,arg2[, ...]]])
接收第一個(gè)參數(shù) thisArg
作為綁定函數(shù)在執(zhí)行時(shí)的上下文環(huán)境挎挖,以及一組參數(shù) arg1, arg2, ...
作為傳參傳入函數(shù)中。它返回一個(gè)新的函數(shù)航夺,綁定了 thisArg
蕉朵。
下列代碼創(chuàng)建了一個(gè)綁定函數(shù)并在之后被調(diào)用:
function multiply(number) {
'use strict'
return this * number;
}
// 創(chuàng)建綁定函數(shù),綁定上下文2
var double = multiply.bind(2);
// 調(diào)用間接調(diào)用
double(3); // => 6
double(10); // => 20
multiply.bind(2)
返回一個(gè)新的函數(shù)對(duì)象 double
阳掐,它綁定了數(shù)字 2
始衅。multiply
和 double
函數(shù)擁有相同代碼和作用域。
對(duì)比方法 .apply()
和 .call()
缭保,它倆都立即執(zhí)行了函數(shù)汛闸,而 .bind()
函數(shù)返回了一個(gè)新方法,綁定了預(yù)先指定好的 this
涮俄,并可以延后調(diào)用蛉拙。
6.1. 綁定函數(shù)中的 this
this
is the first argument of.bind()
when invoking a bound function
.bind()
方法的作用是創(chuàng)建一個(gè)新的函數(shù),執(zhí)行時(shí)的上下文環(huán)境為 .bind()
傳遞的第一個(gè)參數(shù)彻亲,它允許創(chuàng)建預(yù)先設(shè)置好 this
函數(shù)孕锄。
讓我們來(lái)看看在綁定函數(shù)中如何設(shè)置 this
:
var numbers = {
array: [3, 5, 10],
getNumbers: function () {
return this.array;
}
};
// 創(chuàng)建一個(gè)綁定函數(shù)
var boundGetNumbers = numbers.getNumbers.bind(numbers);
boundGetNumbers(); // => [3, 5, 10]
// 從對(duì)象中抽取方法
var simpleGetNumbers = numbers.getNumbers;
simpleGetNumbers(); // => undefined 或者嚴(yán)格模式下拋出錯(cuò)誤
numbers.countNumbers.bind(numbers)
返回了綁定 numbers
對(duì)象的函數(shù) boundGetNumbers
,它在調(diào)用時(shí)的 this
指向的是 numbers
并且返回正確的數(shù)組對(duì)象苞尝。
.bind()
創(chuàng)建了一個(gè)永恒的上下文鏈并不可修改畸肆。一個(gè)綁定函數(shù)即使使用了 .call()
或者 .apply()
傳入其他不同的上下文環(huán)境,也不會(huì)更改它之前連接的上下文環(huán)境宙址,重新綁定也不會(huì)起任何作用轴脐。
只有在構(gòu)造器調(diào)用時(shí),綁定函數(shù)可以改變上下文,然而并不是特別推薦的做法大咱。
下面這個(gè)例子聲明了一個(gè)綁定函數(shù)恬涧,然后試圖更改其預(yù)定上下文的情況:
function getThis() {
'use strict`;
return this;
}
var one = getThis.bind(1);
// 綁定函數(shù)調(diào)用
one(); // => 1
// 使用 .apply() 和 .call() 綁定函數(shù)
one.call(2); // => 1
one.apply(2); // => 1
// 重新綁定
one.bind(2); // => 1
// 利用構(gòu)造器方式調(diào)用綁定函數(shù)
new one(); // => object
只有 new one()
時(shí)可以改變綁定函數(shù)的上下文環(huán)境,其他類(lèi)型的調(diào)用結(jié)果是 this
永遠(yuǎn)指向 1
碴巾。
7. 箭頭函數(shù)
箭頭函數(shù)的設(shè)計(jì)意圖是以精簡(jiǎn)的方式創(chuàng)建函數(shù)溯捆,并綁定定義時(shí)的上下文環(huán)境。
var hello = (name) => {
return 'Hello ' + name;
};
hello('World'); // => 'Hello World'
// 保留偶數(shù)
[1, 2, 5, 6].filter(item => item % 2 === 0); // => [2, 6]
箭頭函數(shù)使用了輕便的語(yǔ)法厦瓢,去除了關(guān)鍵詞 function
的書(shū)寫(xiě)提揍,甚至當(dāng)函數(shù)只有一個(gè)句子時(shí),可以省去 return
不寫(xiě)煮仇。
箭頭函數(shù)是匿名的劳跃,意味著函數(shù)的屬性 name
是一個(gè)空字符串 ''
,它沒(méi)有一個(gè)詞匯式的函數(shù)名浙垫,意味著不利于使用遞歸或者解除事件處理刨仑。
同時(shí)它不同于普通函數(shù),它不提供 arguments
對(duì)象绞呈,在 ES6 中可以用另外的參數(shù)代替:
var sumArguments = (...args) => {
console.log(typeof arguments); // => 'undefined'
return args.reduce((result, item) => result + item);
};
sumArguments.name; // => ''
sumArguments(5, 5, 6); // => 16
7.1. 箭頭函數(shù)中的 this
this
is the enclosing context where the arrow function is defined
箭頭函數(shù)并不創(chuàng)建它自身執(zhí)行的上下文贸人,使得 this
取決于它在定義時(shí)的外部函數(shù)间景。
下面的例子表示了上下文的透明屬性:
class Point {
constructor(x, y) {
this.x = x;
this.y;
}
log() {
console.log(this === myPoint); // => true
setTimeout( () => {
console.log(this === myPoint); // => true
console.log(this.x + ':' + this.y); // => '95:165'
}, 1000);
}
}
setTimeout
調(diào)用了箭頭函數(shù)佃声,它的上下文和 log()
方法一樣都是 myPont
對(duì)象√纫可以看出來(lái)圾亏,箭頭函數(shù)“繼承”了它在定義時(shí)的函數(shù)上下文。
如果嘗試在上述例子中使用正常函數(shù)封拧,那么它會(huì)創(chuàng)建自身的作用域(window
或者嚴(yán)格模式下 undefined
)志鹃。因此,要使同樣的代碼可以正確運(yùn)行就必須人工綁定上下文泽西,即 setTimeout(function() {...}.bind(this))
曹铃。使用箭頭函數(shù)就可以省略這么詳細(xì)的函數(shù)綁定,用更加干凈簡(jiǎn)短的代碼綁定函數(shù)捧杉。
如果箭頭函數(shù)在最外層作用域定義陕见,那么上下文環(huán)境將永遠(yuǎn)是全局對(duì)象,一般來(lái)說(shuō)在瀏覽器中即為 window
味抖。
var getContext = () => {
console.log(this === window); // => true
return this;
};
console.log(getContext() === window); // => true
箭頭函數(shù)一次綁定上下文后便不可更改评甜,即使使用了上下文更改的方法:
var numbers = [1, 2];
(function() {
var get = () => {
console.log(this === numbers); // => true
return this;
}
console.log(this === numbers); // => true
get(); // => [1, 2]
// 箭頭函數(shù)使用 .apply() 和 .call()
get.call(0); // => [1 ,2]
get.apply(0); // => [1, 2]
// Bind
get.bind([0])(); // => [1, 2]
}).call(numbers);
函數(shù)表達(dá)式可以間接調(diào)用 .call(numbers)
讓 this
指向 numbers
,然而 get
箭頭函數(shù)的 this
也是指向 numbers
的仔涩,因?yàn)樗壎硕x時(shí)的外部函數(shù)忍坷。
無(wú)論怎么調(diào)用 get
函數(shù),它的初始化上下文始終是 numbers
,間接地調(diào)用其他上下文(使用 .call()
或者 .apply()
)佩研,或者重新綁定上下文(使用 .bind()
)都沒(méi)有任何作用柑肴。
箭頭函數(shù)不可以用作構(gòu)造器,如果使用 new get()
作構(gòu)造器調(diào)用旬薯,JavaScript 會(huì)拋出錯(cuò)誤:TyperError: get is not a constructor
嘉抒。
7.2. 陷阱:使用箭頭函數(shù)定義方法
開(kāi)發(fā)者可能會(huì)想使用箭頭函數(shù)在對(duì)象中聲明方法,箭頭函數(shù)的聲明((param) => {...}
)要比函數(shù)表達(dá)式的聲明(function(param) {...}
)簡(jiǎn)短的多袍暴。
下面的例子在類(lèi) Period
中 使用箭頭函數(shù)定義了方法 format()
:
function Period (hours, minutes) {
this.hour = 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'
當(dāng) format
是一個(gè)箭頭函數(shù)些侍,且被定義在全局環(huán)境下,它的 this
指向的是 window
對(duì)象政模。
即使 format
執(zhí)行的時(shí)候掛載在對(duì)象上 walkPeriod.format()
岗宣,window
對(duì)象依舊存在在調(diào)用的上下文環(huán)境下。這是因?yàn)榧^函數(shù)擁有靜態(tài)的上下文環(huán)境淋样,不會(huì)因?yàn)椴煌恼{(diào)用而改變耗式。
this
指向的是 window
,因此 this.hour
和 this.minutes
都是 undefined
趁猴。方法返回的結(jié)果為:'undefined hours and undefined minutes'
刊咳。
正確的函數(shù)表達(dá)式可以解決這個(gè)問(wèn)題,因?yàn)槠胀ê瘮?shù)可以改變調(diào)用時(shí)的上下文環(huán)境:
function Period(hours, minutes) {
this.hour = 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()
是一個(gè)在對(duì)象中的方法調(diào)用儡司,它的上下文環(huán)境為 walkPeriod
娱挨,this.hours
指向 2
,this.minutes
指向的是 30
捕犬,因此可以返回正確的結(jié)果:'2 hours and 30 minutes'
跷坝。
8. 結(jié)尾
因?yàn)楹瘮?shù)調(diào)用會(huì)極大地影響到 this
,所以從現(xiàn)在開(kāi)始不要直接問(wèn)自己:
this
是從哪里來(lái)的碉碉?
而是要開(kāi)始思考:
當(dāng)前函數(shù)是怎么被調(diào)用的柴钻?
遇到箭頭函數(shù)時(shí),考慮:
當(dāng)箭頭函數(shù)被定義時(shí)垢粮,
this
是指向什么贴届?
以上思路可以幫助開(kāi)發(fā)者減少判斷 this
帶來(lái)的煩惱。
9. 總結(jié)
- 在 JavaScript 中蜡吧,
this
是指當(dāng)前函數(shù)中正在執(zhí)行的上下文環(huán)境 - 四種函數(shù)調(diào)用類(lèi)型:函數(shù)調(diào)用毫蚓、方法調(diào)用、構(gòu)造函數(shù)調(diào)用斩跌、間接調(diào)用
- 在函數(shù)調(diào)用中绍些,上下文執(zhí)行環(huán)境為全局對(duì)象。在瀏覽器中耀鸦,全局對(duì)象即為
window
柬批。但是在嚴(yán)格模式下上下文環(huán)境中的this
轉(zhuǎn)為undefined
- 內(nèi)部函數(shù)的上下文環(huán)境取決于調(diào)用環(huán)境啸澡,而不是外部函數(shù)的上下文環(huán)境
- 在方法調(diào)用中,當(dāng)在一個(gè)對(duì)象里調(diào)用方法時(shí)氮帐,
this
代表的是對(duì)象它本身嗅虏,在 ECMAScript 5 的class
語(yǔ)法中,方法調(diào)用指的是實(shí)例本身上沐。 - 注意:一個(gè)對(duì)象中的方法可能會(huì)被提取抽離成一個(gè)變量(比如上文中
setTimeout(myCat.logInfo), 1000)
的例子皮服。當(dāng)使用這個(gè)變量調(diào)用方法時(shí)會(huì)變成一個(gè)函數(shù)調(diào)用,解決方式是創(chuàng)建函數(shù)綁定参咙。 - 構(gòu)造函數(shù)調(diào)用:欣慰構(gòu)造器調(diào)用創(chuàng)建了一個(gè)空的對(duì)象龄广,然后從構(gòu)造器的原型中繼承屬性,所以
this
是指向新創(chuàng)建的對(duì)象蕴侧。 - 間接調(diào)用表現(xiàn)為當(dāng)一個(gè)函數(shù)使用了
.call()
或者.apply()
方法择同。其中的上下文環(huán)境都是.call()
和.apply()
接收的第一個(gè)參數(shù)。當(dāng)函數(shù)執(zhí)行需要特別指定上下文時(shí)净宵,間接調(diào)用非常有用敲才,它可以解決函數(shù)調(diào)用中的上下文問(wèn)題,同時(shí)也可以用來(lái)模擬方法調(diào)用對(duì)象择葡。 - 綁定函數(shù)調(diào)用時(shí)將函數(shù)綁定為一個(gè)對(duì)象紧武,它是一個(gè)原始函數(shù)使用了
.bind()
方法。原始綁定函數(shù)共享相同的代碼和作用域敏储,但是在執(zhí)行時(shí)擁有不同的上下文環(huán)境阻星。其中的上下文環(huán)境是.bind()
方法接收的第一個(gè)參數(shù)。對(duì)比方法.apply()
和.call()
虹曙,他倆都立即執(zhí)行了函數(shù)迫横,而.bind()
函數(shù)返回了一個(gè)新方法番舆,綁定了預(yù)先定義好的this
酝碳,并可以延后調(diào)用。 - 注意:
.bind()
創(chuàng)建了一個(gè)永恒的上下文鏈并不可修改恨狈。只有在構(gòu)造器調(diào)動(dòng)時(shí)疏哗,綁定函數(shù)可以改變上下文,然后這并不是特別推薦的做法禾怠。 - 箭頭函數(shù)的設(shè)計(jì)意圖是以精簡(jiǎn)的方式創(chuàng)建函數(shù)返奉,并綁定定義時(shí)的上下文環(huán)境。箭頭函數(shù)并不創(chuàng)建它自身執(zhí)行的上下文吗氏,使得
this
取決于它在定義時(shí)的外部函數(shù)芽偏。 - 注意:箭頭函數(shù)擁有靜態(tài)的上下文環(huán)境,不會(huì)因?yàn)椴挥玫恼{(diào)用而改變弦讽。