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 相关文章推荐
jQuery 常见学习网站与参考书
Nov 09 Javascript
JavaScript 语法集锦 脚本之家基础推荐
Nov 15 Javascript
cnblogs 代码高亮显示后的代码复制问题解决实现代码
Dec 14 Javascript
iframe 上下滚动条如何默认在下方实现原理
Dec 10 Javascript
浅析js中2个等号与3个等号的区别
Aug 06 Javascript
用jQuery模拟select下拉框的简单示例代码
Jan 26 Javascript
vue2.0实现音乐/视频播放进度条组件
Jun 06 Javascript
angularjs结合html5实现拖拽功能
Jun 25 Javascript
vue打包之后生成一个配置文件修改接口的方法
Dec 09 Javascript
小程序自定义单页面、全局导航栏的实现代码
Mar 15 Javascript
vue在index.html中引入静态文件不生效问题及解决方法
Apr 29 Javascript
浅析vue-router实现原理及两种模式
Feb 11 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
PHP 和 MySQL 开发的 8 个技巧
2007/01/02 PHP
php面向对象全攻略 (二) 实例化对象 使用对象成员
2009/09/30 PHP
redis 队列操作的例子(php)
2012/04/12 PHP
Smarty foreach控制循环次数的实现详解
2013/07/03 PHP
php 获取SWF动画截图示例代码
2014/02/10 PHP
PHP生成不重复随机数的方法汇总
2014/11/19 PHP
php使用socket post数据到其它web服务器的方法
2015/06/02 PHP
js下弹出窗口的变通
2007/04/18 Javascript
Javascript document.referrer判断访客来源网址
2020/05/15 Javascript
再次分享18个非常棒的jQuery表格插件
2011/04/10 Javascript
javascript 对象数组根据对象object key的值排序
2015/03/09 Javascript
jQuery通过改变input的type属性实现密码显示隐藏切换功能
2017/02/08 Javascript
JavaScrpt的面向对象全面解析
2017/05/09 Javascript
JS全角与半角转化实例(分享)
2017/07/04 Javascript
解决vue初始化项目时,一直卡在Project description上的问题
2019/10/31 Javascript
使用p5.js临摹动态图片
2019/11/04 Javascript
[03:28]2014DOTA2国际邀请赛 EG战队官方纪录片
2014/07/21 DOTA
[01:04:35]2018DOTA2亚洲邀请赛 4.3 突围赛 Secret vs VG 第一场
2018/04/04 DOTA
Python根据区号生成手机号码的方法
2015/07/08 Python
解析Mac OS下部署Pyhton的Django框架项目的过程
2016/05/03 Python
python 线程的暂停, 恢复, 退出详解及实例
2016/12/06 Python
python sys.argv[]用法实例详解
2018/05/25 Python
python+numpy实现的基本矩阵操作示例
2019/07/19 Python
jupyter note 实现将数据保存为word
2020/04/14 Python
opencv python 图片读取与显示图片窗口未响应问题的解决
2020/04/24 Python
Python sklearn中的.fit与.predict的用法说明
2020/06/28 Python
如何在mac版pycharm选择python版本
2020/07/21 Python
python使用yaml 管理selenium元素的示例
2020/12/01 Python
HTML5页面直接调用百度地图API获取当前位置直接导航目的地的实现代码
2018/03/02 HTML / CSS
怎样创建、运行java程序
2014/08/01 面试题
会计专业大学生职业生涯规划范文
2014/01/11 职场文书
数控专业毕业生求职信
2014/06/12 职场文书
重点工程汇报材料
2014/08/27 职场文书
导游词之徐州云龙湖
2019/11/19 职场文书
浅谈怎么给Python添加类型标注
2021/06/08 Python
Python matplotlib绘制条形统计图 处理多个实验多组观测值
2022/04/21 Python