對(duì)象與原型鏈
基于類和基于原型
我們都知道 JavaScript 是一個(gè)面向?qū)ο蟮恼Z言,但是它卻沒有其他諸如 Java、C++ 這些面向?qū)ο蟮恼Z言中都存在類的這個(gè)概念。取而代之的是原型的概念。這其實(shí)就是兩種不同的編程范式。
-
基于類的面向?qū)ο?/p>
在這種范式中,類定義了對(duì)象的結(jié)構(gòu)和行為以及繼承關(guān)系,所有基于該類的對(duì)象都有相同的行為和結(jié)構(gòu),不同的只是他們的狀態(tài)。
創(chuàng)建新的對(duì)象通過類的構(gòu)造器來創(chuàng)建。只有少數(shù)基于類的面向?qū)ο笳Z言允許類在運(yùn)行時(shí)進(jìn)行修改。
-
基于原型的面向?qū)ο?/p>
在這種范式中,關(guān)注的是一系列對(duì)象的行為,將擁有相似行為的對(duì)象通過原型鏈串聯(lián)起來。
創(chuàng)建新的對(duì)象通過拓展原有對(duì)象創(chuàng)建。很多的基于原型的語言提倡運(yùn)行時(shí)對(duì)原型進(jìn)行修改。
對(duì)比

圖片來自 MDN
總的來說基于原型相對(duì)來說更加靈活。這也許是 JavaScript 選擇基于原型構(gòu)建面向?qū)ο蟮脑蛑话伞?/p>
對(duì)象:無序?qū)傩缘募?/h2>
ECMA262 把對(duì)象定義為:無序?qū)傩缘募?,其屬性可以包含基本值、?duì)象或者函數(shù)。
var obj = {
a: 5,
b: function() {},
c:{ d: 10 }
}
基本類型 a,函數(shù) b,對(duì)象 c 都是對(duì)象 obj 的屬性。
實(shí)際上 JavaScript 中函數(shù)也可以添加屬性。
var fun = function(){}
fun.a = 5
fun.b = function() {}
fun.c = { d: 10 }
因此函數(shù)也是屬性的集合,它也是對(duì)象。
構(gòu)造函數(shù)
‘面向?qū)ο缶幊獭牡谝徊?,就是要生成?duì)象。在基于類的語言中類都有創(chuàng)建對(duì)象的構(gòu)造函數(shù)。而在 JavaScript 中沒有類,那么生成對(duì)象的工作就由函數(shù)來完成。這種函數(shù)被稱為構(gòu)造函數(shù)。
所有對(duì)象都有一個(gè) constructor 的屬性指向它的構(gòu)建函數(shù)。
你可能會(huì)提出反對(duì)意見:
var obj = { a: 5, b: 10 }
var fun = function(){}
你會(huì)說 obj 和 fun 都是對(duì)象,但他們都沒有通過函數(shù)生成啊。
其實(shí)這是 JavaScript 提供的語法糖,本質(zhì)上他們會(huì)分別調(diào)用 Object 和 Function (注意大寫)這兩個(gè)函數(shù)來生成。
obj.constructor // ? Object() { [native code] }
fun.constructor // ? Function() { [native code] }
等同于
// var obj = { a: 5, b: 10 }
var obj = new Object()
obj.a = 5
obj.b = 10
// var fun = function(){}
var fun = new Function()
除了 Object 和 Function 這兩個(gè)函數(shù)外,你也可以自定義構(gòu)造函數(shù)。函數(shù)要具備下面的特征:
- 為區(qū)別于普通函數(shù),通常構(gòu)造函數(shù)名首字母大寫;
- 構(gòu)造函數(shù)必須通過 new 命令調(diào)用;
- 構(gòu)造函數(shù)內(nèi)部使用 this 關(guān)鍵字,this 指向當(dāng)前構(gòu)造函數(shù)生成的對(duì)象;
- 構(gòu)造函數(shù)沒有 return,默認(rèn)返回 this。
一個(gè)例子
function Person(name) {
this.name = name
}
var peter = new Person('Peter')
其中 new 運(yùn)算符都做了以下工作:
- 創(chuàng)建一個(gè)空對(duì)象,作為將要返回的對(duì)象實(shí)例;
- 將空對(duì)象的原型
__proto__指向了構(gòu)造函數(shù)的prototype屬性; - 將空對(duì)象賦值給構(gòu)造函數(shù)內(nèi)部的 this 關(guān)鍵字;
- 開始執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼;
- 如果構(gòu)造器返回的是對(duì)象,則返回,否則返回第一步創(chuàng)建的對(duì)象。
這里出現(xiàn)了兩個(gè)容易混淆的概念:__proto__ 和 prototype。
-
__proto__是每個(gè)對(duì)象都有的一個(gè)屬性。指向創(chuàng)建該對(duì)象的函數(shù)的 prototype。用它來產(chǎn)生一個(gè)鏈,一個(gè)原型鏈,用于尋找方法名或?qū)傩?,等等。它是個(gè)隱藏屬性,早期低版本的瀏覽器甚至不支持這個(gè)屬性。 -
prototype是每個(gè)函數(shù)都有的一個(gè)屬性。它本身是一個(gè)對(duì)象,它的 constructor 指向函數(shù)本身。這個(gè)屬性存在的目的就是在通過 new 來創(chuàng)建對(duì)象時(shí)構(gòu)造對(duì)象的__proto__屬性。
只是通過上面的敘述可能理解起來比較困難,我們通過代碼和內(nèi)存布局來仔細(xì)分析。
對(duì)于 Person 函數(shù)
// Person.prototype 是一個(gè)對(duì)象,它有兩個(gè)屬性 constructor 指向 Person 函數(shù),__proto__ 指向 Object。(先不用理會(huì) Object 這個(gè)對(duì)象,后面會(huì)詳細(xì)介紹)
Person.prototype
/*
* {constructor: ?}
* constructor: ? Person()
* __proto__: Object
*/
Person 是函數(shù)因此它擁有 constructor、__proto__、prototype 屬性。
Person.prototype 是只是個(gè)普通對(duì)象,因此 Person.prototype 擁有 constructor、__proto__ 屬性。

