JavaScript繼承總結

原型鏈

當讀取實例的屬性時,如果找不到,就會查找與對象關聯的原型中的屬性,如果還查不到,就去找原型的原型,一直找到最頂層爲止。
如果讓原型對象指向另一個類型的實例.....有趣的事情便發生了.
即: Person.prototype = animal2
鑑於上述遊戲規則生效,如果試圖引用Person構造的實例person1的某個屬性:
1).首先會在instance1內部屬性中找一遍;
2).接着會在instance1.__proto__(constructor1.prototype)中找一遍,而constructor1.prototype 實際上是animal2, 也就是說在animal2中尋找該屬性p1;
3).如果animal2中還是沒有,此時程序不會灰心,它會繼續在animal2.__proto__(Animal.prototype)中尋找...直至Object的原型對象

搜索軌跡: person1--> animal2--> Animal.prototype-->Object.prototype

這種搜索的軌跡,形似一條長鏈, 又因prototype在這個遊戲規則中充當鏈接的作用,於是我們把這種實例與原型的鏈條稱作原型鏈 .

JavaScript繼承

繼承意味着複製操作,然而 JavaScript 默認並不會複製對象的屬性,相反,JavaScript 只是在兩個對象之間創建一個關聯,這樣,一個對象就可以通過委託訪問另一個對象的屬性和函數,所以與其叫繼承,委託的說法反而更準確些。
——《你不知道的JavaScript》

基於原型鏈

不同於其它大部分語言,JavaScript是基於原型的對象系統,而不是基於類。
基於原型的面向對象設計方法總共有三種。

  • 拼接繼承: 是直接從一個對象拷貝屬性到另一個對象的模式。被拷貝的原型通常被稱爲mixins。ES6爲這個模式提供了一個方便的工具Object.assign()。在ES6之前,一般使用Underscore/Lodash提供的.extend(),或者 jQuery 中的$.extend(), 來實現。上面那個對象組合的例子,採用的就是拼接繼承的方式。
  • 原型代理:JavaScript中,一個對象可能包含一個指向原型的引用,該原型被稱爲代理。如果某個屬性不存在於當前對象中,就會查找其代理原型。代理原型本身也會有自己的代理原型。這樣就形成了一條原型鏈,沿着代理鏈向上查找,直到找到該屬性,或者找到根代理Object.prototype爲止。原型就是這樣,通過使用new關鍵字來創建實例以及Constructor.prototype前後勾連成一條繼承鏈。當然,也可以使用Object.create()來達到同樣的目的,或者把它和拼接繼承混用,從而可以把多個原型精簡爲單一代理,也可以做到在對象實例創建後繼續擴展。
  • 函數繼承:在JavaScript中,任何函數都可以用來創建對象。如果一個函數既不是構造函數,也不是 class,它就被稱爲工廠函數。函數繼承的工作原理是:由工廠函數創建對象,並向該對象直接添加屬性,藉此來擴展對象(使用拼接繼承)。函數繼承的概念最先由道格拉斯·克羅克福德提出,不過這種繼承方式在JavaScript中卻早已有之。

藉助構造函數實現繼承(經典繼承)

 function Parent1() {
   this.name = 'parent1';
 }
 
 Parent1.prototype.say = function () {}
 
 function Child1() {
   Parent1.call(this);
   this.type = 'child';
 }

 console.log(new Child1);


這個主要是借用call 來改變this的指向,通過 call 調用 Parent ,此時 Parent 中的 this 是指 Child1。
有個缺點,從打印結果看出 Child並沒有say方法,所以這種只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法。
注意 constructor 屬性, new 操作爲了記錄「臨時對象是由哪個函數創建的」,所以預先給「Child.prototype」加了一個 constructor 屬性:

藉助原型鏈實現繼承

function Parent2() {
  this.name = 'parent2';
  this.play = [1, 2, 3];
}

function Child2() {
  this.type = 'child2';
}
Child2.prototype = new Parent2();

console.log(new Child2);

通過一講的,我們知道要共享莫些屬性,需要 對象.__proto__ = 父親對象的.prototype,但實際上我們是不能直接 操作__proto__,
這時我們可以借用 new 來做,所以Child2.prototype = new Parent2(); <=> Child2.prototype.__proto__ = Parent2.prototype; 這樣我們藉助 new 這個語法糖,就可以實現原型鏈繼承。
缺點:給 s1.play新增一個值 ,s2 也跟着改了。所以這個是原型鏈繼承的缺點,原因是 s1.__pro__ 和 s2.__pro__指向同一個地址即父類Child2的prototype。

