JavaScript 中的原型原來是這樣的

墨夜_發表於2019-03-09

什麼是原型

原型其實就是一個特殊的物件,在宣告函式的時候自動建立的。

比如,我們現在宣告一個建構函式 A ,除了會申請儲存函式的記憶體空間,還會額外申請一個記憶體空間,用於儲存建構函式 A 的原型物件。所有函式中(Function.prototype.bind 除外)預設都有一個 prototype 的屬性,它儲存了函式的原型物件的地址(引用)(也就是它指向了原型物件)。 而在原型物件中預設有一個 constructor 屬性儲存了建構函式的地址(引用)(也就是 constructor 指向了建構函式)。如果不理解上面所說的,那我們看下面的圖:

原型

瀏覽器控制檯中:

原型

_ _proto_ _prototype

剛開始接觸原型的時候這兩個東西很容易就搞混了。

先記住以下兩點,就很容易就區分了:

  • prototype 是函式中才有的屬性
  • __proto__ 是所有物件都有的屬性

_ _proto_ _ 與 prototye

我們已經知道了函式中的 prototype 屬性指向的是它的原型物件,那麼物件中的 __proto__ 代表什麼?

一般情況下,物件中的 __proto__ 屬性是指向它的建構函式的原型物件的,即和建構函式中的 prototype 屬性所指向的物件是同一個物件。

用一段簡單的程式碼:

function A() {}
var a = new A()
複製程式碼

JavaScript 中的原型原來是這樣的

上圖看著不夠簡便,我們簡化一下:

JavaScript 中的原型原來是這樣的

還有一點,__proto__ 不是一個規範屬性,ie(除了 ie10) 不支援。對應的標準屬性是 [[Prototype]] ,但是這個屬性我們沒法直接訪問到。開發者儘量不要用這種方式去訪問,因為操作不慎會改變這個物件的繼承原型鏈。

在使用 Object.create(引數) 方式建立物件時,物件的 __proto__ 屬性指向的是傳入的引數。

原型鏈

由於 __proto__ 是所有物件都具有的屬性,而 __proto__ 本身指向的原型(函式.prototype)也是一個物件,它也有 __proto__ 屬性。所以這樣會形成由 __proto__物件原型連起來的鏈條。這就是原型鏈。原型鏈的頂端是 Object.prototype(Object 是所有物件的祖宗) ,Object.prototype.__proto__的值為 null

還是看之前的程式碼:

function A() {}
var a = new A()
複製程式碼

它的原型鏈如下:

原型鏈

建構函式 A 其實也是一個物件。所有函式都是由 Function 函式構造的。(宣告函式 function A() {} 等價於 var A = new Function()) 。所以所有函式的 __proto__ 指向的都是 Function.prototype 。更新上圖:

原型鏈

Function 也是一個函式,它的 __proto__ 指向的也是 Functon.prototypeFuntion.__proto__ === Function.prototype。繼更新上圖:

原型鏈

Object 同樣是一個函式,所以 Object.__proto__ === Function.prototype

到了這裡,我們應該可以看懂下面這張圖了:

原型鏈

原型的作用

當 JS 引擎查詢物件屬性時,先查詢物件本身是否存在該屬性,如果不存在,會在物件的 __proto__ 裡找,還找不到就會沿著原型鏈一直找到原型鏈頂端(Object.prototype) 直到找到屬性為止,最後在原型鏈頂端都沒找到就返回 undefined

由於上面的機制,原型的作用就很明顯了——共享屬性,節省記憶體空間。

function Animal() {
    this.name = '動物'
    this.eat = function() {
        console.log('在吃···')
    }
}
var a1 = new Animal()
var a2 = new Animal()

console.log(a1.eat === a2.eat)  // false
// 每個物件的 eat 方法不是同一個,但方法類容一樣,浪費記憶體
複製程式碼

使用原型解決:

function Animal(name) {
    this.name = '動物'
}
Animal.prototype.eat = function() {
    console.log('吃')
}

var a1 = new Animal()
var a2 = new Animal()

console.log(a1.eat === a2.eat)  //true
// a1.eat 和 a2.eat 都同是一個方法(Animal.prototype.eat)
複製程式碼

原型非常適合封裝共享的方法。但是上面的程式碼把建構函式和原型分開寫了。封裝不到位。使用動態型別模式解決。

function Animal() {
    this.name = '動物'
    
    /*
      判斷 this.eat 是不是 函式型別,
      如果不是,則表示是第一次建立物件或者呼叫 Animal 函式,
      會將 eat 新增到原型中去。
      如果是,則表示原型中存在了 eat 方法,不需要再新增。
    */
    if(typeof this.eat !== 'function') {
        Animal.prototype.eat = function() {
            console.log('吃')
        }
    }
}

var a = new Animal()
a.eat()
複製程式碼

原型基於之前的共享屬性和方法,是實現 JS 中繼承的基礎。

與原型有關的方法

hasOwnProperty()

通過之前的學習,我們知道了去訪問一個物件的屬性時,會在原型鏈上查詢。所以我們並不知道這個屬性來自哪裡。

hasOwnProperty() 方法返回一個布林值,可以判斷一個屬性是否來自物件本身。

function Animal() {}
Animal.prototype.name = '動物'
var a = new Animal()
a.age = 3

console.log(a.hasOwnProperty('name'))  // false
console.log(a.hasOwnProperty('age')  // true
複製程式碼

in 操作符

in 操作符用返回一個布林值,用來判斷一個屬效能否在物件上找到。在物件的原型鏈上找到也返回 true

function Animal() {}
Animal.prototype.name = '動物'
var a = new Animal()
a.age = 3

console.log('name' in a)  // true
console.log('age' in  a)  // true
console.log('sex' in  a)  // false

複製程式碼

總結

  • 原型就是一個物件,宣告函式就會建立原型物件
  • prototype 只存在於函式中
  • 所有物件都有一個 __proto__ 屬性,它指向物件的建構函式的原型
  • 原型也是物件,也有 __proto__ 屬性,__proto__ 將物件和原型連線起來,形成原型鏈
  • Object.prototype 是原型鏈的頂端
  • 訪問物件的屬性會沿著物件的原型鏈找下去
  • 原型可以共享屬性和方法,是繼承的基礎

閱讀原文

參考資料:

juejin.im/post/583585…

blog.csdn.net/u012468376/…

juejin.im/book/5bdc71…

相關文章