一个对象其实就是一个单例,每当我们创建一个新的对象,实际上它就是一个单例对象,class 也是单例的实现方式。
工厂可以批量创建单例,根据传入的数据不同而生成不同的单例对象返回,更加灵活和可以处理复杂的逻辑。
工厂模式,是使用工厂函数来创建对象的。它可以使我们调用工厂,而不是直接使用 new 运算符或 Object.create() 从类中创建新对象。在 JavaScript 中,工厂模式只不过是一个不使用 new 关键字就返回对象的函数。
工厂允许我们将对象的创建与其实现分开。本质上,工厂包装了新实例的创建,从而为我们提供了更多的灵活性和控制权。在工厂内部,我们可以选择使用 new 运算符创建类的新实例,或者利用闭包动态构建有状态的对象字面量,甚至可以根据特定条件返回不同的对象类型。工厂的消费者完全不知道如何创建实例。
工厂模式在 JavaScript 中有很多体现。比如 Object() 本身就像工厂。因为它根据输入创建不同的对象。如果你将数字传递给它,它可以在后台使用 Number() 构造函数创建一个对象。类似的还有字符串和布尔值。任何其他值,包括空值,都将创建一个普通对象。下面我们可以看一个例子。Object() 也是一个工厂这一事实并没有什么实际用途,只是通过这个例子,你能看到工厂模式在 JavaScript 中是无处不在。var o = new Object(), n = new Object(1), s = Object('1'), b = Object(true); // test o.constructor === Object; // true n.constructor === Number; // true s.constructor === String; // true b.constructor === Boolean; // true
很多其他语言实现原型模式是使用class,js则有天然的优势,原型继承和创建原型共享属性还是比较熟悉了。
原型模式对于 JavaScript 来说不算陌生了,原型就是在许多相同类型的对象之间共享属性。
代理对象在调用者和主体对象之间,主要起到的作用是保护和控制调用者对主体对象的访问。代理会拦截所有或部分要在主体对象上执行的操作,有时会增强或补充它的行为。
一般代理和主体具有相同的接口,这对调用者来说是透明的。代理将每个操作转发给主体,通过额外的预处理或后处理增强其行为。这种模式可能看起来像“二道贩子”,但存在即合理,代理特别是在性能优化方面还是起到了很大作用的。
代理模式在 JavaScript 中有很多种实现方式。其中包含了:1. 对象组合或对象字面量加工厂模式;2. 对象增强;3. 使用从 ES6 开始自带的内置的 Proxy。
组合模式,通过继承类的属性和方法进行覆写,达到代理的效果。不会改变传入的主体对象。
优点在于:不会暴露主体,不会改变主体,它可以达到延迟初始化、缓存的效果。
缺点在于:父类的每一个方法都要重写。
class Calculator {
constructor () {
/*...*/
}
plus () { /*...*/ }
minus () { /*...*/ }
}
class ProxyCalculator {
constructor (calculator) {
this.calculator = calculator
}
// 代理的方法
plus () { return this.calculator.divide() }
minus () { return this.calculator.multiply() }
}
var calculator = new Calculator();
var proxyCalculator = new ProxyCalculator(calculator);
function factoryProxyCalculator (calculator) {
return {
// 代理的方法
plus () { return calculator.divide() },
minus () { return calculator.multiply() }
}
}
var calculator = new Calculator();
var proxyCalculator = new factoryProxyCalculator(calculator);
第二种模式对象增强(Object Augmentation),对于对象增强来说,它的优点就是不需要 delegate 所有方法。但是它最大的问题是改变了主体对象。用这种方式确实是简化了代理创建的工作,但弊端是会造成函数式编程思想中的“副作用”,因为在这里,主体不再具有不可变性。
function patchingCalculator (calculator) {
var plusOrig = calculator.plus
calculator.plus = () => {
// 额外的逻辑
// 委托给主体
return plusOrig.apply(calculator)
}
return calculator
}
var calculator = new Calculator();
var safeCalculator = patchingCalculator(calculator);
从 ES6 之后,JavaScript 便支持了 Proxy。它结合了对象组合和对象增强各自的优点,我们既不需要手动的去 delegate 所有的方法,也不会改变主体对象,保持了主体对象的不变性。
但是它也有一个缺点,就是它几乎没有 polyfill。也就是说,如果使用内置的代理,就要考虑在兼容性上做出一定的牺牲。真的是鱼和熊掌不能兼得。
var ProxyCalculatorHandler = {
get: (target, property) => {
if (property === 'plus') {
// 代理的方法
return function () {
// 额外的逻辑
// 委托给主体
return target.divide();
}
}
// 委托的方法和属性
return target[property]
}
}
var calculator = new Calculator();
var proxyCalculator = new Proxy(calculator, ProxyCalculatorHandler);
Vue.js 通过代理创建了一种 Change Obsverver 的设计模式。
Vue.js 最显着的特点之一是无侵入的反应系统(unobtrusive reactivity system)。组件状态是响应式 JavaScript 对象,当被修改时,UI 会更新。这也是在前面说过很多次的在函数式编程思想中的副作用(side effect)。
响应式编程(Reactive Programming)是一种基于声明式编程的范式。如果要做到响应式编程,我们就会需要下面示例中这样一个 update 的更新功能。
这个功能会使得每次当 A0 或 A1 发生变化时,更新 A2 的值。这样做,其实就产生了副作用,update 就是这个副作用。A0 和 A1 被称为这个副作用的依赖。这个副作用是依赖状态变化的订阅者。whenDepsChange 在这里是个伪代码的订阅功能。
var A2;
function update() {
A2 = A0 + A1;
}
whenDepsChange(update);
Vue.js 能做的,是拦截对象属性的读写。JavaScript 中有两种拦截属性访问的方法:getter/setter 和 Proxies。由于浏览器支持限制,Vue 2 仅使用 getter/setter。在 Vue 3 中,Proxies 用于响应式对象,getter/setter 用于通过属性获取元素的 refs。
// eg.
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key); // 触发对象的属性变化,将变化发布出去。
}
})
}
const oddNumArr = new Proxy([], {
get: (target, index) => index % 2 === 1 ? index : Number(index)+1,
has: (target, number) => number % 2 === 1
})
console.log(4 in oddNumArr) // false
console.log(7 in oddNumArr) // true
console.log(oddNumArr[15]) // 15
console.log(oddNumArr[16]) // 17
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`获取 ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`设置 ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
obj.count = 1; // 返回:设置 count!
obj.count;
// 返回:获取 count!
// 返回:1
享元模式(flyweight)的核心思想是通过减少对象的创建数量来节约内存。
jQuery 用于将初始点击绑定到 container div 上,把许多独立的行为转化为共享的行为。(事件委托)
门面模式(facade)是一种经常可以在 jQuery 等 JavaScript 库中看到的结构,它的特点是把很多的解决复杂的兼容性问题的实现隐藏在背后,只通过“门面”将对外的接口抽象提供给使用者。
比如我们常用的 jQuery 的 $() 查询器做的就是把很多复杂的用来接收和解析多种类型的查询功能在后端通过 Sizzle 引擎处理,呈现给开发者的是一套更加简便的选择器。
组合模式(composite)指的是可以通过同一种方式处理单个或一组对象。
在 jQuery 中,可以用统一的方式处理单个元素以及一个元素的合集,因为它们返回的都是一个 jQuery 对象。可以为单个元素,比如具有唯一 ID 的元素,或具有相同标签名称元素类型或类属性的一组元素的两个选择添加同一个展示类类的属性。
// 单个元素
$( "#specialNote" ).addClass( "show" );
$( "#mainContainer" ).addClass( "show" );
// 一组元素
$( "div" ).addClass( "show" );
$( ".item" ).addClass( "show" );
比如现在常用的类外面包裹的@方式装饰类(decorator),它不会改变原本类的属性和方法,只是新注入一些处理方法和属性。
如果我们从一个非自研的、不想或不能直接操作的组件中提取类,那么就可以用到装饰器。而且,装饰器可以让我们的程序中减少大量的子类。装饰器模式可以提供方便的一个特点,就是对预期行为的定制和配置。
比如 CSS 中关于透明度的 get 和 set,只需要通过以下方式就可以使用,实际上就是适配器模式。Jquery实际上在背后做了很多的适配操作。
// Cross browser opacity:
// opacity: 0.9; Chrome 4+, FF2+, Saf3.1+, Opera 9+, IE9, iOS 3.2+, Android 2.1+
// filter: alpha(opacity=90); IE6-IE8
// Setting opacity
$( ".container" ).css( { opacity: .5 } );
// Getting opacity
var currentOpacity = $( ".container" ).css('opacity');
它们之间的相同性在于它们都是在无法直接改变主体对象的情况下,加了一层包装。而区别在于不同使用场景的包装方式不同,装饰器更多是通过包装嵌套添加一些不同的特征,适配器的包装更多是一个对象和另一个对象之间的接口映射。