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

本文实例讲述了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程序设计有所帮助。

(0)

相关推荐

  • JavaScript创建对象方式总结【工厂模式、构造函数模式、原型模式等】

    本文实例总结了JavaScript创建对象方式.分享给大家供大家参考,具体如下: 这里主要是对<JavaScript高级程序设计>第六章(面向对象的程序设计)的总结,书上的这章至少看了4遍是有的.该章主要讲对象的创建与继承.其中创建对象和继承方式至少6种,再加上一些方法属性,很容易搞得晕头转向的.因此有必要对本章的内容理一理,以后忘了也好过来看一看. 由于文章长度的限制,本文主要讲创建对象. 1 创建对象 1.1 一般方法 使用Object或者采用对象字面量的方法. var o = {a: 1

  • 深入了解js原型模式

    一.什么是原型模式 在js中,创建对象的方式有工厂模式和构造函数模式等: 而构造函数模式最大的问题在于:构造函数中的每个方法都需要在实例对象中重新创建一遍,不能复用,所以为了解决这一个问题,就需要使用原型模式来创建对象. 原型模式是把所有实例共享的方法和属性放在一个叫做prototype(原型)的属性中 ,在创建一个函数时都会有个prototype属性, 这个属性是一个指针,指向一个对象,是通过调用构造函数而创建的那个对象实例的原型对象. // 构造函数 function Person() {}

  • 详解js产生对象的3种基本方式(工厂模式,构造函数模式,原型模式)

    1.工厂模式 工厂模式是软件工程领域一种广为人知的设计模式,而由于在ECMAScript中无法创建类,因此用函数封装以特定接口创建对象.其实现方法非常简单,也就是在函数内创建一个对象,给对象赋予属性及方法再将对象返回即可. function a(name){ var b = new object(); b.name = name; b.say = function(){ alert(this.name); } return b } 函数内部产生b对象并返回. 2.构造函数模式 function

  • JavaScript设计模式之原型模式分析【ES5与ES6】

    本文实例讲述了JavaScript设计模式之原型模式.分享给大家供大家参考,具体如下: 从设计模式的角度讲,原型模式是用于创建对象的一种模式,若需要创建一个对象,一种方法是先指定其类型,然后通过类来创建这个对象,另一种方法是通过原型来创建,不必关心对象的具体类型,而是找到一个对象,然后通过克隆来创建一个一模一样的对象. ES5提供了Object.create(obj)方法来克隆对象,在不支持Object.create(obj)方法的浏览器中,可以使用以下代码: Object.create = O

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

    本文实例讲述了JS中创建自定义类型的常用模式.分享给大家供大家参考,具体如下: 虽然在 ES6 中,已经出了 class 的语法,貌似好像不用了解 ES5 中的这些老东西了,但是越深入学习,你会发现理解这些模式的重要性. 在本文中,我会描述 7 种常用的创建自定义类型的模式:工厂模式.构造函数模式.原型模式.组合使用构造函数模式.动态原型模式.寄生构造函数模式.稳妥构造函数模式.分别给出他们的示例代码,并分析他们的利弊,方便读者选择具体的方式来构建自己的自定义类型. 最后,我会指出 ES6 中的

  • JavaScript实现创建自定义对象的常用方式总结

    本文实例讲述了JavaScript实现创建自定义对象的常用方式.分享给大家供大家参考,具体如下: 1. 对象字面量方式 对象字面量方式是创建自定义对象的首选模式,简单方便. var per = { name:'zhangsan', age:25, job:'html', sayName:function(){ alert(this.name); } } 缺点:使用同一个接口创建很多对象,会产生大量的重复代码.比如我想再创建一个per1对象,我就得把上面的代码再重新写一遍,改变不同的属性值. 2.

  • Javascript 中创建自定义对象的方法汇总

    Javascript 中创建对象,可以有很多种方法. Object构造函数/对象字面量: 抛开设计模式不谈,使用最基本的方法,就是先调用Object构造函数创建一个对象,然后给对象添加属性. 复制代码 代码如下: var student = new Object();      student.name = "xiao ming";      student.age = 20;      student.getName = function () {          alert(th

  • 在WordPress中创建自定义页面模板方法详解

    WordPress 页面模板是特定的模板文件,用于特定页面或页面组,这些用于单页数据的模板显示在前端.我们还可以在 WordPress 中创建自定义页面模板. 在本文中,我们将学习如何在 WordPress 中创建自定义页面模板,以及我们如何将自定义模板分配给 WordPress 中的特定页面或页面组. 在 WordPress 中创建自定义页面模板 默认情况下,WordPress 允许使用默认模板文件page.php创建页面和文章来处理前端的外观. 我们还可以对默认模板文件 ( page.php

  • C语言中的自定义类型之结构体与枚举和联合详解

    目录 1.结构体 1.1结构的基础知识 1.2结构的声明 1.3特殊的声明 1.4结构的自引用 1.5结构体变量的定义和初始化 1.6结构体内存对齐 1.7修改默认对齐数 1.8结构体传参 2.位段 2.1什么是位段 2.2位段的内存分配 2.3位段的跨平台问题 2.4位段的应用 3.枚举 3.1枚举类型的定义 3.2枚举的优点 3.3枚举的使用 4.联合 4.1联合类型的定义 4.2联合的特点 4.3联合大小的计算 1.结构体 1.1结构的基础知识 结构是一些值的集合,这些值称为成员变量.结构

  • js中判断变量类型函数typeof的用法总结

    1.作用: typeof 运算符返回一个用来表示表达式的数据类型的字符串. 可能的字符串有:"number"."string"."boolean"."object"."function" 和 "undefined". 2.常用返回值说明 表达式 返回值 typeof undefined 'undefined' typeof null 'object' typeof true 'boole

  • Vue.js 中制作自定义选择组件的代码附演示demo

    定制 select 标签的设计非常困难.有时候,如果不使用样式化的 div 和自定义 JavaScript 的结合来构建自己的脚本,那是不可能的.在本文中,你将学习如何构建使用完全自定义 CSS 设置样式的 Vue.js 组件. Demo: https://codesandbox.io/s/custom-vuejs-select-component-8nqgd HTML <template> <div class="custom-select" :tabindex=&

  • javascript中对Date类型的常用操作小结

    javascript中对Date类型的常用操作小结 /** 3. * 日期时间脚本库方法列表: 4. * (1)Date.isValiDate:日期合法性验证 5. * (2)Date.isValiTime:时间合法性验证 6. * (3)Date.isValiDateTime:日期和时间合法性验证 7. * (4)Date.prototype.isLeapYear:判断是否闰年 8. * (5)Date.prototype.format:日期格式化 9. * (6)Date.stringToD

  • Golang中的自定义类型之间的转换的实现(type conversion)

    这里不讨论数值与字符串之间.或者整型与浮点型之间的转换.这里要讨论的是自定义类型之间的转换,这个转换与其他语言都不一样,而且在go的源码中也被大量使用. 这里列举两个实用的例子. 转换成实现了某个(些)接口的自定义类型 比如:sort包里面的IntSlice,是一个[]int的自定义类型,并且实现了sort.Interface接口,如下所示: type IntSlice []int // 实现sort.Interface接口的方法 func (p IntSlice) Len() int { re

  • 详细介绍在pandas中创建category类型数据的几种方法

    在pandas中创建category类型数据的几种方法之详细攻略 T1.直接创建 category类型数据 可知,在category类型数据中,每一个元素的值要么是预设好的类型中的某一个,要么是空值(np.nan). T2.利用分箱机制(结合max.mean.min实现二分类)动态添加 category类型数据 输出结果 [NaN, 'medium', 'medium', 'fat'] Categories (2, object): ['medium', 'fat']    name    ID

随机推荐