幫助物件導向開發者理解JavaScript物件機制

物件未定義發表於2018-08-19

本文是以一個熟悉OO語言的開發者視角,來解釋JavaScript中的物件。

對於不瞭解JavaScript 語言,尤其是習慣了OO語言的開發者來說,由於語法上些許的相似會讓人產生心理預期,JavaScript中的原型繼承機制和class語法糖是讓人迷惑的。

如果你已經對prototype機制已有了解,但是由於兩者物件機制的巨大(本質)差異,對它和建構函式,例項物件的關係仍有疑惑,本文或許可以解答你的問題。

我們看下面的程式碼,可以看出和OO語言相比,語法上也有很大分別:

// 定義一個類
class Foo {
  constructor() {
    this.a = 'a';
  }
}

//例項化物件
const foo = new Foo();

//定義原型的屬性
Foo.prototype.b = 'b';

//例項可以訪問屬性
foo.b // "b"

//修改原型的屬性
Foo.prototype.b= 'B';


//例項屬性值沒有被修改
foo.b // "b"
複製程式碼

類已經定義了怎麼還能修改呢?prototype又是什麼?

不存在物件導向

對於熟悉了物件導向的開發者而言JS中種種非預期操作的存在,都是因為JavaScript中根本沒有物件導向的概念,只有物件,沒有類。

即使ES6新添了class語法,不意味著JS引入了物件導向,只是原型繼承的語法糖。

原型是什麼

什麼是原型?如果說類是面嚮物件語言中物件的模版,原型就是 JS中創造物件的模版。

在面向類的語言中,例項化類,就像用模具製作東西一樣。例項化一個類就意味著“把類的形態複製到物理物件中”,對於每一個新例項來說都會重複這個過程。

但是在JavaScript中,並沒有類似的複製機制。你不能建立一個類的多個例項,只能建立多個物件,它們[[Prototype]]關聯的是同一個物件。

//建構函式
function Foo(){
}
//在函式的原型上新增屬性
Foo.prototype.prototypeAttribute0 = {status: 'initial'};

const foo0 = new Foo();
const foo1 = new Foo();
foo0.prototypeAttribute0 === foo1.prototypeAttribute0 //true
複製程式碼

物件、建構函式和原型的關係

當我們建立一個新物件的時候,發生了什麼,物件、建構函式和原型到底什麼。

先簡單地概括:

原型用於定義共享的屬性和方法。

建構函式用於定義例項屬性和方法,僅負責創造物件,與物件不存在直接的引用關係。

我們先不用class語法糖,這樣便於讀者理解和暴露出他們之間真正的關係。

// 先建立一個建構函式 定義原型的屬性和方法
function Foo() {
    this.attribute0 = 'attribute0';
}
複製程式碼

當建立了一個函式,就會為該函式建立一個prototype屬性,它指向函式原型。

所有的原型物件都會自動獲得一個constructor屬性,這個屬性的值是指向原型所在的建構函式的指標。

幫助物件導向開發者理解JavaScript物件機制

現在定義原型的屬性和方法


Foo.prototype.prototypeMethod0 = function() {
    console.log('this is prototypeMethod0');
}

Foo.prototype.prototypeAttribute0 = 'prototypeAttribute0';
複製程式碼

好了,現在,新建一個物件,

const foo = new Foo();

foo.attribute0 // "attribute0"
foo.prototypeAttribute0 //"prototypeAttribute0"
foo.prototypeMethod0() // this is prototypeMethod0
複製程式碼

它擁有自己的例項屬性attribute0,並且可以訪問在原型上定義的屬性和方法,他們之間的引用關係如圖所示。

幫助物件導向開發者理解JavaScript物件機制

當呼叫建構函式建立例項後,該例項的內部會包含一個指標(內部物件),指向建構函式的原型物件。

當讀取例項物件的屬性時,會在例項中先搜尋,沒有找到,就會去原型鏈中搜尋,且總是會選擇原型鏈中最底層的屬性進行訪問。

物件的原型可以通過__proto__在chrome等瀏覽器上訪問。

__proto__是物件的原型指標,prototype是建構函式所對應的原型指標。

語法糖做了什麼

ES6推出了class語法,為定義建構函式和原型增加了便利性和可讀性。

class Foo {
    constructor(){
        this.attribute0 = 'attribute0';
    }

    prototypeMethod0(){
        console.log('this is prototypeMethod0')
    }
}

/* 相當於下面的宣告*/
function Foo() {
    this.attribute0 = 'attribute0';
}

Foo.prototype.prototypeMethod0 = function() {
    console.log('this is prototypeMethod0')
}
複製程式碼

class中的constractor相當於建構函式,而class中的方法相當於原型上的方法。、

值得注意的特性

屬性遮蔽 —— 避免例項物件無意修改原型

看這段程式碼,思考輸出的結果。

class Foo {
    prototypeMethod0(){
        console.log('this is prototypeMethod0')
    }
}

const foo0 = new Foo();
const foo1 = new Foo();

foo0.prototypeMethod0 === foo0.__proto__.prototypeMethod0 // true

foo0.prototypeMethod0 = () => console.log('foo0 method');
foo0.prototypeMethod0(); //??
foo1.prototypeMethod0(); //??
foo0.prototypeMethod0 === foo0.__proto__.prototypeMethod0 // ??
複製程式碼

輸出的結果是

foo0.prototypeMethod0(); // foo0 method
foo1.prototypeMethod0(); // this is prototypeMethod0
foo0.prototypeMethod0 === foo0.__proto__.prototypeMethod0 // false
複製程式碼

我們知道物件(即便是原型物件),都是執行時的。

建立之初,foo本身沒有prototypeMethod0這個屬性,訪問foo0.prototypeMethod0將會讀取foo0.__proto__.prototypeMethod0

直接修改foo0.prototypeMethod0沒有改變__proto__上的方法原因是存在屬性遮蔽

現在的情況是:想要修改foo0.prototypeMethod0prototypeMethod0foo中不存在而在上層(即foo.__proto__中存在),並且這不是一個特殊屬性(如只讀)。

那麼會在foo中新增一個新的屬性。

這便是為什麼直接修改卻沒有影響__proto__的原因。

小結

再溫習一遍這些定義:

原型用於定義共享的屬性和方法。

建構函式用於定義例項屬性和方法,僅負責創造物件,與物件不存在直接的引用關係。

__proto__是物件的原型指標,prototype是建構函式的原型指標。

在解釋原型作用的文章或書籍中,我們會聽到繼承這樣的術語,其實更準確地,委託對於JavaScript中的物件模型來說,是一個更合適的術語。

委託行為意味著某些物件在找不到屬性或者方法引用時會把這個請求委託給另一個物件。物件之間的關係不是複製而是委託。


參考

《JavaScript高階程式設計》

《你不知道的JavaScript》

本文僅供解惑,要在腦袋裡形成系統的概念,還是要看書呀。

有疑問歡迎大家一起討論。

相關文章