动态

详情 返回 返回

ES5 繼承 - 动态 详情

首先要明白兩點:
一、非方法屬性每個子類實例需要獨立
二、方法屬性每個子類實例需要共享
為什麼?
如果非方法屬性為引用類型,且非方法屬性共享,在一個實例中改變,其他實例中就會做出改變,這樣每個實例就會相互影響,而方法屬性一般是不需要進行改變的,只是對方法調用。

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

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

代碼主要來自於紅寶書4

1. 基於原型鏈的繼承

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

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 可以正確調用父類的方法,拿到父類的屬性

原型雖然實現了繼承,但是還是有缺點的
劣勢:

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上的屬性的共享的,在一個實例上改了該屬性,其他實例的該屬性也會被改掉。

為什麼方法不寫在構造函數內部?

方法寫在子類內部:每次實例化構造函數,方法都是新的;方法只是用來調用,不需要修改,所以實例共享就行了。
方法寫在父類內部:不同的子類繼承父類都需要實例化父類;方法只是用來調用,不需要做修改,所以實例共享就行了,包括子類實例。如果子類需要修改父類方法,直接在子類中定義相同方法名,進行覆蓋就行了。

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() 方法正常有效。寄生式組合繼承可以
算是引用類型繼承的最佳模式

user avatar chongdianqishi 头像 kanshouji 头像 pengxiaohei 头像 munergs 头像 panpanpanya 头像 shenyongweiwudemaozi 头像 juanerma 头像 humi 头像 zxbing0066 头像
点赞 9 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.