【前端面试】JavaScript设计模式

什么是设计模式

设计模式(Design pattern)是一套被反复使用、大多数人知晓的,经过分类编目的、代码设计经验的总结。
设计模式是为了可重用代码,让代码更容易理解,保证代码可靠性。
设计模式使代码编写真正工程化。
设计模式是软件工程的基石脉络。

单体模式

单体指的是一个用来划分命名空间,并将一批相关的属性和方法组织在一起的对象,如果可以被实例化,只能被实例化一次。

特点:

  • 可以用来划分命名空间,从而避免全局变量渲染、或者被覆盖的问题。
  • 利用分支技术来封装浏览器之间的差异
  • 把代码组织的更为一体,提高阅读性和可维护性

代码实现

const Singleton = {
	attribute: true,
	method1: function() {},
	method2: function() {}
}

应用场景

单体模式在平时应用的比较多,相当于把代码封装起来,只是暴露一个入口,从而避免全局变量的污染。

工厂模式

提供创建对象的接口。工厂内,是根据领导者的指示生产相应的产品的;在设计模式中,简单通俗点理解,就是根据调用者的参数创建对象。
创建一个对象不仅过程复杂、也可能会出现大量重复的代码。
而工厂模式就是把成员对象的创建工作转交给一个外部对象,这样可以消除对象间的耦合。

优点:

可以实现一些相同的方法,这些相同的方法我们可以放在父类中编写代码,那么需要实现具体的业务逻辑,那么可以放在子类中重写该父类的方法,去实现自己的业务逻辑。

  • 弱化对象间的耦合,减少重复代码。在一个方法中进行类的实例化,可以消除重复性的代码。
  • 重复性的代码可以放在父类去编写,子类继承于父类的所有成员属性和方法,子类只专注于实现自己的业务逻辑。

缺点:

当工厂内的业务逻辑增加到一定程度时,代码的复杂度增加,可读性下降。
无法直到一个对象的类型,没有解决对象的识别问题。

分类:

  • 简单工厂模式

    实现一个类,通常为单体,来生成实例。

  • 复杂工厂模式

    将其成员对象的实例化推到子类中,子类可以重写父类接口方法以便创建的时候指定自己的对象类型。
    父类只对创建过程中的一般性问题进行处理,这些处理会被子类继承,子类之间是相互独立的,具体的业务逻辑会放在子类中进行编写。

代码实现

  1. 简单工厂模式
const XMLHTTPFactory = function () {};
XMLHTTPFactory.ceateXMLHttp = function () {
	let XMLHttp;
	if (window.XMLHttpRequest) {
		XMLHttp = new XMLHttpRequest();
	} else if (window.ActiveXObject) {
		XMLHttp = new ActiveXObject('Microsoft.XMLHTTP');
	}

	return XMLHttp;
}
  1. 复杂工厂模式
    先设计一个抽象类,只能用来派生子类,不能被实例化,最后通过对子类的扩展实现工程方法。
const XMLHTTPFactory = function() {}; // 抽象工厂模式
// 直接调用createFactory方法,抛出错误
XMLHTTPFactory.prototype = {
	createFactory: function() {
		throw new Error('abstract class');
	}
}

const XHRHandler = funciton () {}; // 子类
// extend(XHRHandler, XMLHTTPFactory);

XHRHandler.prototype = new XMLHTTPFactory(); // 父类原型引用传给子类,实现继承
XHRHandler.prototype.constructor = XHRHandler; // 子类构造函数重置为子类自身

XHRHandler.prototype.createFactory = function () {
	let XMLHttp = null;
	if (window.XMLHttpRequest) {
		XMLHttp = new XMLHttpRequest();
	} else if (window.ActiveXObject) {
		XMLHttp = new ActiveXObject('Microsoft.XMLHTTP');
	}

	return XMLHttp;
}

应用场景

  1. 对象的构建十分复杂
  2. 需要依赖具体环境创建不同实例
  3. 处理大量具有相同属性的小对象

单例模式

单例模式定义了一个对象的创建过程,此对象只有一个单独的实例,并提供一个访问它的全局访问点。
单例模式保证了一个类只有一个实例,实现的方法一般是先判断实例是否存在,如果存在直接返回,如果不存在就创建了再返回。

代码实现:

// 闭包
const single = (function() {
	let unique;
	function getInstance() {
		if (single) {
			return unique;
		}
	
		unique = new Constructor();
	}

	function Constructor() {
	}
	return {
		getInstance
	}
})();

single.getInstance(); // 多次调用返回的都是一个对象(单例)

使用场景

单例模式是一种非常常用的模式,有些对象往往只需要一个,比如浏览器window对象、全局缓存等。
在js开发中,单例模式的用途非常广泛。举例子,用户点击出现弹窗,每次点击出现的弹窗都是一样的,这个弹窗就适合使用单例模式,这样就可以只创建一次。

具体使用场景:

  1. 划分命名空间
  2. 把代码组织的更为一致,方便阅读和维护。

