Skip to content
本页目录

对象创建模式

该篇内容是结合自己理解后总结自红宝书《JavaScript高级程序设计(第4版)》相关内容。

我们最常见的的创建对象方法一定是使用Object构造函数或者直接通过对象字面量的方式创建对象,这类方式的优点就是创建单一对象时,极为方便。 然而这并不能满足我们日常开发需求,本篇将总结常见的对象创建模式及优缺点。

题外话:当创建空对象时,对象字面量{}、构造函数new Object()create方法Object.create(null)这仨是完全等效的!

工厂模式

JavaScript
function Kon(name, age) {
  const _o = new Object()
  _o.name = name
  _o.age = age
  _o.info = function () {
    return this.name + '-' + this.age
  }
  return _o
}
const yui = Kon('yui', 14)
console.log(yui.info())  // yui-14
const mio = Kon('mio', 14)
console.log(mio.info())  // mio-14
// 缺点:不能正确查找原型链
yui instanceof Kon  // false
mio instanceof Kon  // false
  • 优点:解决了使用一个接口创建多个相似对象时会产生大量重复代码的问题;
  • 缺点:未解决对象识别(instanceof不能正确查找原型链)问题。

寄生构造模式

和工厂模式唯一的区别就是:工厂模式直接调用构造函数,而寄生构造模式是通过new操作符调用!

JavaScript
function Kon(name, age) {
  const _o = new Object()
  _o.name = name
  _o.age = age
  _o.info = function () {
    return this.name + '-' + this.age
  }
  return _o
}
const yui = new Kon('yui', 14)
console.log(yui.info())  // yui-14
const mio = new Kon('mio', 14)
console.log(mio.info())  // mio-14
// 缺点:不能正确查找原型链
yui instanceof Kon  // false
mio instanceof Kon  // false
  • 缺点:同工厂模式,未解决对象识别(instanceof不能正确查找原型链)问题;
  • 优点:某些特殊情况下,可以用来为对象创建构造函数,比如在不改变Array构造函数(原型构造函数Array.prototype上)的前提下,为实例添加排序方法:
JavaScript
function sortArray() {
  const sort = new Array()
  sort.push.apply(sort, arguments)
  sort.up = function (flag) {
    if (flag) {
      return sort.sort((a, b) => a - b)
    } else {
      return sort.sort((a, b) => b - a)
    }
  }
  return sort
}
const sort = new sortArray(1, 5, 6, 27, 8, 2, 3)
sort.up(true)   // [1, 2, 3, 5, 6, 8, 27, up: ƒ]
sort.up(false)  // [27, 8, 6, 5, 3, 2, 1, up: ƒ]

稳妥构造模式

此处的稳妥是指稳妥对象:没有公共属性,其方法也不引用this的对象。
与寄生构造模式的区别:新创建的实例方法不引用this、不使用new操作符调用构造函数。

JavaScript
function Kon(name, age) {
  const o = new Object(),
    _name = name,
    _age = age
  o.info = function () {  // 闭包
    return _name + '-' + _age;
  }
  return o
}
const yui = Kon('yui', 14)
yui.info()  // 'yui-14'
const mio = Kon('mio', 14)
mio.info()  // 'mio-14'
// 缺点:不能正确查找原型链
yui instanceof Kon  // false
mio instanceof Kon  // false
  • 优点:不可以通过为函数添加方法来访问、修改函数的内部数据;
  • 缺点:同工厂模式、寄生构造模式一样,未解决对象识别(instanceof不能正确查找原型链)问题。

构造函数模式

