前言
在面试过程中,常常会遇到一些实际应用场景的问题,本文将继续介绍几个常考的场景题,分别是:发布订阅模式
、中间件的实现
、解析url
、红绿灯问题
、浮点数计算
。
正文
场景一:发布订阅模式
发布订阅模式是一种软件设计模式,它允许对象之间基于事件进行通信,而不必直接引用对方。在这种模式下,发送者不会向特定的接收者发送消息,而是将消息发布到一个公共频道上,感兴趣的接收者则订阅这些消息。
代码片段及解释
class Event {
constructor(){
this.event = {}; // 保存事件类型及其对应的函数
}
on(type, fn){ // 订阅
if (!this.event[type]) {
this.event[type] = [];
}
this.event[type].push(fn);
}
off(type, fn){ // 取消订阅
this.event[type] = this.event[type].filter(item => item !== fn); // 通过 filter 方法过滤掉指定的回调函数
}
emit(type, data){ // 发布事件
this.event[type].forEach(item => item(data));
}
once(type, fn){ //发布多次只执行一次
const reWriteFn = () => {
fn();
this.off(type, reWriteFn);
};
this.on(type, reWriteFn);
}
}
// 定义几个事件处理函数
function buy(msg){
console.log('剑哥买到了房子', msg);
}
function buy1(){
console.log('宇哥买到了房子');
}
function buyCarPlace(){
console.log('发哥买到了车位');
}
// 创建一个事件实例
const e = new Event();
// 订阅
e.on('houseSource', buy);
e.on('houseSource', buy1);
e.on('carPlace', buyCarPlace);
e.once('houseSource', buy);
e.once('houseSource', buy);
e.once('houseSource', buy);
// 取消订阅
e.off('houseSource', buy1);
// 发布
e.emit('carPlace');
e.emit('houseSource', '事件数据');
e.emit('houseSource', '事件数据');
解释
on方法
on(type, fn) {
if (!this.event[type]) {
this.event[type] = [];
}
this.event[type].push(fn);
}
on
方法用于订阅事件。它接受两个参数:事件类型 type
和回调函数 fn
。如果当前事件类型还没有对应的监听器列表,则创建一个空数组;然后将回调函数 fn
添加到该类型的监听器列表中。
off方法
off(type, fn) {
this.event[type] = this.event[type].filter(item => item !== fn);
}
off
方法用于取消订阅事件。它接受事件类型 type
和回调函数 fn
。通过 filter
方法过滤掉指定的回调函数 fn
,从而移除该监听器。
emit方法
emit(type, data) {
this.event[type].forEach(item => item(data));
}
emit
方法用于发布事件。它接受事件类型 type
和可选的数据 data
。遍历该事件类型的监听器列表,并依次调用每个监听器,将数据 data
作为参数传递给监听器函数。
once方法
once(type, fn) {
const reWriteFn = () => {
fn();
this.off(type, reWriteFn);
};
this.on(type, reWriteFn);
}
once
方法用于订阅只能执行一次的事件。它接受事件类型 type
和回调函数 fn
。创建一个新的函数 reWriteFn
,在这个新函数内部调用原始的回调函数 fn
,然后取消订阅自身。最后将 reWriteFn
注册为该事件类型的监听器。
这个简单的事件发布/订阅机制实现了基本的事件管理功能,包括订阅 (on
)、取消订阅 (off
)、发布 (emit
) 和一次性的订阅 (once
)。通过这种方式,可以方便地管理和触发事件,适用于需要解耦组件之间通信的应用场景。
场景二:中间件的实现
中间件是一种可以拦截请求和响应流程的函数,通常用于添加额外的功能,如日志记录、身份验证等。下面我们来看看如何通过一个简单的例子来实现中间件。
代码片段及解释
// 实现koa洋葱模型
let middleware = [];
middleware.push((ctx, next) => {
console.log(1);
next();
console.log(2);
});
middleware.push((ctx, next) => {
console.log(3);
next();
console.log(4);
});
middleware.push((ctx, next) => {
console.log(5);
next();
console.log(6);
});
let fn = compose(middleware);
fn(); // 输出: 1 3 5 6 4 2
function compose(middleware) {
return function fn(context, next) {
return dispatch(0);
function dispatch(i) {
if (i >= middleware.length) return;
let fn = middleware[i]; // 取当前中间件
const next = dispatch.bind(context, i + 1); // 绑定下一个中间件
fn(context, next); // 调用当前中间件
}
};
}
//1. 递归
//2. 函数执行到next()时要让下一个函数触发
解释
从外部调用 fn()
-
调用
fn()
: 当调用fn()
时,实际上是在调用由compose
函数返回的一个函数。这个函数有两个参数context
和next
,但在实际调用中并没有传入这两个参数,因此默认情况下它们将是undefined
。 -
执行
dispatch(0)
: 在fn
函数内部,dispatch(0)
被立即调用。这里的0
表示从索引为0
的中间件开始执行。
dispatch
函数的执行
-
进入
dispatch
函数function dispatch(i) { if (i >= middleware.length) return; let fn = middleware[i]; next = dispatch.bind(context, i + 1); fn(context, next); }
- 条件检查:
i
的初始值为0
,而middleware.length
为3
,因此i >= middleware.length
不成立,继续执行。 - 获取当前中间件:
fn
被设置为middleware[0]
,即第一个中间件。 - 绑定下一个中间件:
next
被重新赋值为dispatch.bind(context, 1)
,即下一个中间件将从索引1
开始执行。 - 执行当前中间件:
fn(context, next)
被调用,即调用第一个中间件。
- 条件检查:
-
第一个中间件执行
middleware[0](context, next);
第一个中间件打印
1
,然后调用next()
,这将导致dispatch
函数再次被调用,这次是dispatch(1)
。 -
进入
dispatch(1)
dispatch(1);
- 条件检查:
i
的值为1
,小于middleware.length
,因此继续执行。 - 获取当前中间件:
fn
被设置为middleware[1]
,即第二个中间件。 - 绑定下一个中间件:
next
被重新赋值为dispatch.bind(context, 2)
。 - 执行当前中间件:
fn(context, next)
被调用,即调用第二个中间件。
- 条件检查:
-
第二个中间件执行
middleware[1](context, next);
第二个中间件打印
3
,然后调用next()
,这将导致dispatch
函数再次被调用,这次是dispatch(2)
。 -
进入
dispatch(2)
dispatch(2);
- 条件检查:
i
的值为2
,小于middleware.length
,因此继续执行。 - 获取当前中间件:
fn
被设置为middleware[2]
,即第三个中间件。 - 绑定下一个中间件:
next
被重新赋值为dispatch.bind(context, 3)
。 - 执行当前中间件:
fn(context, next)
被调用,即调用第三个中间件。
- 条件检查:
-
第三个中间件执行
middleware[2](context, next);
第三个中间件打印
5
,然后调用next()
,这将导致dispatch
函数再次被调用,这次是dispatch(3)
。 -
进入
dispatch(3)
dispatch(3);
- 条件检查:
i
的值为3
,大于或等于middleware.length
,因此dispatch
函数返回,不再执行任何中间件。
- 条件检查:
回溯执行
现在,所有的中间件都已经调用了 next()
,并且已经到达了最后一个中间件,因此开始回溯。
-
回溯到
middleware[2]
1middleware[2](context, next);
第三个中间件执行其后半部分,打印
6
。 -
回溯到
middleware[1]
middleware[1](context, next);
第二个中间件执行其后半部分,打印
4
。 -
回溯到
middleware[0]
middleware[0](context, next);
第一个中间件执行其后半部分,打印
2
。
通过这种方式,我们可以实现类似于 Koa 框架中的洋葱模型,使得中间件的执行既有序又灵活。
场景三:URL解析
在 Web 开发中,经常需要解析 URL 来获取其中的信息。下面我们来看看如何手动实现一个简单的 URL 解析器。
代码片段及解释
const url = 'https://www.domain.com/order/detail?user=admin&id=123&id=234&city=%E5%8D%97%E6%98%8C&enabled'
console.log(parse(url));
// 解析后的结果
// {
// protocol: 'http',
// hostname: 'www.domain.com',
// path: '/order',
// query: {
// user: 'admin',
// id: ["123", '234'],
// city: '南昌',
// enabled: true
// }
// }
// 方法一:字符分割
function parse(url) {
const protocolArr = url.split('://')
const protocol = protocolArr[0]
const hostnameArr = protocolArr[1].split('/') // ['www.domain.com', 'order', 'detail?user=adminxxxxxx']
const hostname = hostnameArr[0]
const pathArr = protocolArr[1].split('?')
// pathArr[0] www.domain.com/order/detail
const path = pathArr[0].split(hostname)[1]
// pathArr[1] user=admin&id=123&city=南昌&enabled
const queryArr = pathArr[1].split('&') // ['user=admin', 'id=123', 'xxx']
const query = {}
queryArr.forEach(item => {
if (!item.includes('=')) {
query[item] = true
return
}
const itemArr = item.split('=')
const key = itemArr[0]
const value = decodeURI(itemArr[1]) // %E5%8D%97%E6%98%8C
if (query[key] !== undefined) {
if (!Array.isArray(query[key])) {
query[key] = [query[key]]
}
query[key].push(value)
} else {
query[key] = value
}
});
return {
protocol,
hostname,
path,
query
}
}
//方法二:
const u=new URL(url)
console.log(u)
解释
方法一:
-
定义 URL (
url
) : 这是一个示例 URL,用于测试我们的解析函数。 -
定义解析函数 (
parse
) :parse
函数接收一个 URL 字符串作为参数,并将其解析成各个部分,包括协议、主机名、路径和查询参数。 通过split
方法分割字符串来提取协议、主机名、路径和查询字符串。 对查询字符串进行进一步处理,将键值对存储到一个对象中,并处理重复的键。 -
输出解析结果:调用
parse
函数并打印结果,可以看到 URL 各个部分被正确解析。
方法二:
用这个实例上的属性就快速可以访问到
协议
、域名
等这些了,但是有的还是得分割。
场景四:红绿灯问题
在处理需要按顺序执行的任务时,异步编程是非常有用的。下面我们来看看如何使用异步函数和 Promise 来控制一系列任务的执行顺序。
代码片段及解释
function red(){
console.log('红色');
}
function green(){
console.log('绿色');
}
function yellow(){
console.log('黄色');
}
//方法一:递归
function run(){
red()
setTimeout(()=>{
green()
setTimeout(()=>{
yellow()
setTimeout(()=>{
run()
},1000)
},2000)
},3000)
}
run()
// 方法二
function timePromise(time){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve()
},time)
})
}
async function changeColor(time,fn){
fn()
await timePromise(time)
}
async function run(){
while(true){
await changeColor(3000,red)
await changeColor(2000,green)
await changeColor(1000,yellow)
}
}
run()
解释
-
定义延时函数 (
timePromise
) : 返回一个 Promise,该 Promise 在指定的时间后解析。 -
定义颜色切换函数 (
changeColor
) : 该函数接收一个时间参数和一个颜色打印函数,先执行颜色打印函数,然后等待指定的时间。 -
定义主循环 (
run
) :是一个异步函数,它无限循环地调用changeColor
函数来改变颜色,并使用await
关键字等待每个颜色的显示时间。
通过这种方式,我们可以模拟交通灯的颜色变换过程,同时保证了颜色切换的顺序性和时间间隔。
场景五:浮点数计算
在 JavaScript 中,由于浮点数的表示方式,直接相加可能会导致精度丢失的问题,但是整数相加就没有这个问题,所有我们可以把它先变为整数进行运行再变回去。
代码片段及解释
const res= 0.1+ 0.2
console.log(res) //0.30000000000000004
function add(...args){
const maxLen=Math.max.apply(null,args.map(item=>{
const str=String(item).split('.')[1]
return str ? str.length : 0
}))
const base =10 ** maxLen
const res=args.reduce((pre,item,i,arr)=>{
return pre + item * base
},0)
return res / base
}
console.log(add(0.1, 0.2)) //0.3
解释
- ...args: 传入的参数,会存入
args
数组中。 - Math.max.apply(): 找到小数点后面最长的小数,在把它们都变成整数。
- args.reduce(): 起始值为零,遍历全部加起来再变为小数。
通过这种方法,可以避免 JavaScript 浮点数计算时的精度问题。
总结
本文通过几个具体的场景题,详细解析了发布订阅模式、中间件的实现、url解析、红绿灯问题以及浮点数计算的相关知识。希望这些对大家有帮助,还可以看看作者其他文章,感谢大家的阅读!