ECMAScript規範中對Function的文檔描述,我認爲是ECMAScript規範中最複雜也是最很差理解的一部分,它涉及到了各方面。光對Function就分了Function Definitions、Arrow Function Definitions、Method Definitions、Generator Function Definitions、Class Definitions、Async Function Definitions、Async Arrow Function Definitions這幾塊。我準備花三章來介紹Function。這篇文章主要是理解ArrowFunction和GeneratorFunction,固然還包括最基本最普通的Function Definitions。segmentfault
在瞭解Function Definitions以前咱們須要知道函數對象(Function Object)。咱們都知道Function本質上也是一個對象,因此普通對象有的方法Function對象都有,此外Function對象還有本身的內部方法。全部Function對象都有一個[[Call]]的內部方法,有了這個方法Function對象才能被用做函數調用,即你***()時內部調用就是[[Call]]方法,固然不是全部有[[Call]]方法的Function對象均可以進行***()調用,常見的Map、Set等方法雖然有[[Call]]方法,可是你不能進行Map()和Set(),這時候就用到了Function對象的另外一個內部方法[[Construct]],當Function做爲構造函數調用時,就會使用[[Construct]]方法。app
注意:不是全部Function對象都有[[Construct]]方法。只有當Function做爲構造函數調用時,纔會有[[Construct]]方法,好比ArrowFunction和GeneratorFunction只有[[Call]]方法沒有[[Construct]]方法。函數
先說[[Call]]方法,看到這個名字很容易讓人想起Function.prototype中的call方法,沒錯Function.prototype.call以及Function.prototype.apply都是顯示的調用了[[Call]]方法,之因此說顯示調用是相比於***()調用,call和apply要簡單直接的多。[[Call]]方法接受兩個參數,一個是thisArgument,另外一個是argumentsList,thisArgument即表示了function中的this對象,argumentsList表明了function中的參數列表,看!和Function.prototype.apply的調用方式是如此的類似。學習
function foo(){} foo(1,2) //當執行foo()方法時,實際上內部是以下調用 foo.[[Call]](undefined,« 1, 2 ») //« »表示ECMAScript的List規範類型 //注意,若是你是隱式調用function,那麼thisArgument是undefined,不是常說的全局對象window, //只是在[[Call]]內部執行時檢測到若是thisArgument是undefined或null, //且在非嚴格模式下才會變成全局對象,即foo(1,2)你能夠認爲等價與下面的: foo.call(null,1,2) foo.apply(undefined,[1,2]) //------------------- var a={ foo:function(){} } a.foo(1,2) //等價與==> foo.[[Call]](a,« 1, 2 ») //等價與==> foo.call(a,1,2) //等價與==> foo.apply(a,[1,2])
這裏有個建議,之後你遇到this指向問題的時候,你把function轉成call或者apply模式,你就能清楚的明白this指向什麼。this
[[Construct]]內部方法主要有new關鍵字調用Function時纔會執行[[Construct]]方法。[[Construct]]方法主要接受兩個參數一個是argumentsList, 還有一個是newTarget。newTarget正常調用下指向調用的function對象。好比foo(),newTarget就是foo,你能夠在函數內部用new.target訪問到。構造函數中的this對象與newTarget有關,若是newTarget.prototype存在,且是Object對象,則this就是ObjectCreate(newTarget.prototype),ObjectCreate是Object.create內部調用的方法,若是newTarget.prototype不存在或者不是Object對象,this至關於ObjectCreate(Object.prototype)。spa
function Foo(){} var fooInstance = new Foo(1,2) //等價與==> var fooInstance = Foo.[[Construct]](« 1, 2 »,Foo); fooInstance instanceof Foo //true Object.create(Foo.prototype) instanceof Foo //true //注意若是構造函數有本身的return返回,那麼狀況有所不一樣。 //返回的是Object,則構造函數的實例就是返回的對象 //返回的不是Object,至關於默認沒有返回 function Foo(){ return {a:1}} var fooInstance = new Foo(1,2) fooInstance instanceof Foo //false,注意不是true,fooInstance不是Foo的實例 Object.create(Foo.prototype) instanceof Foo //true //只要Foo.prototype存在且是對象,那麼Object.create(Foo.prototype)永遠是Foo的一個實例
Function Definitions包含了FunctionDeclaration和FunctionExpression,有一些早期錯誤檢測添加到Function Definitions中,其中在function中的let、const和var聲明的變量規則參考上一篇文章var、let、const聲明的區別,另外有一些附加的早期錯誤:prototype
function中的參數被認爲是var聲明,所以:code
function foo(a,b){ let a = 1; //SyntaxError,重複聲明a } foo();
若是函數體是嚴格模式而參數列表不是簡單參數列表,則語法錯誤:對象
//不是簡單參數指的是包含解構賦值 function foo(a=1,...c){ 'use strict' //SyntaxError } //若是'use strict'在函數體外定義則沒有錯誤 'use strict' function foo(a=1,...c){} //ok
函數體以及函數參數不能直接出現superblog
function foo(super){} //SyntaxError function foo(){ super();} //SyntaxError
FunctionDeclaration分爲帶變量名的函數聲明以及匿名函數聲明,匿名函數聲明只能在export中可用,其它任何地方使用匿名函數聲明都報錯。
在進行評估腳本和函數的時候會對包含在其中的函數聲明進行InstantiateFunctionObject方法,即初始化函數對象。注:該方法是在執行腳本和函數代碼以前進行的。
InstantiateFunctionObject方法簡單來講作了三步:1.FunctionCreate 2.makeConstructor 3. SetFunctionName。分開說
function foo(a,b){}; foo.__proto__ === Function.prototype; foo.length === 2; foo.prototype.__proto__ === Object.prototype; foo.prototype.constructor === foo; foo.name === 'foo';
FunctionExpression也分爲兩類,有變量名的函數表達式和匿名函數表達式。函數表達式在執行時也會建立Function對象,步驟和函數聲明類似。其中匿名函數表達式不會定義屬性name,即不會執行第三步中的SetFunctionName。有變量名的函數表達式與函數聲明以及匿名函數表達式的區別在於做用域鏈,咱們都知道一旦函數表達式中定義了變量名,咱們就能夠在函數體內經過該變量名調用函數自身。可問題來了,該函數變量名是定義在哪裏呢?函數外仍是在函數內呢?
var func = function foo(){}; foo(); //Uncaught ReferenceError: foo is not defined //顯然沒有在函數外定義函數表達式的變量名,那麼是定義在函數內的? //我提到過在全局做用域和函數做用域中,var、function聲明的變量,let和const不能重複聲明。 var func = function foo(){ let foo = 1; //ok,可見函數表達式的變量名也不是在函數內聲明的。 }; foo();
看到這可能有人會認爲函數表達式的變量名可能容許let和const進行覆蓋。其實不是,有變量名的函數表達式在建立Function對象的時候,建立了一個匿名做用域,在該做用域中定義了函數表達式的變量名。按上面這個例子,foo函數的外部做用域並非全局做用域,而是一個匿名做用域,匿名做用域的外部做用域纔是真正的全局做用域。匿名函數表達式和函數聲明都不會建立匿名做用域。
ArrowFunction(箭頭函數)是ES6新增的一種新語法,主要是用來簡化function的寫法,更準確的說是簡化匿名函數表達式的一種寫法。所以匿名函數表達式的規則也適用於ArrowFunction,不過二者仍是有區別的,ArrowFunction中沒有規定不能直接出現super,也就是說在ArrowFunction中能夠用super方法,其次ArrowFunction內部沒有[[Construct]]方法,所以不能做爲構造器調用,因此在建立Function對象時不執行makeConstructor方法。最重要一點就是ArrowFunction沒有本地的this對象。
咱們上面提道全部Function對象都有[[Call]]內部方法,接受this對象和參數列表兩個字段。此外Function對象還有一個[[ThisMode]]內部屬性,用來判斷是ArrowFunction仍是非ArrowFunction,若是是ArrowFunction,那麼無論[[Call]]中傳來的this是什麼都會被丟棄。此外arguments, super和new.target和this也是同樣的。我在之前的文章中稍微提到過ArrowFunction中的this對象,我在這從新講一下:
var name = 'outer arrow'; var obj = { name:'inner arrow', arrow: () => { console.log(this.name) } } obj.arrow(); //outer arrow,不是inner arrow
咱們在ArrowFunction遇到this對象時,你不要把this當作是ArrowFunction的一部分,你從ArrowFunction中拿出this放到ArrowFunction的外部,觀察外部的this對象是什麼,外部的this對象就是ArrowFunction的this對象。此外還要清楚不論是call仍是apply都是對ArrowFunction無效的,它們最終調用的都是[[Call]]內部方法,固然bind也是無效的。
咱們看一下ArrowFunction中的super應用,仍是改編了MDN中的例子:
var obj1 = { method() { console.log("method 1"); } } var obj2 = { method() { console.log("method 2"); return ()=>{super.method();} } } Object.setPrototypeOf(obj2, obj1); var arrow = obj2.method() //method 2 arrow(); //method 1
注意:method1和method2其實就是Method Definitions。
若是單看arrow這個函數,它自己是不可能有super的,由於沒有任何繼承關係,只是一個單一的ArrowFunction,可是你放在obj2中就有了意義,Object.setPrototypeOf(obj2, obj1);這句話把obj1變爲obj2的原型對象,obj2繼承了obj1的屬性和方法,obj2的super對象就是obj1,所以ArrowFunction中的super參照this可知,該super是obj1。
總的一句話歸納ArrowFunction中的this,arguments, super和new.target都是經過原型鏈來查找的,不是動態建立的。
關於GeneratorFunction我不許備講怎麼用它,我只談一下它的工做原理。說實話GeneratorFunction用到的狀況實在太少了,我本身在作項目的時候基本不會用到GeneratorFunction,但這不妨礙咱們學習GeneratorFunction。
GeneratorFunction也是Function的一種,Function的規則也適用於GeneratorFunction,此外在GeneratorFunction的參數中不能出現yield表達式。
GeneratorFunction與普通的Function同樣,都會建立Function對象,可是區別也在這裏,上面提到了Function的[[Prototype]]原型值是Function.prototype,可是GeneratorFunction不一樣,它的[[Prototype]]原型值是%Generator%,此外Function的prototype屬性是ObjectCreate(Object.prototype),可是GeneratorFunction的倒是ObjectCreate(%GeneratorPrototype%)並且prototype中沒有constructor屬性,不能做爲構造器調用。
注:Function.prototype也寫做%FunctionPrototype%,Object.prototype也寫做%ObjectPrototype%。%Generator%和%GeneratorPrototype%沒有全局名稱,不能直接訪問。
執行函數時,內部調用了[[Call]]方法,可是和Function不一樣的是GeneratorFunction返回的不是函數的執行結果,而是一個對象,這個對象是GeneratorFunction的一個實例,這跟[[Construct]]方法很像。
function* gen(){} gen(); //返回的實際上是Object.create(gen.prototype)對象。
你能夠比較gen()和Object.create(gen.prototype)這兩個對象,你會發現它們很像,只是Object.create(gen.prototype)缺乏了Generator對象的一些內部狀態。能夠說雖然GeneratorFunction沒有[[Construct]]方法,不能做爲構造器調用,可是你能夠認爲GeneratorFunction自己就是一個構造器。
此外在建立GeneratorFunction對象時,還作了一些其餘操做,咱們在之前的文章中提到了執行上下文,GeneratorFunction對象有個[[GeneratorContext]]內部插槽,當評估GeneratorFunction定義的代碼時,GeneratorFunction對象把當前正在運行的執行上下文存在了[[GeneratorContext]]中,並掛起了該正在運行的執行上下文,所以對GeneratorFunction中代碼的評估被暫停了,從而執行其它代碼,當你調用GeneratorFunction對象的next方法時,他把[[GeneratorContext]]中保存的執行上下文取出放到執行上下文棧頂部,成爲正在運行的執行上下文,此時GeneratorFunction中暫定評估的代碼又從新開始執行,直到執行完畢或者遇到yield表達式。當遇到yield表達式時,它又把正在運行的執行上下文從棧中移除,暫停對GeneratorFunction代碼的執行,等待下次next方法調用以後繼續執行。
簡單來講GeneratorFunction的實現原理實際上是運行的執行上下文之間不停來回切換。
GeneratorFunction基本就提到這裏了,最後附上ECMAScript關於GeneratorFunction的一張關係圖片:
關於Function的簡單介紹就暫時告一段落,在下一篇文章中我會來簡單介紹Promise和AsyncFunction。