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


Posted in Javascript onJanuary 19, 2019

本文实例讲述了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.3water.com/code/HtmlJsRun测试上述代码运行结果。

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

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

Javascript 相关文章推荐
JScript中的undefined和&quot;undefined&quot;的区别
Mar 08 Javascript
JS OffsetParent属性深入解析
Jan 13 Javascript
一个简单的实现下拉框多选的插件可移植性比较好
May 05 Javascript
JavaScript验证电子邮箱的函数
Aug 22 Javascript
javascript轻量级库createjs使用Easel实现拖拽效果
Feb 19 Javascript
基于javascript实现全屏漂浮广告
Mar 31 Javascript
javaScript数组迭代方法详解
Apr 14 Javascript
js遍历获取表格内数据的方法(必看)
Apr 06 Javascript
在Vue中如何使用Cookie操作实例
Jul 27 Javascript
解决vue 更改计算属性后select选中值不更改的问题
Mar 02 Javascript
说说如何在Vue.js中实现数字输入组件的方法
Jan 08 Javascript
解决vue-router 嵌套路由没反应的问题
Sep 22 Javascript
详解vue-router导航守卫
Jan 19 #Javascript
JS尾递归的实现方法及代码优化技巧
Jan 19 #Javascript
javascriptvoid(0)含义以及与&quot;#&quot;的区别讲解
Jan 19 #Javascript
js实现延迟加载的几种方法详解
Jan 19 #Javascript
15分钟深入了解JS继承分类、原理与用法
Jan 19 #Javascript
js嵌套的数组扁平化:将多维数组变成一维数组以及push()与concat()区别的讲解
Jan 19 #Javascript
js的各种数据类型判断的介绍
Jan 19 #Javascript
You might like
利用switch语句进行多选一判断的实例代码
2016/11/14 PHP
javascript天然的迭代器
2010/10/29 Javascript
jQuery boxy弹出层插件中文演示及使用讲解
2011/02/24 Javascript
jquery中prop()方法和attr()方法的区别浅析
2013/09/06 Javascript
jquery中常用的函数和属性详细解析
2014/03/07 Javascript
浅谈javascript中的instanceof和typeof
2015/02/27 Javascript
javascript自定义in_array()函数实现方法
2015/08/03 Javascript
谷歌showModalDialog()方法不兼容出现对话窗口的解决办法
2016/02/15 Javascript
javascript每日必学之继承
2016/02/23 Javascript
javascript实现数组去重的多种方法
2016/03/14 Javascript
AngularJS表单详解及示例代码
2016/08/17 Javascript
浅析Javascript ES6中的原生Promise
2016/08/25 Javascript
移动端触屏幻灯片图片切换插件idangerous swiper.js
2017/04/10 Javascript
JavaScript 闭包的使用场景
2020/09/17 Javascript
JavaScript Html实现移动端红包雨功能页面
2021/01/10 Javascript
实例解析Python的Twisted框架中Deferred对象的用法
2016/05/25 Python
Python list列表中删除多个重复元素操作示例
2019/02/27 Python
Python-while 计算100以内奇数和的方法
2019/06/11 Python
python统计指定目录内文件的代码行数
2019/09/19 Python
获取python运行输出的数据并解析存为dataFrame实例
2020/07/07 Python
如何基于python实现年会抽奖工具
2020/10/20 Python
解决pycharm修改代码后第一次运行不生效的问题
2021/02/06 Python
高中自我评价分享
2013/12/05 职场文书
物流专员岗位职责
2014/02/17 职场文书
百年校庆节目主持词
2014/03/27 职场文书
春风行动实施方案
2014/03/28 职场文书
酒店员工培训方案
2014/06/02 职场文书
师范生求职信
2014/06/14 职场文书
基层党员群众路线整改措施及努力方向
2014/10/28 职场文书
2014年档案室工作总结
2014/12/01 职场文书
2015年企业新年寄语
2014/12/08 职场文书
入党积极分子半年考察意见
2015/06/02 职场文书
行政复议答复书
2015/07/01 职场文书
高一地理教学工作总结
2015/08/12 职场文书
react国际化react-intl的使用
2021/05/06 Javascript
golang 实现并发求和
2021/05/08 Golang