導(dǎo)讀
如果你查閱了javascript箭頭函數(shù)的資料,大抵會(huì)得出這樣的結(jié)論:
1、箭頭函數(shù)最大的特點(diǎn)是沒有this,如果在箭頭函數(shù)內(nèi)部使用this,則this指向函數(shù)被定義時(shí)所在的作用域所指向的this;
2、不能作為構(gòu)造函數(shù);
3、不能用new操作符調(diào)用;
4、沒有prototype屬性;
5、不能用call/apply/bind去改變this指向;
6、也沒有屬于自己的arguments、super、new.target。
我們知道this指向問題是箭頭函數(shù)誕生的最主要原因,但是,它為什么不能作為構(gòu)造函數(shù)、為什么不能用new操作符調(diào)用、又為什么沒有prototype,是刻意的設(shè)計(jì)還是修改this問題帶來的副作用?如果你有興趣深入研究,歡迎往下看并留言指正和補(bǔ)充。
一、箭頭函數(shù)的特點(diǎn)
這一章我會(huì)把箭頭函數(shù)常見的8個(gè)特點(diǎn)用舉例的方式羅列出來。
1、箭頭函數(shù)沒有this,箭頭函數(shù)如果內(nèi)部使用了this,那么這個(gè)this永遠(yuǎn)等于:箭頭函數(shù)定義時(shí)所在的詞法作用域所指向的this,如果這個(gè)詞法作用域的this是動(dòng)態(tài)的,那箭頭函數(shù)的this也是動(dòng)態(tài)的。
function Foo() {
setTimeout( () => {
console.log(this.id);
}, 100);
}
var id = 21;
var obj = { id: 42 }
Foo(); //21
Foo.call(obj); //42
箭頭函數(shù)作為實(shí)參傳入setTimeout,是在Foo(){...}這個(gè)函數(shù)作用域里定義的,而這個(gè)作用域的this指向哪兒,箭頭函數(shù)的this就指向哪兒。所以:
(1)、Foo()調(diào)用的時(shí)候,this指向全局,所以箭頭函數(shù)的this也指向全局。
(2)、Foo.call(obj)調(diào)用的時(shí)候,this指向obj,所以箭頭函數(shù)的this也指向obj。
2、如果箭頭函數(shù)外層沒有普通函數(shù),無論嚴(yán)格模式還是寬松格模式,它的this都會(huì)指向window(全局對(duì)象),而普通函數(shù)嚴(yán)格模式下this為undefined。
3、箭頭函數(shù)因?yàn)闆]有this,所以不能用call、apply、bind去改變this指向。
(x => this.a+x).call( { a:20 } ) //不會(huì)報(bào)錯(cuò),只是this指向不會(huì)被改變
4、箭頭函數(shù)不能用new操作符調(diào)用。
var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor
5、箭頭函數(shù)因?yàn)椴荒苡胣ew操作符調(diào)用,所以也沒有new.target屬性。
PS:new.target屬性用于確定構(gòu)造函數(shù)是否為new調(diào)用
6、箭頭函數(shù)不存在 prototype 屬性。
var Foo = () => {};
console.log(Foo.prototype); // undefined
7、箭頭函數(shù)也沒有自己的arguments、super。如果箭頭函數(shù)內(nèi)部使用到,指向外層函數(shù)的arguments、super。
function Foo() {
setTimeout(() => {
console.log(arguments);
}, 100);
}
Foo(2, 4, 6, 8); //[2, 4, 6, 8]
但是可以用rest參數(shù)去獲取箭頭函數(shù)不定數(shù)量的參數(shù):
var Foo = (...rest) => {
console.log(rest)
}
Foo(2, 4, 6, 8); //[2, 4, 6, 8]
8、箭頭函數(shù)都是匿名函數(shù)。
我們知道匿名函數(shù)有以下幾個(gè)不足:
(1) 匿名函數(shù)在棧追蹤中不會(huì)顯示出有意義的函數(shù)名,使得調(diào)試很困難。
(2) 如果沒有函數(shù)名,當(dāng)函數(shù)需要引用自身時(shí)只能使用已經(jīng)過期的 arguments.callee 引用,比如在遞歸中。另一個(gè)函數(shù)需要引用自身的例子,是在事件觸發(fā)后事件監(jiān)聽器需要解綁自身。
(3) 匿名函數(shù)省略了對(duì)于代碼可讀性/可理解性很重要的函數(shù)名。在不污染命名空間的時(shí)候,一個(gè)語義化的名稱可以讓代碼不言自明。
匿名函數(shù)的特點(diǎn)也隱隱指出哪些場合可以用箭頭函數(shù),哪些場合不合適。此外,用作IIFE立即執(zhí)行函數(shù)時(shí),箭頭函數(shù)和普通函數(shù)也有一個(gè)細(xì)微區(qū)別:
/**普通函數(shù)這兩種寫法都可以*/
( function(x){console.log(x)} )(1);
( function(x){console.log(x)}(1) );
/**箭頭函數(shù)只支持一種寫法*/
( x => {console.log(x)} )(1);
( x => {console.log(x)}(1) ); //不支持
二、箭頭函數(shù)為什么有這些特點(diǎn)?
更少的字符、沒有麻煩的this、匿名函數(shù)(沒有函數(shù)名)、沒有prototype、不允許用new調(diào)用。這一切看起來都讓箭頭函數(shù)看上去是普通函數(shù)的精簡版。從掌握的資料來看,有理由認(rèn)為箭頭函數(shù)是一種職責(zé)更單一的函數(shù)。讓我們往下分析!
1、this是引擎創(chuàng)建執(zhí)行上下文(Execution Contexts)時(shí),提供給出來的API,是外部JS代碼訪問當(dāng)前執(zhí)行上下文的唯一通道,而箭頭函數(shù)關(guān)閉了這個(gè)通道,它并不希望提供當(dāng)前執(zhí)行上下文給外部。
(1)首先明確一點(diǎn):箭頭函數(shù)是真的沒有this,而不是所謂的繼承外層函數(shù)的this。箭頭函數(shù)內(nèi)部使用this時(shí)看上去更像閉包!翻閱ECMAScript規(guī)范看到:
規(guī)范明確說明了箭頭函數(shù)沒有this、argument、super和new.target。如果在這種情況下你仍然在箭頭函數(shù)內(nèi)部使用this,那么此時(shí)是往外層作用域找到這個(gè)this的,看上去非常像閉包不是嗎:
//箭頭函數(shù)內(nèi)部使用this
function Foo() {
var Bar = () => {
console.log(this);
};
return Bar;
};
Foo()();
//用閉包方式引用外層this
function Foo() {
var self = this;
var Bar = () => {
console.log(self);
};
return Bar;
};
Foo()();
(2)每個(gè)函數(shù)被調(diào)用之時(shí)引擎都會(huì)主動(dòng)創(chuàng)建一個(gè)執(zhí)行上下文(execution context)。這個(gè)上下文會(huì)包含函數(shù)在哪里被調(diào)用(通常是函數(shù)環(huán)境記錄Function Environment Records)、函數(shù)的調(diào)用方法、傳入的參數(shù)等信息。從ECMAScript規(guī)范可以看到:


