0%

JS核心篇-物件 和 原型練

JS是以什麼為基礎的物件導向的語言呢?

JS不同於其他物件導向的程式語言,像C++是以「類別」為基礎的物件導向的程式語言。
而JS是以「原型」為基礎的物件導向程式語言。

JS創建物件的方法

以下記錄幾種JS裡面創建新物件的方法
Way1. 用new關鍵字

1
2
const obj = new Object()
obj.name = '物件'

Way2. 物件實字創建

1
2
3
4
const ming = {
name: '小明'
}
console.log(ming.name) // 小明

Way3. 透過「建構式」創建

1
2
3
4
5
6
7
8
9
10
function Animal(name, height) {
this.name = name;
this.height = height;

this.bark = function() {
console.log(this.name + '汪汪');
}
}
const dog = new Animal('小胖', 100);
dog.bark(); // 小胖汪汪

Way4. 用Object.create()創建
以下這個範例,Object.create是透過Animal這個原型物件來創建新的物件。
那會因為JS原型的原理,所以,透過Object.create所創建出來的物件都會繼承這個原型物件的屬性和方法。
那新創建的物件屬性和方法在沒有重新賦值的狀況下,它的值都會是預設值,也就是原型物件一開始
設定的值。

1
2
3
4
5
6
7
8
9
10
11
const Animal = {
name: 'default name',
height: 0,
bark: function() {
console.log(this.name + '汪汪');
}
}
const dog = Object.create(Animal);
dog.bark(); // default name汪汪
dog.name = '小胖'
dog.bark(); // 小胖汪汪

以下的範例,是利用Object.create()來創建新物件,並透過屬性描述器來更改新創物件的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const Animal = {
name: 'default name',
height: 0,
bark: function() {
console.log(this.name + '汪汪');
}
}

const dog = object.create(Animal, {
name: {
writable: true,
configurable: true,
value: '小胖'
},
height: {
writable: true,
configurable: true,
value: 100
}
})
console.log(dog.name) // 小胖

屬性描述器

在這部分書中有蠻詳細的描述,但目前我開發的專案蠻少用到這部分的功能,所以,我就先不記錄這部分的功能,但至少,知道了在目前很多的框架,像是Vue,都是會自訂義屬性描述器來達成框架想要的效果。

原型練

JS實現繼承的方法

在JavaScript中,並沒有明確的子類和父類,它全靠原型練來實現繼承的效果。

以下會介紹兩種原型練與繼承產生的狀況

  1. Huli這一篇文章中,它是利用建構函式的方式來介紹原型練的產生和繼承怎麼應用。
  2. 而在Kuro大大的書中,有介紹到利用物件的方式來達成原型練的產生和繼承怎麼應用。
    我覺得兩邊的應用都蠻重要的所以,就都記錄下來囉~

用建構函式產生原型練與繼承的應用

在JS中,有一個關鍵字new,我們在new後面加一個建構函式,以此建構函式為原型,來創建出一個新物件。
那我們之前有提到過,利用new所創建的建構函式裡面的this是指向被創建出來的新物件。

用new創建的缺點

我們利用new所創建出來的物件們,無法共享屬性和方法。
那考慮到這一點,JS的創世神們就引入了prototype屬性到構造函數中

所以,在每個函數中都會有一個prototype屬性,那這個屬性裡面原本是沒有內容的。
那我們就可以將想要共享的屬性和方法放到這個prototype屬性裡面,就可以讓新創物件們共享。
若不想要共享的方法和屬性,就放到構造函數裡面就可以囉~

繼承的應用

1
2
3
4
5
6
7
8
9
10
11
12
13
function Dog(name) {
this.name = name // 不共享的屬性
}
Dog.prototype = {
species: '犬科' // 共用的屬性
}
const dogA = new Dog('小胖')
const dogB = new Dog('大胖')

console.log(dogA.name) // 小胖
console.log(dogB.name) // 大胖
console.log(dogA.species) // 犬科
console.log(dogB.species) // 犬科

透過以上的範例,prototype看起來很像物件的原型,而新創的物件就好像繼承了prototype的內容。

proto 屬性 與 原型鍊的誕生

