0%

物件原型鍊

會有這一篇文章, 是因為在研究promise的時候, 聽到一句話 『function也是物件的一種, 可以透過__proto__看到他的最底層也是Object』, 於是就那麼簡單和輕易的開啟我的研究之路了, 此篇文章不敢說深入但至少淺出到自己可以看懂.

原型在哪裡?

在探討這個問題前, 先試試看把Array展開會出現什麼?

  • 出現原生一堆語法和prototype
1
console.dir(Array)


  • Array其實也是一個物件, 可以用 bbb[0], bbb.length取值, 這兩者都是物件取值的方法, 如何證明呢?
    可以看底下的終極展開
1
2
const bbb = [1, 2, 4, 5, 6]
console.dir(bbb)


  • 出現proto, 往下翻會看到Object, 這就是陣列的原型, 裡面有很多的方法可以用
    所以陣列的結構從這一個範例得知是: real-array -> Array -> Object
1
console.dir(bbb)


那物件呢?

1
2
3
4
5
6
7
8
// 出現原生一堆語法和prototype
console.dir(Object)

const aaa = {
xxx: 123,
yyy: 456
}
console.dir(aaa) // 出現__proto__

知道這些有什麼用? 這是上課補充的知識, 先知道就好

  • js是透過物件來建立, 沒有class的概念, 透過原型繼承做出類似類別繼承的方法

  • 透過new這個方法所新增的物件, 會有繼承的特性, 屬於原型繼承

  • ES6 的class就是語法糖, 本質上還是原型繼承

來看看這張圖吧

知道你沒辦法秒理解, 但其實根本很常在用啊!

1
const aaa = [1, 2, 3]

obj.Prop1的意思就是要取得原本物件的屬性 => Ex: aaa.length
obj.PrototypeProp1 => 要用原型物件的方法 => Ex: aaa.forEach()
這個forEach就是透過原型拿到的方法, 不是原本存在在a裡面的方法

原型觀念

截至目前為止, 原型有兩個重要的觀念

  1. 原型可共用方法和屬性, 就像forEach(), 這個方法在所有陣列中都可以用到
  2. 原型可以向上查找

在原型內新增自己的方法

除了forEach這種原本就在Object裡面定義好的方法以外, 也可以自行定義方法.
現在要示範在a的原型, 加上新的方法(getLast), 另外一個陣列b是不是也符合共用呢?

請注意: __proto__這種寫法是不正式的

1
2
3
4
5
6
const aaa = [1, 2, 3] 
aaa.__proto__.getLast = function(){
return this[this.length -1]
}
// 可以看到在__proto_新增了getLast這個方法
console.log(aaa)

再來看看b?

1
2
const bbb = [4, 5, 6]
console.log(bbb)

因為原型是共用的, 所以都取用的到, 可以看到在__proto_新增了getLast這個方法


使用建構式自定義原型

那要怎麼自己建立起自己的原型?
也就是我在Object的底層上, 在建立起一個新的Object?

我們會用建構函式來建構

舉例如下:

1
2
3
4
5
function Dog (name, color, size){
this.name = name
this.color = color
this.size = size
}

但這只是模型, 還不是真正的狗, 如果要建立兩隻狗, 要先有建構函式. 簡單來說, 你沒有模型是沒辦法建立狗的, 因此要用new這個關鍵字來建立

1
const Bibi = new Dog('比比', '棕色', '大型犬')

在原型裡面新增方法

因為Dog是自行產生的物件, 所以在Dog這個基礎之上, 再加入方法.
要新增方法要透過prototype在原型中新增, 又因為是在原型裡面, 所以可以調用this.

1
2
3
4
5
6
7
Dog.prototype.bark = function(){
return (this.name + '吠叫')
}

const aaa = new Dog('比比', '棕', '小')
console.dir(aaa) // 透過__proto__新建constructor
aaa.bark() // 比比吠叫

再原型上加上一個方法, 那這個方法就會套用到所有透過狗函式所產生的實體上, 除了有各自的屬性外, 還有共用的方法

小結

原型的優勢可以透過少許記憶體, 產生大量的物件.


原始型別的包裹物件與原型的關聯

那除了物件以外, 字串和number也可以在自己的原型上建立各自的方法嗎? 答案是可行的!

1
2
console.dir(String) // 會出現很多方法
console.dir(Number) // 也會出現很多方法

字串上新增方法

1
2
3
4
5
6
const aaa = '123'
String.prototype.lastText = function () {
return this[this.length - 1]
}

console.log(aaa.lastText()) // 3

數字上新增方法

1
2
3
4
5
6
const bbb = 5
Number.prototype.square = function (){
return this*this
}

console.log(bbb.square()) // 2

其他應用

這個例子是直接在Date物件上, 直接新增一個today的方法.
依此建立以後, 要知道今天的日期, 就可以在Date中call today即可