對(duì)于 peter 來說,它是個(gè)對(duì)象因此具有 constructor、__proto__ 屬性。
// peter 對(duì)象的構(gòu)造函數(shù)就是 Person()
peter.constructor // ? Person() {...}
// peter 對(duì)象的 __proto__ 屬性指向 Person.prototype
peter.__proto__ == Person.prototype // true

可以清楚的看到對(duì)象和它的構(gòu)造函數(shù)之間的聯(lián)系,總結(jié)一下:
- 每個(gè)對(duì)象都是由其構(gòu)造函數(shù)生成,并且對(duì)象有個(gè)
constructor屬性指向構(gòu)造函數(shù); - 每個(gè)對(duì)象都有個(gè)原型屬性
__proto__,指向其構(gòu)造函數(shù)的prototype屬性; - 每個(gè)函數(shù)都有一個(gè)
prototype屬性用于充當(dāng)構(gòu)造函數(shù)時(shí)構(gòu)建對(duì)象的__proto__屬性。
原型鏈
到此你也許會(huì)疑惑為什么要這樣設(shè)計(jì),這是因?yàn)?JavaScript 也是面向?qū)ο蟮恼Z言,它通過這樣的設(shè)計(jì)來構(gòu)建原型鏈以實(shí)現(xiàn)繼承。
在上面代碼的基礎(chǔ)上我們?cè)俾暶饕粋€(gè) Student 的構(gòu)造函數(shù)
function Student(name, score) {
Person.call(this, name)
this.score = score
}
// 只執(zhí)行上面的語句還不夠,需要通過這行代碼將它們產(chǎn)生鏈接也就是繼承關(guān)系
Student.prototype = Object.create(Person.prototype)
// 這段代碼是為了讓 Student.prototype 的構(gòu)造函數(shù)指向 Student 函數(shù),不指定的話會(huì)指向 Person 函數(shù)。(許多地方都沒有這一步,也可以不寫。這里為了保持 constructor 指向一致)
Student.prototype.constructor = Student
// 創(chuàng)建一個(gè)對(duì)象
var jim = new Student('Jim', 90)
分析下內(nèi)存布局
Student.prototype.constructor == Student // true
Student.prototype.__proto__ == Person.prototype // true
jim.__proto__ == Student.prototype // true
jim.constructor == Student // true

