JS原型、原型鏈、構造函數、實例與繼承


https://cloud.tencent.com/developer/article/1408283

https://cloud.tencent.com/developer/article/1195938

https://cloud.tencent.com/developer/article/1359936

https://cloud.tencent.com/developer/article/1079079

https://cloud.tencent.com/developer/article/1408335

 

圖解原型和原型鏈

原型和原型鏈是 JS 中不可避免需要碰到的知識點?,本文使用圖片思維導圖的形式縷一縷原型、原型鏈、實例、構造函數等等概念之間的關系?

Constructor 構造函數

首先我們先寫一個構造函數 Person,構造函數一般為了區別普通函數要求首字母大寫:

1function Person(){}

prototype 原型

原型指的就是一個對象,實例“繼承”那個對象的屬性。在原型上定義的屬性,通過“繼承”,實例也擁有了這個屬性。“繼承”這個行為是在 new 操作符內部實現的。

先不說實例,原型與構造函數的關系就是,構造函數內部有一個名為 prototype 的屬性,通過這個屬性就能訪問到原型:

20190314132908.png

Person 就是構造函數,Person.prototype 就是原型

20190314132934.png

instance 實例

有個構造函數,我們就可以在原型上創建可以“繼承”的屬性,並通過 new 操作符創建實例

20190314141908.png

比方說 Person,我們要創建一個 person 實例,那么使用 new 操作符就可以實現,並通過 instanceof 來檢查他們之間的關系:

20190314132309.png

我們在原型上定義一個屬性,那么實例上也就可以“繼承”這個屬性:

20190314133215.png

proto 隱式原型

實例通過 __proto__ 訪問到原型,所以如果是實例,那么就可以通過這個屬性直接訪問到原型:

20190314141947.png

所以這兩者是等價的:

20190314142041.png

constructor 構造函數

既然構造函數通過 prototype 來訪問到原型,那么原型也應該能夠通過某種途徑訪問到構造函數,這就是 constructor:

20190314142246.png

因此兩者的關系應該是這樣:

20190314142755.png

注意這里的 constructor 是原型的一個屬性,Constructor 指的才是真正的構造函數。兩者名字不要弄混了?

實例、構造函數、原型之間的關系

這里我們可以看到如果實例想要訪問構造函數,那么應當是:

20190314143125.png

沒有從實例直接訪問到構造函數的屬性或方法:

20190314143254.png

實例與原型則是通過上文中提到的 __proto__ 去訪問到。

在讀取一個實例的屬性的過程中,如果屬性在該實例中沒有找到,那么就會循着 __proto__ 指定的原型上去尋找,如果還找不到,則嘗試尋找原型的原型?:

20190314143837.png

我們把注釋刪掉,給實例同名屬性,可以看到打印出來的屬性就指向這個:

20190314143944.png

原型鏈

原型同樣也可以通過 __proto__ 訪問到原型的原型,比方說這里有個構造函數 Person 然后“繼承”前者的有一個構造函數 People,然后 new People 得到實例 p

當訪問 p 中的一個非自有屬性的時候,就會通過 __proto__ 作為橋梁連接起來的一系列原型、原型的原型、原型的原型的原型直到 Object 構造函數為止。

這個搜索的過程形成的鏈狀關系就是原型鏈

20190314144733.png

如下圖:

20190314145239.png

看到 null 了么,原型鏈搜索搜到 null 為止,搜不到那訪問的這個屬性就是不存在的:

20190314145540.png

以上,這就是原型、原型鏈、構造函數、實例、null 之間的關系。

 

構造函數、原型對象和實例之間的關系

要弄懂extends繼承之前,先來復習一下構造函數、原型對象和實例之間的關系。

代碼表示:

function F(){} var f = new F(); // 構造器 F.prototype.constructor === F; // true F.__proto__ === Function.prototype; // true Function.prototype.__proto__ === Object.prototype; // true Object.prototype.__proto__ === null; // true // 實例 f.__proto__ === F.prototype; // true F.prototype.__proto__ === Object.prototype; // true Object.prototype.__proto__ === null; // true

筆者畫了一張圖表示:

 

JavaScript實現繼承

繼承的幾種思路:

