1. 程式人生 > 程式設計 >JS原形與原型鏈深入詳解

JS原形與原型鏈深入詳解

本文例項講述了JS原形與原型鏈。分享給大家供大家參考,具體如下:

前言

在JS中,我們經常會遇到原型。字面上的意思會讓我們認為,是某個物件的原型,可用來繼承。但是其實這樣的理解是片面的,下面通過本文來了解原型與原型鏈的細節,再順便談談繼承的幾種方式。

原型

在講到原型之前,我們先來回顧一下JS中的物件。在JS中,萬物皆物件,就像字串、數值、布林、陣列等。ECMA-262把物件定義為:無序屬性的集合,其屬性可包含基本值、物件或函式。物件是擁有屬性和方法的資料,為了描述這些事物,便有了原型的概念。

無論何時,只要建立了一個新函式,就會根據一組特定的規則為該函式建立一個prototype屬性,這個屬性指向該函式的原型物件。所有原型物件都會獲得一個constructor

屬性,這個屬性包含一個指向prototype屬性所在函式的指標。

這段話摘自《JS高階程式設計》,很好理解,以建立例項的程式碼為例。

function Person(name,age) {
  this.name = name;
  this.age = age;
  this.sayName = function() {
    alert(this.name);
  };
}

const person1 = new Person("gali",18);
const person2 = new Person("pig",20);

JS原形與原型鏈深入詳解

上面例子中的person1跟person2都是建構函式Person()

的例項,Person.prototype指向了Person函式的原型物件,而Person.prototype.constructor又指向Person。Person的每一個例項,都含有一個內部屬性__proto__,指向Person.prototype,就像上圖所示,因此就有下面的關係。

console.log(Person.prototype.constructor === Person); // true
console.log(person1.__proto__ === Person.prototype); // true
console.log(person2.__proto__ === Person.prototype); // true

繼承

JS是基於原型的語言,跟基於類的面嚮物件語言有所不同,JS中並沒有類這個概念,有的是原型物件這個概念,原型物件作為一個模板,新物件可從原型物件中獲得屬性。那麼JS具體是怎樣繼承的呢?

在講到繼承這個話題之前,我們先來理解原型鏈這個概念。

原型鏈

建構函式,原型和例項的關係已經很清楚了。每個建構函式都有一個原型物件,原型物件都包含一個指向建構函式的指標,而例項物件都包含一個指向與原型物件的指標。這樣的關係非常好理解,但是如果我們想讓原型物件等於另一個型別的例項物件呢?那麼就會衍生出相同的關係,此時的原型物件就會含有一個指向另一個原型物件的指標,而另一個原型物件會含有一個指向另一個建構函式的指標。如果另一個原型物件又是另一個型別的例項物件呢?這樣就構成了原型鏈。文字可能有點難理解,下面用程式碼舉例。

function SuperType() {
  this.name = "張三";
}
SuperType.prototype.getSuperName = function() {
  return this.name;
};

function SubType() {
  this.subname = "李四";
}
SubType.prototype = new SuperType();
SubType.prototype.getSubName = function() {
  return this.subname;
};

const instance = new SubType();
console.log(instance.getSuperName()); // 張三

上述例子中,SubType的原型物件作為SuperType建構函式的例項物件,此時,SubType的原型物件就會有一個__proto__屬性指向SuperType的原型物件,instance作為SubType的例項物件,必然能共享SubType的原型物件的屬性,又因為SubType的原型物件又指向SuperType原型物件的屬性,因此可得,instance繼承了SuperType原型的所有屬性。

我們都知道,所有函式的預設原型都是Object的例項,所以也能得出,SuperType的預設原型必然有一個__proto__指向Object.prototype。

圖中由__proto__屬性組成的鏈子,就是原型鏈,原型鏈的終點就是null

JS原形與原型鏈深入詳解

上圖可很清晰的看出原型鏈的結構,這不禁讓我想到JS的一個運算子instanceof,instanceof可用來判斷一個例項物件是否屬於一個建構函式。

A instanceof B; // true

實現原理其實就是在A的原型鏈上尋找是否有原型等於B.prototype,如果一直找到A原型鏈的頂端null,仍然找不到原型等於B.prototype,那麼就可返回false。下面手寫一個instanceof,這個也是很多大廠常用的手寫面試題。

function Instance(left,right) {
  left = left.__proto__;
  right = right.prototype;
  while (true) {
    if (left === null) return false;
    if (left === right) return true;
    // 繼續在left的原型鏈向上找
    left = left.__propo__;
  }
}
原型鏈繼承

