写点什么

前端常见手写面试题(持续更新中)

  • 2022-10-26
    浙江
  • 本文字数:8298 字

    阅读完需:约 27 分钟

实现简单路由

// hash路由class Route{  constructor(){    // 路由存储对象    this.routes = {}    // 当前hash    this.currentHash = ''    // 绑定this,避免监听时this指向改变    this.freshRoute = this.freshRoute.bind(this)    // 监听    window.addEventListener('load', this.freshRoute, false)    window.addEventListener('hashchange', this.freshRoute, false)  }  // 存储  storeRoute (path, cb) {    this.routes[path] = cb || function () {}  }  // 更新  freshRoute () {    this.currentHash = location.hash.slice(1) || '/'    this.routes[this.currentHash]()  }}
复制代码

实现 forEach 方法

Array.prototype.myForEach = function(callback, context=window) {  // this=>arr  let self = this,        i = 0,      len = self.length;
for(;i<len;i++) { typeof callback == 'function' && callback.call(context,self[i], i) }}
复制代码

实现观察者模式

观察者模式(基于发布订阅模式) 有观察者,也有被观察者


观察者需要放到被观察者中,被观察者的状态变化需要通知观察者 我变化了 内部也是基于发布订阅模式,收集观察者,状态变化后要主动通知观察者