其實理解繼承,主要是理解構造函數,實例屬性和原型屬性的關系。要想實現繼承,將不同的對象或者函數聯系起來,總共就以下幾種思路:

  1. 原型鏈:父類的實例當做子類的原型。如此子類的原型包含父類定義的實例屬性,享有父類原型定義的的屬性。
  2. 借用構造函數:子類直接使用父類的構造函數。如此子類的實例直接包含父類定義的實例屬性。
  3. 原型式:復制父類原型屬性給子類原型。如此,子類實例享有父類定義的原型屬性。
  4. 寄生式:思路與3一樣,只是利用工廠模式對復制的父類原型對象進行增強。

然后,1,2思路結合,實例屬性繼承用借用構造函數保證獨立性,方法繼承用原型鏈保證復用性,就是組合模式。 4,2思路結合,或者說3,4與1,2思路結合,實例屬性繼承用借用構造函數保證獨立性,方法繼承用原型復制增強的方式,就是寄生組合模式。

 

簡介

本文不准備深入細節,主要是對《JavaScript高級程序設計中》介紹的JS如何實現繼承做一個總結,畢竟好記性不如爛筆頭。文末會附帶一張神圖,搞清楚這張圖,原型鏈也就沒有什么問題了。

ES5實現繼承的幾種方式

1. 原型鏈

基本思想:

利用原型鏈讓一個引用類型繼承另一個引用類型的屬性和方法。

function SuperType () { this.property = true; } SuperType.prototype.getSuperValue = function () { return this.property; }; // 子類 SubType function SubType () { this.subProperty = false; } SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function () { return this.subProperty; }; // 實例 var instance = new SubType(); console.log(instance); console.log(instance.getSuperValue()); // true console.log(instance instanceof SubType); // true console.log(instance instanceof SuperType); // true console.log(instance instanceof Object); // true console.log(SubType.prototype.isPrototypeOf(instance)); // true console.log(SuperType.prototype.isPrototypeOf(instance)); // true console.log(Object.prototype.isPrototypeOf(instance)); // true 復制代碼

缺點:

1. 來自原型對象的引用屬性是所有實例共享的。

2. 創建子類實例時,無法向父類構造函數傳參。

舉例如下:

// 1. 來自原型對象的引用屬性是所有實例共享的

// 父類 function SuperType () { this.colors = ['red', 'blue', 'green']; } // 子類 function SubType () { } SubType.prototype = new SuperType(); // 實例 var instance1 = new SubType(); instance1.colors.push('black'); console.log(instance1.colors); // ['red', 'blue', 'green', 'black'] var instance2 = new SubType(); console.log(instance2.colors); // ['red', 'blue', 'green', 'black'] // 因為修改colors是修改的SubType.prototype.colors,所以所有的實例都會更新 復制代碼
// 2. 創建子類實例時,無法向父類構造函數傳參

// 調用父類是在 SubType.prototype = new SuperType() // 新建子類實例調用 new SubType() // 所以無法再new SubType() 的時候給父類 SuperType() 傳參 復制代碼

2. 借用構造函數

基本思想:

在子類構造函數的內部通過call()以及apply()調用父類構造函數。

// 父類 SuperType
function SuperType (name) { this.name = name; this.colors = ['red', 'blue', 'green']; this.getName = function () { return this.name; } } // 子類 function SubType (name) { // 繼承了SuperType,同時還傳遞了參數 SuperType.call(this, name); // 實例屬性 this.age = 20; } // 實例 var instance1 = new SubType('Tom'); instance1.colors.push('black'); console.log(instance1.name); // "Tom" console.log(instance1.getName()); // "Tom" console.log(instance1.age); // 20 console.log(instance1.colors); // ['red', 'blue', 'green', 'black'] var instance2 = new SubType('Peter'); console.log(instance2.name); // "Peter" console.log(instance2.getName()); // "Peter" console.log(instance2.age); // 20 console.log(instance2.colors); // ['red', 'blue', 'green'] 復制代碼

可以看到,借用構造函數實現繼承,解決了原型鏈繼承的兩個問題,既可以在新建子類實例的時候給父類構造函數傳遞參數,也不會造成子類實例共享父類引用變量。

但是你注意到了嗎,這里我們把父類方法也寫在了SuperType()構造函數里面,可以像前面一樣寫在SuperType.prototype上嗎?

答案是不可以,必須寫在SuperType()構造函數里面。因為這里是通過調用SuperType.call(this)來實現繼承的,並沒有通過new生成一個父類實例,所以如果寫在prototype上,子類是無法拿到的。

