这些模式已经出现了相当长的一段时间,并被证明在许多情况下都非常有用。这也是为什么需要自己熟悉并谈论这些模式的原因。

  虽然这些设计模式是与语言和实现方式无关的,并且人们已经对此研究了多年,但都主要是从强类型的静态类语言的角度开展研究,比如C++和Java语言。

  JavaScript是一种弱类型、动态的、基于原型的语言,这种语言特性使得它非常容易、甚至是普通的方式实现其中的一些模式。

  让我们先从第一个例子开始,即单体模式,理解其与基于类的静态语言相比时,JavaScript中存在哪些区别。

一、单体模式

  单体(singleton)模式的思想在于保证一个特定类仅有一个实例。这意味着当您第二次使用同一个创建新对象的时候,应该得到与第一次创建的对象完全相同的对象。

  但是,如何将这种模式应用到JavaScript?在JavaScript中没有类,只有对象。当您创建一个对象时,实际上没有其他对象与其类似,因此新对象已经是单体了。使用对象字面量创建一个简单的对象也是一个单体的例子:

var obj = {
myprop: 'my value'
}

  在JavaScript中,对象之间永远不会完全相等,除非它们是同一个对象,因此即使创建一个具有完全相同成员的同类对象,它也不会与第一个对象完全相同:

var obj2 = {
myprop : 'my value'
};
console.log(obj === obj2);
console.log(obj == obj2);

  因此,可以认为每次在使用对象字面量创建对象的时候,实际上就正在创建一个单体,并且不涉及任何特殊语法。

  请注意,有时当人们在JavaScript上下文中谈论单体时,他们的意思是指第五章中所讨论的模块模式。

使用new操作符

  JavaScript中并没有类,因此对单体咬文嚼字的定义严格来说并没有意义。但是JavaScript中具有new语法可使用构造函数来创建对象,而且有时可能需要使用这种语法的单体实现。这种思想在于当使用同一个构造函数以new操作符来创建多个对象时,应该仅获得指向完全相同的对象的新指针。

  对于在一些基于类的语言(即静态的、强类型语言)中,其函数不是“第一类型对象”的那些语言来说,下面讨论的主题并不是那么有用,而是更多的作为一种理论上的模仿变通方法的运用。

  下面的代码片段显示了其与其行为(假定不认可多元宇宙的观点,并且接受外在世界只有一个宇宙的观点):

var uni = new Universe();
var uni2 = new Universe();
console.log(uni === uni2);

  在上面这个例子中,uni对象仅在第一次调用构造函数时被创建。在第二次(以及第二次以后的每一次)创建时都会返回头一个uni对象。这就是为什么uni === uni2,因为它们本质上是指向同一个对象的两个引用。那么如何在JavaScript中实现这种模式呢?

  需要Universe构造函数缓存该对象实例的this,以便当第二次调用该构造函数时能够创建并返回同一个对象。有多种选择可以实现这一目标:

  • 可以使用全局变量来存储该实例。但是并不推荐使用这种方法,因为在一般原则下,全局变量是有缺点的。此外,任何人都能够覆盖该全局变量,即使是意外事件。因此,让我们不要再进一步讨论这种方法。
  • 可以在构造函数的静态属性中缓存该实例。JavaScript中的函数也是对象,因此它们也可以有属性。您可以使用类似Universe.instance的属性并将实例缓存在该属性中。这是一种很好的实现方法,这种简介的解决方案唯一的缺点在于instance属性是公开可访问的属性,在外部代码中可能会修改该属性,以至于让您丢失了该实例。
  • 可以将该实例包装在闭包中。这样可以保证该实例的私有性并且保证该实例不会被构造函数之外的代码所修改,其代价是带来了额外的闭包开销。

  下面,我们来看下第二种和第三种方法的实现示例:

静态属性中的实例

  下面代码是一个在Universe构造函数的静态属性中缓存单个实例的例子:

function Universe() {
// 我们有一个现有的实例么?
if(typeof Universe.instance === 'object') {
return Universe.instance
} // 正常进行
this.start_time = 0;
this.bang = 'Big'; // 缓存
Universe.instance = this; // 隐式返回
return this;
}

  正如您所看到的,这是一个非常直接的解决方法,其唯一的缺点在于其instance属性是公开的。虽然其他代码不太可能会无意中修改该属性,但是仍然存在这种可能性。