我們利用構造函數新創出來的物件都會有一個特別屬性,就是__proto__,而它的值就是構造函數的prototype的引用,
也就是說新創建出來的物件的__proto__屬性會指向構造函數的prototype物件。

1
2
3
4
5
6
7
8
9
10
function Animal(name) {
this.name = name
}
Animal.prototype = {
species: '犬科'
}
const dog = new Animal('小胖')
console.log(dog.__proto__ === Animal.prototype) // true
console.log(dog.__proto__.__proto__ === Animal.prototype.__proto__) // true
console.log(Animal.prototype.__proto__ === Object.prototype) // true

上面這個例子,就可以看到新創物件dog利用__proto__去指向構造函數Animal的prototype
而構造函數Animal又是繼承自Object。
所以,構造函數Animal的prototype也透過它自己的__proto__去指向最上層Object的prototype
那這樣一層一層的透過__proto__去指向上層的prototype就構成了原型練

那如果今天物件實體的屬性和方法跟構造函數中的屬性和方法的名稱相同的話,會怎樣呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(name, height) {
this.name = name;
this.height = height;
this.sayhello = function() {
console.log(this.name +'身高為'+ this.height);
}
}

Person.prototype = {
sayhello: function() {
console.log('來自構造函數的' + this.name +'身高為'+ this.height);
}
}

const ming = new Person('小明', 150)
min.sayhello() // ??

上面這個範例的結果會是 小明身高為150,也就是ming物件自己的方法。
由此可以看出,如果,物件方法和屬性的名稱跟構造函數有相同的時候,物件本身的順位會比較高,
如果物件本身沒有的話,才會透過__proto__,原型練的方式一層一層往上查找。

用物件產生原型練與繼承的應用

這邊我們直接用物件實字的方式來創建物件,而不是透過構造函數來創建,那這樣的話我們要怎麼去搭建
兩個不相關的物件之間的原型練的關係 與 繼承的效果呢?

用setPrototypeOf搭建原型鍊

我們可以透過setPrototypeOf來設定物件的原型。
直接引用kuro大大書中洛克人的範例XDD 因為我覺得蠻有趣的,哈哈

1
2
3
4
5
6
const rockman = { buster: true }
const cutman = { cut: true }
const gutsman = { superArm: true }
Object.setPrototypeOf(rockman, cutman)
console.log(rockman.buster) // true
console.log(rockman.cut) // true

以上寫到這邊,我們就搭建了洛克俠和剪刀俠之間的關係。
但是,如果我們想要洛克俠也繼承氣力俠的能力呢?
這邊要提到一個重要的概念,JS的原型繼承有一個重要的規則,就是同一個物件無法同時指定兩個原形
故我們要透過原型鍊的概念來達成。

1
2
3
4
5
6
7
8
const rockman = { buster: true }
const cutman = { cut: true }
const gutsman = { superArm: true }
Object.setPrototypeOf(rockman, cutman)
Object.setPrototypeOf(cutman, gutsman)
console.log(rockman.buster) // true
console.log(rockman.cut) // true
console.log(rockman.superArm) // true

看到上面的案例,我們可以看出來我們透過setPrototypeOf搭建出一條原型鍊,
這條原型練是 洛克人->剪刀人->氣力人
那氣力人的能力先由件刀人來繼承。那因為洛克人繼承剪刀人,而因為原型練的效果,洛克人可以循著原型鍊取得氣力人的能力囉。

用物件實字來搭建原型鍊

1
2
3
4
5
6
7
8
9
const Person = {
name: 'default name',
sayhello: function(){
console.log(this.name + ', Hello')
}
}
const ming = Object.create(Person);
ming.name = '小明';
ming.sayhello(); // 小明, Hello

那上面這個方法,ming物件的__proto__屬性,就會直接指向Person的prototype原型。
這就是另外一種,物件搭建出原型鍊的方法。

不支援proto

在某些執行環境並沒有支援__proto__,那在ES5之後,我們可以利用Object.getPrototypeOf()來取得某個物件的原型物件,來解決這樣的窘境。

參考文章:

  1. 008天重新認識JavaScript
  2. https://blog.huli.tw/2017/08/27/the-javascripts-prototype-chain/
  3. https://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_javascript.html