主页 > 其他  > 

2023前端必会手写面试题整理

2023前端必会手写面试题整理
实现一个compose函数

组合多个函数,从右到左,比如:compose(f, g, h) 最终得到这个结果 (...args) => f(g(h(...args))).

题目描述:实现一个 compose 函数

// 用法如下: function fn1(x) { return x + 1; } function fn2(x) { return x + 2; } function fn3(x) { return x + 3; } function fn4(x) { return x + 4; } const a = compose(fn1, fn2, fn3, fn4); console.log(a(1)); // 1+4+3+2+1=11

实现代码如下

function compose(...funcs) { if (!funcs.length) return (v) => v; if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => { return (...args) => a(b(...args))) } }

compose创建了一个从右向左执行的数据流。如果要实现从左到右的数据流,可以直接更改compose的部分代码即可实现

更换Api接口:把reduce改为reduceRight交互包裹位置:把a(b(...args))改为b(a(...args)) 实现迭代器生成函数

我们说迭代器对象全凭迭代器生成函数帮我们生成。在ES6中,实现一个迭代器生成函数并不是什么难事儿,因为ES6早帮我们考虑好了全套的解决方案,内置了贴心的 生成器 (Generator)供我们使用:

// 编写一个迭代器生成函数 function *iteratorGenerator() { yield '1号选手' yield '2号选手' yield '3号选手' } const iterator = iteratorGenerator() iterator.next() iterator.next() iterator.next()

丢进控制台,不负众望:

写一个生成器函数并没有什么难度,但在面试的过程中,面试官往往对生成器这种语法糖背后的实现逻辑更感兴趣。下面我们要做的,不仅仅是写一个迭代器对象,而是用ES5去写一个能够生成迭代器对象的迭代器生成函数(解析在注释里):

// 定义生成器函数,入参是任意集合 function iteratorGenerator(list) { // idx记录当前访问的索引 var idx = 0 // len记录传入集合的长度 var len = list.length return { // 自定义next方法 next: function() { // 如果索引还没有超出集合长度,done为false var done = idx >= len // 如果done为false,则可以继续取值 var value = !done ? list[idx++] : undefined // 将当前值与遍历是否完毕(done)返回 return { done: done, value: value } } } } var iterator = iteratorGenerator(['1号选手', '2号选手', '3号选手']) iterator.next() iterator.next() iterator.next()

此处为了记录每次遍历的位置,我们实现了一个闭包,借助自由变量来做我们的迭代过程中的“游标”。

运行一下我们自定义的迭代器,结果符合预期:

实现单例模式

核心要点: 用闭包和Proxy属性拦截

function proxy(func) { let instance; let handler = { constructor(target, args) { if(!instance) { instance = Reflect.constructor(fun, args); } return instance; } } return new Proxy(func, handler); } 实现reduce方法 初始值不传怎么处理回调函数的参数有哪些,返回值如何处理。 Array.prototype.myReduce = function(fn, initialValue) { var arr = Array.prototype.slice.call(this); var res, startIndex; res = initialValue ? initialValue : arr[0]; // 不传默认取数组第一项 startIndex = initialValue ? 0 : 1; for(var i = startIndex; i < arr.length; i++) { // 把初始值、当前值、索引、当前数组返回去。调用的时候传到函数参数中 [1,2,3,4].reduce((initVal,curr,index,arr)) res = fn.call(null, res, arr[i], i, this); } return res; } 实现redux中间件

简单实现

function createStore(reducer) { let currentState let listeners = [] function getState() { return currentState } function dispatch(action) { currentState = reducer(currentState, action) listeners.map(listener => { listener() }) return action } function subscribe(cb) { listeners.push(cb) return () => {} } dispatch({type: 'ZZZZZZZZZZ'}) return { getState, dispatch, subscribe } } // 应用实例如下: function reducer(state = 0, action) { switch (action.type) { case 'ADD': return state + 1 case 'MINUS': return state - 1 default: return state } } const store = createStore(reducer) console.log(store); store.subscribe(() => { console.log('change'); }) console.log(store.getState()); console.log(store.dispatch({type: 'ADD'})); console.log(store.getState());

2. 迷你版

export const createStore = (reducer,enhancer)=>{ if(enhancer) { return enhancer(createStore)(reducer) } let currentState = {} let currentListeners = [] const getState = ()=>currentState const subscribe = (listener)=>{ currentListeners.push(listener) } const dispatch = action=>{ currentState = reducer(currentState, action) currentListeners.forEach(v=>v()) return action } dispatch({type:'@@INIT'}) return {getState,subscribe,dispatch} } //中间件实现 export applyMiddleWare(...middlewares){ return createStore=>...args=>{ const store = createStore(...args) let dispatch = store.dispatch const midApi = { getState:store.getState, dispatch:...args=>dispatch(...args) } const middlewaresChain = middlewares.map(middleware=>middleware(midApi)) dispatch = compose(...middlewaresChain)(store.dispatch) return { ...store, dispatch } } // fn1(fn2(fn3())) 把函数嵌套依次调用 export function compose(...funcs){ if(funcs.length===0){ return arg=>arg } if(funs.length===1){ return funs[0] } return funcs.reduce((ret,item)=>(...args)=>ret(item(...args))) } //bindActionCreator实现 function bindActionCreator(creator,dispatch){ return ...args=>dispatch(creator(...args)) } function bindActionCreators(creators,didpatch){ //let bound = {} //Object.keys(creators).forEach(v=>{ // let creator = creator[v] // bound[v] = bindActionCreator(creator,dispatch) //}) //return bound return Object.keys(creators).reduce((ret,item)=>{ ret[item] = bindActionCreator(creators[item],dispatch) return ret },{}) } 实现数组扁平化flat方法

题目描述: 实现一个方法使多维数组变成一维数组

let ary = [1, [2, [3, [4, 5]]], 6]; let str = JSON.stringify(ary);

第0种处理:直接的调用

arr_flat = arr.flat(Infinity);

第一种处理

ary = str.replace(/(\[|\])/g, '').split(',');

第二种处理

str = str.replace(/(\[\]))/g, ''); str = '[' + str + ']'; ary = JSON.parse(str);

