ES5 繼承

首先要明白兩點:
1、非方法屬性每一個子類實例須要獨立
2、方法屬性每一個子類實例須要共享
爲何?
若是非方法屬性爲引用類型,且非方法屬性共享,在一個實例中改變,其餘實例中就會作出改變,這樣每一個實例就會相互影響,而方法屬性通常是不須要進行改變的,只是對方法調用。chrome

方法跟屬性分別能夠定義在構造函數內部跟prototype上。函數

繼承的目的是子類繼承父類的方法跟屬性,換句話說一些類的相同的方法屬性須要共享,將這些須要共享的方法屬性抽取到一個地方,這就是父類。this

代碼主要來自於紅寶書4spa

1. 基於原型鏈的繼承

每一個函數都有個prototype屬性,每一個對象都有__proto__屬性(在chrome中表現如此,prototype也是如此) 如圖,屬性的查找會從當前層級依次向原型鏈上查找,直到查找到原型鏈的頂端null,具體可參考js proto
image.png
既然屬性的查找是按照原型鏈向上查找的,且繼承就是繼承父類的屬性跟方法,那麼就能夠利用這個特性,進行繼承。prototype

function SuperType() {
  this.property = true;
}

SuperType.prototype.getSuperValue = function () {
  return this.property;
};
function SubType() {
  this.subproperty = false;
}
// 繼承SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
  return this.subproperty;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // true 能夠正確調用父類的方法,拿到父類的屬性

原型雖然實現了繼承,可是仍是有缺點的
劣勢:code

1. 子類或者父類的屬性爲引用類型時,改變一個實例的引用類型屬性,其餘實例的該引用類型屬性也會發生改變,這樣其實例就會相互污染了。
function SuperType() {
  this.colors = ["red", "blue", "green"];
}

function SubType() {} // 繼承SuperType
SubType.prototype = new SuperType();

let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);
// "red,blue,green,black";

let instance2 = new SubType();
console.log(instance2.colors);
// "red,blue,green,black";

爲何非方法屬性不寫在prototype上?對象

由於prototype上的屬性的共享的,在一個實例上改了該屬性,其餘實例的該屬性也會被改掉。blog

爲何方法不寫在構造函數內部?繼承

方法寫在子類內部:每次實例化構造函數,方法都是新的;方法只是用來調用,不須要修改,因此實例共享就好了。
方法寫在父類內部:不一樣的子類繼承父類都須要實例化父類;方法只是用來調用,不須要作修改,因此實例共享就好了,包括子類實例。若是子類須要修改父類方法,直接在子類中定義相同方法名,進行覆蓋就好了。ip

2. 子類在實例化時不能給父類的構造函數傳參,由於父類的實例化是在前面,而不是構造函數調用的時候。

2. 盜用構造函數

爲了解決父類中屬性爲引用類型致使子類實例化後,引用屬性共享的問題,跟父類構造函數沒法傳參的問題。引入了「盜用構造函數「方式實現繼承。思路是在子類構造函數中調用父類構造函數。

  • 不一樣實例的引用屬性不會相互影響
function SuperType() {
  this.colors = ["red", "blue", "green"];
}
function SubType() {
  // 繼承SuperType
  SuperType.call(this);
}

let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);
// "red,blue,green,black";

let instance2 = new SubType();
console.log(instance2.colors);
// "red,blue,green";

instance1 instance2兩個實例就不會相互影響。

  • 能夠爲父類構造函數傳參
function SuperType(name) {
  this.name = name;
}
function SubType(name) {
  // 繼承SuperType並傳參
  SuperType.call(this, name);
  // 實例屬性
  this.age = 29;
}
let instance = new SubType("geek");
console.log(instance.name); // "geek";
console.log(instance.age); // 29

動態傳遞參數到父類構造函數

劣勢:

  • 定義在父類prototype上的方法,子類沒法繼承
function SuperType(name) {
  this.name = name;
}
SuperType.prototype.say = function () {
  console.info("hello");
};

function SubType(name) {
  // 繼承SuperType並傳參
  SuperType.call(this, name);
  // 實例屬性
  this.age = 29;
}

let instance = new SubType("geek");
console.log(instance.name); // "geek";
console.log(instance.age); // 29
instance.say() // 獲取不到該函數

經過 new 實例化後,實例才能拿到prototype上的方法,a.__proto__===Animal.prototype,因此instance.say()不存在

  • 定義在父類構造函數中方法沒法共享