class Subject { // 被观察者 学生  constructor(name) {    this.state = 'happy'    this.observers = []; // 存储所有的观察者  }  // 收集所有的观察者  attach(o){ // Subject. prototype. attch    this.observers.push(o)  }  // 更新被观察者 状态的方法  setState(newState) {    this.state = newState; // 更新状态    // this 指被观察者 学生    this.observers.forEach(o => o.update(this)) // 通知观察者 更新它们的状态  }}
class Observer{ // 观察者 父母和老师 constructor(name) { this.name = name } update(student) { console.log('当前' + this.name + '被通知了', '当前学生的状态是' + student.state) }}
let student = new Subject('学生');
let parent = new Observer('父母'); let teacher = new Observer('老师');
// 被观察者存储观察者的前提,需要先接纳观察者student. attach(parent); student. attach(teacher); student. setState('被欺负了');
复制代码

实现 apply 方法

思路: 利用this的上下文特性。apply其实就是改一下参数的问题


Function.prototype.myApply = function(context = window, args) {  // this-->func  context--> obj  args--> 传递过来的参数
// 在context上加一个唯一值不影响context上的属性 let key = Symbol('key') context[key] = this; // context为调用的上下文,this此处为函数,将这个函数作为context的方法 // let args = [...arguments].slice(1) //第一个参数为obj所以删除,伪数组转为数组
let result = context[key](...args); // 这里和call传参不一样
// 清除定义的this 不删除会导致context属性越来越多 delete context[key];
// 返回结果 return result;}
复制代码


// 使用function f(a,b){ console.log(a,b) console.log(this.name)}let obj={ name:'张三'}f.myApply(obj,[1,2])  //arguments[1]
复制代码

基于 Generator 函数实现 async/await 原理

核心:传递给我一个Generator函数,把函数中的内容基于Iterator迭代器的特点一步步的执行


function readFile(file) {    return new Promise(resolve => {        setTimeout(() => {            resolve(file);    }, 1000);    })};
function asyncFunc(generator) { const iterator = generator(); // 接下来要执行next // data为第一次执行之后的返回结果,用于传给第二次执行 const next = (data) => { let { value, done } = iterator.next(data); // 第二次执行,并接收第一次的请求结果 data
if (done) return; // 执行完毕(到第三次)直接返回 // 第一次执行next时,yield返回的 promise实例 赋值给了 value value.then(data => { next(data); // 当第一次value 执行完毕且成功时,执行下一步(并把第一次的结果传递下一步) }); } next();};
asyncFunc(function* () { // 生成器函数:控制代码一步步执行 let data = yield readFile('a.js'); // 等这一步骤执行执行成功之后,再往下走,没执行完的时候,直接返回 data = yield readFile(data + 'b.js'); return data;})
复制代码

数组中的数据根据 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 },// ];
复制代码

实现 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;}
复制代码

实现 redux-thunk

redux-thunk 可以利用 redux 中间件让 redux 支持异步的 action


// 如果 action 是个函数,就调用这个函数// 如果 action 不是函数,就传给下一个中间件// 发现 action 是函数就调用const thunk = ({ dispatch, getState }) => (next) => (action) => {  if (typeof action === 'function') {    return action(dispatch, getState);  }
return next(action);};export default thunk
复制代码

原生实现

function ajax() {  let xhr = new XMLHttpRequest() //实例化,以调用方法  xhr.open('get', 'https://www.google.com')  //参数2,url。参数三:异步  xhr.onreadystatechange = () => {  //每当 readyState 属性改变时,就会调用该函数。    if (xhr.readyState === 4) {  //XMLHttpRequest 代理当前所处状态。      if (xhr.status >= 200 && xhr.status < 300) {  //200-300请求成功        let string = request.responseText        //JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象        let object = JSON.parse(string)      }    }  }  request.send() //用于实际发出 HTTP 请求。不带参数为GET请求}
复制代码


参考:前端手写面试题详细解答

实现模板字符串解析功能

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; // 如果模板没有模板字符串直接返回}
复制代码

实现 filter 方法

Array.prototype.myFilter=function(callback, context=window){
let len = this.length newArr = [], i=0
for(; i < len; i++){ if(callback.apply(context, [this[i], i , this])){ newArr.push(this[i]); } } return newArr;}
复制代码

实现 new 的过程

new 操作符做了这些事:


  • 创建一个全新的对象

  • 这个对象的__proto__要指向构造函数的原型 prototype

  • 执行构造函数,使用 call/apply 改变 this 的指向

  • 返回值为object类型则作为new方法的返回值返回,否则返回上述全新对象


function myNew(fn, ...args) {  // 基于原型链 创建一个新对象  let newObj = Object.create(fn.prototype);  // 添加属性到新对象上 并获取obj函数的结果  let res = fn.apply(newObj, args); // 改变this指向
// 如果执行结果有返回值并且是一个对象, 返回执行的结果, 否则, 返回新创建的对象 return typeof res === 'object' ? res: newObj;}
复制代码


// 用法function Person(name, age) {  this.name = name;  this.age = age;}Person.prototype.say = function() {  console.log(this.age);};let p1 = myNew(Person, "poety", 18);console.log(p1.name);console.log(p1);p1.say();
复制代码

实现 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)
复制代码

实现 findIndex 方法

var users = [  {id: 1, name: '张三'},  {id: 2, name: '张三'},  {id: 3, name: '张三'},  {id: 4, name: '张三'}]
Array.prototype.myFindIndex = 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 i } }}
var ret = users.myFind(function (item, index) { return item.id === 2})
console.log(ret)
复制代码

实现发布订阅模式

简介:


发布订阅者模式,一种对象间一对多的依赖关系,但一个对象的状态发生改变时,所依赖它的对象都将得到状态改变的通知。


主要的作用(优点):


  1. 广泛应用于异步编程中(替代了传递回调函数)

  2. 对象之间松散耦合的编写代码


缺点:


  • 创建订阅者本身要消耗一定的时间和内存

  • 多个发布者和订阅者嵌套一起的时候,程序难以跟踪维护


实现的思路:


  • 创建一个对象(缓存列表)

  • on方法用来把回调函数fn都加到缓存列表中

  • emit 根据key值去执行对应缓存列表中的函数

  • off方法可以根据key值取消订阅


class EventEmiter {  constructor() {    // 事件对象,存放订阅的名字和事件    this._events = {}  }  // 订阅事件的方法  on(eventName,callback) {    if(!this._events) {      this._events = {}    }    // 合并之前订阅的cb    this._events[eventName] = [...(this._events[eventName] || []),callback]  }  // 触发事件的方法  emit(eventName, ...args) {    if(!this._events[eventName]) {      return    }    // 遍历执行所有订阅的事件    this._events[eventName].forEach(fn=>fn(...args))  }  off(eventName,cb) {    if(!this._events[eventName]) {      return    }    // 删除订阅的事件    this._events[eventName] = this._events[eventName].filter(fn=>fn != cb && fn.l != cb)  }  // 绑定一次 触发后将绑定的移除掉 再次触发掉  once(eventName,callback) {    const one = (...args)=>{      // 等callback执行完毕在删除      callback(args)      this.off(eventName,one)    }    one.l = callback // 自定义属性    this.on(eventName,one)  }}
复制代码


测试用例


let event = new EventEmiter()
let login1 = function(...args) { console.log('login success1', args)}let login2 = function(...args) { console.log('login success2', args)}// event.on('login',login1)event.once('login',login2)event.off('login',login1) // 解除订阅event.emit('login', 1,2,3,4,5)event.emit('login', 6,7,8,9)event.emit('login', 10,11,12)
复制代码


发布订阅者模式和观察者模式的区别?