組合繼承

是指將原型鏈和構造函數的相結合,發揮二者之長的一種繼承模式。其思路是使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。這樣,即通過在原型上定義方法實現了函數複用,又能夠保證每個實例都有它自己的屬性。

初級版

function Super(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

Super.prototype.sayName = function (){
    alert(this.name);
};

function Sub(name, age){
    Super.call(this, name);    //繼承了Super 屬性 (第二次調用Sup構造函數)
    this.age = age;
}

// 繼承了Super 原型鏈上的方法 (第一次調用Sup構造函數) 注意後面需要改造這裏,因爲我們只想要方法,卻生成了屬性
Sub.prototype = new Super();    

Sub.prototype.constructor = Sub;// 
Sub.prototype.sayAge = function (){
    alert(this.age);
};

var instance1 = new Sub("Luke", 18);
instance1.colors.push("black");
alert(instance1.colors);    //"red, blue, green, black"
instance1.sayName();    //"Luke"
instance1.sayAge()    //18

var instance2 = new Sub("Jack", 20);
alert(instance2.colors);    //"red, blue, green"
instance2.sayName();    //"Jack"
instance2.sayAge()    //20

優化後的組合繼承

function Super(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

Super.prototype.sayName = function (){
    alert(this.name);
};

function Sub(name, age){
    Super.call(this, name);    //繼承了Super 屬性
    this.age = age;
}

function F(){
}
F.prototype = Super.prototype; 
Sub.prototype = new F();    // 繼承了Super 原型鏈上的方法

Sub.prototype.constructor = Sub;
Sub.prototype.sayAge = function (){
    alert(this.age);
};

var instance1 = new Sub("Luke", 18);
console.log(instance1 )
instance1.colors.push("black");
alert(instance1.colors);    //"red, blue, green, black"
instance1.sayName();    //"Luke"
instance1.sayAge()    //18

var instance2 = new Sub("Jack", 20);
alert(instance2.colors);    //"red, blue, green"
instance2.sayName();    //"Jack"
instance2.sayAge()    //20

疑問

爲什麼要這麼寫?

function F(){
}
F.prototype = Super.prototype; 
Sub.prototype = new F();    // 繼承了Super 原型鏈上的方法

而不是

Sub.prototype = Super.prototype; 

下面的方法沒有辦法區分一個對象是直接由它的子類實例化還是父類呢?
下面這是第一個方法無法判斷

instance1 instanceof Sub//true
instance1 instanceof Super//true

我們還有一個方法判斷來判斷對象是否是類的實例,那就是用 constructor,我在控制檯打印以下內容也無法分辨:

原型繼承

藉助原型可以基於已有的對象創建新對象, 同時還不必因此創建自定義類型
在object()函數內部, 先創建一個臨時性的構造函數, 然後將傳入的對象作爲這個構造函數的原型,最後返回了這個臨時類型的一個新實例.

function object(o){
    function F(){}
    F.prototype = o;//重寫F的原型,將他指向傳入的o,這就相當於繼承自o
    return new F();//返回F的實例對象
}
var person = {
    friends : ["Van","Louis","Nick"]
};
var anotherPerson = object(person);
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.friends.push("Style");
alert(person.friends);//"Van,Louis,Nick,Rob,Style"

從本質上講, object() 對傳入其中的對象執行了一次淺複製.所用的子類都指向傳入的person對象

object.create() 方法規範化了上面的原型式繼承. 上篇文章有這個方法的詳細解釋

var person = {
    friends : ["Van","Louis","Nick"]
};
var anotherPerson = Object.create(person);
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.friends.push("Style");
alert(person.friends);//"Van,Louis,Nick,Rob,Style"
console.log(anotherPerson)

缺點:

原型鏈繼承多個實例的引用類型屬性指向相同,存在篡改的可能。
無法傳遞參數

寄生式繼承

核心:在原型式繼承的基礎上,增強對象,返回構造函數
函數的主要作用是爲構造函數新增屬性和方法,以增強函數

function createAnother(original){
  var clone = object(original); // 通過調用 object() 函數創建一個新對象,object是一個任何能夠返回對象的函數
  clone.sayHi = function(){  // 以某種方式來增強對象
    alert("hi");
  };
  return clone; // 返回這個對象
}

var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"

缺點(同原型式繼承):
原型鏈繼承多個實例的引用類型屬性指向相同,存在篡改的可能。
無法傳遞參數

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章