你不知道的javaScript箭頭函數,this只是冰山一角

導讀

如果你查閱了javascript箭頭函數的資料,大抵會得出這樣的結論:
1、箭頭函數最大的特點是沒有this,如果在箭頭函數內部使用this,則this指向函數被定義時所在的作用域所指向的this;
2、不能作爲構造函數;
3、不能用new操作符調用;
4、沒有prototype屬性;
5、不能用call/apply/bind去改變this指向;
6、也沒有屬於自己的arguments、super、new.target。
我們知道this指向問題是箭頭函數誕生的最主要原因,但是,它爲什麼不能作爲構造函數、爲什麼不能用new操作符調用、又爲什麼沒有prototype,是刻意的設計還是修改this問題帶來的副作用?如果你有興趣深入研究,歡迎往下看並留言指正和補充。

一、箭頭函數的特點

這一章我會把箭頭函數常見的8個特點用舉例的方式羅列出來。

1、箭頭函數沒有this,箭頭函數如果內部使用了this,那麼這個this永遠等於:箭頭函數定義時所在的詞法作用域所指向的this,如果這個詞法作用域的this是動態的,那箭頭函數的this也是動態的。
function Foo() {
  setTimeout( () => {
    console.log(this.id);
  }, 100);
}

var id = 21;
var obj = { id: 42 }

Foo();  //21
Foo.call(obj);  //42

箭頭函數作爲實參傳入setTimeout,是在Foo(){...}這個函數作用域裏定義的,而這個作用域的this指向哪兒,箭頭函數的this就指向哪兒。所以:
(1)、Foo()調用的時候,this指向全局,所以箭頭函數的this也指向全局。
(2)、Foo.call(obj)調用的時候,this指向obj,所以箭頭函數的this也指向obj。

2、如果箭頭函數外層沒有普通函數,無論嚴格模式還是寬鬆格模式,它的this都會指向window(全局對象),而普通函數嚴格模式下this爲undefined。
3、箭頭函數因爲沒有this,所以不能用call、apply、bind去改變this指向。
(x => this.a+x).call( { a:20 } )   //不會報錯,只是this指向不會被改變
4、箭頭函數不能用new操作符調用。
 var Foo = () => {};
 var foo = new Foo();  // TypeError: Foo is not a constructor
5、箭頭函數因爲不能用new操作符調用,所以也沒有new.target屬性。

PS:new.target屬性用於確定構造函數是否爲new調用

6、箭頭函數不存在 prototype 屬性。
var Foo = () => {};
console.log(Foo.prototype);  // undefined
7、箭頭函數也沒有自己的arguments、super。如果箭頭函數內部使用到,指向外層函數的arguments、super。
function Foo() {
  setTimeout(() => {
    console.log(arguments);
  }, 100);
}
Foo(2, 4, 6, 8); //[2, 4, 6, 8]

但是可以用rest參數去獲取箭頭函數不定數量的參數:

var Foo = (...rest) => {
  console.log(rest)
}
Foo(2, 4, 6, 8); //[2, 4, 6, 8]
8、箭頭函數都是匿名函數。

我們知道匿名函數有以下幾個不足:
(1) 匿名函數在棧追蹤中不會顯示出有意義的函數名,使得調試很困難。
(2) 如果沒有函數名,當函數需要引用自身時只能使用已經過期的 arguments.callee 引用,比如在遞歸中。另一個函數需要引用自身的例子,是在事件觸發後事件監聽器需要解綁自身。
(3) 匿名函數省略了對於代碼可讀性/可理解性很重要的函數名。在不污染命名空間的時候,一個語義化的名稱可以讓代碼不言自明。
匿名函數的特點也隱隱指出哪些場合可以用箭頭函數,哪些場合不合適。此外,用作IIFE立即執行函數時,箭頭函數和普通函數也有一個細微區別:

/**普通函數這兩種寫法都可以*/
( function(x){console.log(x)} )(1);
( function(x){console.log(x)}(1) );

