JavaScript 核心觀念(51)- 繼承與原型鍊 - 使用 Object.create 建立多層繼承

前言

接下來這一章節將會來介紹如何製作屬於自己的原型鍊唷~

使用 Object.create 建立多層繼承

首先我們在前面其實有寫過一點點原型鍊

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

Dog.prototype.back = function() {
console.log(this.name + ' 吠叫');
}

var bibi = new Dog('比比', '棕色', '小');
var pupu = new Dog('噗噗', '白色', '大');

但這個原型鍊其實並不完整,什麼意思呢?每一個舉例來講狗這個原型鍊通常我們會給他一個共同科別,例如:貓科、人科,但我們並不可能這樣子改寫程式碼

1
2
3
4
5
6
7
8
9
10
11
12
function Animal(family, name, color, size) {
this.family = family;
this.name = name;
this.color = color;
this.size = size;
}

Animal.prototype.back = function() {
console.log(this.name + ' 吠叫');
}

var bibi = new Animal('犬科', '比比', '棕色', '小');

這樣子變成將會變成一種大混戰,例如:貓咪不可能會喵喵叫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Animal(family, name, color, size) {
this.family = family;
this.name = name;
this.color = color;
this.size = size;
}

Animal.prototype.back = function() {
console.log(this.name + ' 吠叫');
}

Animal.prototype.meow = function() {
console.log(this.name + ' 喵喵叫');
}

var bibi = new Animal('犬科', '比比', '棕色', '小');

這樣子會形成一種很奇怪的狀況,所有貓跟狗的特徵能力都混合在一起,因此我們正常來講必須將科別與動物分開來,在此就會介紹到 Object.create()

那麼 Object.create() 是什麼呢?簡單來講它可以建立出一個新的物件

1
2
var qq = Object.create({});
console.log(qq); // Object {}

但是如果你在 Object.create 寫入屬性的話,就會發生奇妙的狀況,你 console 出來後 qq 依然是空的物件

1
2
var qq = Object.create({ name: 'Ray' });
console.log(qq); // Object {}

但是如果你再往下展開的話,就可以看到這個屬性是掛載在 prototype(在此會不太一樣是因為 Firefox 是 prototype,而 Chrome 則是 __proto__)

Object.create

那麼這是怎麼回事呢?讓我們看一下 MDN 對於 Object.create 的解釋

Object.create() 指定其原型物件與屬性,創建一個新物件。
指定新物件的原型 (prototype) 物件。

好啦,我知道看起來不像人話,白話文簡單來講就是,它會依據你傳入的物件跟屬性,然後設定新物件的 prototype 並且建立一個新的物件,那麼這是什麼意思呢?就是可以用於繼承的用途,我 B 繼承於 A 的概念,關於繼承概念太模糊的話,你可以想像成你出生到這個世界上你會繼承於你父母親的特徵,例如:你爸爸身高比較高、你媽媽睫毛比較長等等,而這些概念就是繼承,講那麼多不如直接開始實作比較乾脆。

首先我們會先建立一個 Animal 的建構函式

1
2
3
4
function Animal(family) {
this.kingdom = '動物界';
this.family = family;
}

接下來我們要給予這個動物界的所有生命,在誕生於這個世界時都會有的共通能力,也就是 呼吸之術(哎不是,是呼吸)

1
2
3
4
5
6
7
8
function Animal(family) {
this.kingdom = '動物界';
this.family = family;
}

Animal.prototype.breathe = function() {
console.log(this.name + ' 正在持續呼吸');
}

那接下來我們可以做什麼呢?我們來建立狗的建構函式以及狗基本上會有的能力「吠叫」

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

Animal.prototype.breathe = function() {
console.log(this.name + ' 正在持續呼吸');
}

function Dog(name, color, size) {
this.name = name;
this.color = color;
this.size = size;
}

Dog.prototype.back = function() {
console.log(this.name + ' 吠叫');
}

接下來奇妙的事情發生了,我們的狗並沒有繼承於 Animal 的建構函式,在前面我們有講到要使用 Object.create,因此這邊就要這樣修改

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

Animal.prototype.breathe = function() {
console.log(this.name + ' 正在持續呼吸');
}

function Dog(name, color, size) {
this.name = name; // 狗的名字
this.color = color; // 狗的顏色
this.size = size; // 狗的體型
}

Dog.prototype = Object.create(Animal.prototype);

Dog.prototype.back = function() {
console.log(this.name + ' 吠叫');
}

Dog.prototype = Object.create(Animal.prototype); 這一段的意思是說,我們狗的 prototype 將會繼承於 Animal 的 prototype,而 Animal 的 prototype 是指 this.kingdom = '動物界';this.family = family; 以及 breathe 這個方法,這樣子我們也就可以在 Dog 底下直接使用來自 Animalbreathe,接下來讓我們來使用 new 建構子來實例 DOG (白話文:使用神力給予生命力。)

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
function Animal(family) {
this.kingdom = '動物界';
this.family = family || '人科'; // 科別
}