JavaScript
function Kon(name, age) {
  this.name = name
  this.age = age
  this.info = function () {
    return this.name + '-' + this.age
  }
}
const yui = new Kon('yui', 14)
console.log(yui.info())  // yui-14
const mio = new Kon('mio', 14)
console.log(mio.info())  // mio-14
// 正确查找原型链
yui instanceof Kon  // true
mio instanceof Kon  // true
  • 优点:解决了工厂模式中对象识别问题(对象中constructor属性最初的作用便是用来表示对象类型) ;

  • 缺点:

    1. 每个方法都会在每个实例上重写一遍(即构造函数中方法不能在不同实例中复用),为什么这样呢?

    因为不同实例中的同名方法均为不同Function的实例(记住在JavaScript中函数是对象,每定义一个函数就等于实例化了一个对象); 创建实例时,构造函数中函数声明函数表达式,会导致不同作用域链和标识符解析,比如:yui.info === mio.info,会返回false,如何修复这一问题呢?

    解决办法:在构造函数外部共享方法

    JavaScript
    function Kon(name, age) {
      this.name = name
      this.age = age
      this.info = info
    }
    function info() {
      return this.name + '-' + this.age
    }
    const yui = new Kon('yui', 14)
    const mio = new Kon('mio', 14)
    yui.info === mio.info  // true
    1. 上述解决方法也会导致新的问题,那就是当对象定义的方法很多时,就没有任何封装性可言了。

原型模式

通过原型链篇我们知道,实例中的隐式原型[[prototype]](即.__proto__),仅指向构造函数的显式原型prototype不指向构造函数

JavaScript
function Kon() { }
Kon.prototype = {
  // constructor: Kon,  // 使Kon.prototype的constructor属性的[[Enumerable]]值变为true
  name: 'yui',
  age: 14,
  info: function () {
    return this.name + '-' + this.age
  }
}
Object.defineProperty(Kon.prototype, 'constructor', {
  enumerable: false,
  value: Kon
})
const yui = new Kon()
console.log(yui.info())  // yui-14
  • 优点:
    1. 解决了构造函数模式中的封装性问题、不正常的作用域和标识符解析问题;
    2. 对对象原型的任何修改都能立即在实例上体现。
  • 缺点:
    1. 不能为构造函数传递初始化参数;
    2. 共享的本性(引用类型)导致的问题,如下例:
    JavaScript
    function Kon() { }
    Kon.prototype = {
      tea: ['yui', 14],
      info: function () {
        return this.tea
      }
    }
    Object.defineProperty(Kon.prototype, 'constructor', {
      enumerable: false,
      value: Kon
    })
    let yui = new Kon()
    yui.tea.push('吉太')
    let mio = new Kon()
    mio.info()  // ["yui", 14, "吉太"]

原型对象上引用类型意外的共享问题,会使实例不再拥有自己的独有属性,这也是原型模式最大的问题!!!

构造原型模式

通常我们称作组合模式,即组合使用构造函数模式和原型模式,其规则定义为: 将实例属性定义在构造函数中;由所有实例共享的属性和方法定义在原型对象中。

JavaScript
function Kon(name, age) {
  this.name = name
  this.age = age
  this.tea = []
}
Kon.prototype = {
  constructor: Kon,  // 使Kon.prototype的constructor属性的[[Enumerable]]值变为true
  info: function () {
    return this.name + '-' + this.age + '-' + this.tea.join('')
  }
}
const yui = new Kon('yui', 14)
yui.tea.push('吉太')
yui.info()  // 'yui-14-吉太'
const mio = new Kon('mio', 14)
mio.tea.push('蓝白碗')
mio.info()  // 'mio-14-蓝白碗'
  • 优点:每个实例都有自己的一份实例属性的副本,同时又共享者对方法的引用。

动态原型模式

动态原型模式,即优化版的组合模式,其规则定义为: 将所有信息都封装在构造函数中,在必要的情况下才会初始化原型。

