JavaScript 创建对象的几种方式

我们通过模拟在战略类游戏中创建「美国大兵」这个对象的情景,来理解各种创建对象的方式。

工厂模式

《JavaScript 高级程序设计》6.2.1 章节对工厂模式的描述

工厂模式是一种软件工程领域的一种常见设计模式,由于在 ECMAScript 中无法创建类,开发人员发明一种函数,用函数来封装以特定接口创建对象的细节

function createSoldier(id){
  var soldier = new Object()
  soldier.ID = id
  soldier.生命值 = 50
  soldier.攻击力 = 5
  return soldier
}
var soldier1 = createSoldier('001')
var soldier2 = createSoldier('002')

我们创建了一个函数createSoldier,只需要传递参数给这个函数,函数内部会创建一个对象 soldier,并在对象中加入各属性和方法,最后将这个对象 return 出去。但这中模式存在的问题是无法识别对象类型

构造函数模式

function Soldier(id){
  this.ID = id
  this.生命值 = 50
  this.攻击力 = 5
  this.攻击 = function(){/* ⚔️ 砍一刀 */}
  this.防御 = function(){/* 🛡 举起盾牌 */}
}
var soldier1 = new Soldier('001')
var soldier2 = new Soldier('002')
soldier1.攻击 === soldier2.攻击 // false

经过这样子的改写成构造函数的模式,我们将能够识别对象的类型。

Soldier1 instanceof Soldier // true
Soldier2 instanceof Soldier // true
Soldier2 instanceof Object // true

但这样每一个创建的实例内都会创建一个新的函数,而这写函数却是做的同一件事情。换个说法就是:这个攻击()函数本应该是每个soldier实例共享方法,但现在却是每个对象自己新建了一个攻击()函数。所以针对这一点,我们可以进行如下的优化:

function Soldier(id){
  this.ID = id
  this.生命值 = 50
  this.攻击力 = 5
  this.攻击 = 攻击
  this.防御 = 防御
}
function 攻击(){
  /* ⚔️ 砍一刀 */
}
function 攻击(){
  /* 🛡 举起盾牌 */
}
Soldier1.攻击 === soldier2.攻击 // true

我们将函数定义转移到构造函数外部,在构造函数里面将 攻击属性设置成等于全局的 攻击()函数。达到了我们共享方法的目的。 但这样一来在,由于攻击保存的是指向函数的指针,因此 Soldier1Soldier2对象就共享了全局作用域中定义的攻击()函数。如果公共用方法很多,将会定义很多全局函数,这样就导致了定义的引用类型没有任何封装性了。

原型模式

  • 原型模式的最纯粹的写法
function Soldier(){

}
Soldier.prototype.ID = '001'
Soldier.prototype.生命值 = 50
Soldier.prototype.攻击力 = 5
Soldier.prototype.攻击 = function (){/* ⚔️ 砍一刀 */}
Soldier.prototype.防御 = function (){/* 🛡 举起盾牌 */}
  • 更简单一点的原型写法
function Soldier(){}
Soldier.prototype = {
  constructor: Solider,
  this.ID = '001'
  this.生命值 = 50
  this.攻击力 = 5
  this.攻击 = function (){/* ⚔️ 砍一刀 */}
  this.防御 = function (){/* 🛡 举起盾牌 */}
}

这样的写法可以减少重复的输入的Soldier.prototype这段代码,而且从视觉上更好的封装了原型的功能。这中方式来写代码,用字面量的形式重写了原型对象。应该了解原型链内部原理(即原型指针的指向情况)从而避免问题

再来讨论一个原型模式的问题:

  1. 所有的属性和方法都共享
  2. 不能传入初始化参数