Animal.prototype.breathe = function() {
console.log(this.name + ' 正在持續呼吸');
}

function Dog(name, color, size) {
this.name = name; // 狗的名字
this.color = color; // 狗的顏色
this.size = size; // 狗的體型
}

Dog.prototype = Object.create(Animal.prototype);

Dog.prototype.back = function() {
console.log(this.name + ' 吠叫');
}

var bibi = new Dog('bibi', '棕色', '小');

bibi.breathe(); // bibi 正在持續呼吸
bibi.back(); // bibi 吠叫

bibi

這邊看起來原型是沒有什麼問題,也可以正常運作,但是當你執行 bibi.kingdom 或是 bibi.family 你會發現出現的是 undefined 而不是 動物界 或是 人科,而原因是為什麼呢?雖然我們有使用 Object.create 繼承了動物的原型,但卻沒有繼承動物的建構函式,這時候你可能會想說那就改成 Dog.prototype = Object.create(Animal); 不就好了?其實不行,這樣是會導致原型跑掉的,因此正確而是在 Dog 的建構函式中使用 call 來繼承動物界的建構函式,並且傳入科別

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

Animal.prototype.breathe = function() {
console.log(this.name + ' 正在持續呼吸');
}

function Dog(name, color, size) {
Animal.call(this, '犬科')
this.name = name; // 狗的名字
this.color = color; // 狗的顏色
this.size = size; // 狗的體型
}

Dog.prototype = Object.create(Animal.prototype);

Dog.prototype.back = function() {
console.log(this.name + ' 吠叫');
}

var bibi = new Dog('bibi', '棕色', '小');

這樣子當你執行 bibi 時才能夠正確地看到從 Animal 繼承下來的建構函式

new Dog

最後這邊看起來程式碼已經相當的完整了,但是如果要讓原型鍊完整的話,其實還必須在 Object.create 底下加上 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
22
23
24
25
26
27
function Animal(family) {
this.kingdom = '動物界';
this.family = family || '人科'; // 科別
}

Animal.prototype.breathe = function() {
console.log(this.name + ' 正在持續呼吸');
}

function Dog(name, color, size) {
Animal.call(this, '犬科')
this.name = name; // 狗的名字
this.color = color; // 狗的顏色
this.size = size; // 狗的體型
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.back = function() {
console.log(this.name + ' 吠叫');
}

var bibi = new Dog('bibi', '棕色', '小');

bibi.breathe(); // bibi 正在持續呼吸
bibi.back(); // bibi 吠叫

首先這邊先講講 constructor 是什麼,constructor 是一個非常特別的東西,中文又稱之為「建構式」,當你使用建構式建立一個新的物件時,這個原型底下的 constructor 就會指向原本的建構函式,而這個 constructor 是本身就會存在的東西

1
2
3
4
5
6
7
function Animal(family) {
this.kingdom = '動物界';
this.family = family || '人科'; // 科別
}

const newAnimal = new Animal('狗科');
console.log(newAnimal.constructor); // Animal 函式本身

但是因為我們前面使用了 Object.create(Animal.prototype); 並賦予到 Dog.prototype,而這個行為會導致原有 Dogconstructor 一併變成了 Animalconstructor

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

Animal.prototype.breathe = function() {
console.log(this.name + ' 正在持續呼吸');
}

function Dog(name, color, size) {
Animal.call(this, '犬科')
this.name = name; // 狗的名字
this.color = color; // 狗的顏色
this.size = size; // 狗的體型
}

Dog.prototype = Object.create(Animal.prototype);

Dog.prototype.back = function() {
console.log(this.name + ' 吠叫');
}

var bibi = new Dog('bibi', '棕色', '小');
console.log(bibi.constructor); // Animal 的建構函式

因此才會必須在 Object.create 底下補上一行 Dog.prototype.constructor = Dog; 將原本 Dogconstructor 給補回來,而這也是為了確保原型鍊的完整性

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
function Animal(family) {
this.kingdom = '動物界';
this.family = family || '人科'; // 科別
}

Animal.prototype.breathe = function() {
console.log(this.name + ' 正在持續呼吸');
}

function Dog(name, color, size) {
Animal.call(this, '犬科')
this.name = name; // 狗的名字
this.color = color; // 狗的顏色
this.size = size; // 狗的體型
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.back = function() {
console.log(this.name + ' 吠叫');
}

var bibi = new Dog('bibi', '棕色', '小');
console.log(bibi.constructor); // Dog 的建構函式

雖然不補回 constructor 程式碼也能夠正常運作,但其實這並不太正確,畢竟在原始建立原型時,本身 constructor 就是指向建構函式本身,因此通常來講為了確保原型的完整性而補回去,主要也就是為了「正確標示該物件的產生函式」,否則在將來開發時反查問題將會找不出所以然。

1
2
3
4
5
6
7
function Animal(family) {
this.kingdom = '動物界';
this.family = family || '人科'; // 科別
}

const newAnimal = new Animal('狗科');
console.log(newAnimal.constructor); // Animal 函式本身

參考文獻