只要不是箭頭函數(shù)(ArrowFunction)就會(huì)提供this綁定。箭頭函數(shù)是個(gè)特例受到了特殊對(duì)待。所以,this是屬于執(zhí)行上下文的一部分,同時(shí)也是引擎唯一一個(gè)暴露給外部JS代碼用以訪問當(dāng)前執(zhí)行上下文的通道,而引擎把箭頭函數(shù)的通道給關(guān)了,表明引擎并不希望把箭頭函數(shù)的執(zhí)行上下文提供給外部。
當(dāng)然this有它自己特定的用途,它提供了一種更優(yōu)雅的方式來隱式“傳遞”一個(gè)對(duì)象引用,因此可以將 API 設(shè)計(jì)得更加簡潔并且易于復(fù)用。 隨著你的使用模式越來越復(fù)雜,顯式傳遞上下文對(duì)象會(huì)讓代碼變得越來越混亂,使用 this 則不會(huì)這樣。但我猜你的代碼里肯定也存在這樣的代碼:
function add(x,y) {
return x + y;
};
這類函數(shù)在業(yè)務(wù)代碼里大量存在,壓根沒有用到this,但引擎仍然會(huì)為其綁定一個(gè)this,無疑是一種不小的開銷和浪費(fèi)。所以單從沒有this這點(diǎn)上講,表明箭頭函數(shù)運(yùn)行時(shí)省去了一些步驟,同時(shí)也表明普通函數(shù)能做的事箭頭函數(shù)不一定能勝任,不可能相互替代。不想面對(duì)麻煩的this也不是選擇箭頭函數(shù)的唯一理由,還有什么理由選擇用箭頭函數(shù)呢,請(qǐng)往下看。
2、箭頭函數(shù)為什么不能用new調(diào)用
(1)一個(gè)函數(shù)能被new操作符調(diào)用的條件是什么?看看規(guī)范怎么說:



規(guī)范指出,一個(gè)函數(shù)能用new操作符調(diào)用那么它必定是一個(gè)constructor,而是一個(gè)constructor的條件是它具有[[Construct]] internal method(內(nèi)置的構(gòu)造方法)。并且
A function object is not necessarily a constructor指出函數(shù)不一定都是constructor。恰好我們的主角箭頭函數(shù)就是這一類不是constructor的函數(shù)。那么為什么箭頭函數(shù)不是constructor?回答這個(gè)問題前,我們首先要知道constructor最最最主要的作用是
creates and initializes objects,也就是創(chuàng)建和初始化對(duì)象,然后我們要把prototype拉進(jìn)來一起討論,因?yàn)閜rototype和constructor相伴相生。先看規(guī)范:

三、總結(jié)
我認(rèn)為ES6之前JS中的函數(shù)做為一等公民,功能非常強(qiáng)大,強(qiáng)大到并不是所有函數(shù)都需要這些功能,事實(shí)上我們編寫的很多函數(shù)都像return x+y;一樣根本不需要this、不需要上下文、不需要原型、不需要?jiǎng)?chuàng)建對(duì)象,而這些引擎都默默的做了,無疑是一種額外的開銷。所以箭頭函數(shù)是被設(shè)計(jì)為一種精簡后的、職責(zé)更單一的函數(shù)存在。
四、引申
1、用Function.prototype.bind生成的函數(shù)行為怪異。
用Function.prototype.bind生成的函數(shù)稱為Bound Function Object,是一類極其特殊的函數(shù)??聪旅娴拇a:
let Foo = function (){
console.log('Foo')
}
var Bar = Foo.bind({})
Bar()
Foo調(diào)用bind后生成一個(gè)新的函數(shù),這個(gè)新生成的函數(shù)就是一個(gè)Bound Function Object??纯碋CMAScript規(guī)范對(duì)它的定義:


規(guī)范指出Bound Function Object可以有內(nèi)置Construct,也可以沒有
(may have a ... ...)。而且它有沒有內(nèi)置Construct完全由所綁定的函數(shù)決定,對(duì)于上例,即是由Foo決定。Foo如果是一個(gè)constructor,那Bar也是一個(gè)constructor,反之則不是。所以:
var Foo1 = function(){};
var Foo2 = () => {};
var Bar1 = Foo1.bind({});
var Bar2 = Foo2.bind({});
new Bar1(); //正常執(zhí)行
new Bar2(); //Bar2 is not a constructor
因?yàn)镕oo2作為箭頭函數(shù)本身不是constructor,所以用bind綁定后生成的新函數(shù)也不是constructor。另外,無論所綁定的函數(shù)是否是constructor,它都沒有prototype,看規(guī)范:
所以:
var Foo1 = function(){};
var Foo2 = () => {};
var Bar1 = Foo1.bind({});
var Bar2 = Foo2.bind({});
console.log(Bar1.prototype); //undefined
console.log(Bar2.prototype); //undefined
我覺得這里應(yīng)該是ECMAScript不嚴(yán)謹(jǐn)?shù)牡胤?。?guī)范里多個(gè)地方都指出做為constructor的函數(shù)就有prototype屬性,但bind這里又是個(gè)特例。
2、箭頭函數(shù)沒有this,這是JS歷史以來頭一遭。但不是constructor的函數(shù)卻不是頭一回了,除了箭頭函數(shù)不是constructor,還有哪些函數(shù)不是constructor呢?
(1)async函數(shù)。
var Foo1 = async function(){};
var Foo2 = async () => {};
console.log(Foo1.prototype); //undefined
console.log(Foo2.prototype); //undefined
new Foo1(); //Foo1 is not a constructor
new Foo2(); //Foo2 is not a constructor
說明無論箭頭函數(shù)、普通函數(shù),只要被設(shè)置成async,那么都不是constructor,自然也沒有prototype屬性。
(2)大部分內(nèi)置函數(shù)(準(zhǔn)確的說是Built-in Functions not implemented as an ECMAScript Function)。什么是”not implemented as an ECMAScript Function“呢?你可以理解為內(nèi)置函數(shù)有兩種實(shí)現(xiàn)方式,一種是實(shí)現(xiàn)為ECMAScript Function Object,一種是非ECMAScript Function Object。凡事以ECMAScript Function Object方式實(shí)現(xiàn)的內(nèi)置函數(shù)都是constructor(也具有prototype屬性)。比如Boolean()、Number()、String()函數(shù)。下面這兩種用法都沒毛病。
Number('1014');
new Number('1024');
之所以能用new調(diào)用是因?yàn)镹umber內(nèi)置函數(shù)是以ECMAScript Function Object方式實(shí)現(xiàn)的。那么剩下的內(nèi)置函數(shù)都是以非ECMAScript Function Object方式實(shí)現(xiàn)的,有decodeURI() encodeURI() decodeURIComponent() encodeURIComponent() isFinite() isNaN() parseInt() parseFloat() 以及Math.random()等Math對(duì)象下的函數(shù),這些內(nèi)置函數(shù)都不是constructor,也沒有prototype,因?yàn)樗鼈冏鰹楣ぞ吆瘮?shù),不需要?jiǎng)?chuàng)建初始化新對(duì)象、不需要共享原型的屬性。所以:
console.log(parseInt.prototype); //undefined
console.log(Math.random.prototype); //undefined
new parseInt(); //parseInt is not a constructor
new Math.random(); //Math.random is not a constructor
也就是規(guī)范里這條指出的:
除非特別聲明,內(nèi)置函數(shù)一般不是constructor、也沒有prototype屬性。而進(jìn)行了特別聲明的就是Boolean()、Number()、String()這3個(gè)了。