观察者模式(发布订阅)

定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新,这种模式也被称为发布订阅。
它需要一种高级的抽象策略,以便订阅者能够彼此独立地发生改变,而发行方能够接受任何有消费意向的订阅者。

优点

需要维护相关对象的一致性时,使用观察者模式,可以避免对象之间的紧密耦合。
例如,一个对象可以通知另外一个对象,但是不需要知晓这个对象的额外信息。

缺点

在观察者模式中,如果需要发布者与订阅者解耦,将会在一些情况下,很难确保我们应用中的特定部分按照预期的那样正常工作。

应用场景

举个例子,抖音关注博主(订阅者)的有A、B、C三人,当博主更新时,就会统一通知A、B、C。

流程

  1. 确定发布者(博主)。
  2. 发布者添加一个订阅列表(关注列表),用于存放回调函数来通知订阅者(关注者)。
  3. 发布消息(博主更新),发布者遍历订阅列表,依次触发里面存放的订阅者回调函数。
  4. 退订(关注者取消关注)。

代码实现

const pubsub = {}; // 定义发布者

(function (q) {
	const list = {}; // 存放订阅者回调函数
	let subUid = -1;
	
	// 订阅方法,由订阅者来执行
	q.follow = function (type, token, fn) {
		if (!list[type]) {
			list[type] = [];
		}

		if (!q.exitFollow(type, fn)) {
			list[type].push({
				token, 
				fn
			});
			console.log(token, '关注了', type);
		}
	}
	// 发布消息,遍历订阅者
	q.publish = function(type, content) {
		if (!list[type].length) {
			return false;
		}

		setTimeout(function() {
			list[type].forEach(follower => {
				const { fn } = follower; 

				fn(type, content);
			});
		}, 0);
	}
	// 退订方法
	q.unfollow = function (type, token, fn) {
		if (list[type].length) {
			for (let exitFnIdx in list[type]) {
				const { token: exitToken } = list[type][exitFnIdx];
				if (token === exitToken) {
					list[type].splice(exitFnIdx, 1);
					console.log(token, '取消关注');
					break;
				}
			}
		}
	}

	q.exitFollow = function(type, token) {
		if (list[type].length) {
			for (let exitFollow of list[type]) {
				const { token: exitToken, fn: exitFn } = exitFollow;
				if (fn === exitFn && token === exitToken) {
					return true;
				}
			}
		}

		return false;
	}
	
}(pubsub));

const girlA = pubsub.follow('video', 'girlA', function(type, content) {
	console.log(`关注的博主更新了${type}: ${content}`);
});
const girlB = pubsub.follow('video', 'girlB', function(type, content) {
	console.log(`关注的博主更新了${type}: ${content}`);
});
const girlC = pubsub.follow('video', 'girlC', function(type, content) {
	console.log(`关注的博主更新了${type}: ${content}`);
});

pubsub.publish('video', '视频111');

setTimeout(() => {
	pubsub.unfollow('video', 'girlC');
	pubsub.publish('video', '视频222');
}, 0);

策略模式

策略模式是指定义一些列的算法,把它们一个个封装起来,目的是将算法的使用与算法的实现分离,可以减少判断的写法,把判断里的内容抽离开来,变成一个个小个体。

代码实现

代码情景:超市促销,vip5折,老客户3折,普通客户原价,然后计算最后需要支付的金额。

// 没有使用策略模式
function Price(personType, price) {
	let discount = 1;
	if (personType === 'vip') {
		discount = 0.5;
	} else if (personType === 'old') {
		discount = 0.3;
	}

	return price * discount;
}

// 使用策略模式
function vipPrice() {
	this.discount = 0.5;
}
vipPrice.prototype.getPrice = function(price) {
	return price * this.discount;
}

function oldPrice() {
	this.discount = 0.3;
}
oldPrice.prototype.getPrice = function(price) {
	return price * this.discount;
}

function Price() {
	this.discount = 1;
}
Price.prototype.getPrice = function(price) {
	return price * this.discount;
}

function Context() {
	this.name = '';
	this.strategy = null;
	this.price = 0;
}
Context.prototype.set = function(name, strategy, price) {
	this.name = name;
	this.strategy = strategy;
	this.price =  price;
}
Context.prototype.getResult = function() {
	console.log(`${this.name}的结账价为${this.strategy.getPrice(this.price)}`);
}

const context = new Context();
const vip = new vipPrice();
context.set('vip客户', vip, 200);
context.getResult();
const old = new oldPrice();
context.set('老客户', old, 200);
context.getResult();
const normal = new Price();
context.set('普通客户', normal, 200);
context.getResult();

使用场景

大量条件性语句,每个分支都有特定的行为以不同的方式做出改变。
与其维护一段庞大的条件性语句,不如将每个特定的行为划分为多个独立的对象。每个对象都被称为策略。
设置多个策略对象,可以提升代码质量,也有利于进行单元测试。

模板模式

