講清楚之 javascript 對象(一)

有了前面幾節的知識,這一節咱們理解起來就要輕鬆不少。在 javascript 裏函數也是對象,瀏覽器的全局上下文也是對象, key - value 的身影在代碼裏比較常見,合理的使用對象多維度、可擴展的特性能夠爲開發中帶來不少樂趣。javascript

若是知識存在盲區,則實際開發中就會就會應爲評估不足,模型設計不合理出現各類問題, 小則打打補丁、模塊API從新設計,作兼容處理。 大則是關鍵數據維度沒法知足應用場景, 就須要費事費力的進行架構調整或者重構了。java

下面咱們來梳理一下 javascript 對象的表現方式和特色,過於細節的知識就不梳理了。設計模式

JavaScript 的設計是一個簡單的基於對象的範式。一個對象就是一系列屬性的集合,一個屬性包含一個屬性名和一個屬性值。一個屬性的值能夠是函數,這種狀況下屬性也被稱爲 方法。除了瀏覽器裏面預約義的那些對象以外,咱們也能夠定義本身的對象。熟悉 javascript 的語法特性,合理的設計數據模型,建立靈活、不含糊的自定義對象可以提升 javascript 的運行效率。

字面量對象

使用字面量方式建立對象佔據了大多數開發場景,字面量對象示例:數組

let foo = {
    a: 1,
    b: '1234',
    c: function () {
        console.log(this.a + this.b)
    }
}
let foo1 = {
    a: 666,
    b: 'hi',
    c: function () {
        console.log(`${this.b}, ${this.a}`)
    }
}
foo.c() // '11234'
foo1.c() // 'hi, 666'

對象字面量的特色主要是直觀、簡單靈活,每個key、value在編碼階段就是肯定的。瀏覽器

使用對象字面量的方式來建立對象的缺點是,當咱們須要建立多個相同對象時必須爲每一個對象在源代碼中編寫變量和方法。當這樣的相同內容的對象不少時就是一場災難。因而咱們發明了不少其餘建立對象的方式,下面進一步探討。網絡

工廠模式

工廠模式建立對象示例:架構

let createFoo = function (a, b, c) {
    let o = new Object()
    o.a = a
    o.b = b
    o.c = c
    return o
}
let foo = createFoo(1, '1234', function(){
    console.log(this.a + this.b)
})
let foo1 = createFoo(666, 'hi', function(){
    console.log(`${this.b}, ${this.a}`)
})

foo.c() // '11234'
foo1.c() // 'hi, 666'

所謂工廠模式就是對象的建立就像'商品'經過工廠按照標準化的流程被加工出來。app

上面就是一個工廠函數的栗子,執行 createFoo 函數時先建立一個對象 o,而後把傳遞進來的實參添加到 o 上面,最後返回對象 o。這樣每次執行 createFoo 函數都會返回一個新的對象,當咱們須要1000個類似對象時 createFoo 就爲咱們在內部生成了1000個獨立的對象 o。經過對這個栗子的分析會發現: 工廠函數在進行大批量對象建立時對資源的消耗比較大,同時因爲每次都返回的是一個新對象,咱們就沒辦法判斷對象的類型。函數

工廠函數與字面量方式建立對象相比,優點就是不用在編碼階段建立大批量類似結構的對象,而這一系列的建立工做都是在運行階段建立的。每次建立實例時都要建立實例對應的全部屬性和方法,因此工廠函數一樣存在建立N個實例須要建立N個屬性、方法的問題。this

工廠函數建立實例同時也面臨實例類型的問題:

foo instanceof createFoo // false
foo1 instanceof createFoo // false

// 返回的對象是構造函數 Object 的實例
foo instanceof Object // true
foo1 instanceof Object // true
爲何實例函數不相等呢?
在 JavaScript 中 objects 是一種引用類型。兩個獨立聲明的對象永遠也不會相等(由於變量 foo 和 foo1 指向的堆地址不一樣),即便他們有相同的屬性,只有在比較一個對象和這個對象的引用時,纔會返回true.
let too = {
    a: 1
}
let too1 = {
    a: 1
}
let too2 = too1

too == too1 // false
too === too1 // false

too1 == too2 // true
too1 ===too2 // true

構造函數

構造函數方式建立自定義對象,就是利用函數中構造函數原形實例對象之間的關係來封裝私有屬性、公有屬性:

function Foo (a, b, c) {
    this.a = a
    this.b = b
    this.c = c
}
let foo1 = new Foo(1, '1234', function(){
    console.log(this.a + this.b)
})
let foo2 = new Foo(666, 'hi', function(){
    console.log(`${this.b}, ${this.a}`)
})

// foo一、foo2 是 Foo 的實例
foo1 instanceof Foo // true
foo2 instanceof Foo // true

構造函數的實現看着要簡單不少,也能經過實例判斷出類型。

構造函數的執行邏輯:

