JavaScript设计模式
构造器模式
创建的多个实例对象都具有相同的属性。
function User(name , age, career) {
this.name = name
this.age = age
this.career = career
}
const user = new User(name, age, career)
简单工厂模式
创建的多个实例对象都既有有相同的属性又有不同的属性。
function User(name , age, career, work) {
this.name = name
this.age = age
this.career = career
this.work = work
}
function Factory(name, age, career) {
let work
switch(career) {
case 'coder':
work = ['写代码','写系分', '修Bug']
break
case 'product manager':
work = ['订会议室', '写PRD', '催更']
break
case 'boss':
work = ['喝茶', '看报', '见客户']
case 'xxx':
// 其它工种的职责分配
...
return new User(name, age, career, work)
}
抽象工厂模式
抽象工厂不干活,抽象工厂里面的具体工厂干活。遵守开放封闭原则。让代码对拓展开放,对修改封闭。例:我们创建一个手机工厂的流水线。
-
定义手机抽象工厂
class MobilePhoneFactory {
// 提供操作系统的接口
createOS(){
throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
}
// 提供硬件的接口
createHardWare(){
throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
}
}
-
定义手机具体工厂,具体工厂继承于抽象工厂
class FakeStarFactory extends MobilePhoneFactory {
createOS() {
// 提供安卓系统实例
return new AndroidOS()
}
createHardWare() {
// 提供高通硬件实例
return new QualcommHardWare()
}
}
-
定义手机操作系统的抽象产品
class OS {
controlHardWare() {
throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
}
}
-
定义手机操作系统的具体产品
class AndroidOS extends OS {
controlHardWare() {
console.log('我会用安卓的方式去操作硬件')
}
}
-
定义手机硬件的抽象产品
class HardWare {
// 手机硬件的共性方法,这里提取了“根据命令运转”这个共性
operateByOrder() {
throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
}
}
-
定义手机硬件的具体产品
class MiWare extends HardWare {
operateByOrder() {
console.log('我会用小米的方式去运转')
}
}
如果我们想再创建一部新的手机,就可以直接重新创建一个类来继承不同的操作系统和硬件。这样就实遵循了开放封闭原则。
单例模式
让一个类的实例只存在一个,不管我们尝试去创建多少次,它都只给你返回第一次所创建的那唯一的一个实例。
-
使用ES6的class实现单例模式
class Requset {
constructor(){}
static instance;
static getInstance(){
if(!this.instance){
this.instance = new Requset();
}
return this.instance;
}
}
const r1:Requset = Requset.getInstance();
const r2:Requset = Requset.getInstance();
console.log(r1 === r2);//true
-
使用闭包实现单例模式
SingleDog.getInstance = (function() {
// 定义自由变量instance,模拟私有变量
let instance = null
return function() {
// 判断自由变量是否为null
if(!instance) {
// 如果为null则new出唯一实例
instance = new SingleDog()
}
return instance
}
})()
-
Vuex中的单例模式
Vuex内部实现了一个install方法,这个方法再被调用时会将Store注入到Vue实例中去,Vuex就是使用单例模式让全局Store唯一。
let Vue // 这个Vue的作用和楼上的instance作用一样
export function install (_Vue) {
// 判断传入的Vue实例对象是否已经被install过Vuex插件(是否有了唯一的state)
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
// 若没有,则为这个Vue实例对象install一个唯一的Vuex
Vue = _Vue
// 将Vuex的初始化逻辑写进Vue的钩子函数里
applyMixin(Vue)
}
单例模式练习
-
实现Storage,使得该对象为单例,基于 localStorage 进行封装。实现方法 setItem(key,value) 和 getItem(key)。使用ES6的class实现
class Storage {
constructor(){};
static instance;
static getInstance(){
if(!this.instance){
this.instance = new Storage();
}
return Storage.instance;
};
getItem(key){
return localStorage.getItem(key);
};
setItem(key, value){
return localStorage.setItem(key, value);
}
}
使用闭包实现
function StorageBase () {}
StorageBase.prototype.getItem = function (key){
return localStorage.getItem(key)
}
StorageBase.prototype.setItem = function (key, value) {
return localStorage.setItem(key, value)
}
const Storage = (function(){
let instance = null
return function(){
// 判断自由变量是否为null
if(!instance) {
// 如果为null则new出唯一实例
instance = new StorageBase()
}
return instance
}
})()
-
实现一个全局的模态框
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>单例模式弹框</title>
</head>
<style>
#modal {
height: 200px;
width: 200px;
line-height: 200px;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 1px solid black;
text-align: center;
}
</style>
<body>
<button id='open'>打开弹框</button>
<button id='close'>关闭弹框</button>
</body>
<script>
// 核心逻辑,这里采用了闭包思路来实现单例模式
const Modal = (function() {
let modal = null
return function() {
if(!modal) {
modal = document.createElement('div')
modal.innerHTML = '我是一个全局唯一的Modal'
modal.id = 'modal'
modal.style.display = 'none'
document.body.appendChild(modal)
}
return modal
}
})()
// 点击打开按钮展示模态框
document.getElementById('open').addEventListener('click', function() {
// 未点击则不创建modal实例,避免不必要的内存占用;此处不用 new Modal 的形式调用也可以,和 Storage 同理
const modal = new Modal()
modal.style.display = 'block'
})
// 点击关闭按钮隐藏模态框
document.getElementById('close').addEventListener('click', function() {
const modal = new Modal()
if(modal) {
modal.style.display = 'none'
}
})
</script>
</html>
原型模式
在JavaScript中只存在原型模式,不存在类模式,JavaScript中的类是使用原型继承的语法糖。例如:
class Dog {
constructor(name, age){
this.name = name;
this.age = age;
}
eat(){
console.log('吃骨头');
}
}
可以将上面的代码写成下面的形式
function Dog(name, age){
this.name = name;
this.age = age;
}
Dog.prototype.eat = function(){
console.log('吃骨头');
}
-
对象的深拷贝注意:深拷贝没有完美的方案,每一种方案都有它的边界
-
第一种方法使用JSON.stringify和JSON.parse的方式
const data = {
name: 'tom',
fn: ()=>{
console.log('我是一个函数');
}
}
const dataStr = JSON.stringify(data);
const copy = JSON.parse(dataStr);
console.log(copy);
上面的方法只拷贝了name参数,没有拷贝fn参数,应为使用JSON.stringify和JSON.parse的方式进行深拷贝不能对函数和正则进行拷贝。
-
第二种使用递归的方式
function deepClone(obj){
if(typeof obj!=='object'||obj===null){
return obj;
}
let ans={};
if(obj.constructor===Array){
ans=[];
}
for(let key in obj){
console.log(obj.hasOwnProperty(key));
if(obj.hasOwnProperty(key)){
ans[key] = deepClone(obj[key]);
}
}
return ans;
}
装饰器模式
装饰器模式为了不被已有的业务逻辑干扰,实现新的逻辑例:我们现在点击按钮,弹窗会打开,我们想在弹窗打开后又将按钮变成灰色,然后文字变成“快去登录”
-
ES5实现方法
//旧的逻辑
function openModal() {
const modal = new Modal()
modal.style.display = 'block'
}
//新的逻辑
function changeButtonText() {
// 按钮文案修改逻辑
const btn = document.getElementById('open')
btn.innerText = '快去登录'
}
function disableButton() {
// 按钮置灰逻辑
const btn = document.getElementById('open')
btn.setAttribute("disabled", true)
}
function changeButtonStatus() {
// 新版本功能逻辑整合
changeButtonText()
disableButton()
}
document.getElementById('open').addEventListener('click', function() {
openModal()
changeButtonStatus()
})
-
ES6实现方法
class OpenButton {
// 点击后展示弹框(旧逻辑)
onClick() {
const modal = new Modal()
modal.style.display = 'block'
}
}
// 定义按钮对应的装饰器(新逻辑)
class Decorator {
// 将旧的逻辑传入
constructor(open_button) {
this.open_button = open_button
}
onClick() {
this.open_button.onClick()
// “包装”了一层新逻辑
this.changeButtonStatus()
}
changeButtonStatus() {
this.changeButtonText()
this.disableButton()
}
disableButton() {
const btn = document.getElementById('open')
btn.setAttribute("disabled", true)
}
changeButtonText() {
const btn = document.getElementById('open')
btn.innerText = '快去登录'
}
}
document.getElementById('open').addEventListener('click', function() {
// openButton.onClick()
decorator.onClick()
})
ES7中的装饰器
下面的代码可能有的浏览器不支持,可以在babeljs中转成js代码再运行。
function classDecorator(target){
target.hasDecorator = true;
return target;
}
@classDecorator
class Button {}
console.log('Button是否被装饰了', Button.hasDecorator);
适配器模式
适配器模式就是将一个类的接口变换成所期待的另一种接口。例如:我有一个圆孔的耳机,需要插到方形耳机孔的手机上面,就需要一个适配器来让手机能够插上耳机。封装一个fetch
export default class Http {
//get方法
static get(url){
return new Promise((resolve, reject)=>{
fetch(url)
.then(response=>response.json())
.then(result=>{
resolve(result);
})
.catch(error=>{
reject(error);
})
})
}
//post方法
static post(url, data){
return new Promise((resolve, reject)=>{
fetch(url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-wform-urlencoded'
},
body: this.changeData(data);
})
.then(response=>response.json())
.then(result=>{
resolve(result);
})
.catch(error=>{
reject(error);
})
})
}
//body请求体的格式化方法
static changeData(obj){
var prop,str = '';
var i = 0;
for(prop in obj){
if(!prop){
return;
}
if(! === 0){
str += prop + '=' + obj[prop];
}else{
str += '&' + prop + '=' + obj[prop];
}
return str;
}
}
}
-
fetch适配ajax
ajax的请求写法如下
Ajax('get', url, data, function(data){
//成功的回调逻辑
},function(error){
//失败的回调逻辑
});
如果我们想用上面的fetch修改ajax请求,如果一个一个修改的话十分麻烦,这时候我们就可以写一个适配器
async function AjaxAdapter(type, url, data, success, failed){
const type = type.toUpperCase();
let result;
try {
if(type === 'GET'){
result = await Http.get(url) || {};
}else if(type === 'POST'){
result = await Http.post(url, data) || {};
}
//假设请求对应的状态码是200
result.code === 200 && success ? success(result) : failed(result.code);
} catch (error) {
if(failed){
failed(error.code);
}
}
}
async function Ajax(type, url, data, success, failed){
await AjaxAdapter(type, url, data, success, failed);
}
代理模式
代理模式就是在某种情况下,一个对象不能直接访问另一个对象,需要一个第三者(代理)来间接达到访问目的
const girl = {
a: 1
};
const proxy = new Proxy(girl, {
get: (girl, key)=>{
console.log(gril, 'key='+key);
},
set: (gril, key)=>{
console.log(gril, key);
}
});
const a = girl.a;
proxy.a = 2;
代理模式的实践
-
事件代理下面的代码实现了,点击哪个a标签就弹窗显示对应a标签上的字,但是a标签多的情况下,需要给每个a标签绑定事件,那么性能的开销就会更大。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件代理</title>
</head>
<body>
<div id="father">
<a href="">1</a>
<a href="">2</a>
<a href="">3</a>
<a href="">4</a>
<a href="">5</a>
<a href="">6</a>
<a href="">7</a>
<a href="">8</a>
<a href="">9</a>
<a href="">10</a>
</div>
<script>
const aNodes = document.getElementById('father').getElementsByTagName('a');
const len = aNodes.length;
for(let i=0;i<len;i++){
aNodes[i].addEventListener('click', function(e){
e.preventDefault();
alert(`我是${aNodes[i].innerText}`);
});
}
</script>
</body>
</html>
我们使用事件代理来修改js代码
const father = document.getElementById('father');
father.addEventListener('click', function(e){
if(e.target.tagName === 'A'){
e.preventDefault();
alert(`我是${e.target.innerText}`);
}
});
-
虚拟代理-实现图片的懒加载
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>虚拟代理</title>
</head>
<body>
<img class="img" src="" alt="11" width="100" height="100">
<script>
class PreLoadImage {
constructor(imgNode){
//获取真实的DOM节点
this.imgNode = imgNode;
}
//操作img节点的src属性
setSrc(imgUrl){
this.imgNode.src = imgUrl;
}
}
class ProxyImage {
static LOADING_URL = 'https://p6-passport.byteacctimg.com/img/user-avatar/620a16ed8f1917ba1825537525a22dce~300x300.image'
constructor(targetImage){
//目标Image,即PreLoadImage实例
this.targetImage = targetImage;
}
serSrc(targetUrl){//targetUrl就是真实的图片路径
//真实img节点初始化时展示的是一个占位图
this.targetImage.setSrc(ProxyImage.LOADING_URL);
//创建一个帮我们加载图片的虚拟Image实例
const virtualImage = new Image();
//监听目标图片加载的情况,完成时再将DOM上的真实img节点的src属性设置为目标图片的url
virtualImage.onload = () => {
setTimeout(()=>{
this.targetImage.setSrc(targetUrl);
},1000)
}
//设置src属性,虚拟Image实例开始加载图片
virtualImage.src = targetUrl;
}
}
const img = document.querySelector('.img');
const set = new ProxyImage(new PreLoadImage(img));
set.serSrc('');//设置真实的图片地址
</script>
</body>
</html>
-
缓存代理
const addAll = function(){
console.log('进行一次新计算');
let result = 0;
const len = arguments.length;
for(let i=0;i<len;i++){
result += arguments[i];
}
return result;
}
// 为求和方法创建代理
const proxyAddAll = (function(){
// 求和结果的缓存池
const resultCache = {}
return function() {
// 将入参转化为一个唯一的入参字符串
const args = Array.prototype.join.call(arguments, ',');
// 检查本次入参是否有对应的计算结果
if(args in resultCache) {
// 如果有,则返回缓存池里现成的结果
return resultCache[args]
}
return resultCache[args] = addAll(...arguments)
}
})()
console.log(proxyAddAll(1,2,3));
console.log(proxyAddAll(1,2,3));
-
保护模式保护模式就是我们之前演示的代理模式,在getter和setter中进行拦截。
策略模式
例:如果输入的是a就执行a的逻辑,如果是b就执行b的逻辑,我们可以如下这样写。
function ans(tag){
if(tag==='a'){
//执行a的逻辑
}else if(tag==='b'){
//执行b的逻辑
}
}
如果逻辑再多一点,上面的代码逻辑就太胖了,而且违背了“开放闭合原则”。可以修改成一下格式。
-
使用职责分离修改代码
function a(){
//执行a的逻辑
}
function b(){
//执行b的逻辑
}
function ans(tag){
if(tag==='a'){
a();
}
if(tag==='b'){
b();
}
}
-
上面的代码是经过开放封闭改造过的,如果想在ansObj里面写新的逻辑,就可以直接通过ansObj.属性,来添加
const ansObj = {
a(){
//执行a的逻辑
},
b(){
//执行b的逻辑
}
}
function ans(tag){
return ansObj[tag]();
}
状态模式
状态模式和策略模式十分的相识。我们还拿策略模式的例子举例。
-
使用职责分离修改代码
class ans {
constructor(){
this.state = 'init';
}
changeState(state){
this.state = state;
if(state==='a'){
this.a();
}else if(state==='b'){
this.b();
}
}
a(){
//执行a的逻辑
}
b(){
//执行b的逻辑
}
}
-
使用开放封闭修改代码
const ansObj = {
a(){
//执行a的逻辑
},
b(){
//执行b的逻辑
}
}
class Ans {
constructor(){
this.state = 'init';
}
changeState(state){
this.state = state;
if(!ansObj[state])return;
ansObj[state]();
}
}
const test = new Ans();
test.changeState('a');
-
修改开放封闭的代码,让Ans于ansObj建立联系
class Ans {
constructor(){
this.state = 'init';
}
const ansObj = {
that:this,//在对象中使用this.that指向Ans中的this
a(){
//执行a的逻辑
},
b(){
//执行b的逻辑
}
}
changeState(state){
this.state = state;
if(!this.ansObj[state])return;
this.ansObj[state]();
}
}
const test = new Ans();
test.changeState('a');
观察者模式(发布-订阅模式)
观察者模式是一种一对多的依赖关系,让多个观察者同时监听一个目标对象,如果目标对象状态发生变化时,回通知所有观察者,使观察者可以自动更新。
//定义发布者类
class Publisher {
constructor(){
this.observers = [];
console.log('created');
}
//增加订阅者
add(observer){
this.observers.push(observer);
console.log('add');
}
//移除订阅者
remove(observer){
this.observers.forEach((item, i)=>{
if(item === observer){
this.observers.splice(i, 1);
}
})
console.log('remove');
}
//通知所有订阅者
notify(){
this.observers.forEach((observer)=>{
observer.update(this);
})
console.log('invoked');
}
}
//定义订阅者类
class Observer {
constructor(){
console.log('Observer created');
}
update(){
console.log('Observer update');
}
}
//定义一个具体的发布类
class PrdPublicher extends Publisher {
constructor(props) {
super(props);
this.prdState = null;
console.log('PrdPublicher created');
}
//获取当前的prdState
getState(){
console.log('PrdPublicher get');
return this.prdState;
}
//设置prdState的值
setState(state){
this.prdState = state;
this.notify();
console.log('PrdPublicher set');
}
}
class DeveloperObserver extends Observer {
constructor() {
super()
// 需求文档一开始还不存在,prd初始为空对象
this.prdState = {}
console.log('DeveloperObserver created')
}
// 重写一个具体的update方法
update(publisher) {
console.log('DeveloperObserver.update invoked')
// 更新需求文档
this.prdState = publisher.getState()
// 调用工作函数
this.work()
}
// work方法,一个专门搬砖的方法
work() {
// 获取需求文档
const prd = this.prdState
// 开始基于需求文档提供的信息搬砖。。。
console.log('996 begins...')
}
}
//目标值
const aim = new PrdPublicher();
//观察者
const view1 = new DeveloperObserver();
const view2 = new DeveloperObserver();
//一对多,让一个目标值对应多个观察者
aim.add(view1);
aim.add(view2);
//当目标值中的值改变,观察者对应的值也都自动更新
aim.setState('1');
迭代器模式
迭代器模式提供一种顺序访问一个对象中的各个元素,但是又不暴露该对象内部的表示。
-
通过forEach方法遍历一个数组
const arr = [1,2,3];
arr.forEach((item,index)=>{
console.log(`索引为${index}的元素是${item}`);
})
forEach不是万能的,如果用forEach遍历伪数组则会报错。
2. ES6对迭代器的实现在ES6中不仅有Array(数组)和Object(对象),还新增了Map和Set。所以ES6也推出了一套统一的接口机制——迭代器(Iterator)。
const arr = [1,2,3];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
-
for…of…就是反复调用迭代器对象的next方法。
const arr = [1,2,3];
for(item of arr) {
console.log(`当前元素是${item}`)
}
-
ES6实现一个迭代器生成函数
function *iteratorGenerator(){
yield '1'
yield '2'
yield '3'
}
const iterator2 = iteratorGenerator();
console.log(iterator2.next());
console.log(iterator2.next());
console.log(iterator2.next());
-
ES5实现一个迭代器生成函数
const arr = [1,2,3];
function iteratorGenerator2(list){
//记录当前的索引
var idx = 0;
//集合的长度
var len = list.length;
return {
next: function(){
//如果索引值没有超过集合长度,done为false
var done = idx >= len;
//如果done为false则可以继续取值
var value = !done ? list[idx++] : undefined;
//将当前值和是否完毕(done)返回
return {
done: done,
value: value
}
}
}
}
var iterator3 = iteratorGenerator2(arr);
console.log(iterator3.next());
console.log(iterator3.next());
console.log(iterator3.next());