缺點:

1. 如果方法都在構造函數中定義,那么就無法復用函數。每次構建實例時都會在實例中保留方法函數,造成了內存的浪費,同時也無法實現同步更新,因為每個實例都是單獨的方法函數。如果方法寫在prototype上,就只會有一份,更新時候會做到同步更新。

3. 組合繼承

基本思想:

將原型鏈和借用構造函數的技術組合到一塊,從而發揮二者之長的一種繼承模式。

使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。

// 父類
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); // 子類實例屬性 this.age = age; } SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function () { console.log(this.age); }; // 實例 var instance1 = new SubType('Tom', 20); instance1.colors.push('black'); console.log(instance1.colors); // ['red', 'blue', 'green', 'black'] instance1.sayName(); // "Tom" instance1.sayAge(); // 20 var instance2 = new SubType('Peter', 30); console.log(instance2.colors); // ['red', 'blue', 'green'] instance2.sayName(); // "Peter" instance2.sayAge(); // 30 復制代碼

缺點:

1. 調用了兩次父類構造函數,一次通過SuperType.call(this)調用,一次通過new SuperType()調用。

4. 原型式繼承

基本思想:

不使用嚴格意義上的構造函數,借助原型可以基於已有的對象創建新的對象,同時還不必因此創建自定義類型。

// 在object函數內部,先創建了一個臨時的構造函數,然后將傳入的對象作為這個構造函數的原型,最后返回這個臨時類型的一個新實例。
// 從本質上講,object()對傳入其中的對象執行了一次淺復制。 function object (o) { function F() {} F.prototype = o; return new F(); } var person = { name: 'Tom', friends: ['Shelby', 'Court', 'Van'] }; var anotherPerson = object(person); anotherPerson.name = 'Greg'; anotherPerson.friends.push('Rob'); var yetAnotherPerson = object(person); yetAnotherPerson.name = 'Linda'; yetAnotherPerson.friends.push('Barbie'); console.log(anotherPerson.friends); // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie'] console.log(yetAnotherPerson.friends); // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie'] console.log(person.friends); // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie'] 復制代碼

ECMAScript5中新增了一個方法Object.create(prototype, descripter)接收兩個參數:

  • prototype(必選),用作新對象的原型對象
  • descripter(可選),為新對象定義額外屬性的對象

在傳入一個參數的情況下,Object.create()與前面寫的object()方法的行為相同。

var person = { name: 'Tom', friends: ['Shelby', 'Court', 'Van'] }; var anotherPerson = Object.create(person); anotherPerson.name = 'Greg'; anotherPerson.friends.push('Rob'); var yetAnotherPerson = Object.create(person, { name: { value: 'Linda', enumerable: true } }); yetAnotherPerson.friends.push('Barbie'); console.log(anotherPerson.friends); // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie'] console.log(yetAnotherPerson.friends); // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie'] console.log(person.friends); // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie'] 復制代碼

缺點:

1. 和原型鏈繼承一樣,所有子類實例共享父類的引用類型。

5. 寄生式繼承

基本原理:

寄生式繼承是與原型式繼承緊密相關的一種思路,創建一個僅用於封裝繼承過程的函數,該函數內部以某種形式來做增強對象,最后返回對象。

function object (o) { function F() {} F.prototype = o; return new F(); } function createAnother (o) { var clone = object(o); clone.sayHi = function () { console.log('Hi'); } return clone; } var person = { name: 'Tom', friends: ['Shelby', 'Court', 'Van'] }; var anotherPerson = createAnother(person); anotherPerson.sayHi(); // "Hi" anotherPerson.friends.push('Rob'); console.log(anotherPerson.friends); // ['Shelby', 'Court', 'Van', 'Rob'] var yerAnotherPerson = createAnother(person); console.log(yerAnotherPerson.friends); // ['Shelby', 'Court', 'Van', 'Rob'] 復制代碼

缺點:

1. 和原型鏈式繼承一樣,所有子類實例共享父類引用類型。

2. 和借用構造函數繼承一樣,每次創建對象都會創建一次方法。

6. 寄生組合式繼承

基本思想:

將寄生式繼承和組合繼承相結合,解決了組合式繼承中會調用兩次父類構造函數的缺點。

組合繼承是JavaScript最常用的繼承模式,它最大的問題就是無論在什么情況下,都會調用兩次父類構造函數:一次是在創建子類原型的時候,另一次是在子類構造函數內部。

