前言
寫C++、Java、Objective-C等語言的猿人們,在接觸JavaScript時(shí)一定被__proto__
和prototype
搞得暈頭轉(zhuǎn)向。筆者正在自學(xué)JavaScript,本文為JS對(duì)象學(xué)習(xí)小結(jié),從傳統(tǒng)“類繼承”的角度來對(duì)比“原型繼承”。修仙不易,且修且珍惜。
JS在設(shè)計(jì)之初并沒有類
這個(gè)概念,ES6新增的class
關(guān)鍵字仍是基于原型繼承
,而非真正的類繼承
。既然沒有class,JS對(duì)象是如何創(chuàng)建的?
對(duì)比
大部分面向?qū)ο笳Z言是通過class extend other class
來完成繼承的。即,用class創(chuàng)建對(duì)象,對(duì)象的屬性及方法通過class的繼承關(guān)系來查找。在這種模式下,對(duì)象的信息是以class為載體的。
JavaScript同樣是一門面向?qū)ο笳Z言,但它摒棄了class設(shè)計(jì)模式,采用更輕便的object link other object
模式完成繼承。為什么說這種模式更輕便?先從對(duì)象
說起。
什么是對(duì)象?
你也許會(huì)說,class的實(shí)例就是對(duì)象。如果拋開class不談,什么是對(duì)象?
對(duì)象是某一可以操縱屬性及方法的具體事物。既然如此,對(duì)象和class并沒有什么必然聯(lián)系,所以class不是必需的,只是我們已經(jīng)習(xí)慣了class的存在。如何創(chuàng)建對(duì)象?
var creature = {
name: "creature",
age: 0,
eat: function() {
console.log(this.name + " is eating")
}
}
creature.age // 0
creature.eat() // creature is eating
只要在大括號(hào)中以鍵值對(duì)的方式就可以描述屬性及方法對(duì)應(yīng)的信息,通過點(diǎn)語法可以操縱對(duì)象的屬性及方法。由于對(duì)象和class無關(guān),因此可以任意給對(duì)象增加或刪除屬性與方法(Objective-C中的對(duì)象雖然和class關(guān)聯(lián),但是由于runtime的存在,仍然可以在運(yùn)行時(shí)動(dòng)態(tài)改變class信息,這是由語言特性決定的。JavaScript與Objective-C屬于動(dòng)態(tài)語言,而C++與Java屬于靜態(tài)語言)
對(duì)象的屬性及方法的刪除與添加操作起來也很方便:
什么是繼承?
你也許會(huì)說,讓子類具有父類屬性與方法的過程就是繼承。OK... 拋開class這個(gè)概念不談呢?
繼承是讓某一事物具備另一事物能力的過程。那么在沒有class概念的情況下,JS如何實(shí)現(xiàn)繼承特性?如何實(shí)現(xiàn)繼承?
回到上文的object link other object
,JS中的繼承是通過公共屬性__proto__關(guān)聯(lián)對(duì)象實(shí)現(xiàn)的:
var creature = {
name: "creature",
age: 0,
eat: function() {
console.log(this.name + " is eating")
}
}
var animal = {
name: "animal",
age: 10,
__proto__: creature
}
var plant = {
name: "plant",
age: 100,
__proto__: creature
}
animal.age // 10
animal.eat() // animal is eating
plant.age // 100
plant.eat() // plant is eating
對(duì)象中的公共__proto__屬性和constructor方法
__proto__屬性
每個(gè)對(duì)象都有__proto__
屬性,將這個(gè)屬性關(guān)聯(lián)的對(duì)象稱為原型
。因此,原型就是對(duì)象,也可稱為原型對(duì)象
。從上面代碼塊可以看到,animal和plant的__proto__屬性關(guān)聯(lián)creature后,在未定義eat的情況下,仍然可以調(diào)用eat函數(shù)。那么JS對(duì)象是怎樣尋找屬性或者方法的?首先在當(dāng)前對(duì)象中查找,如果沒有,到當(dāng)前對(duì)象的原型中查找,如果還沒有,到當(dāng)前對(duì)象原型的原型中查找,如此遞歸直到原型為null結(jié)束。這條鏈稱為原型鏈
,如果沒在原型鏈中找到對(duì)應(yīng)的屬性或方法,則返回undefined。(對(duì)比class的繼承方式,兩者尋找屬性和方法的途徑一致,只是class在“類”中遞歸查找,JS在原型中遞歸查找)-
constructor方法
每個(gè)對(duì)象都有constructor方法,constructor默認(rèn)指向當(dāng)前對(duì)象對(duì)應(yīng)的構(gòu)造函數(shù)。
constructor
可見,通過大括號(hào)創(chuàng)建的對(duì)象其實(shí)調(diào)用了Object構(gòu)造函數(shù)。大括號(hào)創(chuàng)建對(duì)象的方式是一種語法糖,實(shí)際是通過Object構(gòu)造函數(shù)創(chuàng)建的對(duì)象。(對(duì)比class中的構(gòu)造函數(shù),兩者都是通過構(gòu)造函數(shù)創(chuàng)建對(duì)象,只是class中的構(gòu)造函數(shù)仍然是和“類”綁定在一起的,而JS中的構(gòu)造函數(shù)是獨(dú)立存在的)
構(gòu)造函數(shù)
既然通過構(gòu)造函數(shù)可以創(chuàng)建對(duì)象,那么構(gòu)造函數(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
以上文中的creature對(duì)象為例,如何通過自定義構(gòu)造函數(shù)創(chuàng)建creature對(duì)象?
- 自定義構(gòu)造函數(shù)
function Creature(name="creature", age=0){
this.name = name
this.age = age
this.eat = function() {
console.log(this.name + " is eating")
}
}
var creature = new Creature()
creature.age // 0
creature.eat() // creature is eating
基于萬物皆對(duì)象
的思想理念,構(gòu)造函數(shù)本身應(yīng)該也是個(gè)對(duì)象(普通函數(shù)也是對(duì)象)。那么構(gòu)造函數(shù)是由誰創(chuàng)建的?創(chuàng)建構(gòu)造函數(shù)的那個(gè)事物又是由誰創(chuàng)建的?這樣遞歸下去,似乎永遠(yuǎn)都找不到源頭。構(gòu)造函數(shù)的根究竟在哪里?
-
構(gòu)造函數(shù)的constructor
以Creature構(gòu)造函數(shù)為例,因?yàn)镃reature也是個(gè)對(duì)象,所以可以通過內(nèi)部的constructor訪問到創(chuàng)建Creature構(gòu)造函數(shù)的事物。即,Creature構(gòu)造函數(shù)的構(gòu)造函數(shù):
構(gòu)造函數(shù)的constructor
可見,Creature構(gòu)造函數(shù)的constructor是Function構(gòu)造函數(shù)
,而Function構(gòu)造函數(shù)的constructor是它本身。這樣就解決了無限遞歸出現(xiàn)的可能,所以構(gòu)造函數(shù)的源頭就是Function構(gòu)造函數(shù)。
既然構(gòu)造函數(shù)是對(duì)象,那么構(gòu)造函數(shù)的原型又是誰?
-
構(gòu)造函數(shù)的__proto__
構(gòu)造函數(shù)的__proto__
可見,普通函數(shù)與構(gòu)造函數(shù)的原型都指向同一個(gè)匿名函數(shù)。這個(gè)匿名函數(shù)又是由誰創(chuàng)建的?
可見,不單構(gòu)造函數(shù)是由Function構(gòu)造函數(shù)創(chuàng)建的,在不手動(dòng)指定constructor的情況下,任意函數(shù)的構(gòu)造函數(shù)都是Function構(gòu)造函數(shù)(包括Function構(gòu)造函數(shù)本身)
函數(shù)與對(duì)象
- 區(qū)別與聯(lián)系
1.函數(shù)屬于對(duì)象
2.對(duì)象未必是函數(shù)
雖然函數(shù)也是對(duì)象的一種,但是作為JS中的一等公民,函數(shù)還存在一個(gè)特殊屬性,prototype
。
- 函數(shù)中的prototype屬性
prototype中存儲(chǔ)的也是原型,但他不是當(dāng)前函數(shù)對(duì)象的原型(當(dāng)前函數(shù)對(duì)象的原型存儲(chǔ)在__proto__屬性中),如果僅把他當(dāng)做函數(shù)看待,可以理解為函數(shù)的原型(注意區(qū)分函數(shù)與函數(shù)對(duì)象的概念),而函數(shù)的原型就指向函數(shù)生成的對(duì)象對(duì)應(yīng)的原型:
function Creature(name="creature", age=0){
this.name = name
this.age = age
this.eat = function() {
console.log(this.name + " is eating")
}
}
var creature = new Creature()
可以看到,Creature構(gòu)造函數(shù)的prototype屬性確實(shí)指向構(gòu)造函數(shù)生成的對(duì)象creature對(duì)應(yīng)的原型。由于Creature.prototype就是creature的原型,所以Creature.prototype.constructor就是Creature構(gòu)造函數(shù)本身。
上文已經(jīng)說過,constructor的源頭是Function構(gòu)造函數(shù),那么原型(對(duì)象)的源頭又是誰?
- 原型(對(duì)象)的源頭
同樣,以creature對(duì)象為例:
function Creature(name="creature", age=0){
this.name = name
this.age = age
this.eat = function() {
console.log(this.name + " is eating")
}
}
var creature = new Creature()
可見,原型鏈的源頭為null。也就是說,可以將對(duì)象的__proto__屬性指向null,以此來創(chuàng)建一個(gè)對(duì)象。因?yàn)閚ull中不存在任何屬性和方法,所以創(chuàng)建出的對(duì)象沒有繼承到任何屬性和方法。ES6中,提供了create方法來創(chuàng)建對(duì)象。
-
Object.create
Object.create(null)
可見,通過null確實(shí)能創(chuàng)建出對(duì)象,并且這個(gè)對(duì)象沒有任何屬性或方法,是最干凈的對(duì)象。上文說過,通過大括號(hào)語法糖創(chuàng)建對(duì)象,其實(shí)會(huì)調(diào)用Object構(gòu)造函數(shù),這也是為什么我們創(chuàng)建出的絕大多數(shù)對(duì)象都擁有某些共同屬性和方法。
- ES6中獲取對(duì)象原型的方法
ES6提供了新的方法來獲取和設(shè)置原型:getPrototypeOf
,setPrototypeOf
,這樣我們不必直接操縱__proto__
屬性:
getPrototypeOf
以getPrototypeOf這個(gè)方法為例,他是通過Object構(gòu)造函數(shù)調(diào)用的,所以必然存在于Object構(gòu)造函數(shù)或者他的原型鏈中。因?yàn)槭切略龇椒ǎ⑶覂H能通過Object構(gòu)造函數(shù)調(diào)用,猜測getPrototypeOf方法應(yīng)該直接屬于Object構(gòu)造函數(shù),而非繼承自某個(gè)原型。那么,如何查看Object構(gòu)造函數(shù)的屬性及方法?
- 查看Object構(gòu)造函數(shù)的屬性與方法
通過上文我們已經(jīng)知道Object.prototype.constructor === Object
,因此可以這樣來查看:
Object構(gòu)造函數(shù)的屬性及方法
可以看到,除getPrototypeOf與setPrototypeOf這兩個(gè)方法外,之前提到的create方法,以及函數(shù)特有的屬性prototype都在Object構(gòu)造函數(shù)中。除此之外,Object構(gòu)造函數(shù)中還提供了很多方便快捷的方法,這里不再贅述。
再次對(duì)比原型繼承與類繼承,其實(shí)兩者很相似,越是通用的能力越沉到下層,越是特有的能力越浮在上層,只是JS擺脫了“類”這個(gè)枷鎖。那么ES6中的class究竟是怎么回事?
ES6中的class
先來感受一下ES6中class的強(qiáng)大:
class Programmer {
constructor(name) {
this.name = name
}
sayHello() {
console.log("Hello, I'm " + this.name)
}
}
var p = new Programmer("Jack")
p.sayHello() // Hello, I'm Jack
class SeniorProgrammer extends Programmer {
constructor(name, skill) {
super(name)
this.skill = skill
}
level = "senior"
static keyboard() {}
}
ar sp = new SeniorProgrammer("Rose", "assembly")
sp.name // Rose
sp.skill // assembly
sp.sayHello() // Hello, I'm Rose
sp.level // senior
整個(gè)世界都更美好了有沒有!雖然寫法用法一樣,然而這里的class真的是我們熟知的class嗎?
可以看到,class SeniorProgrammer的原型為class Programmer,而class Programmer的原型為匿名函數(shù),匿名函數(shù)的原型為Object構(gòu)造函數(shù)。因此,這里的繼承其實(shí)還是基于原型繼承,而非類繼承。
- class本質(zhì)
可見,JS中的class并非真正的“類”,是由函數(shù)包裝成的,而函數(shù)是對(duì)象的一種。
既然class是對(duì)象,那么他的構(gòu)造函數(shù)和原型又是誰?
class的構(gòu)造函數(shù)也是Function構(gòu)造函數(shù)。如果class存在extends,那么class的原型為其extends對(duì)應(yīng)的class。如果不存在extends,那么class的原型為Object構(gòu)造函數(shù)的原型。
既然JS中的class仍然是以原型方式實(shí)現(xiàn)繼承的,并且class本身是個(gè)函數(shù),那么class中定義的屬性及方法存儲(chǔ)在哪里?
- 存儲(chǔ)位置
寫在constructor中由this.
生成的屬性都存儲(chǔ)在class創(chuàng)建的對(duì)象中,寫在constructor之外的屬性仍存儲(chǔ)在由class創(chuàng)建的對(duì)象中,但是寫在constructor之外的函數(shù)存儲(chǔ)在class的原型中,static修飾的函數(shù)存儲(chǔ)在類本身中。JS的class內(nèi)部方法存儲(chǔ)在原型中這點(diǎn)和“類繼承”的面型對(duì)象很相似,這樣不用每通過類創(chuàng)建一個(gè)對(duì)象就新開辟一塊空間用來存儲(chǔ)方法函數(shù)。
這里有一點(diǎn)值得關(guān)注,由于JS中的class其實(shí)是function,并非我們傳統(tǒng)認(rèn)知的class,所以當(dāng)通過this.
某個(gè)屬性賦值時(shí),即使class的原型鏈有這個(gè)屬性,仍然會(huì)在當(dāng)前對(duì)象中生成新的屬性來存儲(chǔ)這個(gè)值,而非使用原型鏈上的值,這一點(diǎn)與“類繼承”區(qū)別很大。
內(nèi)置對(duì)象
作為補(bǔ)充,最后簡單說一下JS中的內(nèi)置對(duì)象(Math、JSON、Window)。
Math和JSON是平級(jí)的,兩者的constructor都是Object構(gòu)造函數(shù),Object的constructor是Function構(gòu)造函數(shù),而Window的constructor就是Function構(gòu)造函數(shù)。
Have fun!