闭包中的实例

  另一种实现类似于类的单体方法是采用闭包来保护该单个实例。可以通过使用在第五章中所讨论的私有静态成员模式实现这种单体模式。这里的秘诀就是重写构造函数:

function Universe() {
// 缓存实例
var instance = this; // 正常进行
this.start_time = 0;
this.bang = 'Big'; // 重写该构造函数
Universe = function () {
return instance;
}
}
// 测试
var uni = new Universe();
var uni2 = new Universe();
console.log(uni === uni2);

  在上述代码运行时,当第一次调用原始构造函数时,它像往常一样返回this。然后,在以后的每次调用时,将执行重写构造函数的部分。该部分通过闭包访问了私有instance变量,并且仅简单的返回了该instance。

  这个实现实际上来自于第四章的自定义函数模式的另一个例子。而这种方法的缺点我们已经在第四章中讨论过,主要在于重写构造函数(本例中也就是构造函数Universe)会丢失所有在初始定义和重定义时刻之间添加到它里面的属性。在这里的特定情况下,任何添加到Universe()原型中的对象都不会存在指向由原始实现所创建实例的活动链接。

  通过下面的一些测试,可以看到这个问题:

// 向原型添加属性
Universe.prototype.nothing = true; var uni = new Universe(); // 在创建初始化对象之后,再次向该原型添加属性 Universe.prototype.everthing = true; var uni2 = new Universe(); // 开始测试 // 仅有最初的原型链接到对象上 console.log(uni.nothing); // true
console.log(uni2.nothing); //true
console.log(uni.everthing); // undefined
console.log(uni2.everthing); // undefined // 结果看上去是正确的
console.log(uni.constructor.name); //Universe
// 但是这个很奇怪:
console.log(uni.constructor === Universe); //false

  之所以uni.constructor不再与Universe()构造函数相同,是因为uni.constructor仍然指向了原始的构造函数,而不是重新定义的那个构造函数。

  从需求上来说,如果需要使原型和构造函数指针按照预期的那样运行,那么可以通过做一些调整来实现这个目标:

function Universe() {
// 缓存实例
var instance // 重写该构造函数
Universe = function Universe() {
return instance;
} // 保留原型属性
Universe.prototype = this; // 实例
instance = new Universe(); // 重置构造函数指针
instance.constructor = Universe; // 所有功能
instance.start_time = 0;
instance.bang = 'Big'; return instance;
} // 更新原型并创建实例
Universe.prototype.nothing = true; var uni = new Universe(); // 在创建初始化对象之后,再次向该原型添加属性 Universe.prototype.everthing = true; var uni2 = new Universe(); // 它们是相同的实例
console.log(uni === uni2); //true
// 无论这些原型属性是何时定义的,所有原型属性都起作用。 console.log(uni.nothing && uni.everthing && uni2.nothing && uni2.everthing); //true // 正常属性起作用
console.log(uni.bang); //'Big' // 该构造函数指向正确
console.log(uni.constructor === Universe); //true

  另一种解决方案也是将构造函数和实例包装在即时函数中。在第一次调用构造函数时,他会创建一个对象,并且使得私有instance指向该对象。从第二次调用之后,该构造函数仅返回该私有变量。通过这个新的实现方式,前面所有代码片段的测试也都会按照预期运行。

var Universe;

(function (){
var instance; Universe = function Universe() {
if(instance) {
return instance;
} instance = this; // 所有功能
this.start_time = 0;
this.bang = 'Big';
}
}());