每次實例化子類,都會調用父類構造函數,其內部定義的方法都是新的,佔用了沒必要要的內存,沒有實現方法的共享。

3. 組合繼承

組合繼承兼顧原型鏈繼承跟盜用構造函數的優勢,這樣既能夠把方法定義在原型上以實現重用,又能夠看讓每一個實力都有本身的屬性。

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
  console.log(this.name);
};
function SubType(name, age) {
  // 繼承屬性,綁定上下文爲SubType的實例
  SuperType.call(this, name);
  this.age = age;
}
// 繼承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function () {
  console.log(this.age);
};
let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors);
// "red,blue,green,black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29

let instance2 = new SubType("Greg", 27);
console.log(instance2.colors);
// "red,blue,green";
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27

能夠傳遞參數到父類構造函數
兩個實例中的引用類型不會相互影響
實例能夠調用父類的方法,且實現方法的共享
組合繼承也保留了 instanceof 操做符和isPrototypeOf() 方法識別合成對象的能力。

劣勢:
SuperType會被調用兩次,SubType實例跟原型鏈上都有name跟colors屬性。

4. 原型式繼承

不定義構造函數經過原型實現對象以前的繼承。

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

返回新對象,讓其原型指向O

let person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"],
};
let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends);
// "Shelby,Court,Van,Rob,Barbie";

父對象中的引用屬性會在子對象中共享,致使相互污染。

ES5引入了Object.create()規範了原型式繼承。

let person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"],
};
let anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends);
// "Shelby,Court,Van,Rob,Barbi

使用Object.create後anotherPerson.__proto__===person成立,因此anotherPerson能夠拿到person的屬性,可是一樣存在父對象屬性共享的問題,改了父對象的屬性,其餘的子對象都跟着改變。

劣勢:
父對象的引用類型會在實例中共享,這樣就會相互污染。

5. 寄生式繼承

寄生式繼承跟原型式繼承很相似,用工廠函數在對返回的新對象包一層,給新對象賦值一些屬性
工廠函數的定義:

function createAnother(original) {
  let clone = object(original);
  // 經過調用函數建立一個新對象;

  clone.sayHi = function () {
    // 以某種方式加強這個對象;
    console.log("hi");
  };
  return clone; // 返回這個對象
}

使用:

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

定義在新對象上的sayHi方法,每次調用新對象都是新的,沒法實現共享。

劣勢:
父對象的引用類型會在實例中共享,這樣就會相互污染。
方法沒法實現共享

6. 寄生式組合繼承

上面提到,組合繼承的缺點就是父類構造函數會被調用兩次,一次是在子類的構造函數中,另外一次在建立子類原型時調用。繼承就是要繼承父類的屬性跟方法,組合繼承實現了這個目標,可是怎麼避免重複調用父類構造函數。
先看下組合繼承:

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

SuperType.prototype.sayName = function () {
  console.log(this.name);
};
function SubType(name, age) {
  SuperType.call(this, name); // 第二次調用,將父類的屬性綁定到子類的實例中
  SuperType();
  this.age = age;
}

SubType.prototype = new SuperType();
// 第一次調用SuperType();

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

image.png

由圖可見s的原型鏈上依然有name跟colors屬性。這也是不須要的。怎麼解決這兩個問題?
父類的屬性是須要的,父類的原型上的方法是須要的,重複的父類屬性不須要,由上圖可見重複的父類屬性是因爲實例化父類給子類原型形成的,咱們不去實例化父類,而是將父類的原型傳遞給子類的原型就好了,結合原型式繼承特色能夠作到

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

SuperType.prototype.sayName = function () {
  console.log(this.name);
};

function SubType(name, age) {
  SuperType.call(this, name); // 將父類的屬性綁定到SubType實例中
  this.age = age;
}

SubType.prototype = Object.create(SuperType.prototype);
// 將子類的prototype關聯到父類的prototype上

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

使用Object.create解決了父類構造函數調用兩次,父類屬性重複的問題,可是子類constructor並無出如今原型鏈中
image.png
下面作出改造:

SubType.prototype = Object.create(SuperType.prototype, {
  constructor: {
    value: SubType, // 修正 constructor 指向
    writable: true,
    configurable: true,
  },
});

image.png
SuperType的constructor出現了,其實constructor並沒什麼用,只是個約定罷了,參考賀老的解釋JavaScript 中對象的 constructor 屬性的做用是什麼?instanceof操做符和 isPrototypeOf() 方法正常有效。寄生式組合繼承能夠算是引用類型繼承的最佳模式

相關文章
相關標籤/搜索