blog
blog copied to clipboard
JavaScript 常用设计模式
前言
当学习深入了解后,发现一些晦涩难懂的技巧与设计模式有关,记录学习日志。
工厂模式
工厂起到的作用就是隐藏了创建实例的复杂度,只需要提供一个函数,把对象放到函数里,用函数封装创建对象的细节。
var Factory = function (type, text) {
// 判断 this 是否是 Factory的实例,代码执行两次
if (this instanceof Factory) {
this[type](text);
} else {
return new Factory(type, text);
}
};
Factory.prototype = {
javascript: function (text) {
console.log(text + "javascript");
},
nodejs: function (text) {
console.log(text + "nodejs");
},
};
const o = Factory("javascript", "万能的"); // log: 万能的javascript
console.log(o instanceof Object); // true
console.log(o instanceof Factory); // true
在 Vue 源码中,你也可以看到工厂模式的使用,比如创建异步组件
export function createComponent(
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// 逻辑处理...
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ""}`,
data,
undefined,
undefined,
undefined,
context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
);
return vnode;
}
在上述代码中,我们可以看到我们只需要调用 createComponent 传入参数就能创建一个组件实例,但是创建这个实例是很复杂的一个过程,工厂帮助我们隐藏了这个复杂的过程,只需要一句代码调用就能实现功能。
构造器模式
构造器模式与工厂模式类似,new 这个对象就是创建对象的实例,实例被标识为特定的类型。构造函数模式一般结合原型进行使用,保证每个实例的方法是同一个。
function Vue(options) {
if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
warn("Vue is a constructor and should be called with the `new` keyword");
}
this._init(options);
}
initMixin(Vue); // _init,_uid,$options 当前 Vue 实例的初始化选项,注意:这是经过 mergeOptions() 后的
stateMixin(Vue); // $data, $props 设为只读属性,继续添加 $set, $delete, $watch
eventsMixin(Vue); // $on, $emit, $off, $once
lifecycleMixin(Vue); // _update, $forceUpdate, $destroy
renderMixin(Vue); // installRenderHelpers 函数中的方法,$nextTick,_render
var vm1 = new Vue();
var vm2 = new Vue();
vm1._init === vm2._init; // true
// el.spec.js
import Vue from 'vue'
describe('Options el', () => {
it('basic usage', () => {
const el = document.createElement('div')
el.innerHTML = '<span>{{message}}</span>'
const vm = new Vue({
el,
data: { message: 'hello world' }
})
expect(vm.$el.tagName).toBe('DIV')
expect(vm.$el.textContent).toBe(vm.message)
})
}
单例模式
单例模式很常用,比如全局缓存、全局状态管理等这些只需要一个对象,就可以使用单例模式。
单例模式的核心就是保证全局一个类仅有⼀个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例存在与否,如果存在则直接返回,如果不不存在就创建了了再返回,这就确保了了一个类只有一个实例对象。
因为 JS 是门无类的语言,所以别的语言实现单例的方式并不能套入 JS 中,我们只需要用一个闭包变量确保实例只创建一次就行,以下是如何实现单例模式的例子。
class Singleton {
constructor() {}
}
Singleton.getInstance = (function () {
let instance;
return function () {
if (!instance) {
instance = new Singleton();
}
return instance;
};
})();
let s1 = Singleton.getInstance();
let s2 = Singleton.getInstance();
console.log(s1 === s2); // true
也可以构建一个工厂函数,用于将正常的类加工成单例模式的类。
function singleton(target) {
let _instance = null;
const _wrapper = function singletoned() {
if (_instance) {
return _instance;
} else {
const ins = Object.create(target.prototype);
_instance = target.apply(ins, arguments) || ins;
return _instance;
}
};
return _wrapper;
}
class Target {
constructor() {
this.name = "target" + Date.now();
}
say() {
console.log(this.name);
}
}
const SingleTarget = singleton(Target);
new SingleTarget().say();
new SingleTarget().say();
new SingleTarget().say();
在 Vuex 源码中,你也可以看到单例模式的使用,虽然它的实现方式不大一样,通过一个外部变量来控制只安装一次 Vuex
let Vue; // bind on install
export function install(_Vue) {
if (Vue && _Vue === Vue) {
// 如果发现 Vue 有值,就不重新创建实例了
return;
}
Vue = _Vue;
applyMixin(Vue);
}
模块模式
使用 jquery
的 getJSON 方法来获取 github repoList 数据列表
<div id="root"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script>
var Module = {
init: function () {
this.id = "root";
this.error = null;
this.fetchOrderList(); // 若有可以扩展添加结束处理的逻辑
},
fetchOrderList: function () {
var y = this;
y.render("loading...");
$.getJSON(
"https://api.github.com/search/repositories?q=javascript&sort=stars"
).then(
(value) => {
y.render(value);
},
(error) => {
y.error = error; // 错误标记
y._fetchDataFailed(error);
}
);
},
render: function (data) {
var y = this;
let html;
if (y.error === null && typeof data !== "string") {
html = this._resolveData(data);
} else {
html = data;
}
document.getElementById(y.id).innerHTML = html;
},
// 需要时格式化处理
_resolveData: function (data) {
var repos = data.items;
var repoList = repos.map(function (repo, index) {
return `<li> <a href=${repo.html_url}>${repo.name}</a> (${repo.stargazers_count} stars) <br /> ${repo.description}</li>`;
});
return `<main>
<h1>Most Popular JavaScript Projects in Github</h1>
<ol> ${repoList.join("")}</ol>
</main> `;
},
// 错误处理
_fetchDataFailed: function (error) {
let errorHtml = `<span>Error: ${error.message}</span>`;
this.render(errorHtml);
},
};
Module.init();
</script>
发布订阅模式
异步处理逻辑的一种方式,需要做全局存储事件调控中心,在原生开发小程序中有应用,支持先订阅后发布,以及先发布后订阅
注意:使用完成后及时卸载
var Event = (function () {
var clientList = {},
pub,
sub,
remove;
var cached = {};
sub = function (key, fn) {
if (!clientList[key]) {
clientList[key] = [];
}
// 使用缓存执行的订阅不用多次调用执行
cached[key + "time"] == undefined ? clientList[key].push(fn) : "";
if (cached[key] instanceof Array && cached[key].length > 0) {
//说明有缓存的 可以执行
fn.apply(null, cached[key]);
cached[key + "time"] = 1;
}
};
pub = function () {
var key = Array.prototype.shift.call(arguments),
fns = clientList[key];
if (!fns || fns.length === 0) {
//初始默认缓存
cached[key] = Array.prototype.slice.call(arguments, 0);
return false;
}
for (var i = 0, fn; (fn = fns[i++]); ) {
// 再次发布更新缓存中的 data 参数
cached[key + "time"] != undefined
? (cached[key] = Array.prototype.slice.call(arguments, 0))
: "";
fn.apply(this, arguments);
}
};
remove = function (key, fn) {
var fns = clientList[key];
// 缓存订阅一并删除
var cachedFn = cached[key];
if (!fns && !cachedFn) {
return false;
}
if (!fn) {
fns && (fns.length = 0);
cachedFn && (cachedFn.length = 0);
} else {
if (cachedFn) {
for (var m = cachedFn.length - 1; m >= 0; m--) {
var _fn_temp = cachedFn[m];
if (_fn_temp === fn) {
cachedFn.splice(m, 1);
}
}
}
for (var n = fns.length - 1; n >= 0; n--) {
var _fn = fns[n];
if (_fn === fn) {
fns.splice(n, 1);
}
}
}
};
return {
pub: pub,
sub: sub,
remove: remove,
};
})();
适配器模式
适配器用来解决两个接口不兼容的情况,不需要改变已有的接口,通过包装一层的方式实现两个接口的正常协作。
以下是如何实现适配器模式的例子
class Plug {
getName() {
return "港版插头";
}
}
class Target {
constructor() {
this.plug = new Plug();
}
getName() {
return this.plug.getName() + " 适配器转二脚插头";
}
}
let target = new Target();
target.getName(); // 港版插头 适配器转二脚插头
在 Vue 中,我们其实经常使用到适配器模式。比如父组件传递给子组件一个时间戳属性,组件内部需要将时间戳转为正常的日期显示,一般会使用 computed 来做转换这件事情,这个过程就使用到了适配器模式。
代理模式
代理是为了控制对对象的访问,不让外部直接访问到对象。在现实生活中,也有很多代理的场景。比如你需要买一件国外的产品,这时候你可以通过代购来购买产品。
在实际代码中其实代理的场景很多,比如事件代理、图片预加载就用到了代理模式。
先通过一张 loading 图占位,然后通过异步的方式加载图⽚片,等图⽚加载好了再把 完成的图⽚加载到 img 标签⾥里面。
var myImage = (function () {
var imgNode = document.createElement("img");
document.body.appendChild(imgNode);
return {
setSrc: function (src) {
imgNode.src = src;
},
};
})();
var proxyImage = (function () {
var img = new Image();
img.onload = function () {
setTimeout(() => {
myImage.setSrc(this.src);
}, 500);
};
return {
setSrc: function (src) {
myImage.setSrc("./loading.gif");
img.src = src;
},
};
})();
proxyImage.setSrc(
"https://www.wangbase.com/blogimg/asset/202001/bg2020013101.jpg"
);
外观模式
外观模式提供了一个接口,隐藏了内部的逻辑,更加方便外部调用。
举个例子来说,我们现在需要实现一个兼容多种浏览器的添加事件方法
function addEvent(elm, evType, fn, useCapture) {
if (elm.addEventListener) {
elm.addEventListener(evType, fn, useCapture);
return true;
} else if (elm.attachEvent) {
var r = elm.attachEvent("on" + evType, fn);
return r;
} else {
elm["on" + evType] = fn;
}
}
对于不同的浏览器,添加事件的方式可能会存在兼容问题。如果每次都需要去这样写一遍的话肯定是不能接受的,所以我们将这些判断逻辑统一封装在一个接口中,外部需要添加事件只需要调用 addEvent 即可。
装饰者模式
装饰者模式在现在的前端开发场景应用很广泛,如:
- react 的高阶函数
-
react-redux 的
connect
方法 -
react-router 的
withouter
方法 -
antd 的
Form.create
方法 -
Taro 编译小程序时 将
getApp()
方法使用@withWeapp('Page') class _C extends Taro.Component {}
传入组件中 - 最后点出来 es6 好用的
{ ...data}
解构方法 - ...
Function.prototype.before = function (beforefn) {
var __self = this; // 保存原函数的引用
return function () {
// 返回包含了原函数和新函数的"代理"函数
beforefn.apply(this, arguments); // 执行新函数,且保证 this 不被劫持,新函数接受的参数
// 也会被原封不动地传入原函数,新函数在原函数之前执行
return __self.apply(this, arguments); // 执行原函数并返回原函数的执行结果,
// 并且保证 this 不被劫持
};
};
Function.prototype.after = function (afterfn) {
var __self = this;
return function () {
var ret = __self.apply(this, arguments);
afterfn.apply(this, arguments);
return ret;
};
};
- 实例存留,装饰者待深入源码研究学习,未完待续 ...
let doSomething = function () {
console.log(1);
};
doSomething = doSomething
.before(() => {
console.log(3);
})
.after(() => {
console.log(2);
});
doSomething(); // 输出 312
参考
- 《JavaScript 设计模式与开发实践》
- Strategy 策略模式