1
2
3
4
5
6
7
8
9
10
11
12
const date = new Date() // 可以取到今天的日期
console.dir(date) // 可以看出有很多方法

Date.prototype.today = function(){
const year = String(this.getFullYear())
const month = String(this.getMonth()+1)
const day = String(this.getDate())

return `${year}/${month}/${day}`
}

console.log(date.today()) // 2020/02/25

使用 Object.create 建立多層繼承

這是這篇文章的重點單元, 先給他打三顆星星再說!

經過上幾小節的介紹, 我們都已經知道一個空陣列的結構是這樣

1
2
var a = []
// Object > Array > a(實體)

那麼經過自定義的原型而後被new出來的實體, 結構是這樣

1
2
3
4
5
6
7
8
9
function Dog(name, color, size){
this.name = name
this.color = color
this.size = size
}

var Bibi = new Dog('比比', '棕', '小')

// Object > Dog > Bibi(實體)

那如果我像要在原型上在新增一個層級, 有辦法嗎?

你不懂? 就是在Object和Dog之間安插一個Animal啊
你還是不懂? 給個圖示

1
2
3
4
// 現在想要在原本的層級在新增一個Animal的層級, 像是底下的階層
Object > Animal > Dog > new Dog
Object > Animal > Cat
Object > Animal > Human

Object.create怎麼用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 先定義一個Object 
var Bibi = {
name: '比比',
color: '綠色',
size: '小',
bark:function(){
console.log(this.name + '吠叫')
}
}

// 透過Object.create()把Bibi當成原型
var Pupu = Object.create(Bibi)
console.dir(Pupu) // {} ->重點

Pupu.name = '比比' // 會出現比比的值, 這就和forEach一樣的觀念, 可以調用__proto__裡面的方法
Pupu.name = '噗噗' // 覆蓋比比的值
console.dir(Pupu) // {name: '噗噗'}

實作Animal看看

  • 首先當然是要建立Animal的原型, 裡面當然也可以有自己的屬性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 可以傳入科別, 例如狗科, 貓科等等
// 使用建構函式自定義原型
function Animal(family){
this.kingdom = '動物界'
this.family = family || '狗科'
}

// 增加一個方法在原型下
Animal.prototype.move = function(){
console.log(this.name + '移動')
}

// 使用建構函式自定義原型
function Dog(name, color, size) {
this.name = name
this.color = color
this.size = size
}

// 對Dog的原型增加bark方法, 所以會增加在Animal上面
Dog.prototype.bark = function(){
console.log(this.name + '吠叫')
}

// 這一段是重點, Dog的原型繼承於Animal的原型之下, 就像把鏈接鏈在一起了
// 注意只有繼承原型, 沒有繼承屬性
Dog.prototype = Object.create(Animal.prototype)

// 實體要在鏈接完以後在建立

var Bibi = new Dog('比比', '棕', '小')
console.dir(Bibi)

現在都可以使用到這些方法
Bibi.bark() -> 吠叫
Bibi.move() -> 移動

但是從上圖看, 很明顯還沒有定義是在哪一個科別, 為什麼會發生這樣的狀況?

有兩個原因:

  1. Bibi是透過Dog產生的實體, 不是透過Animal
  2. Bibi只有繼承動物界的原型, 但是沒有繼承動物界的建構函式
1
2
3
4
5
6
7
8
9
10
11
// 更改Dog的建構式
function Dog(name, color, size) {
Animal.call(this, '犬科')
this.name = name
this.color = color
this.size = size
}

// Animal這一行釋義
// 綁定Animal所有的屬性, 並且因為Animal裡面有傳參數family
// 所以把他帶入到建立的Dog實體中


Constructor補充

當我們用建構式來產生一個新的物件時, 那這個物件的原型就會指向這個建構函式, 如下圖所示

1
2
var newAnimal = new Animal('新物種')
console.dir(newAnimal)

但是因為Object.create的關係, 把這個constructor給覆蓋住了, 所以還要把他加回來

1
Dog.prototype.constructor = Dog

新建貓科

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Animal(family){
this.kingdom = '動物界'
this.family = family || '狗科'
}

Animal.prototype.move = function(){
console.log(this.name + '移動')
}

function Cat(name) {
Animal.call(this, '貓科')
this.name = name
}

Cat.prototype = Object.create(Animal.prototype)
Cat.prototype.constructor = Cat
var Kitten = new Cat('凱蒂')

console.dir(Kitten)
Kitten.move(this.name) // 凱蒂可以移動
Kitten.bark(this.name) // 凱蒂無法吠叫(裡面沒有bark)


原型鏈、建構函式整體結構概念

最後再來補充所有課堂尚有提及過的範例,並且把它彙整再一起

從左上角的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


延伸閱讀

new關鍵字

mdn-繼承與原形鍊

codepen-最終範例程式碼

codepen-自己建立的原型鏈練習