iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 8
0
Modern Web

從技術文章深入學習 JavaScript系列 第 8

Day 08 [原型鍊04] JavaScript常用八種繼承方案

文章選自

作者:木易杨说

連接:https://juejin.im/post/6844903696111763470

來源:掘金

一、原型鏈繼承

代碼

// 以下為圖一
function Animal() {
  this.haveHead = true
}

Animal.prototype.sleep = function () {
  console.log("zzZ");
}

function Fish(name) {
  this.name = name
  this.haveCheek = true
}

// 透過原型鍊繼承!! (以下為圖二)
Fish.prototype = new Animal()

// 以下為圖三
let salmon = new Fish('salmon')

console.log(salmon);

https://ithelp.ithome.com.tw/upload/images/20200918/201243504tMxv89UAP.png

示意圖

  • 圖一

    https://ithelp.ithome.com.tw/upload/images/20200918/20124350vFIisSUx6j.png

  • 圖二

    https://ithelp.ithome.com.tw/upload/images/20200918/20124350cXpcEQHlHd.png

    這個時候的Fish.prototype是Animal的實例對象

    https://ithelp.ithome.com.tw/upload/images/20200918/20124350LZaNIjXQd0.png

  • 圖三

    https://ithelp.ithome.com.tw/upload/images/20200918/20124350lu01fDwsu1.png

缺點

如果有多個實例,操作其中一個實例有可能會影響到其他實例

代碼:

function Animal() {
  this.hobbies = ["sleep", "watch"];
}
function Fish() { }

Fish.prototype = new Animal();

var salmon = new Fish();
instance1.hobbies.push("swim");
console.log(instance1.hobbies);

var salmon2 = new Fish();
console.log(instance2.hobbies); 

https://ithelp.ithome.com.tw/upload/images/20200918/20124350pXMzM4iKi1.png

解析

// 主要原因是因為這一行,他改的其實是instance.__proto__.hobbies(意思就是改變Fish.prototype.hobbies(內存地址相同)) 
instance1.hobbies.push("swim"); 
// 因此假設我們繼續創建實例會繼續繼承已經改變的原型鍊

改變前 

https://ithelp.ithome.com.tw/upload/images/20200918/201243506uAgKsWSDc.png

改變時(Fish.prototype.hobbies被push)

https://ithelp.ithome.com.tw/upload/images/20200918/20124350D5TCOkPT2Q.png

創建鮭魚二號

繼承自被改變的Fish.prototype

https://ithelp.ithome.com.tw/upload/images/20200918/20124350jrSsoqgtvy.png

二、借用構造函数繼承

代碼

function Animal() {
  this.hobbies = ["sleep", "watch"];
}
Animal.prototype.walk = function () {
  console.log('走走走');
}
function Fish() {
  // 構造函數繼承
  Animal.call(this) // 執行Animal函數(這裡的this是實例本身) 
}

var instance1 = new Fish();
instance1.hobbies.push("swim"); 
console.log(instance1);

var instance2 = new Fish();
console.log(instance2.hobbies); 

https://ithelp.ithome.com.tw/upload/images/20200918/20124350aJsFYv3Q6p.png

缺點

  1. 雖然確實不會改變其他實例,但是我們會發現 (Animal.prototype) 不會被 fish 繼承
  2. 浪費性能,每個子類都存在父類實例函數的副本

三、組合繼承(綜合上面兩種繼承)

  1. 利用原型鍊繼承方法讓原型鍊繼承 prototype 跟 __ proto __

  2. 利用構造函數讓實例本身也繼承屬性

簡單來說就是透過實例(是一個對象)本身也繼承屬性(比方說Animal裡面的name以及hobbies) ,解決原型鍊繼承實例修改可能會不小心改到原型鍊的問題

代碼

function Animal(name) {
  this.name = name;
  this.hobbies = ["sleep", "watch"];
}
Animal.prototype.sayName = function () {
  console.log(this.name);
};

function Fish(name, age) {
  // 讓Fish的實例也繼承Animal屬性
  // 比方說下面
  // let instance1 = new Fish("Salmon", 29);
  // instance自身會等於對象(這樣我們push就不會push到原型鍊)
  // {
  //   name: 'Salmon',
  //   age: 29,
  //   hobbies: ["sleep", "watch"]
  // }
  Animal.call(this, name);
  this.age = age;
}

// 繼承原型鍊
Fish.prototype = new Animal();

// 重寫Fish.prototype的constructor属性(方便辨識prototype屬於誰)
Fish.prototype.constructor = Fish;
Fish.prototype.sayAge = function () {
  console.log(this.age);
};

let salmon = new Fish("Salmon1", 15);
console.log(salmon);
salmon.hobbies.push("swim");
console.log(salmon.hobbies); // ["sleep", "watch", "swim"]
salmon.sayName(); //"Salmon1";
salmon.sayAge(); //15

let salmon2 = new Fish("salmon2", 10);
console.log(salmon2.hobbies); // ["sleep", "watch"]
salmon2.sayName(); //"salmon2";
salmon2.sayAge(); //10

溫馨提醒

https://ithelp.ithome.com.tw/upload/images/20200918/20124350AYKpkJCnZa.png

缺點

