前端面试题


前言

本文主要记录在面试过程中,所遇到的题目。


一、lodash.get方法?

问题:
object (Object): 要检索的对象。
path (string): 要获取属性的路径。
[defaultValue] (*): 如果解析值不存在,会返回 default。

用例:

const object = { 'a': [{ 'b': { 'c': 3 } }] };

console.log(_get(object, 'a[0].b.c'));
// => 3

console.log(_get(object, ['a', '0', 'b', 'c']));
// => 3

console.log(_get(object, 'a.b.c', 'default'));
// => 'default'

分析:

  • lodash.get是用来解决一些链路调用问题,使其能够在各种情况下都返回正确的值,而不是报错。
  • 官方API中path不止是string还可以是Array,也就是使用连续字符串也可以使用。
  • 在这里先将path进行处理,处理成数组[‘a’, ‘0’, ‘b’, ‘c’]形式,然后通过迭代去取值
function _get(object, path, defaultVal='default') {
    // 在这里实现
    let newPath = [] //存放预处理的path
    if (Array.isArray(path)){// 如果传入路径为数组形式直接赋值不用处理
        newPath = path
    }else {// 处理path为数组,利用replace替换'[]'为'.',利用split将字符串分割成字符数组
        newPath  = path.replace(/\[/g,'.').replace(/\]/g,'').split('.')
    }
    return newPath.reduce((o,k)=>{//通过reduce迭代newPath找路径没找到则返回defaultVal
        // { a: [ { b: [Object] } ] } a
        // [ { b: { c: 3 } } ] 0
        // { b: { c: 3 } } b
        // { c: 3 } c
        return (o ||{})[k]
    },object) || defaultVal
}

二、实现一个EventEmitter.js

问题:
实现一个 EventEmitter。

用例:

const eventEmitter = new EventEmitter()

function callback() {
    console.log('hit!')
}

// 监听事件, 其中有一个 once 单次监听
eventEmitter.on('custom-event', callback)
eventEmitter.once('custom-event', callback)

// 连续触发两次
eventEmitter.emit('custom-event')
eventEmitter.emit('custom-event')
// 预期输出 3 次 "hit!"

// 删除并再次=触发
eventEmitter.removeListener('custom-event');
eventEmitter.emit('custom-event')
// 预期没有输出

这里查了一下EventEmitter属于node服务端events模块对外提供的一个EventEmitter对象,用于对Node.js中对事件进行统一管理,表示没学Node,根本不知道啊,只知道浏览器事件EventTarget,不过二者都差不多,都是用来对事件进行处理的,不过浏览器事件会存在冒泡,因为在Node中不存在层级关系,浏览器DOM是存在层级关系的,且浏览器事件是基于观察者模式的,而EventEmitter的事件是基于发布订阅模式的。

分析:
需要编写一个类,实现内部方法on、once、emit、removeListener

  • on:注册事件监听器,接受两个参数,第一个参数是事件名称,第二个参数是事件监听器。
    先判断是否存在该事件,不存在旧创建空数组并将事件处理函数添加到数组中
  • once:注册事件监听器,只会触发一次,触发后会自动移除。
    本质还是调用on方法,只不过事件处理函数会被额外包裹一层,其中事件处理函数最后会调用off方法,
    off方法,会根据传入事件处理函数名称来去除不是 callback 的函数,这样旧形成了只调用一次
  • emit:按照注册的顺序同步调用为名为传入名称的事件注册的每个侦听器
    循环遍历事件集合,执行事件处理函数
  • removeListener:移除事件监听器,接受两个参数,第一个参数是事件名称,第二个参数是事件监听器。
    直接移除事件对象对应的属性
class EventEmitter {
    constructor() {
        this.events = {}// 存储事件
    }
    // 在这里实现
    on(event,callback) {// 监听
        if (!this.events[event]){// 是否存在该事件
            this.events[event] = []// 不存在创建一个空数组
        }
        this.events[event].push(callback)// 将事件处理函数添加到数组集合中
    }
    once(event,callback){// 单次监听
        const wrapper = () => {// 在外包裹一层,使调用时同时清除该次事件处理函数
            callback();
            this.off(event);
        };
        this.on(event, wrapper);
    }
    off(event){
        if (!this.events[event]) {
            return;
        }
        this.events[event] = this.events[event].filter((cb) => cb!== callback);
    }
    emit(event){// 触发事件
        if (!this.events[event]) {
            return;
        }
        this.events[event].forEach((callback) => callback());// 循环执行事件
    }
    removeListener(event){
        if (!this.events[event]) {
            return;
        }
        delete this.events[event]// 删除事件
    }
}

三、渲染VNode.js

问题:
写个函数用来渲染这个结构
就是将一个虚拟DOM渲染成真实DOM的过程

用例:

const renderJSON = {
    type: 'div',
    props: {
        className: '',
    },
    childrens:[
        {
            type: 'p',
            props: {
                text:'xxxxx'
            },
            childrens:['xxxx']
        }
    ]
}

