JS中创建自定义类型的常用模式总结【工厂模式,构造函数模式,原型模式,动态原型模式等】

(编辑:jimmy 日期: 2024/11/19 浏览:2)

本文实例讲述了JS中创建自定义类型的常用模式。分享给大家供大家参考,具体如下:

虽然在 ES6 中,已经出了 class 的语法,貌似好像不用了解 ES5 中的这些老东西了,但是越深入学习,你会发现理解这些模式的重要性。

在本文中,我会描述 7 种常用的创建自定义类型的模式:工厂模式、构造函数模式、原型模式、组合使用构造函数模式、动态原型模式、寄生构造函数模式、稳妥构造函数模式。分别给出他们的示例代码,并分析他们的利弊,方便读者选择具体的方式来构建自己的自定义类型。

最后,我会指出 ES6 中的 class 语法,本质上其实还是利用了组合使用构造函数模式进行创建自定义类型。

1. 工厂模式

废话不多说,先上工厂模式的实例代码:

function createPerson(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 person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");

优点:解决了创建多个相似对象的问题;

缺点:没有解决对象识别的问题(即不知道这个对象是什么类型),对于对象的方法没有做到复用。

2. 构造函数模式

function Person(name, age, job){
  this.name = name;    // 对象的所有细节全部挂载在 this 对象下面
  this.age = age;
  this.job = job;
  this.sayName = function(){
    alert(this.name);
  };
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

说到构造函数模式就不得不提到 new 操作符了。我们来看看 new 这个操作符到底做了什么:

① 创建一个对象;
② 将构造函数内的 this 指向这个新创建的对象,同时将该函数的 prototype 的引用挂载在新对象的原型下;
③ 执行函数内的细节,也就是将属性和方法挂载在新对象下;
④ 隐式的返回新创建的对象。

优点:解决了对象识别的问题;

缺点:对于自定义类型的方法每次都要新创建一个方法函数实例,没有做到函数复用。如果把所有方法函数写到父级作用域中,是做到了函数复用,但同时方法函数只能在父级作用域的某个类型中进行调用,这对于父级作用域有点名不副实,同时对于自定义引用类型没有封装性可言。

3. 原型模式

function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
  alert(this.name);
};
var person1 = new Person();
person1.sayName();   //"Nicholas"
var person2 = new Person();
person2.sayName();   //"Nicholas"
alert(person1.sayName == person2.sayName);   //true

理解要点:

① 无论什么时候,只要创建了一个新函数,就会根据一组特定规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。
② 在默认情况下,所有原型对象都会自动获得一个 constructor 属性,这个属性包含一个指向 prototype 属性所在函数的指针。至于原型中的其他方法则都是从 Object 继承而来。
③ 当调用构造函数创建了一个新实例后,该实例的内部将包含一个指针 [[prototype]](内部属性) ,指向构造函数的原型对象。
④ 当调用构造函数创建一个新实例后,该实例的实例环境,即构造函数,会针对原型对象上的非引用类型的原型属性,在构造函数中自动构建相应的实例环境属性。也就是说,之后根据构造函数创建的实例,它的实例属性中的非引用类型属性,都仍是根据构造函数中的实例环境属性创建的。

但是为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。如下所示:

function Person(){
}
Person.prototype = {
  name : "Nicholas",
  age : 29,
  job: "Software Engineer",
  sayName : function () {
    alert(this.name);
  }
};

但是这种写法,其本质上完全重写了默认的 prototype 对象,因此 constrctor 属性也就变成了新对象的 constructor 属性(指向 Object 构造函数),不在指向 Person 函数。尽管此时,instanceOf 操作符还能返回正确的结果。

如果 constructor 属性真的很重要,可以像下面这样特意将它设置回适当的值:

function Person(){
}
Person.prototype = {
  constructor : Person,
  name : "Nicholas",
  age : 29,
  job: "Software Engineer",
  sayName : function () {
    alert(this.name);
  }
};

注意,以这种方式重设 constructor 属性会导致他的 [[Enumerable]] 特性被设置为 true 。默认情况下,原生的 constructor 属性是不可枚举的,因此,如果你使用兼容 ECMAScript 5 的 JavaScript 引擎,你可以试试 Object.defineProperty() 方法:

function Person(){
}
Person.prototype = {
  name : "Nicholas",
  age : 29,
  job : "Software Engineer",
  sayName : function () {
    alert(this.name);
  }
};
//重设构造函数,只适用于 ECMAScript 5 兼容的浏览器
Object.defineProperty( Person.prototype, "constructor", {
  enumerable: false,
  value: Person
});

注意,重写原型对象会切断新原型与已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。

优点:对自定义类型的方法解决了函数复用的问题。

缺点:

① 不能为构造函数传递初始化参数;
② 原型模式中实现了对于包含引用类型值的属性的共享,这就意味着一个实例中修改了该引用类型值,所有实例的该属性都会被修改!!!

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

在组合使用构造函数模式和原型模式中,构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性,而且还支持向构造函数传递参数。如以下示例代码所示:

function Person(name, age, job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ["Shelby", "Court"];
}
Person.prototype = {
  sayName : function(){
    alert(this.name);
  }
}
Object.defineProperty( Person.prototype, "constructor", {
  enumerable: false,
  value: Person
);
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends);  //"Shelby,Count,Van"
alert(person2.friends);  //"Shelby,Count"
alert(person1.friends === person2.friends);   //false
alert(person1.sayName === person2.sayName);   //true

优点:能为构造函数传递初始化参数;该复用复用,不该复用的没复用。
缺点:封装性不好,构造函数和原型分别独立于父级作用域进行申明。

5. 动态原型模式(推荐)

该模式把所有信息都封装在构造函数中,通过构造函数来实现初始化原型 (仅在必要的情况下),又保持了同时使用构造函数和原型的优点。请看以下示例代码:

function Person(name, age, job){
  //属性
  this.name = name;
  this.age = age;
  this.job = job;
  //方法
  if (typeof this.sayAge != "function"){   // 此处应该永远去判断新添加的属性和方法
    Person.prototype.sayName = function(){
      alert(this.name);
    };
    Person.prototype.sayAge = function(){
      alert(this.age);
    };
  }
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

if 语句检查的可以是初始化之后应该存在的任何属性或方法——不必用一大堆 if 语句检查每个属性和每个方法;只要检查其中一个即可。

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

优点:封装性非常好;还可使用 instanceOf 操作符确定它的类型。
缺点:无。

6. 寄生构造函数模式

除了使用 new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。请看以下代码:

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 语句,可以重写调用构造函数时返回的值。

缺点:没有解决对象识别的问题(即不知道这个对象是什么类型),不能依赖 instanceOf 操作符来确定对象类型;对于对象的方法没有做到复用。

7. 稳妥构造函数模式

先来了解下稳妥对象:指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥对象最适合在一些安全的环境中 (这些环境中会禁止使用 this 和 new),或者再防止数据被其他应用程序 (如 Mashup 程序) 改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用 this;二是不使用 new 操作符调用构造函数。以下为示例代码:

function Person(name, age, job){
  var o = new Object();    //创建要返回的对象
  //可以在这里定义私有变量和函数
  o.sayName = function(){   //添加方法
    alert(name);
  };
  return o;    //返回对象
}
var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"

其原理就是利用闭包,保有对私有变量和私有方法的引用。

优点:不可能有别的方法访问到传入到构造函数中的原始数据。
缺点:没有解决对象识别的问题(即不知道这个对象是什么类型),不能依赖 instanceOf 操作符来确定对象类型;对于对象的方法没有做到复用。

8. ES6 中的 class

咱们这块以 class 实例来展开讲述:

class Parent {
  name = "qck";
  sex = "male";
  //实例变量
  sayHello(name){
    console.log('qck said Hello!',name);
  }
  constructor(location){
   this.location = location;
  }
}

我们来看看这段代码通过 babel 编译后的 _createClass 函数:

var _createClass = function () {
  function defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i];
      // 对属性进行数据特性设置
      descriptor.enumerable = descriptor.enumerable || false; // enumerable设置
      descriptor.configurable = true;             // configurable设置
      if ("value" in descriptor) descriptor.writable = true; // 如果有value,那么可写
      Object.defineProperty(target, descriptor.key, descriptor); // 调用defineProperty() 进行属性设置
    }
  }
  return function (Constructor, protoProps, staticProps) {
    // 设置到第一个 Constructor 的 prototype 中
    if (protoProps) defineProperties(Constructor.prototype, protoProps);
    // 设置 Constructor 的 static 类型属性
    if (staticProps) defineProperties(Constructor, staticProps);
    return Constructor;
  };
}();

首先该方法是一个自执行函数,接收的一参是构造函数本身,二参是为构造函数的原型对象需要添加的方法或者属性,三参是需要为构造函数添加的静态属性对象。从这个函数就可以看出 class 在创建自定义类型时,用了原型模式。

我们看看编译后的结果是如何调用 _createClass 的:

var Parent = function () {   // 这里是自执行函数
  _createClass(Parent, [{   // Parent的实例方法,通过修改Parent.prototype来完成
    key: "sayHello",
    value: function sayHello(name) {
      console.log('qck say Hello!', name);
    }
  }]);
  function Parent(location) {   //在Parent构造函数中添加实例属性
    _classCallCheck(this, Parent);
    this.name = "qck";
    this.sex = "male";
    this.location = location;
  }
  return Parent;
}();
function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

这里调用 _createClass 的地方就证实了我们刚才的想法——确实应用了原型模式:我们的 class 上的方法,其实是通过修改该类 (实际上是函数) 的 prototype 来完成的。

而通过返回的构造函数,我们可以发现:实例属性还是通过构造函数方式来添加的。

最后,我们来看看 _classCallCheck 方法,它其实是一层校验,保证了我们的实例对象是特定的类型。

所以,综上所述,ES6 中的 class 只是个语法糖,它本质上还是用组合使用构造函数模式创建自定义类型的,这也就是为什么我们要学上面那些知识的初衷。

感兴趣的朋友还可以使用本站在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行结果。

更多关于JavaScript相关内容还可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》

希望本文所述对大家JavaScript程序设计有所帮助。

一句话新闻
一文看懂荣耀MagicBook Pro 16
荣耀猎人回归!七大亮点看懂不只是轻薄本,更是游戏本的MagicBook Pro 16.
人们对于笔记本电脑有一个固有印象:要么轻薄但性能一般,要么性能强劲但笨重臃肿。然而,今年荣耀新推出的MagicBook Pro 16刷新了人们的认知——发布会上,荣耀宣布猎人游戏本正式回归,称其继承了荣耀 HUNTER 基因,并自信地为其打出“轻薄本,更是游戏本”的口号。
众所周知,寻求轻薄本的用户普遍更看重便携性、外观造型、静谧性和打字办公等用机体验,而寻求游戏本的用户则普遍更看重硬件配置、性能释放等硬核指标。把两个看似难以相干的产品融合到一起,我们不禁对它产生了强烈的好奇:作为代表荣耀猎人游戏本的跨界新物种,它究竟做了哪些平衡以兼顾不同人群的各类需求呢?