// 組合繼承
function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function () { alert(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 () { alert(this.age); }; 復制代碼

組合繼承在第一次調用SuperType構造函數時,SubType.prototype會得到兩個屬性:name和colors;它們都是 SuperType 的實例屬性,只不過現在位於 SubType的原型中。當調用SubType構造函數時,又會調用一次SuperType構造函數,這一次又在新對象上創建了實例屬性name和colors。於是,這兩個屬性就屏蔽了原型中的兩個同名屬性。

所謂寄生組合式繼承,即通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。

其背后的基本思路是:不必為了指定子類型的原型而調用父類的構造函數,我們需要的無非就是父類原型的一個副本而已。本質上,就是使用寄生式繼承來繼承父類的prototype,然后再將結果指定給子類的prototype。

寄生組合式繼承的基本模型如下:

function inheritPrototype(SubType, SuperType) { var prototype = object(SuperType.prototype); // 創建對象 prototype.constructor = SubType; // 增強對象 SubType.prototype = prototype; // 指定對象 } 復制代碼

實現一個完整的寄生組合式繼承:

function object(o) { function F() { } F.prototype = o; return new F(); } function inheritPrototype(SubType, SuperType) { var prototype = object(SuperType.prototype); // 創建對象 prototype.constructor = SubType; // 增強對象 SubType.prototype = prototype; // 指定對象 } // 父類 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); // 子類實例屬性 this.age = age; } // 繼承父類方法 inheritPrototype(SubType, SuperType); // 子類方法 SubType.prototype.sayAge = function () { console.log(this.age); }; // 實例 var instance1 = new SubType('Tom', 20); instance1.colors.push('black'); instance1.sayAge(); // 20 instance1.sayName(); // "Tom" console.log(instance1.colors); // ["red", "blue", "green", "black"] var instance2 = new SubType('Peter', 30); instance2.sayAge(); // 30 instance2.sayName(); // "Peter" console.log(instance2.colors); // ["red", "blue", "green"] 復制代碼

寄生組合式繼承的高效率體現在它只調用了一次SuperType構造函數,並且因此避免了再SubType.prototype上面創建不必要的、多余的屬性。與此同時,原型鏈還能保持不變。因此,還能夠正常使用instanceof和isPrototypeOf()。

開發人員普遍認為寄生組合式繼承是引用類型最理想的繼承方式。

7. 對象冒充

function Person(name,age){

this.name = name;

this.age = age;

this.show = function(){

console.log(this.name+", "+this.age);

}

}

function Student(name,age){

this.student = Person; //將Person類的構造函數賦值給this.student

this.student(name,age); //js中實際上是通過對象冒充來實現繼承的

delete this.student; //移除對Person的引用

}

var s = new Student("小明",17);

s.show();

var p = new Person("小花",18);

p.show();

// 小明, 17

// 小花, 18

 


ES6實現繼承

// 父類
class SuperType { constructor(name) { this.name = name; this.colors = ["red", "blue", "green"]; } sayName() { console.log(this.name); }; } // 子類 class SubType extends SuperType { constructor(name, age) { // 繼承父類實例屬性和prototype上的方法 super(name); // 子類實例屬性 this.age = age; } // 子類方法 sayAge() { console.log(this.age); } } // 實例 var instance1 = new SubType('Tom', 20); instance1.colors.push('black'); instance1.sayAge(); // 20 instance1.sayName(); // "Tom" console.log(instance1.colors); // ["red", "blue", "green", "black"] var instance2 = new SubType('Peter', 30); instance2.sayAge(); // 30 instance2.sayName(); // "Peter" console.log(instance2.colors); // ["red", "blue", "green"] 復制代碼

用ES6的語法來實現繼承非常的簡單,下面是把這段代碼放到Babel里轉碼的結果圖片:

可以看到,底層其實也是用寄生組合式繼承來實現的。

總結

ES5實現繼承有6種方式:

  1. 原型鏈繼承
  2. 借用構造函數繼承
  3. 組合繼承
  4. 原型式繼承
  5. 寄生式繼承
  6. 寄生組合式繼承

寄生組合式繼承是大家公認的最好的實現引用類型繼承的方法。

ES6新增class和extends語法,用來定義類和實現繼承,底層也是采用了寄生組合式繼承。

附圖:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM