恶补 JavaScript 基础之创建对象的多种方式及其优缺点

注意:本文大量引用《JavaScript高级程序设计》的内容,可看作是一篇笔记

1.工厂模式

工厂模式是最常见的设计模式,每次我们看设计模式相关的资料,第一个都是它。在 ECMAscript 中没有类的相关概念(直到ECMAscript2015),所以开发人员发明了一种函数来创建有类似结构的对象,如下面例子:

  1. function createPerson(name, age, job){
  2. var o = new Object()
  3. o.name = name
  4. o.age = age
  5. o.job = job
  6. o.sayName = function() {
  7. console.log(this.name)
  8. }
  9. return o
  10. }
  11. var person1 = createPerson("Nicholas", 29, "Software Engineer")
  12. var person2 = createPerson("Greg", 27, "Doctor")

这个函数可以通过不同的参数创建结构相同的 Person 对象,这使得我们需要创建相似对象的时候只需要调用一下这个函数就好了。但是这样创建出来的对象无法知道它的对象的类型

2.构造函数模式

在 ECMAscript 中存在像 Object 和 Array 这样的内置构造函数,用他们创建的实例是可以检测到自己是什么构造函数类型创建出来的。我们可以定义一个自定义的构造函数,用来创建对象,以解决工厂函数的问题:

  1. function Person (name, age, job) {
  2. this.name = name
  3. this.age = age
  4. this.job = job
  5. this.sayName = function(){
  6. console.log(this.name)
  7. }
  8. }
  9. var person1 = new Person("Nicholas", 29, "Software Engineer");
  10. var person2 = new Person("Greg", 27, "Doctor");

