作者:Dmitri Pavlutin
譯者:前端小智
來源:dmitripavlutin
你知道的越多,你不知道的越多
點贊再看,養成習慣
本文 GitHub:github.com/qq449245884… 上已經收錄,更多往期高贊文章的分類,也整理了很多我的文件,和教程資料。歡迎Star和完善,大家面試可以參照考點複習,希望我們一起有點東西。
為了保證的可讀性,本文采用意譯而非直譯。
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); // => 31
window.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); // => 11
strictSum(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 calc
calc.increment(); // => 1
calc.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 指向 myDog
myDog.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 earth
earth.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 invocation
const 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 invocation
const 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 invocation
const barInstance = new Bar();
barInstance.property; // => 'Default Value'
複製程式碼
4.2. 陷阱: 忘了使用 new
有些JS函式不是隻在作為建構函式呼叫的時候才建立新的物件,作為函式呼叫時也會,例如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+')
時,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 // => 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;
}
// Constructor invocation
const car = new Vehicle('Car', 4);
car.type // => 'Car'
car.wheelsCount // => 4
car 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); // => 11
increment.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); // => 6
double(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 object
const 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); // => 1
one.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][2]。
總結
為函式呼叫對this
影響最大,從現在開始不要問自己:
this 是從哪裡來的?
而是要看看
函式是怎麼被呼叫的?
對於箭頭函式,需要想想
在這個箭頭函式被定義的地方,this是什麼?
這是處理this
時的正確想法,它們可以讓你免於頭痛。
交流(歡迎加入群,群工作日都會發紅包,互動討論技術)
為了回饋讀者,《大遷世界》不定期舉行(每個月一到三次),現金抽獎活動,保底200,外加使用者讚賞,希望你能成為大遷世界的小錦鯉,快來試試吧
乾貨系列文章彙總如下,覺得不錯點個Star,歡迎 加群 互相學習。
我是小智,公眾號「大遷世界」作者,對前端技術保持學習愛好者。我會經常分享自己所學所看的乾貨,在進階的路上,共勉!
關注公眾號,後臺回覆福利,即可看到福利,你懂的。
每次整理文章,一般都到2點才睡覺,一週4次左右,挺苦的,還望支援,給點鼓勵
[1]: github.com/qq449245884… [2]: www.fundebug.com/?utm_source…