會有這一篇文章, 是因為在研究promise的時候, 聽到一句話 『function
也是物件的一種, 可以透過__proto__
看到他的最底層也是Object
』, 於是就那麼簡單和輕易的開啟我的研究之路了, 此篇文章不敢說深入但至少淺出到自己可以看懂.
原型在哪裡?
在探討這個問題前, 先試試看把Array展開會出現什麼?
- 出現原生一堆語法和prototype
1 | console.dir(Array) |
- Array其實也是一個物件, 可以用 bbb[0], bbb.length取值, 這兩者都是物件取值的方法, 如何證明呢?
可以看底下的終極展開
1 | const bbb = [1, 2, 4, 5, 6] |
- 出現proto, 往下翻會看到Object, 這就是陣列的原型, 裡面有很多的方法可以用
所以陣列的結構從這一個範例得知是: real-array -> Array -> Object
1 | console.dir(bbb) |
那物件呢?
1 | // 出現原生一堆語法和prototype |
知道這些有什麼用? 這是上課補充的知識, 先知道就好
js是透過物件來建立, 沒有class的概念, 透過原型繼承做出類似類別繼承的方法
透過new這個方法所新增的物件, 會有繼承的特性, 屬於原型繼承
ES6 的class就是語法糖, 本質上還是原型繼承
來看看這張圖吧
知道你沒辦法秒理解, 但其實根本很常在用啊!
1 | const aaa = [1, 2, 3] |
obj.Prop1的意思就是要取得原本物件的屬性 => Ex: aaa.length
obj.PrototypeProp1 => 要用原型物件的方法 => Ex: aaa.forEach()
這個forEach就是透過原型拿到的方法, 不是原本存在在a裡面的方法
原型觀念
截至目前為止, 原型有兩個重要的觀念
- 原型可共用方法和屬性, 就像forEach(), 這個方法在所有陣列中都可以用到
- 原型可以向上查找
在原型內新增自己的方法
除了forEach這種原本就在Object裡面定義好的方法以外, 也可以自行定義方法.
現在要示範在a的原型, 加上新的方法(getLast), 另外一個陣列b是不是也符合共用呢?
請注意: __proto__
這種寫法是不正式的
1 | const aaa = [1, 2, 3] |
再來看看b?
1 | const bbb = [4, 5, 6] |
因為原型是共用的, 所以都取用的到, 可以看到在__proto_新增了getLast這個方法
使用建構式自定義原型
那要怎麼自己建立起自己的原型?
也就是我在Object的底層上, 在建立起一個新的Object?
我們會用建構函式來建構
舉例如下:
1 | function Dog (name, color, size){ |
但這只是模型, 還不是真正的狗, 如果要建立兩隻狗, 要先有建構函式. 簡單來說, 你沒有模型是沒辦法建立狗的, 因此要用new這個關鍵字來建立
1 | const Bibi = new Dog('比比', '棕色', '大型犬') |
在原型裡面新增方法
因為Dog是自行產生的物件, 所以在Dog這個基礎之上, 再加入方法.
要新增方法要透過prototype在原型中新增, 又因為是在原型裡面, 所以可以調用this.
1 | Dog.prototype.bark = function(){ |
再原型上加上一個方法, 那這個方法就會套用到所有透過狗函式所產生的實體上, 除了有各自的屬性外, 還有共用的方法
小結
原型的優勢可以透過少許記憶體, 產生大量的物件.
原始型別的包裹物件與原型的關聯
那除了物件以外, 字串和number也可以在自己的原型上建立各自的方法嗎? 答案是可行的!
1 | console.dir(String) // 會出現很多方法 |
字串上新增方法
1 | const aaa = '123' |
數字上新增方法
1 | const bbb = 5 |
其他應用
這個例子是直接在Date物件上, 直接新增一個today的方法.
依此建立以後, 要知道今天的日期, 就可以在Date中call today即可
1 | const date = new Date() // 可以取到今天的日期 |
使用 Object.create 建立多層繼承
這是這篇文章的重點單元, 先給他打三顆星星再說!
經過上幾小節的介紹, 我們都已經知道一個空陣列的結構是這樣
1 | var a = [] |
那麼經過自定義的原型而後被new出來的實體, 結構是這樣
1 | function Dog(name, color, size){ |
那如果我像要在原型上在新增一個層級, 有辦法嗎?
你不懂? 就是在Object和Dog之間安插一個Animal啊
你還是不懂? 給個圖示
1 | // 現在想要在原本的層級在新增一個Animal的層級, 像是底下的階層 |
Object.create怎麼用
1 | // 先定義一個Object |
實作Animal看看
- 首先當然是要建立Animal的原型, 裡面當然也可以有自己的屬性
1 | // 可以傳入科別, 例如狗科, 貓科等等 |
現在都可以使用到這些方法
Bibi.bark() -> 吠叫
Bibi.move() -> 移動
但是從上圖看, 很明顯還沒有定義是在哪一個科別, 為什麼會發生這樣的狀況?
有兩個原因:
- Bibi是透過Dog產生的實體, 不是透過Animal
- Bibi只有繼承動物界的原型, 但是沒有繼承動物界的建構函式
1 | // 更改Dog的建構式 |
Constructor補充
當我們用建構式來產生一個新的物件時, 那這個物件的原型就會指向這個建構函式, 如下圖所示
1 | var newAnimal = new Animal('新物種') |
但是因為Object.create的關係, 把這個constructor給覆蓋住了, 所以還要把他加回來
1 | Dog.prototype.constructor = Dog |
新建貓科
1 | function Animal(family){ |
原型鏈、建構函式整體結構概念
最後再來補充所有課堂尚有提及過的範例,並且把它彙整再一起
從左上角的Bibi開始, 為什麼牠會存在, 是因為有自建的Dog建構式.
並且透過__proto__
和prototype可以把兩者建立起來.
__proto__
有往上查找的功能, 因此Bibi.__proto__
找到Dog.prototype- 會有Dog.prototype, 是因為透過constructor找到Dog原型
- Dog.prototype裡面還有
__proto__
, 因此又可以往上找到Animal.prototype
在原型裡面透過prototype來新增方法的探究
新增方法是用prototype的方法, 但是為什麼呢?
-> 因為每一個物件的proto_都是繼承於Function. 簡單來說, 不管是Dog, 還是Animal 的proto, 都會找到Function的prototype
prototype(可以看底下藍色箭頭), 而最終Function的源頭就是Object.
所以底下的恆等式都是成立的
console.log(Dog.proto === Function.prototype) // true
console.log(Function.proto == Function.prototype) // true
console.log(Function.proto.proto = Object.prototype) // true
延伸閱讀