JavaScript 物件 & 原型

斑碼發表於2019-12-09

前言

這次的 why what or how 主題:JavaScript 物件 & 原型。

此類文章在百度上一搜一大把,其實不用再寫了,但是本著把這個問題解釋的清清楚楚明明白白,還是開始寫了。

原因如下:

  1. 面試寶典類文章,弄張圖片一糊弄,讓人覺得自己理解了。
  2. 解釋類文章,告訴你一堆語法,對語法一頓解釋,告訴你就是這樣的,還是沒說清楚。
  3. 很少有文章單獨解釋這個點!但這個點是基礎!真的很重要!

所以本篇文章想說一說物件 & 原型,但為了確保能順利理解,請先看完 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。那麼它在堆中是如何儲存的呢?

JavaScript 下物件的儲存

看過了 JS 變數儲存?棧 & 堆?NONONO! 相信大家對於上圖應該不陌生。

物件的取值

我們繼續看一個較為複雜的物件(物件下某個屬性也是一個物件):

let complexObj = {
    num: 1,
    str: 'string',
    obj: {
        foo: 'bar',
    }
}
複製程式碼

在記憶體中的模型如下:

複雜物件模型

我們模擬一下 complexObj.obj.foo 這個取值過程。

複雜物件取值過程

取值過程:碰到儲存的值是地址值時,就到相應的地址值繼續進行操作。

物件,總的來說,知識點有兩點:

  1. 建立物件:在記憶體堆中開闢一塊用於儲存一系列 屬性 & 資料 的空間。
  2. 物件取值:根據儲存的資料取值,如果是地址值,則到相應記憶體堆中繼續進行操作。

物件到這裡就差不多了,那原型又是什麼呢?

原型 & 原型鏈

原型是物件下的一個屬性,每個物件都有,同時原型也是一個物件。

文字的描述總是不直觀的,我們通過程式碼來看:

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);
複製程式碼

class 的 prototype

如上圖所示,A 仍有 prototype 屬性,並且定義中除了 constructor 函式,其他的函式都在 prototype 屬性內。

可以認為是 ES5 function 語法糖吧,至少這裡可以這麼認為,但請不要認定,相比較於 ES5 function 還是有差別的,這塊內容不屬於本篇範疇,有機會單獨寫一篇討論討論。

物件的建立

繞了一圈,又繞了回來,現在我們回過頭來看看物件的建立。

通過以上的闡述,我們知道了以下幾點:

  1. 物件是一些列 屬性 & 資料 的集合。
  2. 原型是物件下的一個屬性,它的值是一個物件(或 null)。

那好,現在我在問一個問題,你用以下程式碼建立的物件,它的原型是什麼?

let obj = {};
複製程式碼

有點疑惑?因為它既沒有主動新增原型,也不是從類建立的物件,那他的原型就沒有了?

答案當然是否定的,只要你把這段程式碼貼到 Chrome 控制檯就可以了。想要進一步知道這個物件到底哪兒來的,試試以下程式碼:

obj.__proto__ === Object.prototype; // true
複製程式碼

很明顯,這個新建立的物件為 Object 這個類的 prototype 屬性,難不成這個 obj 是通過 Object 類建立的?

bingo ~ 你離真相又進了一步,在 JavaScript 中所有的物件都由 Object 所建立(PS:不管你用什麼姿勢!)。

好,物件的直接建立弄明白了,在來個間接建立的問題,請問以下程式碼所建立的 obj 的原型是什麼?

function A (){};
let obj = new A();
複製程式碼

當然是 Aprototype 啊,這還用問?那 Aprototype 又是什麼呢?

放到 Chrome 下一看便知:

function 的 prototype

由上圖可見,是一個簡單的物件,最原始的 prototype 其中僅僅包含了 constructor__proto__

constructor 就是引用它自己。

A.prototype.constructor === A; // true
複製程式碼

__proto__ 就是這個物件的原型,只要是一個物件,自然而然會有這個屬性,那麼這個屬性的值是什麼?

A.prototype.__proto__ === Object.prototype; // true
複製程式碼

當然是 Object.prototype 啊,所有物件都由 Object 這個類所建立嘛!

小結

好了,一個簡單的點,反反覆覆,回頭一看,這麼長了,原本不想寫這麼長的... 看來原理的東西雖說不難,但想解釋清楚還是要點時間的。最後提幾個問題當做是小結了吧:

  1. 物件是什麼?
  2. 原型又是什麼?
  3. 物件和原型的關係是什麼?
  4. 原型和物件的關係又是什麼?
  5. 原型如何建立,它的預設值是是嘛?
  6. 原型鏈是怎麼組成的?
  7. 原型鏈的終點是啥子?

最後,大部分文章提到了原型(鏈)都會提到,繼承。emmmm 先放過繼承吧,繼承只是程式設計的一種方式,不是原理性的東西,下次講吧 ~~

最後的最後

該系列所有問題由 minimo 提出,愛你喲~~~

相關文章