前言 接下來這一章節將會來介紹如何製作屬於自己的原型鍊唷~
使用 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.create 寫入屬性的話,就會發生奇妙的狀況,你 console 出來後 qq 依然是空的物件
1 2 var qq = Object .create ({ name : 'Ray' });console .log (qq);
但是如果你再往下展開的話,就可以看到這個屬性是掛載在 prototype(在此會不太一樣是因為 Firefox 是 prototype,而 Chrome 則是 __proto__)
那麼這是怎麼回事呢?讓我們看一下 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 底下直接使用來自 Animal 的 breathe,接下來讓我們來使用 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.back ();
這邊看起來原型是沒有什麼問題,也可以正常運作,但是當你執行 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 繼承下來的建構函式
最後這邊看起來程式碼已經相當的完整了,但是如果要讓原型鍊完整的話,其實還必須在 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.back ();
首先這邊先講講 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 );
但是因為我們前面使用了 Object.create(Animal.prototype); 並賦予到 Dog.prototype,而這個行為會導致原有 Dog 的 constructor 一併變成了 Animal 的 constructor
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 );
因此才會必須在 Object.create 底下補上一行 Dog.prototype.constructor = Dog; 將原本 Dog 的 constructor 給補回來,而這也是為了確保原型鍊的完整性
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 );
雖然不補回 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 );
參考文獻
整理這些技術筆記真的很花時間,如果你願意 關閉 Adblock 支持我,我會把這份感謝轉換成更多「踩坑轉避坑」的內容給你!ヽ(・∀・)ノ
Advertisement