前言
這次的 why what or how
主題:JavaScript
物件 &
原型。
此類文章在百度上一搜一大把,其實不用再寫了,但是本著把這個問題解釋的清清楚楚明明白白,還是開始寫了。
原因如下:
- 面試寶典類文章,弄張圖片一糊弄,讓人覺得自己理解了。
- 解釋類文章,告訴你一堆語法,對語法一頓解釋,告訴你就是這樣的,還是沒說清楚。
- 很少有文章單獨解釋這個點!但這個點是基礎!真的很重要!
所以本篇文章想說一說物件 & 原型
,但為了確保能順利理解,請先看完 JS 變數儲存?棧 & 堆?NONONO!
,因為該篇文章從變數儲存的角度來解釋 JavaScript 物件 & 原型
,請確保看完,在看這篇文章。
什麼是物件?
既然要說清楚,先問一個最最基本的問題:什麼是物件?
物件一句話就能解釋清楚:
物件是一系列屬性 & 資料的集合。
在 JavaScript
中建立一個物件的方式有很多,常見的如下:
// 字面量直接建立
let obj1 = {foo: 'bar'};
// 通過例項化 Object 類
let obj2 = new Object({foo: 'bar'});
class A {
constructor(foo){
this.foo = foo;
}
}
// 通過自定義類建立
let obj3 = new A('bar');
複製程式碼
以上程式碼最終都產生了 {foo: 'bar'}
這個物件。物件下有一個叫 foo
的屬性,它的值為 bar
。那麼它在堆中是如何儲存的呢?
看過了 JS 變數儲存?棧 & 堆?NONONO!
相信大家對於上圖應該不陌生。
物件的取值
我們繼續看一個較為複雜的物件(物件下某個屬性也是一個物件):
let complexObj = {
num: 1,
str: 'string',
obj: {
foo: 'bar',
}
}
複製程式碼
在記憶體中的模型如下:
我們模擬一下 complexObj.obj.foo
這個取值過程。
取值過程:碰到儲存的值是地址值時,就到相應的地址值繼續進行操作。
物件,總的來說,知識點有兩點:
- 建立物件:在記憶體堆中開闢一塊用於儲存一系列
屬性 & 資料
的空間。 - 物件取值:根據儲存的資料取值,如果是地址值,則到相應記憶體堆中繼續進行操作。
物件到這裡就差不多了,那原型又是什麼呢?
原型 & 原型鏈
原型是物件下的一個屬性,每個物件都有,同時原型也是一個物件。
文字的描述總是不直觀的,我們通過程式碼來看:
let proto = {
foo: 'bar'
}
let obj = {
// ...
__proto__: proto
}
複製程式碼
設定物件 __proto__
屬性的過程,就是給物件設定了原型,同時該屬性指向一個物件。
注: 當然 ES6
已經不建議這麼做了,有專門的方法(setPrototypeOf
)設定原型,為了解釋方便,這裡用 ES5
的程式碼作為示例。
有人可能會有疑問:既然是賦值操作,那應該也可以給物件的原型設定為非物件,為什麼原型是物件呢?關於這個問題的答案,可以用以下程式碼測試:
let a = {};
a.__proto__ = 1;
console.log(a.__proto__);
複製程式碼
複製到瀏覽器即可看到效果,將物件的原型設定為非物件這個操作,是被禁止的,也就是說你設定了也沒用。
原型的定義也很簡單,那麼原型是幹嘛用的呢?
原型的作用
原型補充了原物件,當需要訪問物件中屬性,但該屬性又不存在時,就會去原型上尋找。
按照上面的例子,obj.foo
返回什麼?以下虛擬碼就是尋值的過程:
function getValue(obj, attr){
let searchObj = obj
while(searchObj 下不存在 attr 屬性){
searchObj = obj 的原型
}
return searchObj 下的 attr 屬性
}
getValue(obj, 'foo')
複製程式碼
以下為圖示過程:
根據上圖(或是虛擬碼)我們可得知最終的結果是: bar
。
那如果物件上的原型還是沒有該屬性呢?
在原型的定義中,提到過:原型同時也是一個物件。轉換一下思路,那麼這個問題就變成了:如果物件上沒有該屬性那會發生什麼?
去物件下的原型下找!對!我們剛說完!
這種層層遞進的關係我們就把它稱為:原型鏈!
到這你可能會問:如果按照這樣一直找下去,那不是無窮無盡了?這就需要引出一個特殊的物件,可以在 Chrome
的控制檯列印 Object.prototype
輸出的物件,你可以仔細找找,該物件有沒有原型?
OH NO!
竟然有原型,你個騙子。
不要激動,繼續去檢視它原型是什麼:null
!
這和我之前說的:原型也是一個物件,有出入,因為 null
明顯不是一個物件。
但是,typeof null
確實是 object
,筆者也有猜想過是不是和這有關係。但猜想是猜想,為了語義的完整性,我們重新定義一下原型:
原型是物件下的一個屬性,每個物件都有,同時原型是物件或是
null
。
同時定義一下原型鏈的特點:
原型鏈由一個個物件組成,原型鏈有終點,這個終點是
null
。
OK
既然原型鏈有了終點,那麼如果一個屬性在原物件和所有的原型鏈上都不存在的話,他的值是什麼?
undefined
(未定義)啊!
現在我們已經清楚原型和物件的關係,那如何設定物件的原型呢?
設定原型
其實上面已經提到過,我們可以直接設定物件的原型:
ES5 中
let proto = {
foo: 'bar'
}
let obj = {
// ...
__proto__: proto
}
複製程式碼
ES6 中
由於 __proto__
是非標準屬性,因此在 ES6
中建議使用 setPrototypeOf
設定物件的原型。
let obj = {};
let proto = {
foo: 'bar'
}
Object.setPrototypeOf(obj, proto);
複製程式碼
那如果一個物件還沒有生成,比如僅僅定義了一個類,但又想控制通過這個類生成的物件的原型,該如何呢?
既然問了,那肯定是有的,解決方案就是函式的 prototype
屬性。
prototype
我們都知道在 JavaScript
中函式也是一個物件,這個特殊物件下有一個 prototype
屬性,是幹嘛用的呢?
在使用
new
關鍵字呼叫該函式時,函式下的prototype
屬性所儲存的物件就是生成物件的原型。
通過程式碼來解釋:
function A(bar) {
this.bar = bar;
}
A.prototype.test = function(){
console.log('test');
}
A.prototype.testAttr = 'testAttr';
let a = new A('bar');
複製程式碼
下面用虛擬碼來解釋 new A('bar')
這個過程:
function fakeNew(A, bar){
// 生成 this 為一個空物件。
let this = {};
Object.setPrototypeOf(this, A.prototype);
// 執行 A 函式內的程式碼
this.foo = bar;
// 將 this 返回
return this;
}
fakeNew(A, 'bar');
複製程式碼
相信大家看完程式碼就能理解 prototype
這個屬性的作用了,但這是 ES5
的程式碼,ES6
中並不建議直接寫 prototype
,而直接使用 class
,但其本質是一樣的。複製下面程式碼到 Chrome
裡即可查到真相:
class A {
constructor(bar) {
this.foo = bar;
}
test() {
console.log('test');
}
}
console.dir(A);
複製程式碼
如上圖所示,A
仍有 prototype
屬性,並且定義中除了 constructor
函式,其他的函式都在 prototype
屬性內。
可以認為是 ES5 function
語法糖吧,至少這裡可以這麼認為,但請不要認定,相比較於 ES5 function
還是有差別的,這塊內容不屬於本篇範疇,有機會單獨寫一篇討論討論。
物件的建立
繞了一圈,又繞了回來,現在我們回過頭來看看物件的建立。
通過以上的闡述,我們知道了以下幾點:
- 物件是一些列
屬性 & 資料
的集合。 - 原型是物件下的一個屬性,它的值是一個物件(或
null
)。
那好,現在我在問一個問題,你用以下程式碼建立的物件,它的原型是什麼?
let obj = {};
複製程式碼
有點疑惑?因為它既沒有主動新增原型,也不是從類建立的物件,那他的原型就沒有了?
答案當然是否定的,只要你把這段程式碼貼到 Chrome
控制檯就可以了。想要進一步知道這個物件到底哪兒來的,試試以下程式碼:
obj.__proto__ === Object.prototype; // true
複製程式碼
很明顯,這個新建立的物件為 Object
這個類的 prototype
屬性,難不成這個 obj
是通過 Object
類建立的?
bingo ~
你離真相又進了一步,在 JavaScript
中所有的物件都由 Object
所建立(PS:不管你用什麼姿勢!)。
好,物件的直接建立弄明白了,在來個間接建立的問題,請問以下程式碼所建立的 obj
的原型是什麼?
function A (){};
let obj = new A();
複製程式碼
當然是 A
的 prototype
啊,這還用問?那 A
的 prototype
又是什麼呢?
放到 Chrome
下一看便知:
由上圖可見,是一個簡單的物件,最原始的 prototype
其中僅僅包含了 constructor
和 __proto__
。
constructor
就是引用它自己。
A.prototype.constructor === A; // true
複製程式碼
__proto__
就是這個物件的原型,只要是一個物件,自然而然會有這個屬性,那麼這個屬性的值是什麼?
A.prototype.__proto__ === Object.prototype; // true
複製程式碼
當然是 Object.prototype
啊,所有物件都由 Object
這個類所建立嘛!
小結
好了,一個簡單的點,反反覆覆,回頭一看,這麼長了,原本不想寫這麼長的... 看來原理的東西雖說不難,但想解釋清楚還是要點時間的。最後提幾個問題當做是小結了吧:
- 物件是什麼?
- 原型又是什麼?
- 物件和原型的關係是什麼?
- 原型和物件的關係又是什麼?
- 原型如何建立,它的預設值是是嘛?
- 原型鏈是怎麼組成的?
- 原型鏈的終點是啥子?
最後,大部分文章提到了原型(鏈)都會提到,繼承。emmmm
先放過繼承吧,繼承只是程式設計的一種方式,不是原理性的東西,下次講吧 ~~
最後的最後
該系列所有問題由 minimo
提出,愛你喲~~~