function Soldier(){}
Soldier.prototype = {
  constructor: Solider,
  this.ID: '001',
  this.生命值: 50,
  this.攻击力: 5,
  this.攻击: function (){/* ⚔️ 砍一刀 */},
  this.防御: function (){/* 🛡 举起盾牌 */},
  this.技能: ['采矿⛏','伐木🌲']
}
// 我们想给实例 soldier1 添加私有属性
soldier1.技能.push['修理🔧']
// 但是看看 soldier2 的输出,结果是依然会被共享
console.log(soldier2.技能)

⭐️ 组合使用构造函数模式和原型模式

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时有共享着对方法的引用,最大限度地节省了内存。

function Soldier(id){
  this.ID = id
  this.生命值 = 50
  this.攻击力 = 5
this.技能 = ['采矿⛏','伐木🌲']
}
Soldier.prototype = {
  constructor: Soldier,
  攻击: function (){/* ⚔️ 砍一刀 */},
  防御: function (){/* 🛡 举起盾牌 */},
}
var soldier1 = new Soldier('001')
var soldier1 = new Soldier('002')

这种模式,是目前在 ECMAScript 中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是同来定义引用类型的一种默认模式。

动态原型模式

在其他面向对象语言经验的程序员看到独立的构造函数和原型是,很可能会感到非常困惑。动态原型模式就是为解决这个问题而提出的一个方案,它把所有信息都封装在了构造函数中,而通过构造函数中初始化原型。看看这种模式的写法

function Soldier(id) {
  this.ID = id
  this.生命值 = 50
  this.攻击力 = 5
  this.攻击 = '测试'
  this.技能 = ['采矿⛏', '伐木🌲']
  if (typeof this.攻击 != 'function') {
    Soldier.prototype.攻击 = function() { /* ⚔️ 砍一刀 */ }
    Soldier.prototype.防御 = function() { /* 🛡 举起盾牌 */ }
  }
}

var soldier1 = new Soldier('001')

这里的 if 语句只是判断这是否为第一次 new Soldier,此后再调用构造函数来实例化对象时,原型已经完成初始化,不需要再做什么修改了,if 语句内便不再执行。所以说不必用一大堆 if 语句检查每个属性、每个方法;只要检查一次即可。不要被例子中的代码给迷惑了

‼️注意: 在 if 语句里就不能再使用字面量的写法了。是因为如果用字面量重写原型对象的话,初次创建的实例会有问题。因为使用构造函数时先创建了一个隐式原型对象。此时第一个实例的__proto__已经指向这个隐式原型对象。然后再运行构造函数的代码,这个 if 语句内的方法就会写入到新的原型对象。而第一个实例对象的__proto__还是指向之前那个隐式原型对象,导致第一个实例对象就没有 if 语句内的定义的这些方法了。这种情况画一个原型图是最好理解的

使用 ES6 的 Class

类声明仅仅是基于已有自定义类型声明的语法糖,使用typeof来查看类型最终的返回结果是function,所以实际上创建的类是一个具有构造函数方法行为的函数。我们在这里只熟悉 Class 的写法,对于和类之前我们前面研究的 ES5 自定义类型的具体差异,留在后面深入学习 ES6 时再具体的了解。

  // 等价于 ES5 中的 Soldier 构造函数
  constructor(id){
    this.ID = id
    this.生命值 = 5
    this.技能 = ['采矿⛏','伐木🌲']
  }
  // 等价于 Soldier.prototype.攻击
  攻击(){
    /* ⚔️ 砍一刀 */
  }
  防御(){
    /* 🛡 举起盾牌 */
  }
}
var soldier1 = new Soldier('001')

上面代码定义了一个「类」,可以看到有一个constructor方法,这就是构造方法,构造方法当中的this关键字则代表实例对象,所以创建的是实例对象的私有属性,不会出现在原型上。且由于这种类使用简洁语法来定义方法,因而不需要添加function关键字

「寄生构造函数模式」和「稳妥构造函数模式」

这两中模式没有看得太明白,就先占个位置在这儿吧。以后能领悟的话再回来写一写吧

参考链接