二、工厂模式

  设计工厂模式的目的是为了创建对象。它通常在类或者类的静态方法中实现,具有下列目标:

  • 当创建相似对象时执行重复操作。
  • 在编译时不知道具体类型(类)的情况下,为工厂客户提供一种创建对象的接口。

  其中,在静态类语言中第二点显得更为重要,因为静态语言创建类的实例是非常平凡的,即事先(在编译时)并不知道实例所属的类。而在JavaScript中,这部分目标实现起来相当容易。

  通过工厂方法(或类)创建的对象在设计上都继承了相同的父对象这个思想,它们都是实现专门功能的特定子类。有时候公共父类是一个包含了工厂方法的同一个类。

  让我们看一个实现示例:

  • 公共父构造函数CarMaker。
  • 一个名为factory()的CarMaker的静态方法,该方法创建car对象。
  • 从CarMaker继承的专门构造函数CarMaker.Compact、CarMaker.SUV和CarMaker.Convertible。所有这些构造函数都被定义为父类的静态属性,以保证全局命名空间免受污染,因此我们也知道了当需要这些构造函数的时候可以在哪找到它们。

  让我们先来看看如何使用这个已经完成的实现:

var corolla = CarMaker.factory('Compact');
var solstice = CarMaker.factory('Convertible');
var cherokee = CarMaker.factory('SUV'); corolla.drive(); // "Vroom, I Have 4 doors"
solstice.drive(); // "Vroom, I Have 2 doors"
cherokee.drive(); // "Vroom, I Have 24 doors"

  其中,这一部分:

var corolla = CarMaker.factory('Compact');

  可能是工厂模式中最易辨别的部分。现在看到工厂方法接受在运行时以字符串形式指定类型,然后创建并返回所请求类型的对象。代码中看不到任何具有new或对象字面量的构造函数,其中仅有一个函数根据字符串所指定类型来创建对象。

  下面是工厂模式的实现示例,这将会使得前面的代码片段正常运行:

// 父构造函数
function CarMaker (){} // a method of the parent CarMaker.prototype.drive = function () {
return "Vroom,I Have " + this.doors + 'doors';
} // 静态工厂方法
CarMaker.factory = function(type) {
var constr = type,
newcar; // 如果构造函数不存在,则发生错误
if(typeof CarMaker[constr] !== 'function') {
throw {
name:"Error",
message: constr + "doesn't exist "
};
} // 在这里,构造函数是已知存在的
// 我们使得原型继承父类,但仅继承一次
if(typeof CarMaker[constr].prototype.drive !== "function") {
CarMaker[constr].prototype = new CarMaker();
} // 创建一个新的实例
newcar = new CarMaker[constr]();
// 可选择性的调用一些方法,然后返回...
return newcar;
}; // 定义特定的汽车制造商
CarMaker.Compact = function () {
this.doors = 4;
} CarMaker.Convertible = function () {
this.doors = 2;
} CarMaker.SUV = function () {
this.doors = 24;
} var corolla = CarMaker.factory('Compact');
var solstice = CarMaker.factory('Convertible');
var cherokee = CarMaker.factory('SUV'); console.log(corolla.drive()); // "Vroom, I Have 4 doors"
console.log(solstice.drive()); // "Vroom, I Have 2 doors"
console.log(cherokee.drive()); // "Vroom, I Have 24 doors"

  实现该工厂模式并没有特别的困难。所有需要做的就是寻找能够创建所需类型对象的构造函数。在这种情况下,简洁的命名习惯可用于将对象类型映射到创建该对象的构造函数中。继承部分仅是可以放进工厂方法的一个公用重复代码片段的范例,而不是对每中类型的构造函数的重复。

内置对象工厂

  而对于“自然工厂”的例子,可以考虑内置的全局Object()构造函数。他也表现出工厂的行为,因为它根据输入类型而创建不同的对象。如果传递一个原始数字,那么它能够在后台以Number()构造函数创建一个对象。对于字符串和布尔值也同样成立。对于任何其他值,甚至包括无输入的值,他都会创建一个常规的对象。

  下面是该行为的一些例子和测试。请注意,无论使用new操作符与否,都可以调用Object():

var o = new Object(),
n= new Object(1),
s = new Object('1'),
b = new Object(true); // test
console.log(o.constructor === Object); //true
console.log(n.constructor === Number); //true
console.log(s.constructor === String); //true
console.log(b.constructor === Boolean); //true

  事实上,Object()也是一个实际用途不大的工厂,值得将它作为例子而提及的原因在于它是我们身边常见的工厂模式。