可以明顯的看到一個(gè)鏈條,一個(gè)靠 __proto__ 屬性串聯(lián)起來的鏈條,這就是所謂的原型鏈。

正是有了原型鏈當(dāng)我們?cè)L問一個(gè)對(duì)象的屬性時(shí),會(huì)先在基本屬性中查找,如果沒有,再沿著 __proto__ 這條鏈向上找。這樣就實(shí)現(xiàn)了繼承。
我們還可以發(fā)現(xiàn)這個(gè)鏈條在 Person.prototype 這里斷了,而且圖中有些屬性沒有標(biāo)注出來。別急,我們后面來把它慢慢補(bǔ)全。
Function 和 Object
我們先看下面的代碼
var obj = { a: 5, b: 10 }
var fun = function(){}
等價(jià)于
// var obj = { a: 5, b: 10 }
var obj = new Object()
obj.a = 5
obj.b = 10
typeof Object // function
// var fun = function(){}
var fun = new Function()
typeof Function // function
由此可見 Object 和 Function 是兩個(gè)比較總要的函數(shù)對(duì)象。我們來探究下它們的內(nèi)存布局。
因?yàn)樗鼈兌际菍?duì)象因此它們都有 constructor、__proto__ 屬性,又因?yàn)樗麄兪呛瘮?shù)對(duì)象,因此它們都有 prototype 屬性。

對(duì)于 Function 來說
// 雖然 Function.prototype 返回的類型是 function 但是它的 prototype 屬性并不存在,因此它是特殊的函數(shù)對(duì)象。
typeof Function.prototype // "function"
typeof Function.prototype.prototype // "undefined"
// Function.prototype 與 Function.__proto__ 指向同一個(gè)對(duì)象
Function.prototype == Function.__proto__
// Function.prototype 的 constructor 是 Function 函數(shù)
Function.prototype.constructor // ? Function() { [native code] }
// Function 的 constructor 是 Function 函數(shù)自己
Function.constructor // ? Function() { [native code] }
可以畫出下面的圖

對(duì)于 Object 來說
// Object.prototype 是一個(gè)對(duì)象
typeof Object.prototype // "object"
// Object.__proto__ 與 Object.prototype 指向的不是同一個(gè)對(duì)象
Object.prototype == Object.__proto__ // false
// Object.__proto__ 指向 Function.prototype 的對(duì)象
Object.__proto__ == Function.prototype // true
// Object.prototype 的 constructor 是 Object 函數(shù)
Object.prototype.constructor // ? Object() { [native code] }
// Object 的 constructor 是 Function 函數(shù)
Object.constructor // ? Function() { [native code] }
可以畫出下面的圖

我們發(fā)現(xiàn)還有兩個(gè)屬性沒確定分別是 Object.prototype.__proto__ 和 Function.prototype.__proto__。我們通過代碼在確認(rèn)一下。
// Function.prototype 的 __proto__ 屬性指向 Object.prototype
Function.prototype.__proto__ == Object.prototype // true
// Object.prototype 的 __proto__ 屬性指向 null
Object.prototype.__proto__ // null
最終得到這個(gè)圖

我們?cè)俳Y(jié)合上面原型鏈那部分的內(nèi)容。
// Person 函數(shù)的構(gòu)造函數(shù)是 Function
Person.constructor // ? Function() { [native code] }
// Person 函數(shù)的原型是 Function.prototype
Person.__proto__ // ? () { [native code] }
Person.__proto__ == Function.prototype // true
// Student 函數(shù)的構(gòu)造函數(shù)是 Function
Student.constructor // ? Function() { [native code] }
// Student 函數(shù)的原型是 Function.prototype
Student.__proto__ // ? () { [native code] }
Student.__proto__ == Function.prototype // true
// Person.prototype 的原型是 Object.prototype
Person.prototype.__proto__ == Object.prototype

我們可以觀察到幾條明顯的原型鏈,見下圖:
- 綠色的是我們自定義的 Person Student 對(duì)象的原型鏈;
- 其他顏色的是 函數(shù)對(duì)象的原型鏈。