因為調用了兩次構造函數

​ 代碼:

function Fish(name, age) {
// 第一次
Animal.call(this, name);
this.age = age;
}
// 第二次
Fish.prototype = new Animal();

所以會讓原型鍊與實例都有相同的屬性名

https://ithelp.ithome.com.tw/upload/images/20200918/20124350kguSkn1Vk5.png

四、原型式繼承

運用一個空的構造函數為媒介,將傳入的對象賦值給空構造函數的prototype,並創建實例

代碼

function object(obj) {
  function F() { }
  F.prototype = obj;
  return new F();
}

let student = {
  year: 2020,
  hobbies: ["sleep", "play"]
};

let Mike = object(student);
Mike.name = "Mike";
Mike.hobbies.push("sing");

let Mary = object(student);
Mary.name = "Mary";
Mary.hobbies.push("read");

console.log(student.hobbies);   

https://ithelp.ithome.com.tw/upload/images/20200918/20124350KVBZ1Rh2pv.png

示意圖

https://ithelp.ithome.com.tw/upload/images/20200918/20124350nusD1fiFjA.png

缺點

跟原型鍊繼承一樣,因為

Mike.hobbies.push("sing"); // 改變的其實是原型鍊,所以其他實例會被影響

五、寄生式繼承

跟上一個繼承法比,其實就只是再封裝一個函數,添加實例的方法或是屬性

代碼

function object(obj) {
  function F() { }
  F.prototype = obj;
  return new F();
}

function createAnother(original) {
  var clone = object(original); // clone是一個實例
  clone.sayHi = function () {  // 其實就只是在實例對象裡添加函數或是屬性
    alert("hi");
  };
  return clone; // 返回整個實例
}

let student = {
  year: 2020,
  hobbies: ["sleep", "play"]
};
var Mike = createAnother(student);
console.log(Mike);
Mike.sayHi(); //"hi"

缺點

  1. 還是沒解決實例的修改可能會讓其他實例跟著修改
  2. 無法傳參(student 都是固定的)

六、寄生組合式繼承

借用構造函數以及原型式繼承

附註:

function object(obj) {
function F() { }
F.prototype = obj;
return new F();
}
// 等價於 
Object.create() 

代碼

function inheritPrototype(subType, superType) {
  // 拷貝父類型的prototype
  var prototype = Object.create(superType.prototype);
  // 讓之後的子類實例可以更明確知道由誰(子類)創造
  prototype.constructor = subType;           
  // 將拷貝父類型的prototype賦值給子類型的prototype
  subType.prototype = prototype;                      
}

// 父類
function Animal(name) {
  this.name = name;
  this.hobbies = ["sleep", "watch"];
}
Animal.prototype.sayName = function () {
  console.log(this.name);
};

// 子類
function Fish(name, age) {
  Animal.call(this, name); // 利用構造函數進行解決可以傳參還有可能改寫原型鍊的問題
  this.age = age; 
}

// 繼承(不再是透過Fish.prototype = new Animal(...))
inheritPrototype(Fish, Animal);

Fish.prototype.sayAge = function () {
  console.log(this.age);
}

var Salmon1 = new Fish("Salmon1", 15);
var Salmon2 = new Fish("Salmon2", 21);

Salmon1.hobbies.push("rock");
Salmon2.hobbies.push("jazz"); 

console.log(Salmon1);
console.log(Salmon2);


https://ithelp.ithome.com.tw/upload/images/20200918/20124350jej9QyWraa.png

分析

解決組合繼承的缺點

核心關鍵:

// 透過自己封裝的函數繼承
inheritPrototype(Fish, Animal);

// 不再是利用(因為創建Animal實例會導致Fish的prototype也創建Animal的屬性)
Fish.prototype = new Animal()

非常完美(可以說是最成熟的方法,也是現在庫實現的方法)

附註

  1. Object.create方法

    做的事情其實跟new很像 (但不會賦予子類型的proto屬性,就是利用這點改進組合繼承的缺點)

    • 代碼:
    function Foo() {
    }
    
    let bar = Object.create(Foo.prototype)
    console.log(bar);
    

    https://ithelp.ithome.com.tw/upload/images/20200918/20124350OAzUBVGdaT.png

    • 示意圖

    https://ithelp.ithome.com.tw/upload/images/20200918/20124350xXid5tVYoe.png

七、混入方式繼承多個對象

function Fish() {
     Animal.call(this);
     marineLife.call(this);
}

// 繼承一個類
Fish.prototype = Object.create(Animal.prototype);

// 混合其它(淺拷貝marineLife.prototype到Fish.prototype)
// 目的是讓Fish可以使用marineLife方法
Object.assign(Fish.prototype, marineLife.prototype);

// 重新指定constructor
Fish.prototype.constructor = Fish;

// 開始定義自己的方法
Fish.prototype.myMethod = function() {
     // do something
};

八、ES6類繼承extends

更全面的分析可參考

https://es6.ruanyifeng.com/#docs/class-extends


上一篇
Day 07 [原型鍊03] JavaScript中,new操作符的工作原理是什么
下一篇
Day 09 [其他01] 7分鐘理解JS的節流、防抖及使用場景
系列文
從技術文章深入學習 JavaScript29
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言