JS-創建對象模式-工廠模式、構造函數模式、原型模式、組合模式、動態原型模式、寄生構造函數模式、穩妥構造函數模式

本文首發於我的個人博客 ruiqima.com
原文鏈接:JS 創建對象模式

工廠模式

本質:用函數封裝創建對象的細節。
特點:

  • 顯式創建了對象(如Object
  • 不使用new
  • return語句
  • 缺點:沒有解決對象識別問題,即怎樣知道一個對象的類型。(如代碼倒數第二行的false
function createPerson(name, age) {
  var o = new Object()
  
  o.name = name
  o.age = age
  o.sayName = function () {}
  
  return o
}

var person = createPerson('Joker', 23)

console.log(person instanceof createPerson) 	// false
console.log(person.__proto__)	 //{}

構造函數模式

特點:

  • 沒有顯式創建對象
  • 直接將屬性和方法賦給了this對象
  • 沒有return語句
  • 創建新實例需使用new操作符
  • 可以解決工廠模式不能確定對象類型的問題
  • 缺點:會導致不同實例中的方法不是同一個Function實例,導致不同的作用域鏈和標識符解析。(同一個名爲sayName 的方法在不同實例中是不同Function對象)(如代碼倒數第三行的false
function Person(name, age) {
  this.name = name
  this.age = age
  this.friends = ['1', '2']
  this.sayName = function () {}
}

var person1 = new Person('Joker', 23)
var person2 = new Person('Joker1', 24)

person1.friends.push('3')
console.log(person1.friends) 	//[ '1', '2', '3' ]
console.log(person2.friends)	 //[ '1', '2' ]
console.log(person1.sayName === person2.sayName) 	//false

console.log(person1 instanceof Person) 	//true
console.log(person1.__proto__)	 //Person {}

原型模式

原型與原型鏈

我們創建的每個函數都有一個prototype屬性,即原型屬性。prototype屬性是一個指針,指向一個包含可以由特定類型的所有實例共享的屬性和方法的對象。

prototype就是通過調用構造函數而創建的那個對象實例的原型對象,使用原型對象的好處是可以讓所有對象實例共享包含的屬性和方法。

構造函數、原型和實例的關係

每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。

什麼叫讓所有對象實例共享包含的屬性和方法

直接在對象實例上定義方法的缺點是,不同對象實例中包含的方法不是同一個Function實例——在ECMAScript中的函數是對象,每定義一個函數,就是實例化一個對象。以這種方式創建函數,會導致不同的作用域鏈和標識符解析。

而使用原型對象則可以做到讓所有對象實例共享包含的屬性和方法。

function Person() {}
Person.prototype={
    name :  'defaultName',
    age = 'defaultAge'
}
Person.prototype.sayName = function () {
  console.log(this.name)
}

var person1 = new Person()
var person2 = new Person()

//使用原型對象的好處,可以讓所有對象實例共享它所包含的屬性和方法
console.log(person1.sayName == person2.sayName) // true

原型鏈
原型鏈圖片
有關方法

ObjectName.prototype.isPrototypeOf(instanceName)
實例instanceName的原型是否是ObjectName.prototype。

Object.getPrototypeOf(instanceName)
獲取實例instanceName的原型的名稱。

var person = new Person()
console.log(Person.prototype.isPrototypeOf(person))		// true
console.log(Object.getPrototypeOf(person) == Person.prototype)			//true

原型的動態性

對原型對象的修改會及時體現在實例上,就算在實例創建以後。

var person1 = new Person()
Person.prototype.sayHi = function () {
  console.log('sayHi')
}
person1.sayHi()		// sayHi

但是,如果重寫整個原型對象,情況則會不一樣。

var person2 = new Person()
Person.prototype = {
  sayName: function () {
    console.log('sayName')
  },
}
 person2.sayName()	// person2.sayName is not a function

原因如下圖,初始化person2時與原來的Person.prototype有聯繫,重寫了Person.prototype之後相當於新new了一個出來,與原來那個已經不是同一個了。

在這裏插入圖片描述
在此之後,如果再新建對象實例,則會與新new的prototype建立聯繫,會擁有新prototype上面的屬性,但不會擁有原prototype上的屬性。

var person3 = new Person()
console.log(person3.name)	// undefined
person3.sayName()		// sayName

在這裏插入圖片描述

原型模式的缺點

本質是原型中的所有屬性被很多實例共享的問題。對於是基本類型值的屬性不要緊,但是對於引用類型屬性,更改一個實例上的該屬性值,會導致所有實例中的該屬性值都被更改——因爲改引用類型值是存在於對象原型Prototype上的,而不是對象實例中。

function Person() {}
Person.prototype = {
  arrays: ['1', '2'],
}
var person1 = new Person()
var person2 = new Person()
person1.arrays.push('3')

console.log(person1.arrays)		// [ '1', '2', '3' ]
console.log(person2.arrays)		// [ '1', '2', '3' ]

因此,很少單獨使用原型模式。

組合使用構造函數模式和原型模式(用得最多)

爲解決上述問題,組合使用構造函數模式與原型模式。

優點:構造函數模式用於定義實例,原型模式用於定義方法和共享的屬性。每個實例都會有自己的一份實例屬性的副本,又共享着對方法的引用,最大限度的節省了內存。這種混成模式還支持向構造函數傳遞參數。

// 1. 構造函數模式
function Person(name, age) {
  this.name = name
  this.age = age
  this.friends = ['A', 'B']
}

// 2. 原型模式
Person.prototype = {
  constructor: Person, 		//會讓constructor變爲可枚舉的屬性
  sayName: function () {
    console.log(this.name)
  },
}

var person3 = new Person('Joker', 23)
var person4 = new Person('Rekoj', 32)

// 共用方法(通過原型定義)
// 但引用類型的屬性不會相互干擾(通過構造函數模式定義)
person3.friends.push('fox')
console.log(person3.friends)		// [ 'A', 'B', 'fox' ]
console.log(person4.friends)		// [ 'A', 'B' ] 
console.log(person3.friends === person4.friends)		// false
console.log(person3.sayName === person4.sayName)		//true

動態原型模式

原理跟組合使用構造函數模式和原型模式一樣,但是把原型定義操作封裝在了構造函數中。本質是通過構造函數初始化原型。

在構造函數中的if語句是爲了判斷是否在該對象上定義了sayName的方法——也就是說,當Person的原型上還未定義sayName方法時(如第一次執行new Person(...)語句時),if語句會執行,即進行原型的初始化;一旦Person的原型被初始化過(如第二次執行new Person(...)語句時),根據原型的動態性,sayName已被定義,不會再進入if語句中。

function Person(name, age, job) {
  this.name = name
  this.age = age
  this.friends = ['A', 'B']
  
  if (typeof this.sayName != 'function') {		// !important
    Person.prototype.sayName = function () {
      console.log(this.name)
    }
  }
}

var person3 = new Person('Joker', 23, 'tricker')
var person4 = new Person('Rekoj', 32, 'rekcirt')

person3.friends.push('fox')
console.log(person3.friends)
console.log(person4.friends)
console.log(person3.friends === person4.friends)
console.log(person3.sayName === person4.sayName)

且如果原型上需要定義多個方法和屬性,也只需要一個if語句判斷,選其中一個屬性或方法判斷即可。

要特別注意的是,if語句中的Person.prototype不能使用對象字面量重寫原型,原因之前說過,會切斷現有實例與新原型之間的聯繫。

寄生構造函數模式

使用情況:如,想創建一個具有額外方法的特殊數組,由於不能直接寫修改Array構造函數,因此可以使用這個模式。

特點:

  • 不能依賴instanceof操作符來確定對象類型(如下面的例子,array是instanceof Array,但不是SpecialArray)
  • 使用new操作符
function SpecialArray() {
  var values = new Array()
  values.push.apply(values, arguments)
  values.toSpecialString = function () {
    return this.join('+')
  }
  return values
}

var array = new SpecialArray('a', 'b', 'c')
console.log(array.toSpecialString()) 	// a+b+c

console.log(array instanceof SpecialArray) 	// false
console.log(array.__proto__) 	// []
console.log(array instanceof Array)		//	true

穩妥構造函數模式

特點:

  • 適用於一些安全的環境中(如會禁止使用thisnew),或防止數據被其它應用程序改動(如只允許通過方法訪問到屬性值)
  • 新創建對象的實例方法不使用this
  • 不適用new操作符
  • 不能依賴instanceof操作符來確定對象類型(跟寄生構造函數模式類似)
function Person(name, age) {
  var o = new Object()

  o.sayName = function () {
    console.log(name)
  }
  return o
}

var person = Person('Joker', 23)
console.log(person.name) 		//undefined
person.sayName() 		//Joker

本文首發於我的個人博客ruiqima.com
原文鏈接:https://www.ruiqima.com/zh/post/js-createobj/
本博客內文章除特別聲明外均爲原創,採用CC BY-NC-SA 4.0 許可協議進行許可。超出CC BY-NC-SA 4.0 許可協議的使用請聯繫作者獲得授權。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章