分析這張圖我們可以得出以下結(jié)論
- 所有函數(shù)的原型都是
Function.prototype,包括 Function 函數(shù)自己 - 所有函數(shù)的構(gòu)造函數(shù)都是 Function,包括 Function 函數(shù)自己
- 所有對(duì)象的原型終點(diǎn)都是
Object.prototype,包括函數(shù)對(duì)象和普通對(duì)象,而Object.prototype.__proto__的原型指向了null
這里面有個(gè)最初讓我比較疑惑的就是 Object 的原型為什么不是 Object.prototype 而是 Function.prototype
Object.__proto__ == Function.prototype // true
其實(shí)這是因?yàn)?Object 本身就是個(gè)函數(shù),它跟其他函數(shù)一樣都是由 Function 來構(gòu)造的。
這里還有張大佬的圖片,相信你可以很清楚的跟上面的圖片對(duì)應(yīng)上。

與原型鏈相關(guān)的方法
instanceof
instanceof 主要的作用就是判斷一個(gè)實(shí)例是否屬于某種類型,實(shí)現(xiàn)原理就是通過原型鏈進(jìn)行判斷。
function new_instance_of(leftVaule, rightVaule) {
let rightProto = rightVaule.prototype; // 取右表達(dá)式的 prototype 值
leftVaule = leftVaule.__proto__; // 取左表達(dá)式的__proto__值
while (true) {
if (leftVaule === null) {
return false;
}
if (leftVaule === rightProto) {
return true;
}
leftVaule = leftVaule.__proto__
}
}
可以看出來 instanceof 的實(shí)現(xiàn)思路就是判斷右值變量的 prototype 是否在左值變量的原型鏈上。
jim instanceof Person // true
jim instanceof Student // true
參考上方的圖我們也可以解釋一些看起來比較詭異的判斷
Object instanceof Object // true

Function instanceof Function // true

Function instanceof Object // true