/**箭頭函數只支持一種寫法*/
( x => {console.log(x)} )(1);
( x => {console.log(x)}(1) );  //不支持

二、箭頭函數爲什麼有這些特點?

更少的字符、沒有麻煩的this、匿名函數(沒有函數名)、沒有prototype、不允許用new調用。這一切看起來都讓箭頭函數看上去是普通函數的精簡版。從掌握的資料來看,有理由認爲箭頭函數是一種職責更單一的函數。讓我們往下分析!

1、this是引擎創建執行上下文(Execution Contexts)時,提供給出來的API,是外部JS代碼訪問當前執行上下文的唯一通道,而箭頭函數關閉了這個通道,它並不希望提供當前執行上下文給外部。

(1)首先明確一點:箭頭函數是真的沒有this,而不是所謂的繼承外層函數的this。箭頭函數內部使用this時看上去更像閉包!翻閱ECMAScript規範看到:

規範明確說明了箭頭函數沒有this、argument、super和new.target。如果在這種情況下你仍然在箭頭函數內部使用this,那麼此時是往外層作用域找到這個this的,看上去非常像閉包不是嗎:

//箭頭函數內部使用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)每個函數被調用之時引擎都會主動創建一個執行上下文(execution context)。這個上下文會包含函數在哪裏被調用(通常是函數環境記錄Function Environment Records)、函數的調用方法、傳入的參數等信息。從ECMAScript規範可以看到:

正在運行的執行上下文始終是此堆棧的頂層元素。每當控制權從與當前運行的執行上下文關聯的可執行代碼轉移到與該執行上下文無關的可執行代碼時,將創建一個新的執行上下文。新創建的執行上下文被推送到堆棧上,成爲正在運行的執行上下文。this的綁定發生在創建Function Environment Records時:可以看到引擎在調用函數時要判斷函數類型(FunctionKind),只要不是箭頭函數(ArrowFunction)就會提供this綁定。箭頭函數是個特例受到了特殊對待。
所以,this是屬於執行上下文的一部分,同時也是引擎唯一一個暴露給外部JS代碼用以訪問當前執行上下文的通道,而引擎把箭頭函數的通道給關了,表明引擎並不希望把箭頭函數的執行上下文提供給外部。
當然this有它自己特定的用途,它提供了一種更優雅的方式來隱式“傳遞”一個對象引用,因此可以將 API 設計得更加簡潔並且易於複用。 隨着你的使用模式越來越複雜,顯式傳遞上下文對象會讓代碼變得越來越混亂,使用 this 則不會這樣。但我猜你的代碼裏肯定也存在這樣的代碼:

function add(x,y) {  
  return x + y;
};

這類函數在業務代碼裏大量存在,壓根沒有用到this,但引擎仍然會爲其綁定一個this,無疑是一種不小的開銷和浪費。所以單從沒有this這點上講,表明箭頭函數運行時省去了一些步驟,同時也表明普通函數能做的事箭頭函數不一定能勝任,不可能相互替代。不想面對麻煩的this也不是選擇箭頭函數的唯一理由,還有什麼理由選擇用箭頭函數呢,請往下看。

2、箭頭函數爲什麼不能用new調用

(1)一個函數能被new操作符調用的條件是什麼?看看規範怎麼說:


規範指出,一個函數能用new操作符調用那麼它必定是一個constructor,而是一個constructor的條件是它具有[[Construct]] internal method(內置的構造方法)。並且A function object is not necessarily a constructor指出函數不一定都是constructor。恰好我們的主角箭頭函數就是這一類不是constructor的函數。
那麼爲什麼箭頭函數不是constructor?回答這個問題前,我們首先要知道constructor最最最主要的作用是creates and initializes objects,也就是創建和初始化對象,然後我們要把prototype拉進來一起討論,因爲prototype和constructor相伴相生。先看規範:規範指出,每個consturctor都是一個函數,它有一個名爲“prototype”的屬性,用於實現基於原型的繼承和共享屬性。所以可以推測,箭頭函數之所以被設計成非constructor,是因爲設計師對它的期望是:不要幹創建初始化對象的活,也不需要和原型共享什麼屬性,這些東西交給普通函數幹就好,箭頭函數你只做簡簡單單的事情。所以關於箭頭函數不能用new調用的結論是:

三、總結

我認爲ES6之前JS中的函數做爲一等公民,功能非常強大,強大到並不是所有函數都需要這些功能,事實上我們編寫的很多函數都像return x+y;一樣根本不需要this、不需要上下文、不需要原型、不需要創建對象,而這些引擎都默默的做了,無疑是一種額外的開銷。所以箭頭函數是被設計爲一種精簡後的、職責更單一的函數存在。

四、引申

1、用Function.prototype.bind生成的函數行爲怪異。

用Function.prototype.bind生成的函數稱爲Bound Function Object,是一類極其特殊的函數。看下面的代碼:

let Foo = function (){
  console.log('Foo')
}
var Bar = Foo.bind({})
Bar()

Foo調用bind後生成一個新的函數,這個新生成的函數就是一個Bound Function Object。看看ECMAScript規範對它的定義:


規範指出Bound Function Object可以有內置Construct,也可以沒有(may have a ... ...)。而且它有沒有內置Construct是有所綁定的函數決定的,對於上例,即是由Foo決定。Foo如果是一個constructor,那Bar也是一個constructor,反之則不是。所以:

var Foo1 = function(){};
var Foo2 = () => {};
var Bar1 = Foo1.bind({});
var Bar2 = Foo2.bind({});

new Bar1();  //正常執行
new Bar2();  //Bar2 is not a constructor

因爲Foo2作爲箭頭函數本身不是constructor,所以用bind綁定後生成的新函數也沒有constructor。另外,無論所綁定的函數是否有constructor,它都沒有prototype,看規範:

所以:

var Foo1 = function(){};
var Foo2 = () => {};
var Bar1 = Foo1.bind({});
var Bar2 = Foo2.bind({});

console.log(Bar1.prototype); //undefined
console.log(Bar2.prototype); //undefined

這裏應該是ECMAScript不嚴謹的地方。規範裏多處地方都指出做爲constructor的函數就有prototype屬性,但bind這裏又是個特例。

2、箭頭函數沒有this,這是JS歷史以來頭一遭。但不是constructor的函數卻不是頭一回了,除了箭頭函數不是constructor,還有哪些函數不是constructor呢?

(1)async函數。

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

說明無論箭頭函數、普通函數,只要被設置成async,那麼都不是constructor,自然也沒有prototype屬性。
(2)大部分內置函數(準確的說是Built-in Functions not implemented as an ECMAScript Function)。什麼是”not implemented as an ECMAScript Function“呢?你可以理解爲內置函數有兩種實現方式,一種是實現爲ECMAScript Function Object,一種是非ECMAScript Function Object。凡事以ECMAScript Function Object方式實現的內置函數都是constructor(也具有prototype屬性)。比如Boolean()、Number()、String()函數。下面這兩種用法都沒毛病。

Number('1014');
new Number('1024');

之所以能用new調用是因爲Number內置函數是以ECMAScript Function方式實現的。那麼剩下的內置函數都是以非ECMAScript Function Object方式實現的,有decodeURI() encodeURI() decodeURIComponent() encodeURIComponent() isFinite() isNaN() parseInt() parseFloat() 以及Math.random()等Math對象下的函數,這些內置函數不是一個constructor,也沒有prototype,因爲它們做爲工具函數,不需要創建初始化新對象、不需要共享原型的屬性。所以:

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

也就是規範裏這條指出的:

除非特別聲明,內置函數一般不是constructor、也沒有prototype屬性。而進行了特別聲明的就是Boolean()、Number()、String()這3個了。

請留言,請點贊,謝謝~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章