JS 原型、原型繼承、原型鏈的理解

視野圖

前言

對於原型、原型鏈、原型繼承,是每一個前端人員必過的一項基礎。幾乎在面試的時候都會被問到原型、原型鏈的這些問題,索性寫一下文章,把這些問題一次性理解清楚(看多遍別人的文章,不如實際來寫寫,看時懂,可過兩天就會忘記了)。javascript

原型的理解

咱們建立的每一個函數都有一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含能夠由特定類型的全部實例共享的屬性和方法。prototype(原型)就是經過調用構造函數而建立的那個對象實例的原型對象。使用原型對象的好處是可讓全部對象實例共享它所包含的屬性和方法。css

function Animal(){}

Animal.prototype.name = 'animal';
Animal.prototype.age = '10';
Animal.prototype.sayName = function(){
    console.log(this.name);
}

let dog = new Animal();
dog.sayName();          // 輸出 animal
let cat = new Animal();
cat.sayName();          // 輸出 animal
複製代碼

上面使用一段代碼來解釋原型共享屬性和方法。將屬性和方法都添加到Animal的prototype中,構造函數變成了空函數。不管是dog實例Animal(),仍是cat實例Animal(),都訪問的是同一組屬性和同一個sayName()函數。當爲對象實例添加一個屬性時,這個屬性就會屏蔽原型對象中保存的同名屬性。前端

function Animal(){}
// 經常使用原型寫法
Animal.prototype = {
    name: 'animal',
    age: '10',
    sayName: function(){
    	console.log(this.name);
    }
};

let dog = new Animal();
dog.sayName();          // 輸出 animal
let cat = new Animal();
cat.name = 'Tom';
cat.sayName();          // 輸出 Tom
複製代碼

前面提到:原型(prototype)是經過調用構造函數而建立的那個對象實例的原型對象。在默認狀況下,全部原型對象都會自動得到一個 constructor(構造函數)屬性,這個屬性包含一個指向prototype屬性所在函數的指針。以下圖所示: java

函數原型

Animal 的每一個實例dog和cat都包含一個內部屬性,該屬性僅僅指向了Animal.prototype;換句話說,它們與構造函數沒有直接的關係。面試

實例化後的原型

原型鏈的理解

原型鏈基本思想是利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。簡單回顧一下構造函數、原型和實例的關係:每一個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。app

function Animal(){}
// 經常使用原型寫法
Animal.prototype = {
    name: 'animal',
    age: '10',
    sayName: function(){
    	console.log(this.name);
    }
};

// 繼承Animal,實際上就是重寫原型
function runAnimal(){}
runAnimal.prototype = new Animal();
runAnimal.prototype.run = function(){
	console.log("我會跑!!!");
}

let cat = new runAnimal();
console.log(cat.name);      // 輸出 animal
console.log(cat.run());     // 輸出 我會跑!!!
複製代碼

原型繼承

咱們知道,全部引用類型默認都繼承了 Object,而這個繼承也是經過原型鏈實現的。 因此上面的繼承能夠轉化成成下圖:函數

原型鏈詳解圖

原型繼承

子類的原型對象實例父類

基本思想:利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。經過子類的原型prototype對父類實例化。測試

function Animal(){
    this.behavior = ['吃飯', '睡覺'];
}
// 經常使用原型寫法
Animal.prototype = {
    name: 'animal',
    age: '10',
    sayName: function(){
        console.log(this.name);
    }
};

// 繼承Animal,實際上就是重寫原型、原型實例化父類
function runAnimal(){}
runAnimal.prototype = new Animal();
runAnimal.prototype.run = function(){
	console.log("我會跑!!!");
}

let cat = new runAnimal();
console.log(cat.name);      // 輸出 animal
console.log(cat.run());     // 輸出 我會跑!!!
console.log(cat.behavior);  // ["吃飯", "睡覺"]

let dog = new runAnimal();
dog.behavior.push('咆哮');
console.log(dog.behavior); // ["吃飯", "睡覺", "咆哮"]

console.log(cat.behavior); // ["吃飯", "睡覺", "咆哮"] =>關注點
複製代碼

缺點:ui

  • 一個子類的實例更改子類原型從父類構造函數中繼承來的共有屬性就會直接影響到其餘子類。
  • 在建立子類型的實例時,不能向超類型的構造函數中傳遞參數。實際上,應該說是沒有辦法在不影響全部對象實例的狀況下,給超類型的構造函數傳遞參數。

構造函數繼承

基本思想至關簡單,即在子類型構造函數的內部調用超類型構造函數。別忘了,函數只不過是在特定環境中執行代碼的對象,所以經過使用apply()和call()方法也能夠在(未來)新建立的對象上執行構造函數。this

// 父類
function Animal(id){
    this.behavior = ['吃飯', '睡覺'];
	this.id = id;
}
Animal.prototype = {
    name: 'animal',
    age: '10',
    sayName: function(){
        console.log('個人編號是:'+this.id);
    }
};

// 聲明子類
function childAnimal(id){
	// 繼承父類
	Animal.call(this, id);
}

let cat = new childAnimal(100);
console.log(cat.id);      // 輸出 100
console.log(cat.behavior);  // ["吃飯", "睡覺"]

console.log(cat.name);  // undifined =>關注點
console.log(cat.sayName()); // error!!! =>關注點

let dog = new childAnimal(101);
dog.behavior.push('咆哮');
console.log(dog.id);      // 輸出 101
console.log(dog.behavior); // ["吃飯", "睡覺", "咆哮"]

console.log(cat.behavior); // ["吃飯", "睡覺"] =>關注點
複製代碼