可以看到我们用 this 来代替了 o ,这样我们不用再创建 o ,也不用 return 一个对象了。我们以 new 操作符的方式来使用 Person 构造函数(注意:大写 P 只是一个约定,用大写字母开头的为构造函数,但实际上在 ECMAscript 中它跟其他函数其实是一样的。你可以理解为:如果用 new 关键字去调用它,他就是构造函数,直接用 Person() 来调用,它就是普通函数。)
用 new 操作符实际上会经历以下步骤(更多关于 new 的解读请阅读 new的模拟实现):

  1. 创建一个新的对象;
  2. 将构造函数的作用域赋给新对象(将 this 指向这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回这个新对象。

例子中的 person1 和 person2 都是 Person 的实例,所以他们的 constructor 属性 指向了 Person。

创建对象的多种方式及其优缺点 - 图1

优点
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式 胜过工厂模式的地方。

缺点
构造函数内部定义的方法被实例化后都会重新创建一遍,也就是说person1 和 person2 的 sayName 方法是不相等的。你可以这么理解:方法也是对象,在构造函数实例化的时候,实际上内部的方法是创建了一个对象,那么两次实例化创建的对象的地址当然不一样。 但是他们的功能是一样的,我们没有必要去创建两份。

构造函数模式优化

优化很简单,就是把构造函数里面的方法定义到外部去:

  1. function Person (name, age, job) {
  2. this.name = name
  3. this.age = age
  4. this.job = job
  5. this.sayName = sayName
  6. }
  7. function sayName(){
  8. console.log(this.name)
  9. }
  10. var person1 = new Person("Nicholas", 29, "Software Engineer")
  11. var person2 = new Person("Greg", 27, "Doctor")

这样虽然解决了方法被创建两次的问题,但是这样毫无封装性可言。聪明的你肯定想到了原型,我们不是通常吧方法定义到原型上嘛。

3.原型模式

我们创建的每个函数(注意:这里指的是正常方式创建的函数,而由 bind 方法返回的函数并不包含 prototype 属性,你可以阅读更多关于函数的创建方式)都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象, 而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。我们如果把函数放到原型对象上就可以实现共享:

  1. function Person () {}
  2. Person.prototype.name = "Nicholas"
  3. Person.prototype.age = 29
  4. Person.prototype.job = "Software Engineer"
  5. Person.prototype.sayName = function () {
  6. console.log(this.name)
  7. }
  8. var person1 = new Person()
  9. person1.sayName() //"Nicholas"
  10. var person2 = new Person()
  11. person2.sayName() //"Nicholas"
  12. console.log(person1.sayName == person2.sayName) //true

优点

  1. 方法是共享的不会重新创建

缺点

  1. 共享了不仅仅是函数属性,而是所有的属性,而且引用类型的属性被共享还会有致命的问题。
  2. 不能初始化参数

你可能已经注意到了,我们每向原型上添加一个属性就要写一遍Person.prototype.xxx 。为了减少输入和更好的封装可以先用字面量先把要加入的属性放到一个对象中,在一次性赋值给Person.prototype,代码如下:

  1. function Person(){}
  2. Person.prototype = {
  3. name : "Nicholas",
  4. age : 29,
  5. job: "Software Engineer",
  6. sayName : function () {
  7. console.log(this.name)
  8. }
  9. }

这样的话Person.prototypeconstructor丢失了,因为创建字面对象的是Object,所以Person.prototype指向了Object。所以需要显示的将 constructor 赋值回去,如下

  1. function Person(){}
  2. Person.prototype = {
  3. constructor: Person,
  4. name : "Nicholas",
  5. age : 29,
  6. job: "Software Engineer",
  7. sayName : function () {
  8. console.log(this.name)
  9. }
  10. }

注意:这样重设 constructor 属性会导致它的 [[Enumerable]] 被设置为 true ,但是默认情况下,原生的 constructor 是不可枚举的,所以你可以用 Object.defineProperty() 去做这件事。

就算是这样写了也只是方便点,原型模式该有的问题依然存在。

4.组合使用构造模式和原型模式

组合两种模式就可以实现属性传参,方法共享,而且实例之间不会相互影响。

  1. function Person (name, age, job) {
  2. this.name = name
  3. this.age = age
  4. this.job = job
  5. this.friends = ["Shelby", "Court"]
  6. }
  7. Person.prototype = {
  8. constructor : Person,
  9. sayName : function () {
  10. console.log(this.name)
  11. }
  12. }
  13. var person1 = new Person("Nicholas", 29, "Software Engineer")
  14. var person2 = new Person("Greg", 27, "Doctor")
  15. person1.friends.push("Van")
  16. console.log(person1.friends) //"Shelby,Count,Van"
  17. console.log(person2.friends) //"Shelby,Count"
  18. console.log(person1.friends === person2.friends) //false
  19. console.log(person1.sayName === person2.sayName) //true

优点

  1. 属性可以通过参数传递,方法可以共享,引用类型的属性互不影响

缺点

  1. 没有太大的问题,就是我觉得属性和方法分开去写,感觉有点乱,封装性不太好

5.动态原型模式

我觉得动态原型模式是对组合模式的一个优化,它把原型也放在构造函数里面:

function Person(name, age, job){
  //属性
  this.name = name
  this.age = age
  this.job = job

  //方法
  if (typeof this.sayName != "function") {
    Person.prototype.sayName = function () {
      console.log(this.name)
    } 
  }
}

var friend = new Person("Nicholas", 29, "Software Engineer")
friend.sayName()

这里只在 sayName()方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。

注意:使用动态原型模式时,不能使用对象字面量重写原型。前面已经解释过了,如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。

6.寄生构造函数模式

function Person (name, age, job) {
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function(){
    alert(this.name);
  };
  return o; 
}

var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();  //"Nicholas"

看到上面的代码你肯定会说这个就是工厂模式和构造函数模式的结合体啊。跟工厂函数不同的是在使用的使用是用 new 操作符,而跟构造函数模式不同的是,他在内部结尾使用了 return 语句,使得这个过程可以由你一手掌控。

举个例子:假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改 Array 构造函数,我们可以使用寄生构造函数模式来创建一个特殊的数组构造函数:

function SpecialArray(){
  // 创建一个数组
  var values = new Array()
  // 添加参数到数组
  values.push.apply(values, arguments)
  // 添加一个方法
  values.toPipedString = function() {
    return this.join("|")
  }

  // 返回数组
  return values
}

var colors = new SpecialArray("red", "blue", "green");
console.log(colors.toPipedString()); // "red|blue|green"

其实SpecialArray 本来也是函数,可以直接调用,其实跟使用 new 的效果是一样的,所以我觉得这种模式的使用场景十分有限。

《JavaScript高级程序设计》一书中也指出了该模式的问题:返回的对象与构造函数或者与构造函数的原型属 性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此, 不能依赖 instanceof 操作符来确定对象类型。所以建议在可以使用其他模式的情况下,不要使用这种模式。

稳妥构造函数模式

function Person (name) {
  //创建要返回的对象
  var o = new Object()
  //可以在这里定义私有变量和函数
  //添加方法
  o.sayName = function () {
    console.log(name)
  }
  //返回对象
   return o
}

var person = Person("Nicholas", 29, "Software Engineer")
person.sayName()  // "Nicholas"
person.name = "sixty"
person.sayName();  // "Nicholas"
console.log(person.name); // sixty

可以发现这种模式没有在 o 对象上添加公共属性,在 sayName 方法中不使用 this 关键字,仅仅是返回了一个“安全的对象”。他的特点就是:

  • 新创建的实例不使用 this 上下文
  • 不使用 new 操作符来调用构造函数

但是,它有着跟寄生构造函数模式类似的问题,它也无法识别对象的所属类型。

恶补JavaScript基础系列

恶补JavaScript基础系列目录地址:https://www.sixtyden.com/archive
恶补JavaScript基础系列是我在从学校毕业入坑前端的学习产物,它主要是我看完书以及其他资料后的一个浓缩总结。以下是我参考的主要资料:

  1. JavaScript高级程序设计
  2. 你不知道的JavaScript(上卷)
  3. 陪你读书(JavaScript web前端)
  4. 王福朋的博客
  5. 冴羽写博客的地方
  6. 汤姆大叔深入 JavaScript 系列

本人能力有限,如果有错误或者不严谨的地方,请务必给予指出,十分感谢!愿与君共勉。

(完)