對象(一)--對象的創建
JavaScript創建對象的幾種模式
前言
我們經常聽到js中一切皆對象
(其實並不,還存在基本數據類型的值), 可以知道對象在javascript中的普遍性和重要性, 但其實上面那句話中的對象側重點的更像是一個整體引用類型
, 而我們在這裏說的是自定義對象, 創建一個自定義對象可以是字面量{}
直接創建, 也可以使用new Object()
來創建, 都很方便
但是當我們要大量創建同一種類型的對象時, 就需要編寫大量的重復代碼, 辟如創建一個有名字和年齡的人let p1 = { name: ‘p1‘, age: 21 }
, 再編寫一個一樣有名字和年齡的人, let p2 = { name: ‘p2‘, age: 22 }
類
,而這些人其實就是類的實例化對象
, 可惜的是js中並沒有類這個概念, 因此就要介紹之後的幾種模式來達到類這樣的效果
工廠模式
創建一個函數來抽象創建具體對象的過程
function createPerson(name, age) { var p = new Object() p.name = name p.age = age p.sayHello = function() { console.log(`Hello, my name is ${this.name}`) } return p } let p1 = createPerson(‘p1‘, 21) console.log(p1.name) // p1 console.log(p1.age) // 21 console.log(p1.sayHello()) // Hello, my name is p1 let p2 = createPerson(‘p2‘, 22) console.log(p2.name) // p2 console.log(p2.age) // 22 console.log(p2.sayHello()) // Hello, my name is p2
我們可以看到用工廠模式利用函數可以大大簡化人
這個對象的實例化操作, 但是有個問題註意到沒有, 就是這些對象無法歸類, 它們到底屬於哪個類呢?從內部代碼中也能容易地看出它只是一個Object
類型的實例化對象, p1 instanceof Object // true
構造函數模式
雖然工廠模式做到了抽象實例化對象的操作,但是它無法歸類。構造函數很好地解決了這個問題
什麽是構造函數呢, 其實吧構造函數就是一個函數, JavaScript並沒有指定語法規則來區分構造函數與普通函數。它們的唯一區別僅在於它們的調用方式, 我們在會構造函數前面加new
操作符來表示這是一次構造函數的調用而不是普通函數, 另外既然js無法區分這是構造函數還是普通函數, 那規則就取決於我們開發者, 一般我們會將構造函數名首字母大寫加以區分
還是實現上面的person的例子
function Person(name, age) {
this.name = name
this.age = age
this.sayHello = function() {
console.log(`Hello, my name is ${this.name}`)
}
}
let p1 = new Person(‘p1‘, 21)
console.log(p1.name) // p1
console.log(p1.age) // 21
console.log(p1.sayHello()) // Hello, my name is p1
同樣的, 我們利用構造函數的模式完成對象的創建, 並且與工廠模式不同的是利用構造函數的方式我們做到了對象的歸類, 如創建好的p1
其實就是Person
這個構造函數的實例化對象, 測試p1 instanceof Person // true
, 可以說是工廠模式的bug修復版了
現在我們來說說這個構造函數, 第一點可以看出來我們用首字母大寫的方式來命名函數名, 其次我們在函數內部看到this
, 這個this
有什麽作用呢。
- 當函數用
new
進行調用時(執行函數內部的[[Constructor]]
方法),函數內部會創建一個對象,this
即指向這個對象, 把屬性和方法都定義在這個對象上最後return返回 - 未使用
new
直接調用的話(執行內部的[[Call]]
方法), 這個this
指向的是全局, 瀏覽器的就是window
, 即會給window
添加屬性和方法
需要註意的地方
- 當作為構造函數的時候內部不應該寫
return
, 否則返回是return
後面跟著的對象而非this
指向的實例對象 - 箭頭函數內部沒有
[[Constructor]]
方法, 因此它無法作為構造函數
看似構造函數模式已經實現地挺歐克了, 但是!但是把方法直接寫入到一個構造函數內部是有問題的, 盡管實例也都有了這個方法, 它的主要問題是每個對象在實例化過程都重新創建了這個sayHello
, 而各個sayHello
指向的又不是同一個函數對象, 這就造成了內存和性能上的浪費, 明明使用同一個方法就行了
let p1 = new Person(‘person1‘, 21)
let p2 = new Person(‘person2‘, 22)
p1.sayHello === p2.sayHello // false
那把這個sayHello
提取出來怎麽樣
function sayHello() {
console.log(`Hello, my name is ${this.name}`)
}
function Person(name, age) {
this.name = name
this.age = age
this.sayHello = sayHello
}
這單純解決了上面的問題, 但又有個問題就是:把方法定義在全局中而不是相關的類中, 這樣就沒什麽封裝性可言。而且要是有很多構造函數,很多方法都是采取這種形式的話, 那全局環境該成什麽樣。為了解決構造函數方法定義的問題, 原型模式登場了
原型模式
- 構造函數中有一個
prototype
屬性,它指向函數的原型對象 - 原型對象有一個
constructor
屬性,它指回構造函數 - 由構造函數
new
實例化的對象,都有一個__proto__
屬性(或者Object.getPrototypeOf()
), 它也指向函數的原型對象
綜上所述, 邏輯結構參照如下
原型模式的特點就在於, 實例對象可以訪問其屬性, 比如訪問實例對象p1
的name
屬性
- 首先會在p1自身中查詢是否有
name
這個屬性, 有的話則返回 - 若自身沒有這個屬性, 就會去構造函數的原型對象中查詢, 有
name
則返回 - 若原型對象中也沒有
name
這個屬性呢, 其實原型對象也是一個對象啊,它也會有它的構造函數的原型對象, 因此繼續按照以上步驟查詢,終點是null
(Object.prototype.__proto__
), 最後還是沒法訪問到的話即返回undefiend
Person = function() {}
Person.prototype = {
name: ‘person‘,
age: ‘20‘,
testArr: [1, 2, 3],
sayHello() {
console.log(`Hello, my name is ${this.name}`)
}
}
p1 = new Person()
p2 = new Person()
console.log(p1.testArr) // [1, 2, 3]
console.log(p2.testArr) // [1, 2, 3]
p1.testArr.push(4)
console.log(p1.testArr) // [1, 2, 3, 4]
console.log(p2.testArr) // [1, 2, 3, 5]
可以看到通過原型模式有兩大問題:
- 無法傳參, 所有對象訪問的屬性其實同一個屬性值
- 基於問題1所有訪問的屬性都是同一個值,若屬性值為引用類型
Array
或者Object
, 往裏添加或者刪除都是會影響所有對象的訪問結果
值的需要註意的是原型對象的重寫
Person = function() {}
p1 = new Person()
Person.prototype = {
name: ‘p‘,
age: ‘21‘
}
console.log(p1.name) // undefined
這段代碼看上去只是修改了下實例對象和原型對象的位置, 但是結果截然不同
因為原型對象已經重新賦值了, 在p1
實例化後它指向的原先的原型對象(此時我們並沒有對它添加屬性, 它只有默認construcotr
還有其他從Object
繼承來的方法), 但是Person.prototyoe
卻指向了另外一個對象
因此原型對象的賦值應該要小心, 還有若賦值的話也要重寫constructor
指回構造函數(默認enumerable
為false
)
function Person() {}
descriptor = Object.getOwnPropertyDescriptor(Person.prototype, ‘constructor‘)
// {value: ?, writable: true, enumerable: false, configurable: true}
Person.prototype = {
constructor: Person
}
descriptor = Object.getOwnPropertyDescriptor(Person.prototype, ‘constructor‘)
// {value: ?, writable: true, enumerable: true, configurable: true}
構造函數和原型組合模式
結合構造函數和原型模式的特點, 新的組合模式誕生
簡單來說就是:
- 把屬性寫到構造函數內部
- 把方法添加到原型對象屬性上
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`)
}
let p1 = new Person(‘person1‘, 21)
let p2 = new Person(‘person2‘, 22)
console.log(p1 instanceof Person && p2 instanceof Person) // true
console.log(p1.sayHello()) // Hello, my name is person1
console.log(p2.sayHello()) // Hello, my name is person2
console.log(p1.sayHello === p2.sayHello) // true
Perfect!其實ES6添加的class就是基於這種構造函數和原型組合的模式的語法糖, 而非引入類
好吧, 先總結到這, 之後再講下繼承和class的繼承
對象(一)--對象的創建