動態

詳情 返回 返回

一文徹底搞懂原型鏈 - 動態 詳情

前言
原型和繼承是js中非常重要的兩大概念。深入瞭解原型,也是學好繼承的前提。

先來看一下構造函數、實例、原型對象之間的關係

image.png

「實例與原型對象之間有直接的聯繫,但實例與構造函數之間沒有。」

兩個概念
js分為「函數對象」和「普通對象」,每個對象都有__proto__屬性,但是隻有函數對象且「非箭頭函數」才有prototype屬性。

屬性__proto__是一個對象【實例通過__proto__隱式原型指向其原型對象】,它有兩個屬性,constructor和__proto__;
原型對象有一個默認的constructor屬性,用於記錄實例是由哪個構造函數創建;
image.png

原型
理解原型
創建一個函數(非箭頭函數),就會按照特定的規則為這個函數創建一個 prototype 屬性(指向原型對象)。默認情況下,所有原型對象自動獲得一個名為 constructor 的屬性,指回與之關聯的構造函數。在自定義構造函數時,原型對象默認只會獲得 constructor 屬性,其他的所有方法都繼承自Object。每次調用構造函數創建一個新實例,這個實例的內部[[Prototype]]指針就會被賦值為構造函數的原型對象。腳本中沒有訪問這個[[Prototype]]特性的標準方式,但 Firefox、Safari 和 Chrome會在每個對象上暴露__proto__屬性,通過這個屬性可以訪問對象的原型。

function Person() {}
// 説明:name,age,job這些本不應該放在原型上,只是為了説明屬性查找機制
Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function() { 
 console.log(this.name); 
};
let person1 = new Person()
let person2 = new Person()

// 聲明之後,構造函數就有了一個與之關聯的原型對象
console.log(Object.prototype.toString.call(Person.prototype)) // [object Object]
console.log(Person.prototype) // {constructor: ƒ}

// 構造函數有一個 prototype 屬性引用其原型對象,而這個原型對象也有一個constructor 屬性,引用這個構造函數
// 換句話説,兩者循環引用
console.log(Person.prototype.constructor === Person); // true

// 構造函數、原型對象和實例是 3 個完全不同的對象
console.log(person1 !== Person); // true 
console.log(person1 !== Person.prototype); // true 
console.log(Person.prototype !== Person); // true

// 實例通過__proto__鏈接到原型對象,它實際上指向隱藏特性[[Prototype]] 
// 構造函數通過 prototype 屬性鏈接到原型對象,實例與構造函數沒有直接聯繫,與原型對象有直接聯繫,後面將會畫圖再次説明這個問題
console.log(person1.__proto__ === Person.prototype); // true 
conosle.log(person1.__proto__.constructor === Person); // true

// 同一個構造函數創建的兩個實例,共享同一個原型對象 
console.log(person1.__proto__ === person2.__proto__); // true

// Object.getPrototypeOf(),返回參數的內部特性[[Prototype]]的值 ,用於獲取原型對象,兼容性更好
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true
複製代碼

如下圖:

image.png

Person.prototype 指向原型對象,而 Person.prototype.contructor 指回 Person 構造函數。原型對象包含 constructor 屬性和其他後來添加的屬性。Person 的兩個實例 person1 和 person2 都只有一個內部屬性指回 Person.prototype,而且兩者都與構造函數沒有直接聯繫。

原型層級
在通過對象訪問屬性時,會按照這個屬性的名稱開始搜索。搜索開始於對象實例本身。如果在這個實例上發現了給定的名稱,則返回該名稱對應的值。如果沒有找到這個屬性,則搜索會沿着指針進入原型對象,然後在原型對象上找到屬性後,再返回對應的值。因此,在調用 person1.sayName()時,會發生兩步搜索。首先,JavaScript 引擎會問:“person1 實例有 sayName 屬性嗎?”答案是沒有。然後,繼續搜索並問:“person1 的原型有 sayName 屬性嗎?”答案是有。於是就返回了保存在原型上的這個函數。在調用 person2.sayName()時,會發生同樣的搜索過程,而且也會返回相同的結果。這就是原型用於在多個對象實例間共享屬性和方法的原理。

