文章目录
前言
本文主要记录在面试过程中,所遇到的题目。
一、lodash.get方法?
问题:
object (Object): 要检索的对象。
path (string): 要获取属性的路径。
[defaultValue] (*): 如果解析值不存在,会返回 default。
用例:
javascript
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']形式,然后通过迭代去取值
javascript
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。
用例:
javascript
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:移除事件监听器,接受两个参数,第一个参数是事件名称,第二个参数是事件监听器。
直接移除事件对象对应的属性
javascript
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的过程
用例:
javascript
const renderJSON = {
type: 'div',
props: {
className: '',
},
childrens:[
{
type: 'p',
props: {
text:'xxxxx'
},
childrens:['xxxx']
}
]
}
分析:
- type:标签名
- props:属性名,是个集合可能包含许多属性,需要遍历挂载,需要处理行内样式和值的绑定
- childrens:子元素,子元素分为两种:一种为标签元素,另外一种为文本元素
查看结构每个对象中都会包含childrens用来储存该DOM下的层级关系,通过递归的形式进行渲染
javascript
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得到一个对象;
同时它有下面几个性质:
- x秒自动过期, 如果cache内的对象, x秒内没有被get或者set过, 则会自动过期;
- 对象数限制, 该cache可以设置一个n, 表示cache最多能存储的对象数;
- LRU置换, 当进行set操作时, 如果此时cache内对象数已经到达了n个, 则cache自动将最久未被使用过的那个对象剔除, 腾出空间放置新对象;
用例:
javascript
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链位置
javascript
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
}
}