上面例子中,instance繼承了SuperType原型的屬性,其繼承的原理其實就是通過原型鏈實現的。原型鏈很強大,可用來實現繼承。可是單純的原型鏈繼承也是有問題存在的。

  • 例項屬性變成原型屬性,影響其他例項
  • 建立子型別的例項時,不能向超型別的建構函式傳遞引數
function SuperType() {
  this.colorArr = ["red","blue","green"];
}
function SubType() {}
SubType.prototype = new SuperType();

const instance1 = new SubType();
instance1.colorArr.push("black");
console.log(instance1.colorArr); // ["red","green","black"]

const instance2 = new SubType();
console.log(instance2.colorArr); // ["red","black"]

當SubType的原型作為SuperType的例項時,此時SubType的例項物件通過原型鏈繼承到colorArr屬性,當修改了其中一個例項物件從原型鏈中繼承到的原型屬性時,便會影響到其他例項。對instance1.colorArr的修改,在instance2.colorArr便能體現出來。

組合繼承

組合繼承指的是組合原型鏈和建構函式的技術,通過原型鏈實現對原型屬性和方法的繼承,而通過借用建構函式實現對例項屬性的繼承。

function SuperType(name) {
  this.name = name;
  this.colors = ["red","green"];
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
};

function SubType(name,age) {
  // 繼承屬性,借用建構函式實現對例項屬性的繼承
  SuperType.call(this,name);
  this.age = age;
}

// 繼承原型屬性及方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
  console.log(this.age);
};

const instance1 = new SubType("gali",18);
instance1.colors.push("black");
console.log(instance1.colors); // ["red","black"]
instance1.sayName(); // gali
instance1.sayAge(); // 18

const instance2 = new SubType("pig",20);
console.log(instance2.colors); // ["red","green"]
instance2.sayName(); // pig
instance2.sayAge(); // 20

上述例子中,借用建構函式繼承例項屬性,通過原型繼承原型屬性與方法。這樣就可讓不同的例項分別擁有自己的屬性,又可共享相同的方法。而不會像原型繼承那樣,對例項屬性的修改影響到了其他例項。組合繼承是JS最常用的繼承方式。

寄生組合式繼承

雖然說組合繼承是最常用的繼承方式,但是有沒有發現,就上面的例子中,組合繼承中呼叫了2次SuperType函式。回憶一下,在第一次呼叫SubType時。

SubType.prototype = new SuperType();

這裡呼叫完之後,SubType.prototype會從SuperType繼承到2個屬性:name和colors。這2個屬性存在SubType的原型中。而在第二次呼叫時,就是在創造例項物件時,呼叫了SubType建構函式,也就會再呼叫一次SuperType建構函式。

SuperType.call(this,name);

第二次呼叫之後,便會在新的例項物件上建立了例項屬性:name和colors。也就是說,這個時候,例項物件跟原型物件擁有2個同名屬性。這樣實在是浪費,效率又低。

為了解決這個問題,引入了寄生組合繼承方式。重點就在於,不需要為了定義SubType的原型而去呼叫SuperType建構函式,此時只需要SuperType原型的一個副本,並將其賦值給SubType的原型即可。

function InheritPrototype(subType,superType) {
  // 建立超型別原型的一個副本
  const prototype = Object(superType.prototype);
  // 新增constructor屬性,因為重寫原型會失去constructor屬性
  prototype.constructor = subType;
  subType.prototype = prototype;
}

將組合繼承中的:

SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;

替換成:

InheritPrototype(SubType,SuperType);

寄生組合繼承的優點在於,只需要呼叫一次SuperType建構函式。避免了在SubType的原型上建立多餘的不必要的屬性。

總結

溫故而知新,再次看回《JS高階程式設計》這本書的原型與原型鏈部分,發現很多以前忽略掉的知識點。而這次回看這個知識點,並輸出了一篇文章,對我來說受益匪淺。寫文章往往不是為了寫出怎樣的文章,其實中間學習的過程才是最享受的。

感興趣的朋友可以使用線上HTML/CSS/JavaScript程式碼執行工具:http://tools.jb51.net/code/HtmlJsRun測試上述程式碼執行效果。

更多關於JavaScript相關內容感興趣的讀者可檢視本站專題:《javascript面向物件入門教程》、《JavaScript錯誤與除錯技巧總結》、《JavaScript資料結構與演算法技巧總結》、《JavaScript遍歷演算法與技巧總結》及《JavaScript數學運算用法總結》

希望本文所述對大家JavaScript程式設計有所幫助。