前言
首先來看看下面的例子
1 | function Person (name) { |
相信大家在各個教程都看過這個例子~
但是這個例子融合了 prototype、new 與 this,要了解每一行在做什麼其實非常不容易
我們先從 prototype 說起
prototype
js 作為 prototype-based programming,是沒有 class 的概念的,es6 出現後,也只是以 prototype 做封裝的
也就是 js 在沒有 class 的概念下,是透過 prototype 進行繼承行為的
那 prototype 可以理解為鍊表,也就是一層一層往上繼承,直到 null 結束
因此如果 object 沒有愈存取的屬性,那 js 會不斷往上查找,直到 null object.__proto__.__proto__...
請看下圖
可以看到在 every is object 的 js 世界,連 Function 也是繼承自 Object
诶? 那 Object 跟 Funtion 差在哪?
對,function 可以執行嘛,還有一點就是 function 可以作為object 的構造函數
function 可以作為 object 的構造函數具體實現就是每個 function 都有自己的 prototype,當 function 作為普通函數執行時,是不會用到 prototype 的,但當 function 作為構造函數,實例化出來的 object 就會繼承自 function 的 prototype
因此實例化的 object 只會有 __proto__
屬性而沒有 prototype,prototype 是 function 專屬用來當構造函數用的
說到構造函數,就得來介紹一下 new
new
new 是實例化 object 的關鍵字,上方的例子就是將 Person 作為構造函數,回傳實例化的 object
1 | var p = new Person('Winnie') |
那 new 到底做了什麼?
1 | function _new(fn, ...arg) { |
step 1: 創建空物件並繼承構造函數的 prototype
1 | const obj = Object.create(fn.prototype); |
首先創建一个空的對象,空對象的 __proto__
指向構造函数(fn)的 prototype
Object.create 事實上做的事就是創建一個 object,讓該 object 繼承自傳進來的 object
1 | // 模擬 Object.create,真正的 polyfill 請去 MDN 看 |
注意直接存取
__proto__
會影響到 JavaScript 的執行效能,我們不應該在 production 的環境上使用(source)
於是帶進去得出 object.__proto__ = fn.prototype
,這可以幹嘛呢,可以用 fn prototype 上的方法阿,以上面的例子來說就是 sayName
step 2: 把空對象賦值構造函數内部的 this
1 | const ret = fn.apply(obj, arg); |
apply() 就是去執行這個 function,並將這個 function 的 this 指向第一個參數的 object
上面的 code 等於是執行 fn,並將 fn 中的 this 指向為 obj,而這個 fn,也就是構造函數執行後會發生什麼?
1 | // 在這例子中 fn 其實就是 Person |
step 3: 反回剛創建的新物件
1 | return ret instanceof Object ? ret : obj; |
最後判斷構造函數(fn) 是否有 return 物件,沒有的話返回剛創建的新物件,但若是在構造函數中 return 了,那剛剛創建的 obect,繼承好的 prototype 則會通通不見喔(function 執行結束會垃圾回收掉)
也就是 return 的物件是沒有 prototype 的方法,this.[property] 出來的屬性也通通沒有!
所以說構造函數別亂返回物件,es6 的 class 已禁止 return,否則會報錯
this
我們來看看 MDN 的解釋: A property of an execution context,既然跟 execution context 有關,那表示跟執行環境有關,也就是 this 會根據 function 如何被呼叫而所不同
當一個 function 執行前的初始化階段(fully parse AST、創建FEC 等等),就會決定好該 EC 的 this 值,接下來就介紹會有什麼情況導致 this 的不同
1. 預設綁定 (Default Binding):
在 non-strict mode 中,當一個 FEC 被創建時,默認 this 指向 window(GEC),在 strict mode 中,則為 undefined
GEC 的 this 則指向自己(GEC.this = GEC)
2. 隱含式綁定 (Implicit Binding)
(source)
1 | function func() { |
如果 function 做為某個物件屬性的參考呼叫時,那 this 就會指向該物件
1 | function func() { |
怎麼將 obj.foo 獨立出來 this 就輸出 undefined?
因為 func2 指向 func,且呼叫時沒有作為物件的屬性呼叫,此時 this 為預設綁定指向 window,全域沒 a 屬性因此輸出 undefined
3. 顯式綁定 (Explicit Binding)
bind、call、apply 三兄弟,都可以指定 this 對象
- fn.bind(obj, arg1, arg2,…): 回傳綁定 this 為的函數,並不會真的執行 fn,只是將回傳函數的 this 指定為 obj,注意一但被綁定,this 無法再被修改
- fn.call(obj, arg1, arg2,…): 執行 fn,將 this 指定為 obj
- fn.apply(obj, [arg1, args,…]): 執行 fn,將 this 指定為 obj,第二個參數必須為陣列
4. 「new」關鍵字綁定
1 | function foo(a) { |
請看上方解釋,new 後的 function 會做為構造函數,將新建的空物件透過 apply 或 call 指定為 this 去執行構造函數,最後回傳該物件
es5 繼承
懂了上述介紹的 new 與 prototype 與 this,我們可以進一步實現繼承
屬性繼承
1 | function Person (name, age) { |
此時如果要產生一個構造函數 Men 可以繼承 Person 的屬性,且擁有自己的屬性的話,該怎麼做?
在 new 的部分有解釋過了,通過在一個構造函數内執行另外一個構造函數,可以讓該類得到另一個構造函數的所有屬性
1 | function Man (name, age, gender) { |
那現在用 martin 呼叫 getName 是會報錯的,因為我們只將 Person 的屬性套用到 Men 上,沒有繼承到 Person 的方法
接著就來介紹方法繼承
方法繼承
通常一個構造函數的方法都會定義在 prototype 中,目的是為了共享方法,若定義在構造函數內,那每次用 apply 執行構造函數來繼承屬性時,都會創造一個新的函數,造成資源的浪費
1 | function Person (name, age) { |
那如何繼承 prototype 上的方法呢?
1 | // step 1 |
step 1
1 | Man.prototype = Object.create(Person.prototype) |
既然我們都把方法定義在 prototype 中,那只要讓 Man 的 prototype 繼承自 Person 的 prototype 就可以啦
這個步驟其實就是將 Man 原本的 prototype 指向為新的 object,而這個 object 的 __proto__
指向Person.prototype
還記得 Object.create 嗎? 來複習一下
1 | const obj = {}; |
step 2
1 | Man.prototype.constructor = Man |
每個 prototype 內都有個屬性 constructor 指向構造函數,那現在改掉 Man 的 prototype 後就需要將 contructor 指回原本的構造函數,免得在類型判斷時出錯
最終 es5 繼承方案
1 | function Person (name, age) { |