分析:

  • type:标签名
  • props:属性名,是个集合可能包含许多属性,需要遍历挂载,需要处理行内样式和值的绑定
  • childrens:子元素,子元素分为两种:一种为标签元素,另外一种为文本元素
    查看结构每个对象中都会包含childrens用来储存该DOM下的层级关系,通过递归的形式进行渲染
const render = (renderJSON) =>{
    const {type, props, childrens} = renderJSON //将三个参数结构出来
    let el = document.createElement(type)// 创建标签元素
    for (let key in props){// 挂载属性
        el.setAttribute(key,props[key])//设置属性上的值,这里没有考虑行内样式以及绑定值的处理
    }
    //创建子节点
    childrens.forEach(child =>{
        if (child instanceof Object){//如果为标签元素
            el.appendChild(render(child))//将子元素添加到父元素内部末尾处,递归创建子元素
        }else {//如果为文本
            let textNode = document.createTextNode(child)//创建一个文本节点,将文本塞入
            el.appendChild(textNode)//添加文本结点到父元素内部
        }
    })
    return el
}

document.body.appendChild(render(renderJSON))// 挂载到body下

四、设计一个Cache.js

问题:
设计一个 Cache
支持下列两个基本操作:

  • set(id, object), 根据id设置对象;
  • get(id): 根据id得到一个对象;
    同时它有下面几个性质:
  1. x秒自动过期, 如果cache内的对象, x秒内没有被get或者set过, 则会自动过期;
  2. 对象数限制, 该cache可以设置一个n, 表示cache最多能存储的对象数;
  3. LRU置换, 当进行set操作时, 如果此时cache内对象数已经到达了n个, 则cache自动将最久未被使用过的那个对象剔除, 腾出空间放置新对象;

用例:

const cache = new Cache(2,3)
cache.set(1,{name:'smz1'})
cache.set(2,{name:'smz2'})
cache.set(1,{name:'smz3'})
setTimeout(()=>{
    console.log(cache.get(1))// 已过期
},4000)

分析:

  • set方法,在设置缓存对象时,我们首先将其封装成一个对象 { obj, timestamp },其中 timestamp 表示缓存对象的时间戳,用于判断对象是否过期。
    然后,我们将该对象存储在缓存中,当缓存中不存在该缓存时将其唯一标识添加到 LRU 链表的末尾。检查缓存的大小,如果超过了最大大小,则自动删除最早添加的缓存对象;如果存在,则更新 LRU 链表位置,以及缓存时间戳信息。
  • get 方法用于获取缓存对象,它接受一个参数 id,表示要获取的缓存对象的唯一标识。
    在获取缓存对象时,我们首先检查该对象是否存在,如果不存在,则返回 不存在。
    如果存在,则检查该对象是否过期,如果过期,则从缓存中删除该对象,并返回 ‘已过期’。
    否则,我们将该对象移动到 LRU 链表的首部,并返回缓存对象。
  • delete 方法用于删除缓存对象,它接受一个参数 id,表示要删除的缓存对象的唯一标识。
    在删除缓存对象时,我们首先从缓存中删除该对象,并从 LRU 链表中删除该对象的唯一标识。
  • delete方法,用于删除缓存及标识
  • _checkSize方法,用于删除最久未使用的
  • _moveToFront方法,用于更新LRU链位置
class Cache {
    constructor(maxSize = 10,maxAge = 60) {
        this.maxSize = maxSize // 最大缓存数
        this.maxAge = maxAge// 最长过期时间
        this.cache = {}// 缓存列表
        this.lruList = [] // 缓存唯一标识
    }
    set(id,obj){
        const item = this.cache[id];// 在缓存中查找是否存在
        const timestamp = Date.now() // 存储建立的时间
        this.cache[id] = {obj, timestamp} // 封装成对象存储在缓存中
        if (!item){// 不存在
            this.lruList.push(id)// 将唯一标识添加到链表末尾
            this._checkSize()// 检查缓存大小
        }else {// 存在
            this._moveToFront(id);//将该id标识移动到最后面
        }
    }
    get(id){
        const item = this.cache[id];// 在缓存中查找是否存在
        if (!item) {// 不存在返回提示
            return '不存在';
        }
        if (Date.now() - item.timestamp > this.maxAge * 1000) {// 判断是否过期,过期删除并返回提示
            this.delete(id);
            return '已过期';
        }
        this._moveToFront(id);//将该id标识移动到最后面
        return item.obj;
    }
    delete(id){// 过期删除缓存及标识
        delete this.cache[id];// 删除缓存
        this.lruList = this.lruList.filter((item) => item!== id);// 移除标识
    }
    _checkSize() {// 缓存满删除缓存
        if (this.lruList.length > this.maxSize) {// 大于了最大储存数时
            const id = this.lruList.shift();// 返回第一个元素
            delete this.cache[id];// 在缓存中删除
        }
    }

    _moveToFront(id) {// 更新id标识位置
        const index = this.lruList.indexOf(id);// 指定元素下标
        if (index!== -1) {// 存在
            this.lruList.splice(index, 1);//移除id标识旧位置
            this.lruList.push(id);// 将id标识添加到链表最后面
        }
    }

    getList(){// 返回存储集合
        return this.cache
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天将降大任于我

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值