  • 发布/订阅模式是观察者模式的一种变形,两者区别在于,发布/订阅模式在观察者模式的基础上,在目标和观察者之间增加一个调度中心。

  • 观察者模式是由具体目标调度,比如当事件触发,Subject 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。

  • 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。

实现 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;}
复制代码

实现 Array.of 方法

Array.of()方法用于将一组值,转换为数组


  • 这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。

  • Array.of()基本上可以用来替代Array()new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一


Array.of(3, 11, 8) // [3,11,8]Array.of(3) // [3]Array.of(3).length // 1
复制代码


实现


function ArrayOf(){  return [].slice.call(arguments);}
复制代码

实现一个 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))

数组去重方法汇总

首先:我知道多少种去重方式


1. 双层 for 循环


function distinct(arr) {    for (let i=0, len=arr.length; i<len; i++) {        for (let j=i+1; j<len; j++) {            if (arr[i] == arr[j]) {                arr.splice(j, 1);                // splice 会改变数组长度,所以要将数组长度 len 和下标 j 减一                len--;                j--;            }        }    }    return arr;}
复制代码


思想: 双重 for 循环是比较笨拙的方法,它实现的原理很简单:先定义一个包含原始数组第一个元素的数组,然后遍历原始数组,将原始数组中的每个元素与新数组中的每个元素进行比对,如果不重复则添加到新数组中,最后返回新数组;因为它的时间复杂度是O(n^2),如果数组长度很大,效率会很低


2. Array.filter() 加 indexOf/includes


function distinct(a, b) {    let arr = a.concat(b);    return arr.filter((item, index)=> {        //return arr.indexOf(item) === index        return arr.includes(item)    })}
复制代码


思想: 利用indexOf检测元素在数组中第一次出现的位置是否和元素现在的位置相等,如果不等则说明该元素是重复元素


3. ES6 中的 Set 去重


function distinct(array) {   return Array.from(new Set(array));}
复制代码


思想: ES6 提供了新的数据结构 Set,Set 结构的一个特性就是成员值都是唯一的,没有重复的值。


4. reduce 实现对象数组去重复


var resources = [    { name: "张三", age: "18" },    { name: "张三", age: "19" },    { name: "张三", age: "20" },    { name: "李四", age: "19" },    { name: "王五", age: "20" },    { name: "赵六", age: "21" }]var temp = {};resources = resources.reduce((prev, curv) => { // 如果临时对象中有这个名字,什么都不做 if (temp[curv.name]) {
}else { // 如果临时对象没有就把这个名字加进去,同时把当前的这个对象加入到prev中 temp[curv.name] = true; prev.push(curv); } return prev}, []);console.log("结果", resources);
复制代码


这种方法是利用高阶函数 reduce 进行去重, 这里只需要注意initialValue得放一个空数组[],不然没法push

实现 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') // undefinedvar a = new bindFun(10) // Person name: poetry1、Person age: 10、Person this: fBound {}a.say() // person say
// 再测试作为普通函数调用var bindNormalFun = normalFun.myBind(obj, 'poetry2') // undefinedbindNormalFun(12) // 普通函数name: poetry2 普通函数 age: 12 普通函数 this: {name: 'poetries', age: 18}
复制代码


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


用户头像

还未添加个人签名 2022-07-31 加入

还未添加个人简介

评论

发布
暂无评论
前端常见手写面试题(持续更新中)_JavaScript_helloworld1024fd_InfoQ写作社区