阅读本文之前,请投票支持这款 全新设计的脚手架 ,让 Java 再次伟大!
单例模式与模块系统
Node 的单例模式既特殊又简单------凡是从模块中导出的实例天生就是单例。
javascript
// database.js
function Database(connect, account, password) {
this.connect = connect;
this.account = account;
this.password = password;
}
const database = new Database("localhost", "root", "123456");
export default database;
// index.js
import database1 from "./database.js";
import database2 from "./database.js";
console.log(database1 === database2); // true
这是由于 Node.js 加载完某个模块后,会创建模块与模块标识符 (module specifier) 的映射关系并将其缓存起来供后续使用,就像下面这样:
pseudo code (CommonJS module)
javascript
function loadModule(filename, module, require) {
const wrappedSrc = `(function(module,exports,require) {
${fs.readFileSync(filename, "utf8")}
})(module,module.exports,require)`;
eval(wrappedSrc);
}
function require(moduleName) {
const id = require.resolve(moduleName);
// 加载缓存
if (require.cache[id]) {
return require.cache[id].exports;
}
const module = { exports: {} };
// 缓存模块
require.cache[id] = module;
loadModule(id, module, require);
return module.exports;
}
require.cache = {};
require.resolve = function (moduleName) {
return moduleName;
};
得益于 CommonJS module 的缓存功能,在 Node 中我们不再需要专门的单例设计模式了。
循环依赖
CommonJS module 基于一个简单的原理,在立即执行函数(IIFE)中创建私有域,然后一边执行代码一边构建图。如果 a,b 两个模块互相依赖,在 a 模块执行完之前 b 模块中引用了 a 模块导出的 变量,就可能会导致 b 模块无法看到 a 模块的最终状态。 ECMAScript Modules 解决这个问题的方式是先通过分析构建图,当图构建完毕以后使用后序深度优先遍历从最后一个节点开始执行代码。这样能保证当其他节点依赖这个模块时,获取到的模块导出变量为最终状态,不会再被 b 模块自身改动。
实时绑定
CommonJS module 的 import 是通过将 b 模块导出对象进行浅拷贝到 a 模块的作用域中来实现的。如果对象的属性是原始类型的话,a 模块获得的是一份拷贝,b 模块的修改可能无法反映到这份拷贝上。 而 ECMAScript Modules 有一种名叫 live bindings 机制,使得 a 模块 import 的 b 模块是一个链接,它指向了 b 模块导出的那个对象。如果 b 模块在运行过程中更改了对象中的属性,a 模块马上就可以获取到最新的状态。而 CommonJS module 的 import 是通过将 b 模块导出对象进行浅拷贝到 a 模块的作用域中来实现的。如果对象的属性是原始类型的话,a 模块获得的是一份拷贝,b 模块的修改可能无法反映到这份拷贝上。
Revealing Constructor
如果你想设计一个只能在初始化时改变状态的类,就可以采用这种技巧。或者换一种说法:该类实例化完毕后就不可变(immutable)。
可能你会觉得 Revealing Constructor 有点陌生,因为它不是我们熟悉的那种设计模式。不过不用担心,因为你一定见过它了。在 Node.js 中这个模式运用的很广泛,其中最常见的就是 Promise。
pseudo code
javascript
class TinyPromise {
constructor(executor) {
this.status = "pending";
this.value = undefined;
executor(this.resolve);
}
resolve = (value) => {
this.status = "resolve";
this.value = value;
};
then = (func) => {
if (this.status === "resolve") {
func(this.value);
}
};
}
new TinyPromise((resolve) => {
resolve(1);
}).then((value) => {
console.log(value);
});
// output: 1
Proxy 代理装饰与适配
ES 内置的 Proxy 支持在运行期 (runtime) 创建代理对象,使通过组合和委派解决问题的设计模式都能通过 Proxy 的方式得到体现,消除了硬编码代理类产生的冗余代码。
javascript
function Subject() {
this.morning = () => {
console.log("morning");
};
this.bye = () => {
console.log("bye");
};
}
const subject = new Subject();
let subjectHandler = {
get: function (target, prop) {
// 返回增强后的方法
if (prop === "morning") {
return function () {
// 增强逻辑
console.log("good");
// 调用原对象逻辑
target[prop]();
};
}
// 返回原对象中的属性
return target[prop];
},
// set、has、delete...
};
const enhanceSubject = new Proxy(subject, subjectHandler);
enhanceSubject.morning(); // good morning
enhanceSubject.bye(); // bye
通过 Proxy 表达的代理、装饰器、适配器设计模式看起来都差不多,差别主要在于应用场景:
- 代理强调对某个对象的访问的控制。
- 装饰器强调对对象属性的增强。
- 适配器则提供与原对象不同的行为以适配新的场景。
Proxy 与对象虚拟化
下面这个例子实现了一个涵盖所有偶数的虚拟化数组------具备数组的行为但不实际存储数据。
javascript
const eventNumbers = new Proxy([], {
get: (target, prop) => index * 2,
has: (target, number) => number % 2 === 0,
});
console.log(2 in eventNumbers); // true
console.log(3 in eventNumbers); // false
console.log(eventNumbers[7]); // 14
举这个例子是想说明,Proxy 的作用不仅仅局限于设计模式,还有很多其他功能可以用 Proxy 来体现,比如:「元编程 (meta-programming)、运算符重载 (operator overloading) 和对象虚拟化 (object virtualization)」
Change Observer 与响应式编程
结合 Proxy 的 set 方法 (trapMethod) 还可以实现观察者模式------当某个对象的状态发生变化时通知观察者。
在强类型语言中观察者一般是一个对象,由于 js 中的所有函数都是闭包的缘故,所以观察者可以是一个回调函数。
javascript
// 被观测对象
const invoice = {
subtotal: 100,
discount: 10,
tax: 20,
};
// 状态发生变化时的回调函数(计算票据总额)
function calculateTotal(invoice) {
return invoice.subtotal - invoice.discount + invoice.tax;
}
// target: 被代理对象 observe: 回调函数(calculateTotal)
function createObservable(target, observe) {
const observable = new Proxy(target, {
// obj: 原始对象 prop: 属性 value: 值
set(obj, prop, value) {
// 如果待设置的属性值和之前的属性不同
if (value !== obj[prop]) {
const prev = obj[prop];
obj[prop] = value;
// callback
observe({ prop, prev, curr: value });
}
return true;
},
});
return observable;
}
let total = calculateTotal(invoice);
console.log(total); // 120
// 创建代理对象(观测该对象属性发生变化)
const obsInvoice = createObservable(invoice, (change) => {
console.log(change);
console.log(calculateTotal(invoice));
});
// 打印票据 total: 210
obsInvoice.subtotal = 200;
// 打印票据 total: 200
obsInvoice.discount = 20;
// 打印票据 total: 200
obsInvoice.discount = 20;
// 打印票据 total: 210
obsInvoice.tax = 30;
像上例这样由实例状态的变化来驱动业务,而不是由客户端(调用方)来发起业务调用的编程范式,称为响应式编程。(reactive programming RP)
结语
- 代理、装饰器、适配器都是结构型模式,体现的设计思想都差不多------通过对象的结构来管理实体间的关系。
- Proxy 不仅用来实现设计模式,也可以用来完成很多其他功能和编程范式。
- 就像 js 中的闭包函数一样,模块导出天生就是单例。