大話JavaScript對(duì)象

前言

寫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ì)象的屬性及方法的刪除與添加操作起來也很方便:

JavaScript動(dòng)態(tài)性
  • 什么是繼承?
    你也許會(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ù)有哪些特征?

  1. 為區(qū)別于普通函數(shù),通常構(gòu)造函數(shù)名首字母大寫
  2. 構(gòu)造函數(shù)必須通過new命令調(diào)用
  3. 構(gòu)造函數(shù)內(nèi)部使用this關(guān)鍵字,this指向當(dāng)前構(gòu)造函數(shù)生成的對(duì)象
  4. 構(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)建的?

函數(shù)的constructor

可見,不單構(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()
prototype屬性

可以看到,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()
對(duì)象的源頭

可見,原型鏈的源頭為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è)置原型:getPrototypeOfsetPrototypeOf,這樣我們不必直接操縱__proto__屬性:
    getPrototypeOf

以getPrototypeOf這個(gè)方法為例,他是通過Object構(gòu)造函數(shù)調(diào)用的,所以必然存在于Object構(gòu)造函數(shù)或者他的原型鏈中。因?yàn)槭切略龇椒ǎ⑶覂H能通過Object構(gòu)造函數(shù)調(diào)用,猜測(cè)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 SeniorProgrammer的原型為class Programmer,而class Programmer的原型為匿名函數(shù),匿名函數(shù)的原型為Object構(gòu)造函數(shù)。因此,這里的繼承其實(shí)還是基于原型繼承,而非類繼承。

  • class本質(zhì)
class == function

可見,JS中的class并非真正的“類”,是由函數(shù)包裝成的,而函數(shù)是對(duì)象的一種。

既然class是對(duì)象,那么他的構(gòu)造函數(shù)和原型又是誰?

class的構(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ǔ)位置
存儲(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)。


內(nèi)置對(duì)象

Math和JSON是平級(jí)的,兩者的constructor都是Object構(gòu)造函數(shù),Object的constructor是Function構(gòu)造函數(shù),而Window的constructor就是Function構(gòu)造函數(shù)。


Have fun!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容