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 相关文章推荐
Javascript miscellanea -display data real time, using window.status
Jan 09 Javascript
ExtJs 表单提交登陆实现代码
Aug 19 Javascript
JavaScript实现SHA-1加密算法的方法
Mar 11 Javascript
基于jQuery实现文本框只能输入数字(小数、整数)
Jan 14 Javascript
利用Node.JS实现邮件发送功能
Oct 21 Javascript
微信小程序 实现拖拽事件监听实例详解
Nov 16 Javascript
webpack2.0搭建前端项目的教程详解
Apr 05 Javascript
three.js加载obj模型的实例代码
Nov 10 Javascript
微信小程序利用canvas 绘制幸运大转盘功能
Jul 06 Javascript
vue移动端轻量级的轮播组件实现代码
Jul 12 Javascript
Vue.js递归组件实现组织架构树和选人功能
Jul 04 Javascript
微信小程序服务器日期格式化问题
Jan 07 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开启安全模式后禁用的函数集合
2011/06/26 PHP
php解析json数据实例
2014/08/19 PHP
php获取文件类型和文件信息的方法
2015/07/10 PHP
Jquery.LazyLoad.js修正版下载,实现图片延迟加载插件
2011/03/12 Javascript
JQuery 返回布尔值Is()条件判断方法代码
2012/05/14 Javascript
js函数中onmousedown和onclick的区别和联系探讨
2013/05/19 Javascript
jQuery中offsetParent()方法用法实例
2015/01/19 Javascript
jQuery源码解读之addClass()方法分析
2015/02/20 Javascript
jQuery选择器源码解读(一):Sizzle方法
2015/03/31 Javascript
如何使用Bootstrap创建表单
2017/03/29 Javascript
浅谈js基础数据类型和引用类型,深浅拷贝问题,以及内存分配问题
2017/09/02 Javascript
BootStrap实现文件上传并带有进度条效果
2017/09/11 Javascript
tween.js缓动补间动画算法示例
2018/02/13 Javascript
看看“疫苗查询”小程序有温度的代码
2018/07/31 Javascript
vue 使用自定义指令实现表单校验的方法
2018/08/28 Javascript
JS的时间格式化和时间戳转换函数示例详解
2020/07/27 Javascript
跟老齐学Python之有容乃大的list(3)
2014/09/15 Python
Python2.x版本中maketrans()方法的使用介绍
2015/05/19 Python
Python实现抓取HTML网页并以PDF文件形式保存的方法
2018/05/08 Python
Python操作mongodb的9个步骤
2018/06/04 Python
Python爬虫框架scrapy实现downloader_middleware设置proxy代理功能示例
2018/08/04 Python
对python pandas 画移动平均线的方法详解
2018/11/28 Python
基于python实现从尾到头打印链表
2019/11/02 Python
Python根据指定文件生成XML的方法
2020/06/29 Python
Python基础教程(一)——Windows搭建开发Python开发环境
2020/07/20 Python
HTML5实现移动端弹幕动画效果
2019/08/01 HTML / CSS
网站域名和主机:Domain.com
2019/04/01 全球购物
日本最大美瞳直送网:Morecontact(中文)
2019/04/03 全球购物
庆国庆国旗下讲话稿2014
2014/09/21 职场文书
党员学习新党章思想汇报
2014/10/25 职场文书
2014年控辍保学工作总结
2014/12/08 职场文书
幼儿园小班个人总结
2015/02/12 职场文书
市场督导岗位职责
2015/04/10 职场文书
2015年会计工作总结范文
2015/05/26 职场文书
Go语言使用select{}阻塞main函数介绍
2021/04/25 Golang
A22国内电台短波广播频率表
2022/05/10 无线电