優缺點

  • 借用構造函數有一個很大的優點,便可以在子類型構造函數中向超類型構造函數傳遞參數。
  • 在超類型的原型中定義的方法,對子類型而言也是不可見的,結果全部類型都只能使用構造函數模式。

組合繼承

指的是將原型鏈和借用構造函數的技術組合到一塊,從而發揮兩者之長的一種繼承模式。其背後的思路是使用原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數來實現對實例屬性的繼承。

// 父類
function Animal(id){
    this.behavior = ['吃飯', '睡覺'];
    this.id = id;
}
Animal.prototype = {
    name: 'animal',
    age: '10',
    sayName: function(){
        console.log('個人編號是:'+this.id);
    }
};

// 聲明子類
function childAnimal(id){
    // 構造函數繼承父類
    Animal.call(this, id);
}

// 子類的原型對象實例父類
childAnimal.prototype = new Animal();

let cat = new childAnimal(100);
console.log(cat.id);      // 輸出 100
console.log(cat.behavior);  // ["吃飯", "睡覺"]

console.log(cat.name);  // animal =>關注點,區別之處
console.log(cat.sayName()); // 個人編號是: 100 =>關注點,區別之處

let dog = new childAnimal(101);
dog.behavior.push('咆哮');
console.log(dog.id);      // 輸出 101
console.log(dog.behavior); // ["吃飯", "睡覺", "咆哮"]

console.log(cat.behavior); // ["吃飯", "睡覺"] =>關注點
複製代碼

優缺點

  • 組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優勢,成爲 JavaScript 中最經常使用的繼 承模式。並且,instanceof 和 isPrototypeOf()也可以用於識別基於組合繼承建立的對象。
  • 組合繼承最大的問題就是不管什麼狀況下,都會調用兩次超類型構造函數:一次是在建立子類型原型的時候,另外一次是 在子類型構造函數內部。

原型式繼承

原型式繼承想法是藉助原型能夠基於已有的對象建立新對象,同時還沒必要所以建立自定義類型。

// 道格拉斯·克羅克福德給出的函數
function object(o){ 
    function F(){} 
    F.prototype = o; 
    return new F(); 
}
複製代碼

從本質上講,object()對傳入其中的對象執行了一次淺複製。

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

let HTML5 = {
	name: 'HTML5 高級程序設計',
	author: ['Peter Lubbers', 'Ric Smith', 'Frank Salim']
};

let myNewBook = new book(HTML5);
console.log(myNewBook.name); // HTML5 高級程序設計
myNewBook.author.push('Brian Albers');
console.log(myNewBook.author); // ["Peter Lubbers", "Ric Smith", "Frank Salim", "Brian Albers"]

let otherBook = new book(HTML5);
otherBook.name = "VUE";
otherBook.author.push('尤');

console.log(otherBook.name); // VUE
console.log(otherBook.author); // ["Peter Lubbers", "Ric Smith", "Frank Salim", "Brian Albers", "尤"]

console.log(myNewBook.author); // ["Peter Lubbers", "Ric Smith", "Frank Salim", "Brian Albers", "尤"]
複製代碼

優缺點

  • 父類對象book中的值類型的屬性被賦值,引用類型的屬性被共同用。引用類型值的屬性始終都會共享相應的值,就像使用原型模式同樣。

寄生式繼承

寄生式繼承的思路與寄生構造函數和工廠模式相似,即建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後再像真地是它作了全部工做同樣返回對象。

function createAnother(original){ 
    var clone = object(original); //經過調用函數建立一個新對象
    clone.sayHi = function(){ //以某種方式來加強這個對象
        console.log("hi"); 
    }; 
    return clone; //返回這個對象
}

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

寄生組合式繼承

所謂寄生組合式繼承,即經過借用構造函數來繼承屬性,經過原型鏈的混成形式來繼承方法。其背後的基本思路是:沒必要爲了指定子類型的原型而調用超類型的構造函數,咱們所須要的無非就是超類型原型的一個副本而已。本質上,就是使用寄生式繼承來繼承超類型的原型,而後再將結果指定給子類型的原型。

// 定義父類
function SuperClass (name){
    this.name = name;
    this.colors = ["red","blue","green"];
}
// 定義父類原型方法
SubClass.prototype.getName = function () {
    console.log(this.name);
}
// 定義子類
function SubClass (name, time){
    SuperClass.call (this, name); // 構造函數式繼承
    this.time = time; // 子類新增屬性
}

function inheritPrototype(subType, superType){ 
    var prototype = object(superType.prototype);  // 建立對象
    prototype.constructor = subType;  // 加強對象
    subType.prototype = prototype;  // 指定對象
}

// 寄生式繼承父類原型
inheriPrototype(SubClass, SuperClass);
// 子類新增原型方法
SubClass.prototype.getTime = function (){
    console.log(this.time);
};
// 建立兩個測試方法
var instance1 = new SubClass("js book", 2018);
var instance2 = new SubClass("css book", 2019);

instance1.colors.push("black");
console.log(instance1.colors);  // ["red","blue","green","black"]
console.log(instance2.colors);  // ["red","blue","green"]
instance2.getName ();   // css book
instance2.getTime ();   // 2019
複製代碼

總結

本身慢慢看了一遍小紅書,本身手動敲了一下代碼,基本上可以理解原型,寫的很差,歡迎指正。原型、原型鏈、原型繼承我的感受就是混合在一塊兒的,提原型,就會提原型鏈、原型繼承。看10遍不如本身動手敲一敲代碼,記憶會更深入,中間出現的問題也會使你眼前一亮(好比你常用的this指向問題)。

參考資料

  • JavaScript高級程序設計(第3版)
相關文章
相關標籤/搜索