三、迭代器模式

  在迭代器模式中,通常有一个包含某种数据集合的对象。该数据可能存储在一个复杂数据结构内部,而要提供一种简单的方法能够访问数据结构中的每个元素。对象的消费者并不需要知道如何组织数据,所有需要做的就是取出单个数据进行工作。

  在迭代器模式中,我们需要提供一个next()方法。一次调用next()必须返回下一个连续的元素。当然,在特定的数据结构中,“下一个”所代表的意义是由您来决定的。

  假定对象名为agg,可以在类似下面这样的一个循环中通过简单调用next()即可访问每个数据元素:

var element;
while(element = agg.next()) {
// 处理该元素...
console.log(element);
}

  在迭代器模式中,聚合对象通常还提供了一个较为方便的hasNext()方法,因此,该对象的用户可以使用该方法来确定是否已经到达了数据的末尾。此外,还有另一种顺序访问所有元素的方法,这次是使用hasNext(),其用法如下所示:

while (agg.hasNext()) {
// 处理下一个元素..
console.log(agg.next());
}

  现在已经有了用例,让我们看看如何实现这样的聚合对象。

  当实现迭代器模式时,私下的存储数据和指向下一个可用元素的指针是很有意义的,为了演示一个实现示例,让我们假定数据只是普通数组,而“特殊”的检索下一个连续元素的逻辑为返回每隔一个的数组元素。

var agg = (function () {
var index = 0,
data = [1,2,3,4,5],
length = data.length; return {
next: function () {
var element;
if(!this.hasNext()) {
return null;
} element = data[index];
index = index + 2;
return element;
},
hasNext:function () {
return index < length;
}
};
}());

  为了提供更简单的访问方式以及多次迭代数据的能力,您的对象可以提供额外的便利方法:

  rewind():重置指针到初始位置。

  current():返回当前元素,因为不可能在不前进指针的情况下使用next()执行该操作。

  实现这些方法不存在任何困难,我们来看加上这两个方法的完整示例:

var agg = (function () {
var index = 0,
data = [1,2,3,4,5],
length = data.length; return {
next: function () {
var element;
if(!this.hasNext()) {
return null;
} element = data[index];
index = index + 2;
return element;
},
hasNext:function () {
return index < length;
},
rewind:function () {
index = 0;
},
current: function () {
return data[index];
}
};
}()); // 测试迭代器
while (agg.hasNext()) {
// 处理下一个元素..
console.log(agg.next());
} // 回退
agg.rewind();
console.log(agg.current());

  输出结果将记录在控制台中:即依次输出1,3,5(从循环中),并且最后输出1(在回绕以后)。

  好了,我们这篇学了三个设计模式,分别是单体模式、工厂模式以及迭代器模式。这三个模式比较简单,也更容易理解。下一篇,我们来学习一下更为复杂的设计模式。

《JavaScript 模式》读书笔记(7)— 设计模式1的更多相关文章

  1. JavaScript模式读书笔记 文章3章 文字和构造

    1.对象字面量     -1.Javascript中所创建的自己定义对象在任务时候都是可变的.能够从一个空对象開始,依据须要添加函数.对象字面量模式能够使我们在创建对象的时候向其加入函数.       ...

  2. JavaScript模式读书笔记 第4章 函数

    2014年11月10日 1.JavaScript函数具有两个特点: 函数是第一类对象    函数能够提供作用域         函数即对象,表现为:         -1,函数能够在执行时动态创建,也 ...

  3. 《你不知道的javascript》读书笔记2

    概述 放假读完了<你不知道的javascript>上篇,学到了很多东西,记录下来,供以后开发时参考,相信对其他人也有用. 这篇笔记是这本书的下半部分,上半部分请见<你不知道的java ...

  4. 《编写可维护的javascript》读书笔记(中)——编程实践

    上篇读书笔记系列之:<编写可维护的javascript>读书笔记(上) 上篇说的是编程风格,记录的都是最重要的点,不讲废话,写的比较简洁,而本篇将加入一些实例,因为那样比较容易说明问题. ...

  5. SQL反模式读书笔记思维导图

    在写SQL过程以及设计数据表的过程中,我们经常会走一些弯路,会做一些错误的设计.<SQL反模式>这本书针对这些经常容易出错的设计模式进行分析,解释了错误的理由.允许错误的场景,并给出更好的 ...

  6. Javascript & JQuery读书笔记

    Hi All, 分享一下我学JS & JQuery的读书笔记: JS的3个不足:复杂的文档对象模型(DOM),不一致的浏览器的实现和便捷的开发,调试工具的缺乏. Jquery的选择器 a. 基 ...

  7. 《Javascript模式》之对象创建模式读书笔记

    引言: 在javascript中创建对象是很容易的,可以使用对象字面量或者构造函数或者object.creat.在接下来的介绍中,我们将越过这些方法去寻求一些其他的对象创建模式. 我们知道js是一种简 ...

  8. 《面向对象的JavaScript》读书笔记

    发现了2004年出版的一本好书,用两天快速刷了一遍,草草整理了一下笔记,在此备忘. 类:对象的设计蓝图或制作配方. 对象 === 实例 :老鹰是鸟类的一个实例 基于相同的类创建出许多不同的对象,类更多 ...

  9. 《你不知道的JavaScript》读书笔记(一)作用域

    名词 引擎:从头到尾负责整个 JavaScript 程序的 编译 及 执行 过程. 编译器:负责 语法分析 及 代码生成. 作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套 ...

  10. 《高性能javascript》读书笔记

    1.每个<script>加载时都会阻塞其他文件(图片.音乐等)的同时加载,同时浏览器会在js代码执行时停止渲染Dom.所以为了减少界面加载的卡顿和空白发生,应尽力将js代码或者文件放在&l ...

