摘要:工廠函數創建實例同時也面臨實例類型的問題返回的對象是構造函數的實例為什么實例函數不相等呢在中是一種引用類型。使用指定的參數調用構造函數,并將綁定到新創建的對象。構造函數返回的對象就是表達式的結果。
有了前面幾節的知識,這一節我們理解起來就要輕松很多。在 javascript 里函數也是對象,瀏覽器的全局上下文也是對象, key - value 的身影在代碼里比較常見,合理的使用對象多維度、可擴展的特性可以為開發中帶來很多樂趣。
如果知識存在盲區,則實際開發中就會就會應為評估不足,模型設計不合理出現各種問題, 小則打打補丁、模塊API重新設計,做兼容處理。 大則是關鍵數據維度無法滿足應用場景, 就需要費事費力的進行架構調整或者重構了。
下面我們來梳理一下 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"
所謂工廠模式就是對象的創建就像"商品"通過工廠按照標準化的流程被加工出來。
上面就是一個工廠函數的栗子,執行 createFoo 函數時先創建一個對象 o,然后把傳遞進來的實參添加到 o 上面,最后返回對象 o。這樣每次執行 createFoo 函數都會返回一個新的對象,當我們需要1000個相似對象時 createFoo 就為我們在內部生成了1000個獨立的對象 o。通過對這個栗子的分析會發現: 工廠函數在進行大批量對象創建時對資源的消耗比較大,同時由于每次都返回的是一個新對象,我們就沒辦法判斷對象的類型。
工廠函數與字面量方式創建對象相比,優勢就是不用在編碼階段創建大批量相似結構的對象,而這一系列的創建工作都是在運行階段創建的。每次創建實例時都要創建實例對應的所有屬性和方法,所以工廠函數同樣存在創建N個實例需要創建N個屬性、方法的問題。
工廠函數創建實例同時也面臨實例類型的問題:
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}`) }) // foo1、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(...) 執行時,會發生以下事情:
一個繼承自 Car.prototype 的新對象被創建。
使用指定的參數調用構造函數 Car ,并將 this 綁定到新創建的對象。new Car 等同于 new Car(),也就是沒有指定參數列表,Car 不帶任何參數調用的情況。
構造函數返回的對象就是 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設計模式
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107884.html
摘要:講清楚之參數傳值參數傳值是指函數調用時,給函數傳遞配置或運行參數的行為,包括通過進行傳值。所以對的賦值會改變上下文棧中標識符保存的具體值此時如果使用的是按引用傳遞,則變量所指向的對象因該也被賦值為。 講清楚之 javascript 參數傳值 參數傳值是指函數調用時,給函數傳遞配置或運行參數的行為,包括通過call、apply 進行傳值。 在實際開發中,我們總結javascript參數傳...
摘要:構造函數和實例都通過屬性指向了原形。代碼示例是構造函數的實例的屬性與的屬性保存的值相等,即他們指向同一個對象原形。 講清楚之javascript原型 標簽: javascript javascript 中原形是一個比較難于理解的概念。javascript 權威指南在原形這一章也花了大量的篇幅進行介紹,也許你已經讀過javascript 權威指南,或者已經是讀第N篇了,然而這篇文章的目...
閱讀 2538·2023-04-26 00:57
閱讀 911·2021-11-25 09:43
閱讀 2221·2021-11-11 16:55
閱讀 2206·2019-08-30 15:53
閱讀 3592·2019-08-30 15:52
閱讀 1459·2019-08-30 14:10
閱讀 3379·2019-08-30 13:22
閱讀 1209·2019-08-29 11:18