原型鏈
重温一下構造函數、原型和實例的關係:每個構造函數都有一個prototype指向原型對象,原型對象有一個constructor屬性指回構造函數,而實例有一個內部指針指向原型。如果原型是另一個類型的實例呢?那就意味着這個原型本身有一個內部指針指向另一個原型,相應地另一個原型也有一個指針指向另一個構造函數。這樣就在實例和原型之間構造了一條原型鏈。這就是原型鏈的基本構想。

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
複製代碼

SuperType 和 SubType這兩個類型分別定義了一個屬性和一個方法。這兩個類型的主要區別是 SubType 通過創建 SuperType 的實例並將其賦值給自己的原型 SubTtype. prototype 實現了對 SuperType 的繼承。這個賦值重寫了 SubType 最初的原型,將其替換為SuperType 的實例。這意味着 SuperType 實例可以訪問的所有屬性和方法也會存在於 SubType. prototype。這樣實現繼承之後,代碼緊接着又給 SubType.prototype,也就是這個 SuperType 的實例添加了一個新方法。最後又創建了 SubType 的實例並調用了它繼承的 getSuperValue()方法。

image.png

模擬new
使用new時,到底發生了什麼?

創建一個空對象,作為將要返回的對象實例
將這個空對象的原型,指向了構造函數的prototype屬性
將這個空對象賦值給函數內部的this關鍵字
開始執行構造函數內部的代碼
如果構造函數返回一個對象,那麼就直接返回該對象,否則返回創建的對象
也就是説,構造函數內部,this指的是一個新生成的空對象,所有針對this的操作,都會發生在這個空對象上。構造函數之所以叫“構造函數”,就是説這個函數的目的,就是操作一個空對象(即this對象),將其“構造”為需要的樣子。

function simulateNew() {
  let newObject = null,result = null,
    constructor = Array.prototype.shift.call(arguments)
  // 參數判斷
  if (typeof constructor !== 'function') {
    console.error('type error')
    return
  }
  // 新建一個空對象,對象的原型為構造函數的 prototype 對象
  newObject = Object.create(constructor.prototype)
  // 將 this 指向新建對象,並執行函數
  result = constructor.apply(newObject, arguments)
  // 判斷返回對象
  const flag =
    result && (typeof result === 'object' || typeof result === 'function')
  // 判斷返回結果
  return flag ? result : newObject
}

/**  測試如下  */
function Person(name) {
  this.name = name
}

const p1 = new Person("p1")
const p2 = simulateNew(Person, 'p2')

console.log("p1",p1, p1 instanceof Person);
console.log('p2', p2, p2 instanceof Person)
複製代碼

模擬instanceof
instanceof 主要的實現原理就是 「只要右邊變量的」 prototype 「在左邊變量的原型鏈上即可」。因此,instanceof 在查找的過程中會遍歷左邊變量的原型鏈,直到找到右邊變量的 prototype,如果查找失敗,則會返回 false,告訴我們左邊變量並非是右邊變量的實例。

function instanceOf(leftVaule, rightVaule) {
  let rightProto = rightVaule.prototype; // 取右表達式的 prototype 值
  leftVaule = leftVaule.__proto__; // 取左表達式的__proto__值
  while (true) {
    if (leftVaule === null) {
      return false;
    }
    if (leftVaule === rightProto) {
      return true;
    }
    leftVaule = leftVaule.__proto__
  }
}
複製代碼

總結
訪問對象的一個屬性,先在自身查找,如果沒有,會訪問對象的__proto__,沿着原型鏈查找,一直找到Object.prototype.__proto__。

每個函數都有prototype屬性,會指向函數的原型對象。

所有函數的原型對象的__proto__,會指向Object.prototype。

原型鏈的盡頭是Object.prototype.__proto__,為null。

最後
如果你覺得此文對你有一丁點幫助,點個贊。或者可以加入我的開發交流羣:1025263163相互學習,我們會有專業的技術答疑解惑

如果你覺得這篇文章對你有點用的話,麻煩請給我們的開源項目點點star:http://github.crmeb.net/u/defu不勝感激 !

PHP學習手冊:https://doc.crmeb.com
技術交流論壇:https://q.crmeb.com

Add a new 評論

Some HTML is okay.