構造函數初始化階段首先會向上下文棧中壓入一個上下文,接着在變量對象建立的時候會收集實參,初始化函數內部的變量申明、肯定 this 的指向、肯定做用鏈。將實參的值分別拷貝給變量a、b、c。而後像普通函數同樣進入執行階段,執行函數內部語句.

構造函數就是函數 既然構造函數就是普通函數, 那麼爲什在函數前面加一個 new 就能實例化並返回一個對象呢?

咱們來建立一個模擬構造函數加深理解,沒錯是建立一個構造函數(思路來源於網絡, 無恥的偷過來了ɖී؀ීϸ)。

// 假設咱們建立一個汽車對象類型, car函數
function Car(make, model, year) {
    this.make = make
    this.model = model
    this.year = year
    this.drive = function (name) {
        console.log(`${name} drives the ${this.model} ${this.make}`)
    }  
}

// 將函數以參數形式傳入
function New(func) {
    // 聲明一箇中間對象,該對象爲最終返回的實例
    let res = {}
    if (func.prototype !== null) {
        // 將實例的原型指向構造函數的原型
        res.__proto__ = func.prototype
    }
    // ret爲構造函數執行的結果,這裏經過apply,將構造函數內部的this指向修改成指向res,即爲實例對象
    var ret = func.apply(res, Array.prototype.slice.call(arguments, 1))
    // 當咱們在構造函數中明確指定了返回對象時,那麼new的執行結果就是該返回對象
    if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
        return ret
    }
    // 若是沒有明確指定返回對象,則默認返回res,這個res就是實例對象
    return res
}
// 經過new聲明建立實例,這裏的p1,實際接收的正是new中返回的res
let mycar  = New(Car, "Tesla", "Model X", 2018)
mycar.drive('小丸子')
console.log(mycar.make);

// mycar 是 Car 的實例
mycar instanceof Car // true

let mycar = new Car(...) 實例化對象的方式看做是let mycar = New(Car, "Tesla", "Model X", 2018) 的一種簡單的語法糖寫法。

代碼 new Car(...) 執行時,會發生如下事情:

  1. 一個繼承自 Car.prototype 的新對象被建立。
  2. 使用指定的參數調用構造函數 Car ,並將 this 綁定到新建立的對象。new Car 等同於 new Car(),也就是沒有指定參數列表,Car 不帶任何參數調用的狀況。
  3. 構造函數返回的對象就是 new 表達式的結果。若是構造函數沒有顯式返回一個對象,則使用步驟1建立的對象。(通常狀況下,構造函數不返回值,可是用戶能夠選擇主動返回對象,來覆蓋正常的對象建立步驟)

實例類型沒法判斷的問題, 經過構造函數的方式來建立對象完美的解決了。可是構造器函數存在和工廠函數同樣的問題:每次建立一個實例對象時都會在內部新建一箇中間對象,實例方法也會建立N次,這樣就存在沒必要要的內層消耗。

原型與構造函數組合

在上面Car構造函數的栗子中,當建立100個 Car 的實例時內部複製了100次 drive 函數。 雖然每一個 drive 函數的功能同樣,可是因爲分別屬於不一樣的實例就每次都分配獨立的內存空間。

相同的功能函數怎麼忍受得了重複建立。回憶以前咱們在原型一節講到的,每一個函數存在prototype 屬性,經過該屬性指向本身的原型對象。那咱們能夠在函數的原型上作文章,將實例公共的屬性和方法掛載在原型上。實例經過__ptoto__屬性指向了構造函數的原型,從而讓構造函數的原型對象在各個實例的原型鏈上,因而咱們經過構造函數的原型來實現公有屬性和方法的封裝,且只會建立一次。

仍是上面 Car的栗子:

function Car(make, model, year) {
    this.make = make
    this.model = model
    this.year = year
}
Car.prototype.drive = function (name) {
    console.log(`${name} drives the ${this.model} ${this.make}`)
}

let mycar  = new Car( "Tesla", "Model X", 2018)
mycar.drive('小丸子')

上面的栗子也還能夠寫成這樣子:

function Car(make, model, year) {
    this.make = make
    this.model = model
    this.year = year
}

Car.prototype = {
    constructor: Car,
    drive: function () {
        console.log(`${name} drives the ${this.model} ${this.make}`)
    }
}

let mycar  = new Car( "Tesla", "Model X", 2018)
mycar.drive('小丸子')

兩種寫法是等價的,須要注意的是後一種至關於建立一個新對象並賦值給了構造函數Car的原型,若是不將新原型的constructor重現指向構造函數,則會致使構造函數Car的實例類型判斷出錯(instanceof Car 爲 false).

不一樣的實現方法都有各自的使用場景。同時對象的實現方式又與數據維度以及另一個話題 設計模式有關。咱們使用原型與構造函數組合模式就可以解決不少問題。

關於 javascript 的各類模式能夠參考:

Javascript設計模式

相關文章
相關標籤/搜索