JavaScript
function Kon(name, age) {
  this.name = name
  this.age = age
  this.tea = []
  if (typeof this.info !== 'function') {
    Kon.prototype.info = function () {
      return this.name + '-' + this.age + '-' + this.tea.join('')
    }
  }
}
const yui = new Kon('yui', 14)
yui.tea.push('吉太')
yui.info()  // 'yui-14-吉太'
const mio = new Kon('mio', 14)
mio.tea.push('蓝白碗')
mio.info()  // 'mio-14-蓝白碗'
  • 优点:将所有信息都封装在构造函数中,在必要的情况下才会初始化原型,同时又保持了组合模式的优点;

  • 缺点:在初始化原型时不能使用对象字面量,像下面的实例(注意代码高亮部分):

    JavaScript
    function Kon(name, age) {
      this.name = name
      this.age = age
      this.tea = []
      if (typeof this.info !== 'function') {
        Kon.prototype = {
          info: function () {
            return this.name + '-' + this.age + '-' + this.tea.join('')
          }
        }
      }
    }
    const yui = new Kon('yui', 14)
    yui.tea.push('吉太')
    yui.info()  // Uncaught TypeError: yui.info is not a function
    const mio = new Kon('mio', 14)
    mio.tea.push('蓝白碗')
    mio.info()  // 'mio-14-蓝白碗'

    为什么会这样呢?为什么行15会报错,而行18有正常执行了呢?

    让我们先回顾下调用new操作符时发生的事情,以const yui = new Kon('yui', 14)为例:

    1. 首先创建实例对象,将实例对象的[[prototype]]属性链接到构造函数的prototype上;
    2. 然后将构造函数的作用域赋值给该实例对象(即将this绑定到该实例);
    3. 接着执行构造函数,为该实例添加属性和方法;
    4. 最后返回该实例对象。

    根据以上过程来分析原因:

    • 首先当步骤1完成时有以下关系:yui.__proto__ === Kon.prototype(重写前,可理解为此时Kon.prototype的内存地址指针为1);
    • 当执行步骤3时,由于yui.info此时为undefined(注意这里其实进行了两次查找:第一次是yui自身;第二次是在原型yui.__proto__上查找), 故if(typeof this.info !== 'function')
      • 使用对象字面量重写了构造函数的原型Kon.prototype(初始化info方法),重写后Kon.prototype对象拥有info方法(可理解为此时Kon.prototype的内存地址指针为2), 而此时的yui.__proto__持有的原型对象引用为重写前的原型对象的引用(内存地址指针为1的那个),此原型对象上并无info方法,故行15报错。

    伪代码来描述这一过程:

    JavaScript
    当执行 const yui = new Kon('yui', 14)时:
      1. 生成实例对象 yui, 链接原型 yui.__proto__ = Kon.prototype = { }
      2. 绑定 this
      3. 执行函数体, 添加属性和方法:
          - 添加属性 yui.name, yui.age
          - 添加方法, 由于 yui.info 此时为 undefined, 注意!!! 此处查找了两次:
            - 第一次是实例对象本身 yui.info, 查找结果为 undefined;  
            - 第二次是沿着原型链在原型上查找 yui.__proto__info, 结果也为 undefined
    typeof this.info !== 'function'true, 执行代码,
          重写构造函数原型, 又由于是对象字面量, Kon.prototype 指向新的(内存地址)引用, 此时
          Kon.prototype = {
            info() { }
          }
      4. 返回实例对象 yui:
          - 注意此时的 yui.__proto__ 依然指向 Kon.prototype 重写前的引用,{ };
          - 而此时的 Kon.prototype 为重写后的 { info() { } }
    所以当代码执行 yui.info()时,由于 yui 自身没有 info方法, 所以会查找原型链,即 yui.__proto__, 
    而 yui.__proto__ = { },也没有 info方法, 故此时JS引擎抛出 Uncaught TypeError: yui.info is not a function
    
    当执行 const mio = new Kon('mio', 14):
      1. 生成实例对象 mio, 链接原型 mio.__proto__ = Kon.prototype = { info() { } }
      2. 绑定 this
      3. 执行函数体, 添加属性和方法:
          - 添加属性 mio.name, mio.age
          - 添加方法, 由于 mio.info 此时是 function, 注意!!! 此处查找了两次
            - 第一次是实例对象本身 mio.info, 查找结果为 undefined;  
            - 第二次是沿着原型链在原型上查找 mio.__proto__info, 结果为 function
         typeof this.info !== 'function'  false, 跳过代码执行
      4. 返回实例对象 mio
    故当代码执行 mio.info()由于 mio 自身没有 info方法, 所以也会查找原型链 mio.__proto__, 
     mio.__proto__ = { info() { } },通过原型链调用 mio.__proto__.info(), 返回 'mio-14-蓝白碗'