随机推荐

  1. java 线程、进程及相关知识点 《笔记一》

    一.线程.进程 线程,就是是进程的一个单位,程序最小执行单位. 进程,就是一个执行中的应用程序:由此可见,进程是由很多线程组成的: 线程生命周期,就是管杀也管埋的过程,生老病死: 线程的5个状态: 新 ...

  2. Servlet——idea创建Servlet模板

    idea创建Servlet模板   以前新建一个Servlet是通过新建一个Class文件   可以直接新建一个idea内的Servlet模板                    可以通过设置 更改 ...

  3. SQL Server的Descending Indexes降序索引

    SQL Server的Descending Indexes降序索引 背景索引是关系型数据库中优化查询性能的重要手段之一.对于需要处理大量数据的场景,合理的索引策略能够显著减少查询时间. 特别是在涉及多 ...

  4. [TK] 送礼物

    题解引用 引理1: 区间 \([l,r]\) 是最优解的必要不充分条件是: \(l,r\) 分别是区间的最小值与最大值. 这很显然,若假设不成立,当区间向内缩小时,一定有分子不变,分母变小,进而算出更 ...

  5. Android Qcom USB Driver学习(四)

    VID/PID识别USB设备 CDC-ACM驱动介绍 CDC-ACM(Communication Device Class--Abstract Control Model)驱动实现以USB设备驱动和t ...

  6. iOS动画之CABasicAnimation的使用方法(移动,旋转,缩放)

    设定动画CABasicAnimation的属性和说明 属性 说明  duration 动画的时间 repeatCount 重复的次数.不停重复设置为 HUGE_VALF repeatDuration ...

  7. 63.CDN优化

    虽然CDN引入组件库可以优化项目,减轻服务器负载,但是在真实的项目开发中不推荐使用CDN : 因为: 1. 使用第三方服务器不稳定 2. 需要后端配置 3. 要知道组件库的全局变量名

  8. 云原生爱好者周刊:Fluentbit Operator 正式成为 Fluent 子项目

    云原生一周动态要闻: Fluentbit Operator 正式成为 Fluent 子项目 Kubernetes 1.22 发布 Rust Cloud Native 组织成立 CNCF 宣布 Graf ...

  9. 云原生爱好者周刊:买个蓝牙打印机实时打印新提交的 PR 吧 | 2022-10-24

    开源项目推荐 blue 这个项目非常有意思,利用树莓派.蓝牙热敏打印机和 GitHub Actions 自动将新提交的 PR 或者 Issue 通过打印机打印出来,非常适合各个项目的维护者使用 Kub ...

  10. 快速部署mysql并开启binlog

    curl -fsSL https://get.docker.com | bash yum -y install docker-ce sudo systemctl start docker sudo s ...