常考面试题:场景题系列(二)

754 阅读9分钟

前言

在面试过程中,常常会遇到一些实际应用场景的问题,本文将继续介绍几个常考的场景题,分别是:发布订阅模式中间件的实现解析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()
  1. 调用 fn() 当调用 fn() 时,实际上是在调用由 compose 函数返回的一个函数。这个函数有两个参数 contextnext,但在实际调用中并没有传入这两个参数,因此默认情况下它们将是 undefined

  2. 执行 dispatch(0)fn 函数内部,dispatch(0) 被立即调用。这里的 0 表示从索引为 0 的中间件开始执行。

dispatch 函数的执行
  1. 进入 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) 被调用,即调用第一个中间件。
  2. 第一个中间件执行

    middleware[0](context, next);
    

    第一个中间件打印 1,然后调用 next(),这将导致 dispatch 函数再次被调用,这次是 dispatch(1)

  3. 进入 dispatch(1)

    dispatch(1);
    
    • 条件检查i 的值为 1,小于 middleware.length,因此继续执行。
    • 获取当前中间件fn 被设置为 middleware[1],即第二个中间件。
    • 绑定下一个中间件next 被重新赋值为 dispatch.bind(context, 2)
    • 执行当前中间件fn(context, next) 被调用,即调用第二个中间件。
  4. 第二个中间件执行

    middleware[1](context, next);
    

    第二个中间件打印 3,然后调用 next(),这将导致 dispatch 函数再次被调用,这次是 dispatch(2)

  5. 进入 dispatch(2)

    dispatch(2);
    
    • 条件检查i 的值为 2,小于 middleware.length,因此继续执行。
    • 获取当前中间件fn 被设置为 middleware[2],即第三个中间件。
    • 绑定下一个中间件next 被重新赋值为 dispatch.bind(context, 3)
    • 执行当前中间件fn(context, next) 被调用,即调用第三个中间件。
  6. 第三个中间件执行

    middleware[2](context, next);
    

    第三个中间件打印 5,然后调用 next(),这将导致 dispatch 函数再次被调用,这次是 dispatch(3)

  7. 进入 dispatch(3)

    dispatch(3);
    
    • 条件检查i 的值为 3,大于或等于 middleware.length,因此 dispatch 函数返回,不再执行任何中间件。
回溯执行

现在,所有的中间件都已经调用了 next(),并且已经到达了最后一个中间件,因此开始回溯。

  1. 回溯到 middleware[2]

    1middleware[2](context, next);
    

    第三个中间件执行其后半部分,打印 6

  2. 回溯到 middleware[1]

    middleware[1](context, next);
    

    第二个中间件执行其后半部分,打印 4

  3. 回溯到 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)

解释

方法一:
  1. 定义 URL (url) : 这是一个示例 URL,用于测试我们的解析函数。

  2. 定义解析函数 (parse) :parse 函数接收一个 URL 字符串作为参数,并将其解析成各个部分,包括协议、主机名、路径和查询参数。 通过 split 方法分割字符串来提取协议、主机名、路径和查询字符串。 对查询字符串进行进一步处理,将键值对存储到一个对象中,并处理重复的键。

  3. 输出解析结果:调用 parse 函数并打印结果,可以看到 URL 各个部分被正确解析。

方法二:

image.png 用这个实例上的属性就快速可以访问到协议域名等这些了,但是有的还是得分割。

场景四:红绿灯问题

在处理需要按顺序执行的任务时,异步编程是非常有用的。下面我们来看看如何使用异步函数和 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()

解释

  1. 定义延时函数 (timePromise) : 返回一个 Promise,该 Promise 在指定的时间后解析。

  2. 定义颜色切换函数 (changeColor) : 该函数接收一个时间参数和一个颜色打印函数,先执行颜色打印函数,然后等待指定的时间。

  3. 定义主循环 (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

解释

  1. ...args: 传入的参数,会存入args数组中。
  2. Math.max.apply(): 找到小数点后面最长的小数,在把它们都变成整数。
  3. args.reduce(): 起始值为零,遍历全部加起来再变为小数。

通过这种方法,可以避免 JavaScript 浮点数计算时的精度问题。

总结

本文通过几个具体的场景题,详细解析了发布订阅模式、中间件的实现、url解析、红绿灯问题以及浮点数计算的相关知识。希望这些对大家有帮助,还可以看看作者其他文章,感谢大家的阅读!

image.png