幫助面向對象開發者理解關于JavaScript對象機制
本文是以一個熟悉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
屬性,這個屬性的值是指向原型所在的構造函數的指針。
現在定義原型的屬性和方法
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
,并且可以訪問在原型上定義的屬性和方法,他們之間的引用關系如圖所示。
當調用構造函數創建實例后,該實例的內部會包含一個指針(內部對象),指向構造函數的原型對象。
當讀取實例對象的屬性時,會在實例中先搜尋,沒有找到,就會去原型鏈中搜索,且總是會選擇原型鏈中最底層的屬性進行訪問。<!--原型對象自己也可以有原型對象,這樣就構成了原型鏈。關于原型鏈這里不作過多介紹-->
對象的原型可以通過__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 // truefoo0.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.prototypeMethod0
,prototypeMethod0
在foo
中不存在而在上層(即foo.__proto__
中存在),并且這不是一個特殊屬性(如只讀)。
那么會在foo
中添加一個新的屬性。
這便是為什么直接修改卻沒有影響__proto__
的原因。
<!--更多屬性屏蔽的場景也不做贅述-->
小結
再溫習一遍這些定義:
原型用于定義共享的屬性和方法。
構造函數用于定義實例屬性和方法,僅負責創造對象,與對象不存在直接的引用關系。
__proto__
是對象的原型指針,prototype
是構造函數的原型指針。
在解釋原型作用的文章或書籍中,我們會聽到繼承
這樣的術語,其實更準確地,委托
對于JavaScript中的對象模型來說,是一個更合適的術語。
委托行為意味著某些對象在找不到屬性或者方法引用時會把這個請求委托給另一個對象。對象之間的關系不是復制而是委托。
參考
《JavaScript高級程序設計》
《你不知道的JavaScript》
本文僅供解惑,要在腦袋里形成系統的概念,還是要看書呀。
有疑問歡迎大家一起討論。