1. 程式人生 > >對象(一)--對象的創建

對象(一)--對象的創建

com 修改 eof 規則 null 對象的訪問 html 無法 prop

JavaScript創建對象的幾種模式

前言

我們經常聽到js中一切皆對象(其實並不,還存在基本數據類型的值), 可以知道對象在javascript中的普遍性和重要性, 但其實上面那句話中的對象側重點的更像是一個整體引用類型, 而我們在這裏說的是自定義對象, 創建一個自定義對象可以是字面量{}直接創建, 也可以使用new Object()來創建, 都很方便

但是當我們要大量創建同一種類型的對象時, 就需要編寫大量的重復代碼, 辟如創建一個有名字和年齡的人let p1 = { name: ‘p1‘, age: 21 }, 再編寫一個一樣有名字和年齡的人, let p2 = { name: ‘p2‘, age: 22 }

, 那如果要再創建100個人或者這個人還要有身高,體重等特征呢,我們會發現代碼編寫的工作量會十分的龐大。其實有其他編程語言的人會很容易地發現,其實這個就是類需要完成的工作,只要人抽象成一個,而這些人其實就是類的實例化對象, 可惜的是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有什麽作用呢。

  1. 當函數用new進行調用時(執行函數內部的[[Constructor]]方法),函數內部會創建一個對象, this即指向這個對象, 把屬性和方法都定義在這個對象上最後return返回
  2. 未使用new直接調用的話(執行內部的[[Call]]方法), 這個this指向的是全局, 瀏覽器的就是window, 即會給window添加屬性和方法

需要註意的地方

  1. 當作為構造函數的時候內部不應該寫return, 否則返回是return後面跟著的對象而非this指向的實例對象
  2. 箭頭函數內部沒有[[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
}

這單純解決了上面的問題, 但又有個問題就是:把方法定義在全局中而不是相關的類中, 這樣就沒什麽封裝性可言。而且要是有很多構造函數,很多方法都是采取這種形式的話, 那全局環境該成什麽樣。為了解決構造函數方法定義的問題, 原型模式登場了

原型模式

  1. 構造函數中有一個prototype屬性,它指向函數的原型對象
  2. 原型對象有一個constructor屬性,它指回構造函數
  3. 由構造函數new實例化的對象,都有一個__proto__屬性(或者Object.getPrototypeOf()), 它也指向函數的原型對象

綜上所述, 邏輯結構參照如下
技術分享圖片
原型模式的特點就在於, 實例對象可以訪問其屬性, 比如訪問實例對象p1name屬性

  1. 首先會在p1自身中查詢是否有name這個屬性, 有的話則返回
  2. 若自身沒有這個屬性, 就會去構造函數的原型對象中查詢, 有name則返回
  3. 若原型對象中也沒有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. 無法傳參, 所有對象訪問的屬性其實同一個屬性值
  2. 基於問題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指回構造函數(默認enumerablefalse)

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的繼承

對象(一)--對象的創建