定义了一个操作中的算法的骨架,将一些步骤延迟放到子类中。通俗来讲,就是将一些公共方法封装到父类,子类可以继承这个父类,并且可以在子类中重写父类的方法,从而实现自己的业务逻辑。
模块方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

代码实现

模拟面试

const Interview = function() {};
// 笔试
Interview.prototype.writtenTest = function() {
	console.log('笔试');
};
// 技术面试
Interview.prototype.techTest = function() {
	console.log('技术面试');
};
// 领导面试
Interview.prototype.ldTest = function() {
	console.log('领导面试');
};
// hr面试
Interview.prototype.hrTest = function() {
	console.log('hr面试');
};
// 等通知
Interview.prototype.waitNotice = function() {
	console.log('等通知');
};
// 初始化
Interview.prototype.init = function() {
	this.writtenTest();
	this.techTest();
	this.ldTest();
	this.hrTest();
	this.waitNotice();
};

const specialInterView = function() {};
specialInterView.prototype = new Interview();

// 重写
specialInterView.prototype.hrTest = function () {
	console.log('特殊的hr面试');
}

const special = new specialInterView();
special.init();

应用场景

模板模式主要应用在一些代码一次性实现并且不变的部分,将来页面有修改,需要更改业务逻辑的部分或者重新添加新业务的情况。主要是通过子类来改写父类的情况,其他不需要改变的部分继承父类。

代理模式

代理模式就是把一个对象的访问,交给另一个代理对象来操作。

代码实现

审批流程,助理帮忙审批,汇总后交给老板查看。

const fillOut = function(name, lateDate) {
	this.name = name;
	this.lateDate = lateDate;
}

const bigBoss = function () {
	this.monthReport = [];
	this.state = function(isSuccess) {
		console.log('月汇总如下:');
		this.monthReport.forEach(item => {
			const { fillOut, isSuccess } = item;
			console.log(`${fillOut.name}忘记打卡日期为:${fillOut.lateDate},补打卡状态:${isSuccess}`);
		});
	}
}

const proxyAssis = function () {
	this.state = function(fillOut, boss, isSuccess) {
		console.log(`${fillOut.name}忘记打卡日期为:${fillOut.lateDate},补打卡状态:${isSuccess}`);
		boss.monthReport.push({ fillOut, isSuccess });
	}
	this.report = function () {
		boss.state();
	}
}

const boss = new bigBoss();
const proxy = new proxyAssis();
proxy.state(new fillOut('张三', '2023-11-12'), boss, '补打卡成功');
proxy.state(new fillOut('张三', '2023-11-13'), boss, '补打卡成功');
proxy.state(new fillOut('李四', '2023-11-14'), boss, '补打卡失败');

proxy.report('补打卡失败');

应用场景

图片懒加载,图片未完成加载前,用loading图片,加载完成替换成实体路径。

// 有代理模式存在,这个方法分只管加载图片
const loadImage = (function() {
	const imgNode = document.createElement('img');
	document.body.appendChild(imgNode);

	return function(src) {
		imgNode.src = src;
	}
})();

// 代理模式
const ProxyImage = (function() {
	const img = new Image();
	img.onload = function() {
		loadImage(this.src);
	}

	return function(src) {
		loadImage('loading.gif');
		img.src = src;
	}
})();

ProxyImage('xx.png');

外观模式

通过编写一个单独的函数,来简化对一个或多个更大型、更复杂的函数的访问。也就是说可以视外观模式为一种简化某些内容的手段。
外观模式就是一个函数,封装了复杂的操作。

代码实现

跨浏览器的ajax调用

function ajaxCall(type, url, callback, data) {
	// 根据当前浏览器获取对ajax连接对象的引用
	const xhr = (function() {
		// 现代浏览器标准方法
		try {
			return new XMLHttpRequest();
		} catch(e) {}
		
		// 兼容老版本IE
		try {
			return new ActiveXObject('Msxml2.XMLHTTP.6.0');
		} catch(e) {}
		try {
			return new ActiveXObject('Msxml2.XMLHTTP.3.0');
		} catch(e) {}
		try {
			return new ActiveXObject('Microsoft.XMLHTTP');
		} catch(e) {}
	}());
	const STATE_LOADED = 4;
	const STATUS_OK = 200;
	
	// 一旦从服务器受到表示成功的相应信息,则执行所给定的回调方法
	xhr.onreadystatechange = function() {
		if (xhr.readyState !== STATE_LOADED) {
			return;
		}
		if (xhr.readyState === STATUS_OK) {
			callback(xhr.responseText);
		}
	}

	// 调用给定的URL
	xhr.open(type.toUpperCase(), url);
	xhr.send(data);
}

ajaxCall('get', '/user/login', function(data) {
	console.log('收到的数据为:', data);
});

应用场景

当需要通过一个单独的函数或方法来访问一系列的函数或方法调用,以简化代码库的其余内容,使得代码更容易跟踪管理或者更好的维护时,可以使用外观模式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值