第三种处理:递归处理

let result = []; let fn = function(ary) { for(let i = 0; i < ary.length; i++) }{ let item = ary[i]; if (Array.isArray(ary[i])){ fn(item); } else { result.push(item); } } }

第四种处理:用 reduce 实现数组的 flat 方法

function flatten(ary) { return ary.reduce((pre, cur) => { return pre.concat(Array.isArray(cur) ? flatten(cur) : cur); }, []); } let ary = [1, 2, [3, 4], [5, [6, 7]]] console.log(flatten(ary))

第五种处理:能用迭代的思路去实现

function flatten(arr) { if (!arr.length) return; while (arr.some((item) => Array.isArray(item))) { arr = [].concat(...arr); } return arr; } // console.log(flatten([1, 2, [1, [2, 3, [4, 5, [6]]]]]));

第六种处理:扩展运算符

while (ary.some(Array.isArray)) { ary = [].concat(...ary); }

参考 前端进阶面试题详细解答

实现深拷贝 简洁版本

简单版:

const newObj = JSON.parse(JSON.stringify(oldObj));

局限性:

他无法实现对函数 、RegExp等特殊对象的克隆会抛弃对象的constructor,所有的构造函数会指向Object对象有循环引用,会报错

面试简版

function deepClone(obj) { // 如果是 值类型 或 null,则直接return if(typeof obj !== 'object' || obj === null) { return obj } // 定义结果对象 let copy = {} // 如果对象是数组,则定义结果数组 if(obj.constructor === Array) { copy = [] } // 遍历对象的key for(let key in obj) { // 如果key是对象的自有属性 if(obj.hasOwnProperty(key)) { // 递归调用深拷贝方法 copy[key] = deepClone(obj[key]) } } return copy }

调用深拷贝方法,若属性为值类型,则直接返回;若属性为引用类型,则递归遍历。这就是我们在解这一类题时的核心的方法。

进阶版

解决拷贝循环引用问题解决拷贝对应原型问题 // 递归拷贝 (类型判断) function deepClone(value,hash = new WeakMap){ // 弱引用,不用map,weakMap更合适一点 // null 和 undefiend 是不需要拷贝的 if(value == null){ return value;} if(value instanceof RegExp) { return new RegExp(value) } if(value instanceof Date) { return new Date(value) } // 函数是不需要拷贝 if(typeof value != 'object') return value; let obj = new value.constructor(); // [] {} // 说明是一个对象类型 if(hash.get(value)){ return hash.get(value) } hash.set(value,obj); for(let key in value){ // in 会遍历当前对象上的属性 和 __proto__指代的属性 // 补拷贝 对象的__proto__上的属性 if(value.hasOwnProperty(key)){ // 如果值还有可能是对象 就继续拷贝 obj[key] = deepClone(value[key],hash); } } return obj // 区分对象和数组 Object.prototype.toString.call } // test var o = {}; o.x = o; var o1 = deepClone(o); // 如果这个对象拷贝过了 就返回那个拷贝的结果就可以了 console.log(o1); 实现完整的深拷贝

1. 简易版及问题

JSON.parse(JSON.stringify());

估计这个api能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的。问题如下:

无法解决循环引用的问题。举个例子: const a = {val:2}; a.target = a;

拷贝a会出现系统栈溢出,因为出现了无限递归的情况。

无法拷贝一些特殊的对象,诸如 RegExp, Date, Set, Map等无法拷贝函数(划重点)。

因此这个api先pass掉,我们重新写一个深拷贝,简易版如下:

const deepClone = (target) => { if (typeof target === 'object' && target !== null) { const cloneTarget = Array.isArray(target) ? []: {}; for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = deepClone(target[prop]); } } return cloneTarget; } else { return target; } }

现在,我们以刚刚发现的三个问题为导向,一步步来完善、优化我们的深拷贝代码。

2. 解决循环引用

现在问题如下:

let obj = {val : 100}; obj.target = obj; deepClone(obj);//报错: RangeError: Maximum call stack size exceeded

这就是循环引用。我们怎么来解决这个问题呢?

创建一个Map。记录下已经拷贝过的对象,如果说已经拷贝过,那直接返回它行了。

const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null; const deepClone = (target, map = new Map()) => { if(map.get(target)) return target; if (isObject(target)) { map.set(target, true); const cloneTarget = Array.isArray(target) ? []: {}; for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = deepClone(target[prop],map); } } return cloneTarget; } else { return target; } }

现在来试一试:

const a = {val:2}; a.target = a; let newA = deepClone(a); console.log(newA)//{ val: 2, target: { val: 2, target: [Circular] } }

好像是没有问题了, 拷贝也完成了。但还是有一个潜在的坑, 就是map 上的 key 和 map 构成了强引用关系,这是相当危险的。我给你解释一下与之相对的弱引用的概念你就明白了

在计算机程序设计中,弱引用与强引用相对,

被弱引用的对象可以在任何时候被回收,而对于强引用来说,只要这个强引用还在,那么对象无法被回收。拿上面的例子说,map 和 a一直是强引用的关系, 在程序结束之前,a 所占的内存空间一直不会被释放。

怎么解决这个问题?

很简单,让 map 的 key 和 map 构成弱引用即可。ES6给我们提供了这样的数据结构,它的名字叫WeakMap,它是一种特殊的Map, 其中的键是弱引用的。其键必须是对象,而值可以是任意的

稍微改造一下即可:

const deepClone = (target, map = new WeakMap()) => { //... }

3. 拷贝特殊对象

可继续遍历

对于特殊的对象,我们使用以下方式来鉴别:

Object.prototype.toString.call(obj);

梳理一下对于可遍历对象会有什么结果:

["object Map"] ["object Set"] ["object Array"] ["object Object"] ["object Arguments"]

以这些不同的字符串为依据,我们就可以成功地鉴别这些对象。

const getType = Object.prototype.toString.call(obj); const canTraverse = { '[object Map]': true, '[object Set]': true, '[object Array]': true, '[object Object]': true, '[object Arguments]': true, }; const deepClone = (target, map = new Map()) => { if(!isObject(target)) return target; let type = getType(target); let cloneTarget; if(!canTraverse[type]) { // 处理不能遍历的对象 return; }else { // 这波操作相当关键,可以保证对象的原型不丢失! let ctor = target.prototype; cloneTarget = new ctor(); } if(map.get(target)) return target; map.put(target, true); if(type === mapTag) { //处理Map target.forEach((item, key) => { cloneTarget.set(deepClone(key), deepClone(item)); }) } if(type === setTag) { //处理Set target.forEach(item => { target.add(deepClone(item)); }) } // 处理数组和对象 for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = deepClone(target[prop]); } } return cloneTarget; }

不可遍历的对象

const boolTag = '[object Boolean]'; const numberTag = '[object Number]'; const stringTag = '[object String]'; const dateTag = '[object Date]'; const errorTag = '[object Error]'; const regexpTag = '[object RegExp]'; const funcTag = '[object Function]';

对于不可遍历的对象,不同的对象有不同的处理。

const handleRegExp = (target) => { const { source, flags } = target; return new target.constructor(source, flags); } const handleFunc = (target) => { // 待会的重点部分 } const handleNotTraverse = (target, tag) => { const Ctor = targe.constructor; switch(tag) { case boolTag: case numberTag: case stringTag: case errorTag: case dateTag: return new Ctor(target); case regexpTag: return handleRegExp(target); case funcTag: return handleFunc(target); default: return new Ctor(target); } }

4. 拷贝函数

虽然函数也是对象,但是它过于特殊,我们单独把它拿出来拆解。提到函数,在JS种有两种函数,一种是普通函数,另一种是箭头函数。每个普通函数都是Function的实例,而箭头函数不是任何类的实例,每次调用都是不一样的引用。那我们只需要处理普通函数的情况,箭头函数直接返回它本身就好了。

那么如何来区分两者呢?

答案是: 利用原型。箭头函数是不存在原型的。

const handleFunc = (func) => { // 箭头函数直接返回自身 if(!func.prototype) return func; const bodyReg = /(?<={)(.|\n)+(?=})/m; const paramReg = /(?<=\().+(?=\)\s+{)/; const funcString = func.toString(); // 分别匹配 函数参数 和 函数体 const param = paramReg.exec(funcString); const body = bodyReg.exec(funcString); if(!body) return null; if (param) { const paramArr = param[0].split(','); return new Function(...paramArr, body[0]); } else { return new Function(body[0]); } }

5. 完整代码展示

const getType = obj => Object.prototype.toString.call(obj); const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null; const canTraverse = { '[object Map]': true, '[object Set]': true, '[object Array]': true, '[object Object]': true, '[object Arguments]': true, }; const mapTag = '[object Map]'; const setTag = '[object Set]'; const boolTag = '[object Boolean]'; const numberTag = '[object Number]'; const stringTag = '[object String]'; const symbolTag = '[object Symbol]'; const dateTag = '[object Date]'; const errorTag = '[object Error]'; const regexpTag = '[object RegExp]'; const funcTag = '[object Function]'; const handleRegExp = (target) => { const { source, flags } = target; return new target.constructor(source, flags); } const handleFunc = (func) => { // 箭头函数直接返回自身 if(!func.prototype) return func; const bodyReg = /(?<={)(.|\n)+(?=})/m; const paramReg = /(?<=\().+(?=\)\s+{)/; const funcString = func.toString(); // 分别匹配 函数参数 和 函数体 const param = paramReg.exec(funcString); const body = bodyReg.exec(funcString); if(!body) return null; if (param) { const paramArr = param[0].split(','); return new Function(...paramArr, body[0]); } else { return new Function(body[0]); } } const handleNotTraverse = (target, tag) => { const Ctor = target.constructor; switch(tag) { case boolTag: return new Object(Boolean.prototype.valueOf.call(target)); case numberTag: return new Object(Number.prototype.valueOf.call(target)); case stringTag: return new Object(String.prototype.valueOf.call(target)); case symbolTag: return new Object(Symbol.prototype.valueOf.call(target)); case errorTag: case dateTag: return new Ctor(target); case regexpTag: return handleRegExp(target); case funcTag: return handleFunc(target); default: return new Ctor(target); } } const deepClone = (target, map = new WeakMap()) => { if(!isObject(target)) return target; let type = getType(target); let cloneTarget; if(!canTraverse[type]) { // 处理不能遍历的对象 return handleNotTraverse(target, type); }else { // 这波操作相当关键,可以保证对象的原型不丢失! let ctor = target.constructor; cloneTarget = new ctor(); } if(map.get(target)) return target; map.set(target, true); if(type === mapTag) { //处理Map target.forEach((item, key) => { cloneTarget.set(deepClone(key, map), deepClone(item, map)); }) } if(type === setTag) { //处理Set target.forEach(item => { cloneTarget.add(deepClone(item, map)); }) } // 处理数组和对象 for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = deepClone(target[prop], map); } } return cloneTarget; } 实现every方法 Array.prototype.myEvery=function(callback, context = window){ var len=this.length, flag=true, i = 0; for(;i < len; i++){ if(!callback.apply(context,[this[i], i , this])){ flag=false; break; } } return flag; } // var obj = {num: 1} // var aa=arr.myEvery(function(v,index,arr){ // return v.num>=12; // },obj) // console.log(aa) 实现map方法 回调函数的参数有哪些,返回值如何处理不修改原来的数组 Array.prototype.myMap = function(callback, context){ // 转换类数组 var arr = Array.prototype.slice.call(this),//由于是ES5所以就不用...展开符了 mappedArr = [], i = 0; for (; i < arr.length; i++ ){ // 把当前值、索引、当前数组返回去。调用的时候传到函数参数中 [1,2,3,4].map((curr,index,arr)) mappedArr.push(callback.call(context, arr[i], i, this)); } return mappedArr; } 实现一个迷你版的vue

入口

// js/vue.js class Vue { constructor (options) { // 1. 通过属性保存选项的数据 this.$options = options || {} this.$data = options.data || {} this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el // 2. 把data中的成员转换成getter和setter,注入到vue实例中 this._proxyData(this.$data) // 3. 调用observer对象,监听数据的变化 new Observer(this.$data) // 4. 调用compiler对象,解析指令和差值表达式 new Compiler(this) } _proxyData (data) { // 遍历data中的所有属性 Object.keys(data).forEach(key => { // 把data的属性注入到vue实例中 Object.defineProperty(this, key, { enumerable: true, configurable: true, get () { return data[key] }, set (newValue) { if (newValue === data[key]) { return } data[key] = newValue } }) }) } }

实现Dep

class Dep { constructor () { // 存储所有的观察者 this.subs = [] } // 添加观察者 addSub (sub) { if (sub && sub.update) { this.subs.push(sub) } } // 发送通知 notify () { this.subs.forEach(sub => { sub.update() }) } }

实现watcher

class Watcher { constructor (vm, key, cb) { this.vm = vm // data中的属性名称 this.key = key // 回调函数负责更新视图 this.cb = cb // 把watcher对象记录到Dep类的静态属性target Dep.target = this // 触发get方法,在get方法中会调用addSub this.oldValue = vm[key] Dep.target = null } // 当数据发生变化的时候更新视图 update () { let newValue = this.vm[this.key] if (this.oldValue === newValue) { return } this.cb(newValue) } }

实现compiler

class Compiler { constructor (vm) { this.el = vm.$el this.vm = vm this pile(this.el) } // 编译模板,处理文本节点和元素节点 compile (el) { let childNodes = el.childNodes Array.from(childNodes).forEach(node => { // 处理文本节点 if (this.isTextNode(node)) { this pileText(node) } else if (this.isElementNode(node)) { // 处理元素节点 this pileElement(node) } // 判断node节点,是否有子节点,如果有子节点,要递归调用compile if (node.childNodes && node.childNodes.length) { this pile(node) } }) } // 编译元素节点,处理指令 compileElement (node) { // console.log(node.attributes) // 遍历所有的属性节点 Array.from(node.attributes).forEach(attr => { // 判断是否是指令 let attrName = attr.name if (this.isDirective(attrName)) { // v-text --> text attrName = attrName.substr(2) let key = attr.value this.update(node, key, attrName) } }) } update (node, key, attrName) { let updateFn = this[attrName + 'Updater'] updateFn && updateFn.call(this, node, this.vm[key], key) } // 处理 v-text 指令 textUpdater (node, value, key) { node.textContent = value new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } // v-model modelUpdater (node, value, key) { node.value = value new Watcher(this.vm, key, (newValue) => { node.value = newValue }) // 双向绑定 node.addEventListener('input', () => { this.vm[key] = node.value }) } // 编译文本节点,处理差值表达式 compileText (node) { // console.dir(node) // {{ msg }} let reg = /\{\{(.+?)\}\}/ let value = node.textContent if (reg.test(value)) { let key = RegExp.$1.trim() node.textContent = value.replace(reg, this.vm[key]) // 创建watcher对象,当数据改变更新视图 new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } } // 判断元素属性是否是指令 isDirective (attrName) { return attrName.startsWith('v-') } // 判断节点是否是文本节点 isTextNode (node) { return node.nodeType === 3 } // 判断节点是否是元素节点 isElementNode (node) { return node.nodeType === 1 } }

实现Observer

class Observer { constructor (data) { this.walk(data) } walk (data) { // 1. 判断data是否是对象 if (!data || typeof data !== 'object') { return } // 2. 遍历data对象的所有属性 Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]) }) } defineReactive (obj, key, val) { let that = this // 负责收集依赖,并发送通知 let dep = new Dep() // 如果val是对象,把val内部的属性转换成响应式数据 this.walk(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get () { // 收集依赖 Dep.target && dep.addSub(Dep.target) return val }, set (newValue) { if (newValue === val) { return } val = newValue that.walk(newValue) // 发送通知 dep.notify() } }) } }

使用

<!DOCTYPE html> <html lang="cn"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Mini Vue</title> </head> <body> <div id="app"> <h1>差值表达式</h1> <h3>{{ msg }}</h3> <h3>{{ count }}</h3> <h1>v-text</h1> <div v-text="msg"></div> <h1>v-model</h1> <input type="text" v-model="msg"> <input type="text" v-model="count"> </div> <script src="./js/dep.js"></script> <script src="./js/watcher.js"></script> <script src="./js/compiler.js"></script> <script src="./js/observer.js"></script> <script src="./js/vue.js"></script> <script> let vm = new Vue({ el: '#app', data: { msg: 'Hello Vue', count: 100, person: { name: 'zs' } } }) console.log(vm.msg) // vm.msg = { test: 'Hello' } vm.test = 'abc' </script> </body> </html> 实现ES6的extends function B(name){ this.name = name; }; function A(name,age){ //1.将A的原型指向B Object.setPrototypeOf(A,B); //2.用A的实例作为this调用B,得到继承B之后的实例,这一步相当于调用super Object.getPrototypeOf(A).call(this, name) //3.将A原有的属性添加到新实例上 this.age = age; //4.返回新实例对象 return this; }; var a = new A('poetry',22); console.log(a); 实现ES6的const

由于ES5环境没有block的概念,所以是无法百分百实现const,只能是挂载到某个对象下,要么是全局的window,要么就是自定义一个object来当容器

var __const = function __const (data, value) { window.data = value // 把要定义的data挂载到window下,并赋值value Object.defineProperty(window, data, { // 利用Object.defineProperty的能力劫持当前对象,并修改其属性描述符 enumerable: false, configurable: false, get: function () { return value }, set: function (data) { if (data !== value) { // 当要对当前属性进行赋值时,则抛出错误! throw new TypeError('Assignment to constant variable.') } else { return value } } }) } __const('a', 10) console.log(a) delete a console.log(a) for (let item in window) { // 因为const定义的属性在global下也是不存在的,所以用到了enumerable: false来模拟这一功能 if (item === 'a') { // 因为不可枚举,所以不执行 console.log(window[item]) } } a = 20 // 报错

Vue目前双向绑定的核心实现思路就是利用Object.defineProperty对get跟set进行劫持,监听用户对属性进行调用以及赋值时的具体情况,从而实现的双向绑定

实现节流函数(throttle)

节流函数原理:指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。总结起来就是: 事件,按照一段时间的间隔来进行触发 。

像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多

手写简版

使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 wait 秒之后才执行一次,并且最后一次触发事件不会被执行

时间戳方式:

// func是用户传入需要防抖的函数 // wait是等待时间 const throttle = (func, wait = 50) => { // 上一次执行该函数的时间 let lastTime = 0 return function(...args) { // 当前时间 let now = +new Date() // 将当前时间和上一次执行函数时间对比 // 如果差值大于设置的等待时间就执行函数 if (now - lastTime > wait) { lastTime = now func.apply(this, args) } } } setInterval( throttle(() => { console.log(1) }, 500), 1 )

定时器方式:

使用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数

function throttle(func, delay){ var timer = null; returnfunction(){ var context = this; var args = arguments; if(!timer){ timer = setTimeout(function(){ func.apply(context, args); timer = null; },delay); } } }

适用场景:

DOM 元素的拖拽功能实现(mousemove)搜索联想(keyup)计算鼠标移动的距离(mousemove)Canvas 模拟画板功能(mousemove)监听滚动事件判断是否到页面底部自动加载更多拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动缩放场景:监控浏览器resize动画场景:避免短时间内多次触发动画引起性能问题

总结

函数防抖 :将几次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。函数节流 :使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。 实现模板字符串解析功能 let template = '我是{{name}},年龄{{age}},性别{{sex}}'; let data = { name: '姓名', age: 18 } render(template, data); // 我是姓名,年龄18,性别undefined function render(template, data) { const reg = /\{\{(\w+)\}\}/; // 模板字符串正则 if (reg.test(template)) { // 判断模板里是否有模板字符串 const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段 template = template.replace(reg, data[name]); // 将第一个模板字符串渲染 return render(template, data); // 递归的渲染并返回渲染后的结构 } return template; // 如果模板没有模板字符串直接返回 } 实现一个简易的MVVM

实现一个简易的MVVM我会分为这么几步来:

首先我会定义一个类Vue,这个类接收的是一个options,那么其中可能有需要挂载的根元素的id,也就是el属性;然后应该还有一个data属性,表示需要双向绑定的数据其次我会定义一个Dep类,这个类产生的实例对象中会定义一个subs数组用来存放所依赖这个属性的依赖,已经添加依赖的方法addSub,删除方法removeSub,还有一个notify方法用来遍历更新它subs中的所有依赖,同时Dep类有一个静态属性target它用来表示当前的观察者,当后续进行依赖收集的时候可以将它添加到dep.subs中。然后设计一个observe方法,这个方法接收的是传进来的data,也就是options.data,里面会遍历data中的每一个属性,并使用Object.defineProperty()来重写它的get和set,那么这里面呢可以使用new Dep()实例化一个dep对象,在get的时候调用其addSub方法添加当前的观察者Dep.target完成依赖收集,并且在set的时候调用dep.notify方法来通知每一个依赖它的观察者进行更新完成这些之后,我们还需要一个compile方法来将HTML模版和数据结合起来。在这个方法中首先传入的是一个node节点,然后遍历它的所有子级,判断是否有firstElmentChild,有的话则进行递归调用compile方法,没有firstElementChild的话且该child.innderHTML用正则匹配满足有/\{\{(.*)\}\}/项的话则表示有需要双向绑定的数据,那么就将用正则new Reg('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm')替换掉是其为msg变量。完成变量替换的同时,还需要将Dep.target指向当前的这个child,且调用一下this.opt.data[key],也就是为了触发这个数据的get来对当前的child进行依赖收集,这样下次数据变化的时候就能通知child进行视图更新了,不过在最后要记得将Dep.target指为null哦(其实在Vue中是有一个targetStack栈用来存放target的指向的)那么最后我们只需要监听document的DOMContentLoaded然后在回调函数中实例化这个Vue对象就可以了

coding :

需要注意的点:

childNodes会获取到所有的子节点以及文本节点(包括元素标签中的空白节点)firstElementChild表示获取元素的第一个字元素节点,以此来区分是不是元素节点,如果是的话则调用compile进行递归调用,否则用正则匹配这里面的正则真的不难,大家可以看一下

完整代码如下:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>MVVM</title> </head> <body> <div id="app"> <h3>姓名</h3> <p>{{name}}</p> <h3>年龄</h3> <p>{{age}}</p> </div> </body> </html> <script> document.addEventListener( "DOMContentLoaded", function () { let opt = { el: "#app", data: { name: "等待修改...", age: 20 } }; let vm = new Vue(opt); setTimeout(() => { opt.data.name = "jing"; }, 2000); }, false ); class Vue { constructor(opt) { this.opt = opt; this.observer(opt.data); let root = document.querySelector(opt.el); this pile(root); } observer(data) { Object.keys(data).forEach((key) => { let obv = new Dep(); data["_" + key] = data[key]; Object.defineProperty(data, key, { get() { Dep.target && obv.addSubNode(Dep.target); return data["_" + key]; }, set(newVal) { obv.update(newVal); data["_" + key] = newVal; }, }); }); } compile(node) { [].forEach.call(node.childNodes, (child) => { if (!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)) { let key = RegExp.$1.trim(); child.innerHTML = child.innerHTML.replace( new RegExp("\\{\\{\\s*" + key + "\\s*\\}\\}", "gm"), this.opt.data[key] ); Dep.target = child; this.opt.data[key]; Dep.target = null; } else if (child.firstElementChild) this pile(child); }); } } class Dep { constructor() { this.subNode = []; } addSubNode(node) { this.subNode.push(node); } update(newVal) { this.subNode.forEach((node) => { node.innerHTML = newVal; }); } } </script>

简化版2

function update(){ console.log('数据变化~~~ mock update view') } let obj = [1,2,3] // 变异方法 push shift unshfit reverse sort splice pop // Object.defineProperty let oldProto = Array.prototype; let proto = Object.create(oldProto); // 克隆了一分 ['push','shift'].forEach(item=>{ proto[item] = function(){ update(); oldProto[item].apply(this,arguments); } }) function observer(value){ // proxy reflect if(Array.isArray(value)){ // AOP return value.__proto__ = proto; // 重写 这个数组里的push shift unshfit reverse sort splice pop } if(typeof value !== 'object'){ return value; } for(let key in value){ defineReactive(value,key,value[key]); } } function defineReactive(obj,key,value){ observer(value); // 如果是对象 继续增加getter和setter Object.defineProperty(obj,key,{ get(){ return value; }, set(newValue){ if(newValue !== value){ observer(newValue); value = newValue; update(); } } }) } observer(obj); // AOP // obj.name = {n:200}; // 数据变了 需要更新视图 深度监控 // obj.name.n = 100; obj.push(123); obj.push(456); console.log(obj); 实现instanceOf

思路:

步骤1:先取得当前类的原型,当前实例对象的原型链​步骤2:一直循环(执行原型链的查找机制) 取得当前实例对象原型链的原型链(proto = proto.__proto__,沿着原型链一直向上查找)如果 当前实例的原型链__proto__上找到了当前类的原型prototype,则返回 true如果 一直找到Object.prototype.__proto__ == null,Object的基类(null)上面都没找到,则返回 false // 实例.__ptoto__ === 类.prototype function _instanceof(example, classFunc) { // 由于instance要检测的是某对象,需要有一个前置判断条件 //基本数据类型直接返回false if(typeof example !== 'object' || example === null) return false; let proto = Object.getPrototypeOf(example); while(true) { if(proto == null) return false; // 在当前实例对象的原型链上,找到了当前类 if(proto == classFunc.prototype) return true; // 沿着原型链__ptoto__一层一层向上查 proto = Object.getPrototypeof(proto); // 等于proto.__ptoto__ } } console.log('test', _instanceof(null, Array)) // false console.log('test', _instanceof([], Array)) // true console.log('test', _instanceof('', Array)) // false console.log('test', _instanceof({}, Object)) // true 实现bind方法

bind 的实现对比其他两个函数略微地复杂了一点,涉及到参数合并(类似函数柯里化),因为 bind 需要返回一个函数,需要判断一些边界问题,以下是 bind 的实现

bind 返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式,我们先来说直接调用的方式对于直接调用来说,这里选择了 apply 的方式实现,但是对于参数需要注意以下情况:因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2),所以我们需要将两边的参数拼接起来最后来说通过 new 的方式,对于 new 的情况来说,不会被任何方式改变 this,所以对于这种情况我们需要忽略传入的 this

简洁版本

对于普通函数,绑定this指向对于构造函数,要保证原函数的原型对象上的属性不能丢失 Function.prototype.myBind = function(context = window, ...args) { // this表示调用bind的函数 let self = this; //返回了一个函数,...innerArgs为实际调用时传入的参数 let fBound = function(...innerArgs) { //this instanceof fBound为true表示构造函数的情况。如new func.bind(obj) // 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true,可以让实例获得来自绑定函数的值 // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context return self.apply( this instanceof fBound ? this : context, args.concat(innerArgs) ); } // 如果绑定的是构造函数,那么需要继承构造函数原型属性和方法:保证原函数的原型对象上的属性不丢失 // 实现继承的方式: 使用Object.create fBound.prototype = Object.create(this.prototype); return fBound; } // 测试用例 function Person(name, age) { console.log('Person name:', name); console.log('Person age:', age); console.log('Person this:', this); // 构造函数this指向实例对象 } // 构造函数原型的方法 Person.prototype.say = function() { console.log('person say'); } // 普通函数 function normalFun(name, age) { console.log('普通函数 name:', name); console.log('普通函数 age:', age); console.log('普通函数 this:', this); // 普通函数this指向绑定bind的第一个参数 也就是例子中的obj } var obj = { name: 'poetries', age: 18 } // 先测试作为构造函数调用 var bindFun = Person.myBind(obj, 'poetry1') // undefined var a = new bindFun(10) // Person name: poetry1、Person age: 10、Person this: fBound {} a.say() // person say // 再测试作为普通函数调用 var bindNormalFun = normalFun.myBind(obj, 'poetry2') // undefined bindNormalFun(12) // 普通函数name: poetry2 普通函数 age: 12 普通函数 this: {name: 'poetries', age: 18}

注意: bind之后不能再次修改this的指向,bind多次后执行,函数this还是指向第一次bind的对象

数组中的数据根据key去重

给定一个任意数组,实现一个通用函数,让数组中的数据根据 key 排重:

const dedup = (data, getKey = () => {} ) => { // todo } let data = [ { id: 1, v: 1 }, { id: 2, v: 2 }, { id: 1, v: 1 }, ]; // 以 id 作为排重 key,执行函数得到结果 // data = [ // { id: 1, v: 1 }, // { id: 2, v: 2 }, // ];

实现

const dedup = (data, getKey = () => { }) => { const dateMap = data.reduce((pre, cur) => { const key = getKey(cur) if (!pre[key]) { pre[key] = cur } return pre }, {}) return Object.values(dateMap) }

使用

let data = [ { id: 1, v: 1 }, { id: 2, v: 2 }, { id: 1, v: 1 }, ]; console.log(dedup(data, (item) => item.id)) // 以 id 作为排重 key,执行函数得到结果 // data = [ // { id: 1, v: 1 }, // { id: 2, v: 2 }, // ]; 实现find方法 find 接收一个方法作为参数,方法内部返回一个条件find 会遍历所有的元素,执行你给定的带有条件返回值的函数符合该条件的元素会作为 find 方法的返回值如果遍历结束还没有符合该条件的元素,则返回 undefined var users = [ {id: 1, name: '张三'}, {id: 2, name: '张三'}, {id: 3, name: '张三'}, {id: 4, name: '张三'} ] Array.prototype.myFind = function (callback) { // var callback = function (item, index) { return item.id === 4 } for (var i = 0; i < this.length; i++) { if (callback(this[i], i)) { return this[i] } } } var ret = users.myFind(function (item, index) { return item.id === 2 }) console.log(ret) 实现事件总线结合Vue应用

Event Bus(Vue、Flutter 等前端框架中有出镜)和 Event Emitter(Node中有出镜)出场的“剧组”不同,但是它们都对应一个共同的角色—— 全局事件总线 。

全局事件总线,严格来说不能说是观察者模式,而是发布-订阅模式。它在我们日常的业务开发中应用非常广。

如果只能选一道题,那这道题一定是 Event Bus/Event Emitter 的代码实现——我都说这么清楚了,这个知识点到底要不要掌握、需要掌握到什么程度,就看各位自己的了。

在Vue中使用Event Bus来实现组件间的通讯

Event Bus/Event Emitter 作为全局事件总线,它起到的是一个沟通桥梁的作用。我们可以把它理解为一个事件中心,我们所有事件的订阅/发布都不能由订阅方和发布方“私下沟通”,必须要委托这个事件中心帮我们实现。

在Vue中,有时候 A 组件和 B 组件中间隔了很远,看似没什么关系,但我们希望它们之间能够通信。这种情况下除了求助于 Vuex 之外,我们还可以通过 Event Bus 来实现我们的需求。

创建一个 Event Bus(本质上也是 Vue 实例)并导出:

const EventBus = new Vue() export default EventBus

在主文件里引入EventBus,并挂载到全局:

import bus from 'EventBus的文件路径' Vue.prototype.bus = bus

订阅事件:

// 这里func指someEvent这个事件的监听函数 this.bus.$on('someEvent', func)

发布(触发)事件:

// 这里params指someEvent这个事件被触发时回调函数接收的入参 this.bus.$emit('someEvent', params)

大家会发现,整个调用过程中,没有出现具体的发布者和订阅者(比如上面的PrdPublisher和DeveloperObserver),全程只有bus这个东西一个人在疯狂刷存在感。这就是全局事件总线的特点——所有事件的发布/订阅操作,必须经由事件中心,禁止一切“私下交易”!

下面,我们就一起来实现一个Event Bus(注意看注释里的解析):

class EventEmitter { constructor() { // handlers是一个map,用于存储事件与回调之间的对应关系 this.handlers = {} } // on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数 on(eventName, cb) { // 先检查一下目标事件名有没有对应的监听函数队列 if (!this.handlers[eventName]) { // 如果没有,那么首先初始化一个监听函数队列 this.handlers[eventName] = [] } // 把回调函数推入目标事件的监听函数队列里去 this.handlers[eventName].push(cb) } // emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数 emit(eventName, ...args) { // 检查目标事件是否有监听函数队列 if (this.handlers[eventName]) { // 如果有,则逐个调用队列里的回调函数 this.handlers[eventName].forEach((callback) => { callback(...args) }) } } // 移除某个事件回调队列里的指定回调函数 off(eventName, cb) { const callbacks = this.handlers[eventName] const index = callbacks.indexOf(cb) if (index !== -1) { callbacks.splice(index, 1) } } // 为事件注册单次监听器 once(eventName, cb) { // 对回调函数进行包装,使其执行完毕自动被移除 const wrapper = (...args) => { cb.apply(...args) this.off(eventName, wrapper) } this.on(eventName, wrapper) } }

在日常的开发中,大家用到EventBus/EventEmitter往往提供比这五个方法多的多的多的方法。但在面试过程中,如果大家能够完整地实现出这五个方法,已经非常可以说明问题了,因此楼上这个EventBus希望大家可以熟练掌握。学有余力的同学

标签:

2023前端必会手写面试题整理由讯客互联其他栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“2023前端必会手写面试题整理