下面這些你可以自行檢測(cè)。
function Foo() { } // 定義一個(gè)函數(shù)
Foo instanceof Object // true
Foo instanceof Function // true
hasOwnProperty
Object.hasOwnProperty() 返回一個(gè)布爾值,表示某個(gè)對(duì)象的實(shí)例是否含有指定的屬性,而且此屬性非原型鏈繼承。用來判斷屬性是來自實(shí)例屬性還是原型屬性。類似還有 in 操作符,in 操作符只要屬性存在,不管實(shí)在實(shí)例中還是原型中,就會(huì)返回 true。同時(shí)使用 in 和 hasOwnProperty 就可以判斷屬性是在原型中還是在實(shí)例中。
isPrototypeOf
返回一個(gè)布爾值,表示指定的對(duì)象是否在本對(duì)象的原型鏈中。
getPrototypeOf
返回該對(duì)象的原型。
創(chuàng)建對(duì)象和生成原型鏈
上面已經(jīng)提到了創(chuàng)建對(duì)象以及實(shí)現(xiàn)繼承的部分方法,其實(shí)還有其他很多的方法來創(chuàng)建和生成原型鏈以實(shí)現(xiàn)繼承。
使用語法結(jié)構(gòu)創(chuàng)建對(duì)象
var o = {a: 1}
o 這個(gè)對(duì)象繼承了 Object.prototype 上面的所有屬性
原型鏈: o ---> Object.prototype ---> null
var a = ["yo", "whadup", "?"]
數(shù)組都繼承于 Array.prototype
原型鏈: a ---> Array.prototype ---> Object.prototype ---> null
function f(){ return 2 }
函數(shù)都繼承于 Function.prototype
原型鏈: f ---> Function.prototype ---> Object.prototype ---> null
使用構(gòu)造器創(chuàng)建對(duì)象
function Person(name) {
this.name = name
}
var peter = new Person('Peter')
構(gòu)造器創(chuàng)建的對(duì)象繼承了對(duì)應(yīng)構(gòu)造函數(shù)的 prototype 屬性
原型鏈: peter ---> peter.prototype ---> Object.prototype ---> null
其實(shí)上面使用語法結(jié)構(gòu)創(chuàng)建對(duì)象本質(zhì)上也是調(diào)用相應(yīng)的構(gòu)造器。
使用 Object.create 創(chuàng)建的對(duì)象
ECMAScript 5 中引入了一個(gè)的新方法:Object.create()。可以調(diào)用這個(gè)方法來創(chuàng)建一個(gè)新對(duì)象。新對(duì)象的原型就是調(diào)用 create 方法時(shí)傳入的第一個(gè)參數(shù)。
var a = {a: 1};
// a ---> Object.prototype ---> null
var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (繼承而來)
var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null
var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined, 因?yàn)閐沒有繼承Object.prototype
上面的創(chuàng)建方法都節(jié)選自 MDN 繼承與原型鏈。其實(shí)還有其他很多的方法來實(shí)現(xiàn)創(chuàng)建對(duì)象和繼承,但是萬變不離其宗。他們的本質(zhì)就是保證原型鏈的正確構(gòu)建就可以了。
class
通過上面的學(xué)習(xí)我們知道了很多創(chuàng)建對(duì)象和實(shí)現(xiàn)繼承的方式,但是這些方式都比較繁瑣,需要使用者自己保證原型鏈的正確構(gòu)建。尤其是對(duì)于熟悉基于類面向?qū)ο笳Z言的同學(xué)來說感覺這種方式比較怪異。
其實(shí)在 ES6 出現(xiàn)之前,就有很多框架來模擬類,使得 JS 能基于類實(shí)現(xiàn)繼承。但是由于社區(qū)和碎片化原因趨于小眾。
直到 ES6 的新特性 class,這讓類的概念成為了語言一個(gè)基礎(chǔ)特性。
當(dāng)然它實(shí)質(zhì)上是 JavaScript 現(xiàn)有的基于原型的繼承的語法糖。類語法不會(huì)為 JavaScript 引入新的面向?qū)ο蟮睦^承模型。
定義 class
類實(shí)際上是個(gè)“特殊的函數(shù)”,與聲明一個(gè)函數(shù)類似,不過這里使用的是 class 關(guān)鍵字。
class Rectangle {
// 屬性
color = 'Red'
// constructor 構(gòu)造函數(shù)
constructor(height, width) {
this.height = height
this.width = width
}
// Getter Setter
get prop() {
return 'getter'
}
set prop(value) {
console.log('setter: ' + value)
}
// Method
calcArea() {
return this.height * this.width
}
static
}
class Square extends Rectangle {
// constructor 構(gòu)造函數(shù)
constructor(edge) {
super(edge, edge)
}
}
let square = new Square(10)
構(gòu)造函數(shù)
constructor 方法是一個(gè)特殊的方法,這種方法用于創(chuàng)建和初始化一個(gè)由 class 創(chuàng)建的對(duì)象。它具有以下特性。
-
一個(gè)類只能擁有一個(gè)名為 constructor 的特殊方法。
如果類包含多個(gè) constructor 的方法,則將拋出 一個(gè) SyntaxError 。
-
一個(gè)構(gòu)造函數(shù)可以使用 super 關(guān)鍵字來調(diào)用一個(gè)父類的構(gòu)造函數(shù)。
比如我們定義的 Square 子類中就調(diào)用了父類的構(gòu)造方法。
但是需要注意的一點(diǎn)是子類的構(gòu)造方法中 this 必須在調(diào)用父類構(gòu)造方法之后才能使用。這是因?yàn)槿绻诟割惙椒ㄖ罢{(diào)用這時(shí)候 this 還沒有被初始化。
constructor 方法不用寫 return 它默認(rèn)返回實(shí)例對(duì)象(即this),當(dāng)然你也可以指定返回另外一個(gè)對(duì)象。
屬性
實(shí)例屬性
我們都知道 JavaScript 對(duì)象有兩類屬性:數(shù)據(jù)屬性和訪問器(getter/setter)屬性。
定義數(shù)據(jù)屬性有兩種方式:
- 在構(gòu)造函數(shù)里創(chuàng)建。
- 也可以定義在類的最頂層,與構(gòu)造函數(shù)同級(jí)的地方。
class Rectangle {
// 實(shí)例屬性 1
color = 'Red'
// constructor 構(gòu)造函數(shù)
constructor(height, width) {
// 實(shí)例屬性 2
this.height = height
this.width = width
}
}
訪問器(getter/setter)屬性的定義跟之前類似。
// Getter Setter
get prop() {
return 'getter'
}
set prop(value) {
console.log('setter: ' + value)
}
靜態(tài)屬性
靜態(tài)屬性指的是 Class 本身的屬性,,即Class.propName,而不是定義在實(shí)例對(duì)象上的屬性。
// 現(xiàn)在的寫法
class Foo {
// ...
}
Foo.prop = 1;
// 提案的寫法
class Foo {
static prop = 1;
}
目前只有個(gè)上面的方法來定義,下面的寫法還只是一個(gè)提案。
方法
類相當(dāng)于實(shí)例的原型,所有在類中定義的方法,都會(huì)被實(shí)例繼承,這些稱為實(shí)例方法。另外如果在一個(gè)方法前,加上 static 關(guān)鍵字,就表示該方法不會(huì)被實(shí)例繼承,而是直接通過類來調(diào)用,這就稱為“靜態(tài)方法”。
靜態(tài)方法
實(shí)例方法沒什么好說的,這里主要介紹下靜態(tài)方法。
class Foo {
static bar() {
this.baz();
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}
Foo.bar() // hello
上面代碼中,靜態(tài)方法 bar 調(diào)用了 this.baz,注意,如果靜態(tài)方法包含this關(guān)鍵字,這個(gè)this指的是類,而不是實(shí)例。因此這里的 this 指的是 Foo 類,而不是 Foo 的實(shí)例,等同于調(diào)用Foo.baz。另外,從這個(gè)例子還可以看出,靜態(tài)方法可以與非靜態(tài)方法重名。
注意點(diǎn)
-
嚴(yán)格模式
類聲明和類表達(dá)式的主體都執(zhí)行在嚴(yán)格模式下。比如,構(gòu)造函數(shù),靜態(tài)方法,實(shí)例方法,getter 和 setter 都在嚴(yán)格模式下執(zhí)行。
只要你的代碼寫在類或模塊之中,就只有嚴(yán)格模式可用。考慮到未來所有的代碼,其實(shí)都是運(yùn)行在模塊之中,所以 ES6 實(shí)際上把整個(gè)語言升級(jí)到了嚴(yán)格模式。
-
不存在提升
函數(shù)聲明和類聲明之間的一個(gè)重要區(qū)別是函數(shù)聲明會(huì)提升,類聲明不會(huì)。你首先需要聲明你的類,然后訪問它,否則代碼會(huì)拋出一個(gè) ReferenceError。
-
this 的指向
類的方法內(nèi)部如果含有 this,它默認(rèn)指向類的實(shí)例。但是,必須非常小心,一旦單獨(dú)使用該方法,很可能報(bào)錯(cuò)。
class Logger { printName(name = 'there') { this.print(`Hello ${name}`); } print(text) { console.log(text); } } const logger = new Logger(); const { printName } = logger; printName(); // TypeError: Cannot read property 'print' of undefinedprintName 方法被提取出來單獨(dú)使用,this 會(huì)指向該方法運(yùn)行時(shí)所在的環(huán)境(由于 class 內(nèi)部是嚴(yán)格模式,所以 this 實(shí)際指向的是 undefined),從而導(dǎo)致找不到 print 方法而報(bào)錯(cuò)。
可以通過在構(gòu)造方法中綁定 this 或者使用箭頭函數(shù)來解決
class Logger { constructor() { this.printName = this.printName.bind(this); } } class Obj { constructor() { this.getThis = () => this; } } const myObj = new Obj(); myObj.getThis() === myObj // true
總結(jié)
通過上面我們可以看出來 JavaScript 中對(duì)象獨(dú)有的特色就是:對(duì)象具有高度的動(dòng)態(tài)性。它是一個(gè)徹底的動(dòng)態(tài)語言。這讓我想到了另一個(gè)動(dòng)態(tài)語言 Objective-C。而且 OC 中的 NSObject class 和 meta class 之間的關(guān)系也類似于 JS 中 Function 和 Object 之間的關(guān)系。由此可見語言之間有些東西都是相通的,多學(xué)習(xí)一門語言也可以觸類旁通。??