您好,登錄后才能下訂單哦!
1. this 的奧秘
很多時候, JS 中的 this 對于咱們的初學者很容易產生困惑不解。 this 的功能很強大,但需要一定付出才能慢慢理解它。
對Java、PHP或其他標準語言來看,this 表示類方法中當前對象的實例。大多數情況下,this 不能在方法之外使用,這樣就比較不會造成混淆。
在J要中情況就有所不同: this表示函數的當前執行上下文,JS 中函數調用主要有以下幾種方式:
函數調用: alert('Hello World!')
方法調用: console.log('Hello World!')
構造函數: new RegExp('\\d')
隱式調用: alert.call(undefined, 'Hello World!')
每種調用類型以自己的方式定義上下文,所以就很容易產生混淆。
此外,嚴格模式也會影響執行上下文。
理解this關鍵是要清楚的知道函數調用及其如何影響上下文。
本文主要說明函數的調用方式及如何影響 this,并且說明執行上下文的常見陷阱。
在開始之前,先知道幾個術語:
調用函數正在執行創建函數體的代碼,或者只是調用函數。 例如,parseInt函數調用是parseInt('15')。
函數調用:執行構成函數主體的代碼:例如,parseInt函數調用是parseInt('15')。
調用的上下文:指 this 在函數體內的值。 例如,map.set('key', 'value')的調用上下文是 map。
函數的作用域:是在函數體中可訪問的變量、對象和函數的集合。
2.函數調用
當一個表達式為函數接著一個(,一些用逗號分隔的參數以及一個)時,函數調用被執行,例如parseInt('18')。
函數調用表達式不能是屬性方式的調用,如 obj.myFunc(),這種是創建一個方法調用。再如 [1,5].join(',')不是函數調用,而是方法調用,這種區別需要記住哈,很重要滴。
函數調用的一個簡單示例:
function?hello(name)?{?return?'Hello?'?+?name?+?'!'; }//?函數調用const?message?=?hello('World');console.log(message);?//?=>?'Hello?World!'復制代碼
hello('World')是函數調用: hello表達式等價于一個函數,跟在它后面的是一對括號以及'World'參數。
一個更高級的例子是IIFE(立即調用的函數表達式)
const?message?=?(function(name)?{ ?return?'Hello?'?+?name?+?'!'; })('World');console.log(message)?//?=>?'Hello?World!'復制代碼
IIFE也是一個函數調用:第一對圓括號(function(name) {...})是一個表達式,它的計算結果是一個函數對象,后面跟著一對圓括號,圓括號的參數是“World”。
2.1. 在函數調用中的this
this 在函數調用中是一個全局對象
局對象由執行環境決定。在瀏覽器中,this是 window 對象。
在函數調用中,執行上下文是全局對象。
再來看看下面函數中的上下文又是什么鬼:
function?sum(a,?b)?{?console.log(this?===?window);?//?=>?true ?this.myNumber?=?20;?//?將'myNumber'屬性添加到全局對象 ?return?a?+?b; }//?sum()?is?invoked?as?a?function//?sum()?中的?`this`?是一個全局對象(window)sum(15,?16);?//?=>?31window.myNumber;?//?=>?20復制代碼
在調用sum(15,16)時,JS 自動將this設置為全局對象,在瀏覽器中該對象是window。
當this在任何函數作用域(最頂層作用域:全局執行上下文)之外使用,this 表示 window 對象
console.log(this?===?window);?//?=>?true this.myString?=?'Hello?World!'; console.log(window.myString);?//?=>?'Hello?World!'<!--?In?an?html?file?--><script?type="text/javascript"> ?console.log(this?===?window);?//?=>?true</script>復制代碼
2.2 嚴格模式下的函數調用 this 又是什么樣的
this 在嚴格模式下的函數調用中為?undefined
嚴格模式是在 ECMAScript 5.1中引入的,它提供了更好的安全性和更強的錯誤檢查。
要啟用嚴格模式,函數頭部寫入use strict 即可。
啟用后,嚴格模式會影響執行上下文,this 在常規函數調用中值為undefined。 與上述情況2.1相反,執行上下文不再是全局對象。
嚴格模式函數調用示例:
function?multiply(a,?b)?{?'use?strict';?//?啟用嚴格模式 ?console.log(this?===?undefined);?//?=>?true ?return?a?*?b; } 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?in?concat()?is?undefined ?concat('Hello',?'?World!');?//?=>?"Hello?World!"} execute(); 復制代碼
'use strict'被插入到執行體的頂部,在其作用域內啟用嚴格模式。 因為函數concat是在執行的作用域中聲明的,所以它繼承了嚴格模式。
單個JS文件可能包含嚴格和非嚴格模式。 因此,對于相同的調用類型,可以在單個腳本中具有不同的上下文行為:
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(5,?6);?//?=>?11strictSum(8,?12);?//?=>?20復制代碼
2.3 陷阱:this 在內部函數中的時候
函數調用的一個常見陷阱是,認為this在內部函數中的情況與外部函數中的情況相同。
正確地說,內部函數的上下文只依賴于它的調用類型,而不依賴于外部函數的上下文。
要將 this 設置為所需的值,可以通過 .call()或.apply()修改內部函數的上下文或使用.bind()創建綁定函數。
下面的例子是計算兩個數的和:
const?numbers?=?{?numberA:?5,?numberB:?10,?sum:?function()?{?console.log(this?===?numbers);?//?=>?true ?function?calculate()?{?console.log(this?===?numbers);?//?=>?false ?return?this.numberA?+?this.numberB; ?}?return?calculate(); ?} }; numbers.sum();?//?=>?NaN?復制代碼
sum()是對象上的方法調用,所以sum中的上下文是numbers對象。calculate函數是在sum中定義的,你可能希望在calculate()中this也表示number對象。
calculate()是一個函數調用(不是方法調用),它將this作為全局對象window(非嚴格模下)。即使外部函數sum將上下文作為number對象,它在calculate里面沒有影響。
sum()的調用結果是NaN,不是預期的結果5 + 10 = 15,這都是因為沒有正確調用calculate。
為了解決這個問題,calculate函數中上下文應該與 sum 中的一樣,以便可以訪問numberA和numberB屬性。
一種解決方案是通過調用calculator.call(this)手動將calculate上下文更改為所需的上下文。
const?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復制代碼
call(this)像往常一樣執行calculate函數,但 call 會把上下文修改為指定為第一個參數的值。
現在this.numberA + this.numberB相當于numbers.numberA + numbers.numberB。 該函數返回預期結果5 + 10 = 15。
另一種就是使用箭頭函數
const?numbers?=?{?numberA:?5,?numberB:?10,?sum:?function()?{?console.log(this?===?numbers);?//?=>?true ?const?calculate?=?()?=>?{?console.log(this?===?numbers);?//?=>?true ?return?this.numberA?+?this.numberB; ?}?return?calculate(); ?} }; numbers.sum();?//?=>?15復制代碼
3.方法調用
方法是存儲在對象屬性中的函數。例如
const?myObject?=?{?//?helloFunction?是一個方法 ?helloFunction:?function()?{?return?'Hello?World!'; ?} };const?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();?//?方法調用const?obj?=?{}; obj.myFunction?=?function()?{?return?new?Date().toString(); }; obj.myFunction();?//?方法調用const?otherFunction?=?obj.myFunction; otherFunction();?//?函數調用parseFloat('16.60');?//?函數調用isNaN(0);?//?函數調用復制代碼
理解函數調用和方法調用之間的區別有助于正確識別上下文。
3.1 方法調用中 this 是腫么樣
在方法調用中,this是擁有這個方法的對象
當調用對象上的方法時,this就變成了對象本身。
創建一個對象,該對象有一個遞增數字的方法
const?calc?=?{?num:?0,?increment:?function()?{?console.log(this?===?calc);?//?=>?true ?this.num?+=?1;?return?this.num; ?} };//?method?invocation.?this?is?calccalc.increment();?//?=>?1calc.increment();?//?=>?2復制代碼
調用calc.increment()使increment函數的上下文成為calc對象。所以使用this.num來增加num屬性是有效的。
再來看看另一個例子。JS對象從原型繼承一個方法,當在對象上調用繼承的方法時,調用的上下文仍然是對象本身
const?myDog?=?Object.create({?sayName:?function()?{?console.log(this?===?myDog);?//?=>?true ?return?this.name; ?} }); myDog.name?=?'Milo';//?方法調用?this?指向?myDogmyDog.sayName();?//?=>?'Milo'復制代碼
Object.create()創建一個新對象myDog,并根據第一個參數設置其原型。myDog對象繼承sayName方法。
執行myDog. sayname()時,myDog是調用的上下文。
在EC6 class 語法中,方法調用上下文也是實例本身
class?Planet?{?constructor(name)?{?this.name?=?name;? ?} ?getName()?{ ?console.log(this?===?earth);?//?=>?true ?return?this.name; ?} }var?earth?=?new?Planet('Earth');//?method?invocation.?the?context?is?earthearth.getName();?//?=>?'Earth'復制代碼
3.2 陷阱:將方法與其對象分離
方法可以從對象中提取到一個單獨的變量const alone = myObj.myMethod。當方法單獨調用時,與原始對象alone()分離,你可能認為當前的this就是定義方法的對象myObject。
如果方法在沒有對象的情況下調用,那么函數調用就會發生,此時的this指向全局對象window嚴格模式下是undefined。
下面的示例定義了Animal構造函數并創建了它的一個實例:myCat。然后setTimout()在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'); ?} }const?myCat?=?new?Animal('Cat',?4);//?The?undefined?has?undefined?legs?setTimeout(myCat.logInfo,?1000);? 復制代碼
你可能認為setTimout調用myCat.loginfo()時,它應該打印關于myCat對象的信息。
不幸的是,方法在作為參數傳遞時與對象是分離,setTimout(myCat.logInfo)以下情況是等效的:
setTimout(myCat.logInfo); //?等價于 const?extractedLogInfo?=?myCat.logInfo;setTimout(extractedLogInfo); 復制代碼
將分離的logInfo作為函數調用時,this是全局 window,所以對象信息沒有正確地打印。
函數可以使用.bind()方法與對象綁定,就可以解決 this 指向的問題。
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'); ?}; }const?myCat?=?new?Animal('Cat',?4);//?logs?"The?Cat?has?4?legs"setTimeout(myCat.logInfo.bind(myCat),?1000); 復制代碼
myCat.logInfo.bind(myCat)返回一個新函數,它的執行方式與logInfo完全相同,但是此時的 this 指向 myCat,即使在函數調用中也是如此。
另一種解決方案是將logInfo()方法定義為一個箭頭函數:
function?Animal(type,?legs)?{?this.type?=?type;?this.legs?=?legs;? ?this.logInfo?=?()?=>?{?console.log(this?===?myCat);?//?=>?true ?console.log('The?'?+?this.type?+?'?has?'?+?this.legs?+?'?legs'); ?}; }const?myCat?=?new?Animal('Cat',?4);//?logs?"The?Cat?has?4?legs"setTimeout(myCat.logInfo,?1000); 復制代碼
4. 構造函數調用
當new關鍵詞緊接著函數對象,(,一組逗號分隔的參數以及)時被調用,執行的是構造函數調用如new RegExp('\\d')。
聲明了一個Country函數,并且將它作為一個構造函數調用:
function?Country(name,?traveled)?{?this.name?=?name???name?:?'United?Kingdom';?this.traveled?=?Boolean(traveled);? } Country.prototype.travel?=?function()?{?this.traveled?=?true; };//?構造函數調用const?france?=?new?Country('France',?false);//?構造函數調用const?unitedKingdom?=?new?Country; france.travel();?//?Travel?to?France復制代碼
new Country('France', false)是Country函數的構造函數調用。它的執行結果是一個name屬性為'France'的新的對象。 如果這個構造函數調用時不需要參數,那么括號可以省略:new Country。
從ES6開始,JS 允許用class關鍵詞來定義構造函數
class?City?{?constructor(name,?traveled)?{?this.name?=?name;?this.traveled?=?false; ?} ?travel()?{?this.traveled?=?true; ?} }//?Constructor?invocationconst?paris?=?new?City('Paris',?false); paris.travel(); 復制代碼
new City('Paris')是構造函數調用。這個對象的初始化由這個類中一個特殊的方法constructor來處理。其中,this指向新創建的對象。
構造函數創建了一個新的空的對象,它從構造函數的原型繼承了屬性。構造函數的作用就是去初始化這個對象。 可能你已經知道了,在這種類型的調用中,上下文指向新創建的實例。
當屬性訪問myObject.myFunction前面有一個new關鍵詞時,JS會執行構造函數調用而不是原來的方法調用。
例如new myObject.myFunction():它相當于先用屬性訪問把方法提取出來extractedFunction = myObject.myFunction,然后利用把它作為構造函數創建一個新的對象: new extractedFunction()。
4.1. 構造函數中的 this
在構造函數調用中 this 指向新創建的對象
構造函數調用的上下文是新創建的對象。它利用構造函數的參數初始化新的對象,設定屬性的初始值,添加事件處理函數等等。
來看看下面示例中的上下文
function?Foo?()?{?console.log(this?instanceof?Foo);?//?=>?true ?this.property?=?'Default?Value'; }//?Constructor?invocationconst?fooInstance?=?new?Foo(); fooInstance.property;?//?=>?'Default?Value'復制代碼
new Foo() 正在進行構造函數調用,其中上下文是fooInstance。 在Foo內部初始化對象:this.property被賦值為默認值。
同樣的情況在用class語法(從ES6起)時也會發生,唯一的區別是初始化在constructor方法中進行:
class?Bar?{?constructor()?{?console.log(this?instanceof?Bar);?//?=>?true ?this.property?=?'Default?Value'; ?} }//?Constructor?invocationconst?barInstance?=?new?Bar(); barInstance.property;?//?=>?'Default?Value'復制代碼
4.2. 陷阱: 忘了使用 new
有些JS函數不是只在作為構造函數調用的時候才創建新的對象,作為函數調用時也會,例如RegExp:
var?reg1?=?new?RegExp('\\w+');var?reg2?=?RegExp('\\w+'); reg1?instanceof?RegExp;?//?=>?truereg2?instanceof?RegExp;?//?=>?truereg1.source?===?reg2.source;?//?=>?true復制代碼
當執行的 new RegExp('\\w+')和RegExp('\\w+')時,JS 會創建等價的正則表達式對象。
使用函數調用來創建對象存在一個潛在的問題(不包括工廠模式),因為一些構造函數可能會忽略在缺少new關鍵字時初始化對象的邏輯。
下面的例子說明了這個問題:
function?Vehicle(type,?wheelsCount)?{?this.type?=?type;?this.wheelsCount?=?wheelsCount;?return?this; }//?忘記使用?new?const?car?=?Vehicle('Car',?4); car.type;?//?=>?'Car'car.wheelsCount?//?=>?4car?===?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; }//?Constructor?invocationconst?car?=?new?Vehicle('Car',?4); car.type?//?=>?'Car'car.wheelsCount?//?=>?4car?instanceof?Vehicle?//?=>?true//?Function?invocation.?Throws?an?error.const?brokenCar?=?Vehicle('Broken?Car',?3); 復制代碼
new Vehicle('Car',4) 運行正常:創建并初始化一個新對象,因為構造函數調用中時使用了new關鍵字。
在構造函數里添加了一個驗證this instanceof Vehicle來確保執行的上下文是正確的對象類型。如果this不是Vehicle,那么就會報錯。這樣,如果執行Vehicle('Broken Car', 3)(沒有new),我們會得到一個異常:Error: Incorrect invocation。
5. 隱式調用
使用myFun.call()或myFun.apply()方法調用函數時,執行的是隱式調用。
JS中的函數是第一類對象,這意味著函數就是對象,對象的類型為Function。從函數對象的方法列表中,.call()和.apply()用于調用具有可配置上下文的函數。
方法 .call(thisArg[, arg1[, arg2[, ...]]])將接受的第一個參數thisArg作為調用時的上下文,arg1, arg2, ...這些則作為參數傳入被調用的函數。
方法.apply(thisArg, [args])將接受的第一個參數thisArg作為調用時的上下文,并且接受另一個類似數組的對象[arg1, arg2, ...]作為被調用函數的參數傳入。
下面是隱式調用的例子
function?increment(number)?{?return?++number;? } increment.call(undefined,?10);?//?=>?11increment.apply(undefined,?[10]);?//?=>?11復制代碼
increment.call()和increment.apply()都用參數10調用了這個自增函數。
兩者的區別是.call()接受一組參數,例如myFunction.call(thisValue, 'value1', 'value2')。而.apply()接受的一組參數必須是一個類似數組的對象,例如myFunction.apply(thisValue, ['value1', 'value2'])。
5.1. 隱式調用中的this
在隱式調用.call()或.apply()中,this是第一個參數
很明顯,在隱式調用中,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'復制代碼
當應該使用特定上下文執行函數時,隱式調用非常有用。例如為了解決方法調用時,this總是window或嚴格模式下的undefined的上下文問題。隱式調用可以用于模擬在一個對象上調用某個方法。
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; }const?myRabbit?=?new?Rabbit('White?Rabbit',?4); myRabbit;?//?{?name:?'White?Rabbit',?countLegs:?4?}復制代碼
Rabbit中的Runner.call(this, name)隱式調用了父類的函數來初始化這個對象。
6. 綁定函數
綁定函數是與對象連接的函數。通常使用.bind()方法從原始函數創建。原始函數和綁定函數共享相同的代碼和作用域,但執行時上下文不同。
方法 myFunc.bind(thisArg[, arg1[, arg2[, ...]]])接受第一個參數thisArg作為綁定函數執行時的上下文,并且它接受一組可選的參數 arg1, arg2, ...作為被調用函數的參數。它返回一個綁定了thisArg的新函數。
function?multiply(number)?{?'use?strict';?return?this?*?number; }const?double?=?multiply.bind(2); double(3);?//?=>?6double(10);?//?=>?20復制代碼
bind(2)返回一個新的函數對象double,double 綁定了數字2。multiply和double具有相同的代碼和作用域。
與.apply()和.call() 方法相反,它不會立即調用該函數,.bind()方法只返回一個新函數,在之后被調用,只是this已經被提前設置好了。
6.1. 綁定函數中的this
在調用綁定函數時,this是.bind()的第一個參數。
.bind()的作用是創建一個新函數,調用該函數時,將上下文作為傳遞給.bind()的第一個參數。它是一種強大的技術,使咱們可以創建一個定義了this值的函數。
來看看,如何在如何在綁定函數設置 this
const?numbers?=?{?array:?[3,?5,?10], ?getNumbers:?function()?{?return?this.array;? ?} };const?boundGetNumbers?=?numbers.getNumbers.bind(numbers); boundGetNumbers();?//?=>?[3,?5,?10]//?Extract?method?from?objectconst?simpleGetNumbers?=?numbers.getNumbers; simpleGetNumbers();?//?=>?undefined?(嚴格模式下報錯)復制代碼
numbers.getNumbers.bind(numbers)返回綁定numbers對象boundGetNumbers函數。boundGetNumbers()調用時的this是number對象,并能夠返回正確的數組對象。
可以將函數numbers.getNumbers提取到變量simpleGetNumbers中而不進行綁定。在之后的函數調用中simpleGetNumbers()的this是window(嚴格模式下為undefined),不是number對象。在這個情況下,simpleGetNumbers()不會正確返回數組。
6.2 緊密的上下文綁定
.bind()創建一個永久的上下文鏈接,并始終保持它。 一個綁定函數不能通過.call()或者.apply()來改變它的上下文,甚至是再次綁定也不會有什么作用。
只有綁定函數的構造函數調用才能更改已經綁定的上下文,但是很不推薦的做法(構造函數調用必須使用常規的非綁定函數)。
下面示例創建一個綁定函數,然后嘗試更改其已預先定義好的上下文
function?getThis()?{?'use?strict';?return?this; }const?one?=?getThis.bind(1);//?綁定函數調用one();?//?=>?1//?使用帶有.apply()和.call()的綁定函數one.call(2);?//?=>?1one.apply(2);?//?=>?1//?再次綁定one.bind(2)();?//?=>?1//?以構造函數的形式調用綁定函數new?one();?//?=>?Object復制代碼
只有new one()改變了綁定函數的上下文,其他方式的調用中this總是等于1。
7. 箭頭函數
箭頭函數用于以更短的形式聲明函數,并在詞法上綁定上下文。它可以這樣使用
const?hello?=?(name)?=>?{?return?'Hello?'?+?name; }; hello('World');?//?=>?'Hello?World'//?Keep?only?even?numbers[1,?2,?5,?6].filter(item?=>?item?%?2?===?0);?//?=>?[2,?6]復制代碼
箭頭函數語法簡單,沒有冗長的function 關鍵字。當箭頭函數只有一條語句時,甚至可以省略return關鍵字。
箭頭函數是匿名的,這意味著name屬性是一個空字符串''。這樣它就沒有詞法上函數名(函數名對于遞歸、分離事件處理程序非常有用)
同時,跟常規函數相反,它也不提供arguments對象。但是,這在ES6中通過rest parameters修復了:
const?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。 換句話說,箭頭函數在詞匯上綁定 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); ?} }const?myPoint?=?new?Point(95,?165); myPoint.log(); 復制代碼
setTimeout使用與log()方法相同的上下文(myPoint對象)調用箭頭函數。正如所見,箭頭函數從定義它的函數繼承上下文。
如果在這個例子里嘗試用常規函數,它創建自己的上下文(window或嚴格模式下的undefined)。因此,要使相同的代碼正確地使用函數表達式,需要手動綁定上下文:setTimeout(function(){…}.bind(this))。這很冗長,使用箭頭函數是一種更簡潔、更短的解決方案。
如果箭頭函數在最頂層的作用域中定義(在任何函數之外),則上下文始終是全局對象(瀏覽器中的 window):
onst?getContext?=?()?=>?{?console.log(this?===?window);?//?=>?true ?return?this; };console.log(getContext()?===?window);?//?=>?true復制代碼
箭頭函數一勞永逸地與詞匯上下文綁定。 即使修改上下文,this也不能被改變:
const?numbers?=?[1,?2]; (function()?{? ?const?get?=?()?=>?{?console.log(this?===?numbers);?//?=>?true ?return?this; ?};?console.log(this?===?numbers);?//?=>?true ?get();?//?=>?[1,?2] ?//?Use?arrow?function?with?.apply()?and?.call() ?get.call([0]);?//?=>?[1,?2] ?get.apply([0]);?//?=>?[1,?2] ?//?Bind ?get.bind([0])();?//?=>?[1,?2]}).call(numbers); 復制代碼
無論如何調用箭頭函數get,它總是保留詞匯上下文numbers。 用其他上下文的隱式調用(通過 get.call([0])或get.apply([0]))或者重新綁定(通過.bind())都不會起作用。
箭頭函數不能用作構造函數。 將它作為構造函數調用(new get())會拋出一個錯誤:TypeError: get is not a constructor。
7.2. 陷阱: 用箭頭函數定義方法
你可能希望使用箭頭函數來聲明一個對象上的方法。箭頭函數的定義相比于函數表達式短得多:(param) => {...} instead of 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'; };const?walkPeriod?=?new?Period(2,?30);? walkPeriod.format();?//?=>?'undefined?hours?and?undefined?minutes'復制代碼
由于format是一個箭頭函數,并且在全局上下文(最頂層的作用域)中定義,因此 this 指向window對象。
即使format作為方法在一個對象上被調用如walkPeriod.format(),window仍然是這次調用的上下文。之所以會這樣是因為箭頭函數有靜態的上下文,并不會隨著調用方式的改變而改變。
該方法返回'undefined hours和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'; };const?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'。
原文:dmitripavlutin.com/gentle-expl…
代碼部署后可能存在的BUG沒法實時知道,事后為了解決這些BUG,花了大量的時間進行log 調試,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug。
總結
為函數調用對this影響最大,從現在開始不要問自己:
this 是從哪里來的?
而是要看看
函數是怎么被調用的?
對于箭頭函數,需要想想
在這個箭頭函數被定義的地方,this是什么?
這是處理this時的正確想法,它們可以讓你免于頭痛。
1. this 的奧秘
很多時候, JS 中的 this 對于咱們的初學者很容易產生困惑不解。 this 的功能很強大,但需要一定付出才能慢慢理解它。
對Java、PHP或其他標準語言來看,this 表示類方法中當前對象的實例。大多數情況下,this 不能在方法之外使用,這樣就比較不會造成混淆。
在J要中情況就有所不同: this表示函數的當前執行上下文,JS 中函數調用主要有以下幾種方式:
函數調用: alert('Hello World!')
方法調用: console.log('Hello World!')
構造函數: new RegExp('\\d')
隱式調用: alert.call(undefined, 'Hello World!')
每種調用類型以自己的方式定義上下文,所以就很容易產生混淆。
此外,嚴格模式也會影響執行上下文。
理解this關鍵是要清楚的知道函數調用及其如何影響上下文。
本文主要說明函數的調用方式及如何影響 this,并且說明執行上下文的常見陷阱。
在開始之前,先知道幾個術語:
調用函數正在執行創建函數體的代碼,或者只是調用函數。 例如,parseInt函數調用是parseInt('15')。
函數調用:執行構成函數主體的代碼:例如,parseInt函數調用是parseInt('15')。
調用的上下文:指 this 在函數體內的值。 例如,map.set('key', 'value')的調用上下文是 map。
函數的作用域:是在函數體中可訪問的變量、對象和函數的集合。
2.函數調用
當一個表達式為函數接著一個(,一些用逗號分隔的參數以及一個)時,函數調用被執行,例如parseInt('18')。
函數調用表達式不能是屬性方式的調用,如 obj.myFunc(),這種是創建一個方法調用。再如 [1,5].join(',')不是函數調用,而是方法調用,這種區別需要記住哈,很重要滴。
函數調用的一個簡單示例:
function?hello(name)?{?return?'Hello?'?+?name?+?'!'; }//?函數調用const?message?=?hello('World');console.log(message);?//?=>?'Hello?World!'復制代碼
hello('World')是函數調用: hello表達式等價于一個函數,跟在它后面的是一對括號以及'World'參數。
一個更高級的例子是IIFE(立即調用的函數表達式)
const?message?=?(function(name)?{ ?return?'Hello?'?+?name?+?'!'; })('World');console.log(message)?//?=>?'Hello?World!'復制代碼
IIFE也是一個函數調用:第一對圓括號(function(name) {...})是一個表達式,它的計算結果是一個函數對象,后面跟著一對圓括號,圓括號的參數是“World”。
2.1. 在函數調用中的this
this 在函數調用中是一個全局對象
局對象由執行環境決定。在瀏覽器中,this是 window 對象。
在函數調用中,執行上下文是全局對象。
再來看看下面函數中的上下文又是什么鬼:
function?sum(a,?b)?{?console.log(this?===?window);?//?=>?true ?this.myNumber?=?20;?//?將'myNumber'屬性添加到全局對象 ?return?a?+?b; }//?sum()?is?invoked?as?a?function//?sum()?中的?`this`?是一個全局對象(window)sum(15,?16);?//?=>?31window.myNumber;?//?=>?20復制代碼
在調用sum(15,16)時,JS 自動將this設置為全局對象,在瀏覽器中該對象是window。
當this在任何函數作用域(最頂層作用域:全局執行上下文)之外使用,this 表示 window 對象
console.log(this?===?window);?//?=>?true this.myString?=?'Hello?World!'; console.log(window.myString);?//?=>?'Hello?World!'<!--?In?an?html?file?--><script?type="text/javascript"> ?console.log(this?===?window);?//?=>?true</script>復制代碼
2.2 嚴格模式下的函數調用 this 又是什么樣的
this 在嚴格模式下的函數調用中為?undefined
嚴格模式是在 ECMAScript 5.1中引入的,它提供了更好的安全性和更強的錯誤檢查。
要啟用嚴格模式,函數頭部寫入use strict 即可。
啟用后,嚴格模式會影響執行上下文,this 在常規函數調用中值為undefined。 與上述情況2.1相反,執行上下文不再是全局對象。
嚴格模式函數調用示例:
function?multiply(a,?b)?{?'use?strict';?//?啟用嚴格模式 ?console.log(this?===?undefined);?//?=>?true ?return?a?*?b; } 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?in?concat()?is?undefined ?concat('Hello',?'?World!');?//?=>?"Hello?World!"} execute(); 復制代碼
'use strict'被插入到執行體的頂部,在其作用域內啟用嚴格模式。 因為函數concat是在執行的作用域中聲明的,所以它繼承了嚴格模式。
單個JS文件可能包含嚴格和非嚴格模式。 因此,對于相同的調用類型,可以在單個腳本中具有不同的上下文行為:
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(5,?6);?//?=>?11strictSum(8,?12);?//?=>?20復制代碼
2.3 陷阱:this 在內部函數中的時候
函數調用的一個常見陷阱是,認為this在內部函數中的情況與外部函數中的情況相同。
正確地說,內部函數的上下文只依賴于它的調用類型,而不依賴于外部函數的上下文。
要將 this 設置為所需的值,可以通過 .call()或.apply()修改內部函數的上下文或使用.bind()創建綁定函數。
下面的例子是計算兩個數的和:
const?numbers?=?{?numberA:?5,?numberB:?10,?sum:?function()?{?console.log(this?===?numbers);?//?=>?true ?function?calculate()?{?console.log(this?===?numbers);?//?=>?false ?return?this.numberA?+?this.numberB; ?}?return?calculate(); ?} }; numbers.sum();?//?=>?NaN?復制代碼
sum()是對象上的方法調用,所以sum中的上下文是numbers對象。calculate函數是在sum中定義的,你可能希望在calculate()中this也表示number對象。
calculate()是一個函數調用(不是方法調用),它將this作為全局對象window(非嚴格模下)。即使外部函數sum將上下文作為number對象,它在calculate里面沒有影響。
sum()的調用結果是NaN,不是預期的結果5 + 10 = 15,這都是因為沒有正確調用calculate。
為了解決這個問題,calculate函數中上下文應該與 sum 中的一樣,以便可以訪問numberA和numberB屬性。
一種解決方案是通過調用calculator.call(this)手動將calculate上下文更改為所需的上下文。
const?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復制代碼
call(this)像往常一樣執行calculate函數,但 call 會把上下文修改為指定為第一個參數的值。
現在this.numberA + this.numberB相當于numbers.numberA + numbers.numberB。 該函數返回預期結果5 + 10 = 15。
另一種就是使用箭頭函數
const?numbers?=?{?numberA:?5,?numberB:?10,?sum:?function()?{?console.log(this?===?numbers);?//?=>?true ?const?calculate?=?()?=>?{?console.log(this?===?numbers);?//?=>?true ?return?this.numberA?+?this.numberB; ?}?return?calculate(); ?} }; numbers.sum();?//?=>?15復制代碼
3.方法調用
方法是存儲在對象屬性中的函數。例如
const?myObject?=?{?//?helloFunction?是一個方法 ?helloFunction:?function()?{?return?'Hello?World!'; ?} };const?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();?//?方法調用const?obj?=?{}; obj.myFunction?=?function()?{?return?new?Date().toString(); }; obj.myFunction();?//?方法調用const?otherFunction?=?obj.myFunction; otherFunction();?//?函數調用parseFloat('16.60');?//?函數調用isNaN(0);?//?函數調用復制代碼
理解函數調用和方法調用之間的區別有助于正確識別上下文。
3.1 方法調用中 this 是腫么樣
在方法調用中,this是擁有這個方法的對象
當調用對象上的方法時,this就變成了對象本身。
創建一個對象,該對象有一個遞增數字的方法
const?calc?=?{?num:?0,?increment:?function()?{?console.log(this?===?calc);?//?=>?true ?this.num?+=?1;?return?this.num; ?} };//?method?invocation.?this?is?calccalc.increment();?//?=>?1calc.increment();?//?=>?2復制代碼
調用calc.increment()使increment函數的上下文成為calc對象。所以使用this.num來增加num屬性是有效的。
再來看看另一個例子。JS對象從原型繼承一個方法,當在對象上調用繼承的方法時,調用的上下文仍然是對象本身
const?myDog?=?Object.create({?sayName:?function()?{?console.log(this?===?myDog);?//?=>?true ?return?this.name; ?} }); myDog.name?=?'Milo';//?方法調用?this?指向?myDogmyDog.sayName();?//?=>?'Milo'復制代碼
Object.create()創建一個新對象myDog,并根據第一個參數設置其原型。myDog對象繼承sayName方法。
執行myDog. sayname()時,myDog是調用的上下文。
在EC6 class 語法中,方法調用上下文也是實例本身
class?Planet?{?constructor(name)?{?this.name?=?name;? ?} ?getName()?{ ?console.log(this?===?earth);?//?=>?true ?return?this.name; ?} }var?earth?=?new?Planet('Earth');//?method?invocation.?the?context?is?earthearth.getName();?//?=>?'Earth'復制代碼
3.2 陷阱:將方法與其對象分離
方法可以從對象中提取到一個單獨的變量const alone = myObj.myMethod。當方法單獨調用時,與原始對象alone()分離,你可能認為當前的this就是定義方法的對象myObject。
如果方法在沒有對象的情況下調用,那么函數調用就會發生,此時的this指向全局對象window嚴格模式下是undefined。
下面的示例定義了Animal構造函數并創建了它的一個實例:myCat。然后setTimout()在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'); ?} }const?myCat?=?new?Animal('Cat',?4);//?The?undefined?has?undefined?legs?setTimeout(myCat.logInfo,?1000);? 復制代碼
你可能認為setTimout調用myCat.loginfo()時,它應該打印關于myCat對象的信息。
不幸的是,方法在作為參數傳遞時與對象是分離,setTimout(myCat.logInfo)以下情況是等效的:
setTimout(myCat.logInfo); //?等價于 const?extractedLogInfo?=?myCat.logInfo;setTimout(extractedLogInfo); 復制代碼
將分離的logInfo作為函數調用時,this是全局 window,所以對象信息沒有正確地打印。
函數可以使用.bind()方法與對象綁定,就可以解決 this 指向的問題。
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'); ?}; }const?myCat?=?new?Animal('Cat',?4);//?logs?"The?Cat?has?4?legs"setTimeout(myCat.logInfo.bind(myCat),?1000); 復制代碼
myCat.logInfo.bind(myCat)返回一個新函數,它的執行方式與logInfo完全相同,但是此時的 this 指向 myCat,即使在函數調用中也是如此。
另一種解決方案是將logInfo()方法定義為一個箭頭函數:
function?Animal(type,?legs)?{?this.type?=?type;?this.legs?=?legs;? ?this.logInfo?=?()?=>?{?console.log(this?===?myCat);?//?=>?true ?console.log('The?'?+?this.type?+?'?has?'?+?this.legs?+?'?legs'); ?}; }const?myCat?=?new?Animal('Cat',?4);//?logs?"The?Cat?has?4?legs"setTimeout(myCat.logInfo,?1000); 復制代碼
4. 構造函數調用
當new關鍵詞緊接著函數對象,(,一組逗號分隔的參數以及)時被調用,執行的是構造函數調用如new RegExp('\\d')。
聲明了一個Country函數,并且將它作為一個構造函數調用:
function?Country(name,?traveled)?{?this.name?=?name???name?:?'United?Kingdom';?this.traveled?=?Boolean(traveled);? } Country.prototype.travel?=?function()?{?this.traveled?=?true; };//?構造函數調用const?france?=?new?Country('France',?false);//?構造函數調用const?unitedKingdom?=?new?Country; france.travel();?//?Travel?to?France復制代碼
new Country('France', false)是Country函數的構造函數調用。它的執行結果是一個name屬性為'France'的新的對象。 如果這個構造函數調用時不需要參數,那么括號可以省略:new Country。
從ES6開始,JS 允許用class關鍵詞來定義構造函數
class?City?{?constructor(name,?traveled)?{?this.name?=?name;?this.traveled?=?false; ?} ?travel()?{?this.traveled?=?true; ?} }//?Constructor?invocationconst?paris?=?new?City('Paris',?false); paris.travel(); 復制代碼
new City('Paris')是構造函數調用。這個對象的初始化由這個類中一個特殊的方法constructor來處理。其中,this指向新創建的對象。
構造函數創建了一個新的空的對象,它從構造函數的原型繼承了屬性。構造函數的作用就是去初始化這個對象。 可能你已經知道了,在這種類型的調用中,上下文指向新創建的實例。
當屬性訪問myObject.myFunction前面有一個new關鍵詞時,JS會執行構造函數調用而不是原來的方法調用。
例如new myObject.myFunction():它相當于先用屬性訪問把方法提取出來extractedFunction = myObject.myFunction,然后利用把它作為構造函數創建一個新的對象: new extractedFunction()。
4.1. 構造函數中的 this
在構造函數調用中 this 指向新創建的對象
構造函數調用的上下文是新創建的對象。它利用構造函數的參數初始化新的對象,設定屬性的初始值,添加事件處理函數等等。
來看看下面示例中的上下文
function?Foo?()?{?console.log(this?instanceof?Foo);?//?=>?true ?this.property?=?'Default?Value'; }//?Constructor?invocationconst?fooInstance?=?new?Foo(); fooInstance.property;?//?=>?'Default?Value'復制代碼
new Foo() 正在進行構造函數調用,其中上下文是fooInstance。 在Foo內部初始化對象:this.property被賦值為默認值。
同樣的情況在用class語法(從ES6起)時也會發生,唯一的區別是初始化在constructor方法中進行:
class?Bar?{?constructor()?{?console.log(this?instanceof?Bar);?//?=>?true ?this.property?=?'Default?Value'; ?} }//?Constructor?invocationconst?barInstance?=?new?Bar(); barInstance.property;?//?=>?'Default?Value'復制代碼
4.2. 陷阱: 忘了使用 new
有些JS函數不是只在作為構造函數調用的時候才創建新的對象,作為函數調用時也會,例如RegExp:
var?reg1?=?new?RegExp('\\w+');var?reg2?=?RegExp('\\w+'); reg1?instanceof?RegExp;?//?=>?truereg2?instanceof?RegExp;?//?=>?truereg1.source?===?reg2.source;?//?=>?true復制代碼
當執行的 new RegExp('\\w+')和RegExp('\\w+')時,JS 會創建等價的正則表達式對象。
使用函數調用來創建對象存在一個潛在的問題(不包括工廠模式),因為一些構造函數可能會忽略在缺少new關鍵字時初始化對象的邏輯。
下面的例子說明了這個問題:
function?Vehicle(type,?wheelsCount)?{?this.type?=?type;?this.wheelsCount?=?wheelsCount;?return?this; }//?忘記使用?new?const?car?=?Vehicle('Car',?4); car.type;?//?=>?'Car'car.wheelsCount?//?=>?4car?===?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; }//?Constructor?invocationconst?car?=?new?Vehicle('Car',?4); car.type?//?=>?'Car'car.wheelsCount?//?=>?4car?instanceof?Vehicle?//?=>?true//?Function?invocation.?Throws?an?error.const?brokenCar?=?Vehicle('Broken?Car',?3); 復制代碼
new Vehicle('Car',4) 運行正常:創建并初始化一個新對象,因為構造函數調用中時使用了new關鍵字。
在構造函數里添加了一個驗證this instanceof Vehicle來確保執行的上下文是正確的對象類型。如果this不是Vehicle,那么就會報錯。這樣,如果執行Vehicle('Broken Car', 3)(沒有new),我們會得到一個異常:Error: Incorrect invocation。
5. 隱式調用
使用myFun.call()或myFun.apply()方法調用函數時,執行的是隱式調用。
JS中的函數是第一類對象,這意味著函數就是對象,對象的類型為Function。從函數對象的方法列表中,.call()和.apply()用于調用具有可配置上下文的函數。
方法 .call(thisArg[, arg1[, arg2[, ...]]])將接受的第一個參數thisArg作為調用時的上下文,arg1, arg2, ...這些則作為參數傳入被調用的函數。
方法.apply(thisArg, [args])將接受的第一個參數thisArg作為調用時的上下文,并且接受另一個類似數組的對象[arg1, arg2, ...]作為被調用函數的參數傳入。
下面是隱式調用的例子
function?increment(number)?{?return?++number;? } increment.call(undefined,?10);?//?=>?11increment.apply(undefined,?[10]);?//?=>?11復制代碼
increment.call()和increment.apply()都用參數10調用了這個自增函數。
兩者的區別是.call()接受一組參數,例如myFunction.call(thisValue, 'value1', 'value2')。而.apply()接受的一組參數必須是一個類似數組的對象,例如myFunction.apply(thisValue, ['value1', 'value2'])。
5.1. 隱式調用中的this
在隱式調用.call()或.apply()中,this是第一個參數
很明顯,在隱式調用中,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'復制代碼
當應該使用特定上下文執行函數時,隱式調用非常有用。例如為了解決方法調用時,this總是window或嚴格模式下的undefined的上下文問題。隱式調用可以用于模擬在一個對象上調用某個方法。
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; }const?myRabbit?=?new?Rabbit('White?Rabbit',?4); myRabbit;?//?{?name:?'White?Rabbit',?countLegs:?4?}復制代碼
Rabbit中的Runner.call(this, name)隱式調用了父類的函數來初始化這個對象。
6. 綁定函數
綁定函數是與對象連接的函數。通常使用.bind()方法從原始函數創建。原始函數和綁定函數共享相同的代碼和作用域,但執行時上下文不同。
方法 myFunc.bind(thisArg[, arg1[, arg2[, ...]]])接受第一個參數thisArg作為綁定函數執行時的上下文,并且它接受一組可選的參數 arg1, arg2, ...作為被調用函數的參數。它返回一個綁定了thisArg的新函數。
function?multiply(number)?{?'use?strict';?return?this?*?number; }const?double?=?multiply.bind(2); double(3);?//?=>?6double(10);?//?=>?20復制代碼
bind(2)返回一個新的函數對象double,double 綁定了數字2。multiply和double具有相同的代碼和作用域。
與.apply()和.call() 方法相反,它不會立即調用該函數,.bind()方法只返回一個新函數,在之后被調用,只是this已經被提前設置好了。
6.1. 綁定函數中的this
在調用綁定函數時,this是.bind()的第一個參數。
.bind()的作用是創建一個新函數,調用該函數時,將上下文作為傳遞給.bind()的第一個參數。它是一種強大的技術,使咱們可以創建一個定義了this值的函數。
來看看,如何在如何在綁定函數設置 this
const?numbers?=?{?array:?[3,?5,?10], ?getNumbers:?function()?{?return?this.array;? ?} };const?boundGetNumbers?=?numbers.getNumbers.bind(numbers); boundGetNumbers();?//?=>?[3,?5,?10]//?Extract?method?from?objectconst?simpleGetNumbers?=?numbers.getNumbers; simpleGetNumbers();?//?=>?undefined?(嚴格模式下報錯)復制代碼
numbers.getNumbers.bind(numbers)返回綁定numbers對象boundGetNumbers函數。boundGetNumbers()調用時的this是number對象,并能夠返回正確的數組對象。
可以將函數numbers.getNumbers提取到變量simpleGetNumbers中而不進行綁定。在之后的函數調用中simpleGetNumbers()的this是window(嚴格模式下為undefined),不是number對象。在這個情況下,simpleGetNumbers()不會正確返回數組。
6.2 緊密的上下文綁定
.bind()創建一個永久的上下文鏈接,并始終保持它。 一個綁定函數不能通過.call()或者.apply()來改變它的上下文,甚至是再次綁定也不會有什么作用。
只有綁定函數的構造函數調用才能更改已經綁定的上下文,但是很不推薦的做法(構造函數調用必須使用常規的非綁定函數)。
下面示例創建一個綁定函數,然后嘗試更改其已預先定義好的上下文
function?getThis()?{?'use?strict';?return?this; }const?one?=?getThis.bind(1);//?綁定函數調用one();?//?=>?1//?使用帶有.apply()和.call()的綁定函數one.call(2);?//?=>?1one.apply(2);?//?=>?1//?再次綁定one.bind(2)();?//?=>?1//?以構造函數的形式調用綁定函數new?one();?//?=>?Object復制代碼
只有new one()改變了綁定函數的上下文,其他方式的調用中this總是等于1。
7. 箭頭函數
箭頭函數用于以更短的形式聲明函數,并在詞法上綁定上下文。它可以這樣使用
const?hello?=?(name)?=>?{?return?'Hello?'?+?name; }; hello('World');?//?=>?'Hello?World'//?Keep?only?even?numbers[1,?2,?5,?6].filter(item?=>?item?%?2?===?0);?//?=>?[2,?6]復制代碼
箭頭函數語法簡單,沒有冗長的function 關鍵字。當箭頭函數只有一條語句時,甚至可以省略return關鍵字。
箭頭函數是匿名的,這意味著name屬性是一個空字符串''。這樣它就沒有詞法上函數名(函數名對于遞歸、分離事件處理程序非常有用)
同時,跟常規函數相反,它也不提供arguments對象。但是,這在ES6中通過rest parameters修復了:
const?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。 換句話說,箭頭函數在詞匯上綁定 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); ?} }const?myPoint?=?new?Point(95,?165); myPoint.log(); 復制代碼
setTimeout使用與log()方法相同的上下文(myPoint對象)調用箭頭函數。正如所見,箭頭函數從定義它的函數繼承上下文。
如果在這個例子里嘗試用常規函數,它創建自己的上下文(window或嚴格模式下的undefined)。因此,要使相同的代碼正確地使用函數表達式,需要手動綁定上下文:setTimeout(function(){…}.bind(this))。這很冗長,使用箭頭函數是一種更簡潔、更短的解決方案。
如果箭頭函數在最頂層的作用域中定義(在任何函數之外),則上下文始終是全局對象(瀏覽器中的 window):
onst?getContext?=?()?=>?{?console.log(this?===?window);?//?=>?true ?return?this; };console.log(getContext()?===?window);?//?=>?true復制代碼
箭頭函數一勞永逸地與詞匯上下文綁定。 即使修改上下文,this也不能被改變:
const?numbers?=?[1,?2]; (function()?{? ?const?get?=?()?=>?{?console.log(this?===?numbers);?//?=>?true ?return?this; ?};?console.log(this?===?numbers);?//?=>?true ?get();?//?=>?[1,?2] ?//?Use?arrow?function?with?.apply()?and?.call() ?get.call([0]);?//?=>?[1,?2] ?get.apply([0]);?//?=>?[1,?2] ?//?Bind ?get.bind([0])();?//?=>?[1,?2]}).call(numbers); 復制代碼
無論如何調用箭頭函數get,它總是保留詞匯上下文numbers。 用其他上下文的隱式調用(通過 get.call([0])或get.apply([0]))或者重新綁定(通過.bind())都不會起作用。
箭頭函數不能用作構造函數。 將它作為構造函數調用(new get())會拋出一個錯誤:TypeError: get is not a constructor。
7.2. 陷阱: 用箭頭函數定義方法
你可能希望使用箭頭函數來聲明一個對象上的方法。箭頭函數的定義相比于函數表達式短得多:(param) => {...} instead of 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'; };const?walkPeriod?=?new?Period(2,?30);? walkPeriod.format();?//?=>?'undefined?hours?and?undefined?minutes'復制代碼
由于format是一個箭頭函數,并且在全局上下文(最頂層的作用域)中定義,因此 this 指向window對象。
即使format作為方法在一個對象上被調用如walkPeriod.format(),window仍然是這次調用的上下文。之所以會這樣是因為箭頭函數有靜態的上下文,并不會隨著調用方式的改變而改變。
該方法返回'undefined hours和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'; };const?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'。
原文:dmitripavlutin.com/gentle-expl…
代碼部署后可能存在的BUG沒法實時知道,事后為了解決這些BUG,花了大量的時間進行log 調試,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug。
總結
為函數調用對this影響最大,從現在開始不要問自己:
this 是從哪里來的?
而是要看看
函數是怎么被調用的?
對于箭頭函數,需要想想
在這個箭頭函數被定義的地方,this是什么?
這是處理this時的正確想法,它們可以讓你免于頭痛。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。