前端js手写面试题汇总(二)

2022-11-16 08:45:18 浏览数 (1)

实现防抖函数(debounce)

防抖函数原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

那么与节流函数的区别直接看这个动画实现即可。

手写简化版:

代码语言:javascript复制
// 防抖函数
const debounce = (fn, delay) => {
  let timer = null;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
};

适用场景:

  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
  • 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似

生存环境请用lodash.debounce

模板引擎实现

代码语言:javascript复制
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
  name: '姓名',
  age: 18
}
render(template, data); // 我是姓名,年龄18,性别undefined
代码语言:javascript复制
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; // 如果模板没有模板字符串直接返回
}

实现日期格式化函数

输入:

代码语言:javascript复制
dateFormat(new Date('2020-12-01'), 'yyyy/MM/dd') // 2020/12/01
dateFormat(new Date('2020-04-01'), 'yyyy/MM/dd') // 2020/04/01
dateFormat(new Date('2020-04-01'), 'yyyy年MM月dd日') // 2020年04月01日
代码语言:javascript复制
const dateFormat = (dateInput, format)=>{
    var day = dateInput.getDate() 
    var month = dateInput.getMonth()   1  
    var year = dateInput.getFullYear()   
    format = format.replace(/yyyy/, year)
    format = format.replace(/MM/,month)
    format = format.replace(/dd/,day)
    return format
}

实现bind方法

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

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

简洁版本

  • 对于普通函数,绑定this指向
  • 对于构造函数,要保证原函数的原型对象上的属性不能丢失
代码语言:javascript复制
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;
}
代码语言:javascript复制
// 测试用例

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的对象

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

模拟Object.create

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的proto

代码语言:javascript复制
// 模拟 Object.create

function create(proto) {
  function F() {}
  F.prototype = proto;

  return new F();
}

throttle(节流)

高频时间触发,但n秒内只会执行一次,所以节流会稀释函数的执行频率。

代码语言:javascript复制
const throttle = (fn, time) => {
  let flag = true;
  return function() {
    if (!flag) return;
    flag = false;
    setTimeout(() => {
      fn.apply(this, arguments);
      flag = true;
    }, time);
  }
}

节流常应用于鼠标不断点击触发、监听滚动事件。

实现 add(1)(2)(3)

函数柯里化概念: 柯里化(Currying)是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。

1)粗暴版

代码语言:javascript复制
function add (a) {
return function (b) {
     return function (c) {
      return a   b   c;
     }
}
}
console.log(add(1)(2)(3)); // 6

2)柯里化解决方案

  • 参数长度固定
代码语言:javascript复制
var add = function (m) {
  var temp = function (n) {
    return add(m   n);
  }
  temp.toString = function () {
    return m;
  }
  return temp;
};
console.log(add(3)(4)(5)); // 12
console.log(add(3)(6)(9)(25)); // 43

对于add(3)(4)(5),其执行过程如下:

  1. 先执行add(3),此时m=3,并且返回temp函数;
  2. 执行temp(4),这个函数内执行add(m n),n是此次传进来的数值4,m值还是上一步中的3,所以add(m n)=add(3 4)=add(7),此时m=7,并且返回temp函数
  3. 执行temp(5),这个函数内执行add(m n),n是此次传进来的数值5,m值还是上一步中的7,所以add(m n)=add(7 5)=add(12),此时m=12,并且返回temp函数
  4. 由于后面没有传入参数,等于返回的temp函数不被执行而是打印,了解JS的朋友都知道对象的toString是修改对象转换字符串的方法,因此代码中temp函数的toString函数return m值,而m值是最后一步执行函数时的值m=12,所以返回值是12。
  5. 参数长度不固定
代码语言:javascript复制
function add (...args) {
    //求和
    return args.reduce((a, b) => a   b)
}
function currying (fn) {
    let args = []
    return function temp (...newArgs) {
        if (newArgs.length) {
            args = [
                ...args,
                ...newArgs
            ]
            return temp
        } else {
            let val = fn.apply(this, args)
            args = [] //保证再次调用时清空
            return val
        }
    }
}
let addCurry = currying(add)
console.log(addCurry(1)(2)(3)(4, 5)())  //15
console.log(addCurry(1)(2)(3, 4, 5)())  //15
console.log(addCurry(1)(2, 3, 4, 5)())  //15

字符串查找

请使用最基本的遍历来实现判断字符串 a 是否被包含在字符串 b 中,并返回第一次出现的位置(找不到返回 -1)。

代码语言:javascript复制
a='34';b='1234567'; // 返回 2
a='35';b='1234567'; // 返回 -1
a='355';b='12354355'; // 返回 5
isContain(a,b);
代码语言:css复制
function isContain(a, b) {
  for (let i in b) {
    if (a[0] === b[i]) {
      let tmp = true;
      for (let j in a) {
        if (a[j] !== b[~~i   ~~j]) {
          tmp = false;
        }
      }
      if (tmp) {
        return i;
      }
    }
  }
  return -1;
}

将数字每千分位用逗号隔开

数字有小数版本:

代码语言:javascript复制
let format = n => {
    let num = n.toString() // 转成字符串
    let decimals = ''
        // 判断是否有小数
    num.indexOf('.') > -1 ? decimals = num.split('.')[1] : decimals
    let len = num.length
    if (len <= 3) {
        return num
    } else {
        let temp = ''
        let remainder = len % 3
        decimals ? temp = '.'   decimals : temp
        if (remainder > 0) { // 不是3的整数倍
            return num.slice(0, remainder)   ','   num.slice(remainder, len).match(/d{3}/g).join(',')   temp
        } else { // 是3的整数倍
            return num.slice(0, len).match(/d{3}/g).join(',')   temp 
        }
    }
}
format(12323.33)  // '12,323.33'

数字无小数版本:

代码语言:javascript复制
let format = n => {
    let num = n.toString() 
    let len = num.length
    if (len <= 3) {
        return num
    } else {
        let remainder = len % 3
        if (remainder > 0) { // 不是3的整数倍
            return num.slice(0, remainder)   ','   num.slice(remainder, len).match(/d{3}/g).join(',') 
        } else { // 是3的整数倍
            return num.slice(0, len).match(/d{3}/g).join(',') 
        }
    }
}
format(1232323)  // '1,232,323'

手写类型判断函数

代码语言:javascript复制
function getType(value) {
  // 判断数据是 null 的情况
  if (value === null) {
    return value   "";
  }
  // 判断数据是引用类型的情况
  if (typeof value === "object") {
    let valueClass = Object.prototype.toString.call(value),
      type = valueClass.split(" ")[1].split("");
    type.pop();
    return type.join("").toLowerCase();
  } else {
    // 判断数据是基本数据类型的情况和函数的情况
    return typeof value;
  }
}

循环打印红黄绿

下面来看一道比较典型的问题,通过这个问题来对比几种异步编程方法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯?

三个亮灯函数:

代码语言:javascript复制
function red() {
    console.log('red');
}
function green() {
    console.log('green');
}
function yellow() {
    console.log('yellow');
}

这道题复杂的地方在于需要“交替重复”亮灯,而不是“亮完一次”就结束了。

(1)用 callback 实现
代码语言:javascript复制
const task = (timer, light, callback) => {
    setTimeout(() => {
        if (light === 'red') {
            red()
        }
        else if (light === 'green') {
            green()
        }
        else if (light === 'yellow') {
            yellow()
        }
        callback()
    }, timer)
}
task(3000, 'red', () => {
    task(2000, 'green', () => {
        task(1000, 'yellow', Function.prototype)
    })
})

这里存在一个 bug:代码只是完成了一次流程,执行后红黄绿灯分别只亮一次。该如何让它交替重复进行呢?

上面提到过递归,可以递归亮灯的一个周期:

代码语言:javascript复制
const step = () => {
    task(3000, 'red', () => {
        task(2000, 'green', () => {
            task(1000, 'yellow', step)
        })
    })
}
step()

注意看黄灯亮的回调里又再次调用了 step 方法 以完成循环亮灯。

(2)用 promise 实现
代码语言:javascript复制
const task = (timer, light) => 
    new Promise((resolve, reject) => {
        setTimeout(() => {
            if (light === 'red') {
                red()
            }
            else if (light === 'green') {
                green()
            }
            else if (light === 'yellow') {
                yellow()
            }
            resolve()
        }, timer)
    })
const step = () => {
    task(3000, 'red')
        .then(() => task(2000, 'green'))
        .then(() => task(2100, 'yellow'))
        .then(step)
}
step()

这里将回调移除,在一次亮灯结束后,resolve 当前 promise,并依然使用递归进行。

(3)用 async/await 实现
代码语言:javascript复制
const taskRunner =  async () => {
    await task(3000, 'red')
    await task(2000, 'green')
    await task(2100, 'yellow')
    taskRunner()
}
taskRunner()

实现发布-订阅模式

代码语言:javascript复制
class EventCenter{
  // 1. 定义事件容器,用来装事件数组
    let handlers = {}

  // 2. 添加事件方法,参数:事件名 事件方法
  addEventListener(type, handler) {
    // 创建新数组容器
    if (!this.handlers[type]) {
      this.handlers[type] = []
    }
    // 存入事件
    this.handlers[type].push(handler)
  }

  // 3. 触发事件,参数:事件名 事件参数
  dispatchEvent(type, params) {
    // 若没有注册该事件则抛出错误
    if (!this.handlers[type]) {
      return new Error('该事件未注册')
    }
    // 触发事件
    this.handlers[type].forEach(handler => {
      handler(...params)
    })
  }

  // 4. 事件移除,参数:事件名 要删除事件,若无第二个参数则删除该事件的订阅和发布
  removeEventListener(type, handler) {
    if (!this.handlers[type]) {
      return new Error('事件无效')
    }
    if (!handler) {
      // 移除事件
      delete this.handlers[type]
    } else {
      const index = this.handlers[type].findIndex(el => el === handler)
      if (index === -1) {
        return new Error('无该绑定事件')
      }
      // 移除事件
      this.handlers[type].splice(index, 1)
      if (this.handlers[type].length === 0) {
        delete this.handlers[type]
      }
    }
  }
}

手写防抖函数

函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。

代码语言:javascript复制
// 函数防抖的实现
function debounce(fn, wait) {
  let timer = null;

  return function() {
    let context = this,
        args = arguments;

    // 如果此时存在定时器的话,则取消之前的定时器重新记时
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  };
}

图片懒加载

可以给img标签统一自定义属性data-src='default.png',当检测到图片出现在窗口之后再补充src属性,此时才会进行图片资源加载。

代码语言:javascript复制
function lazyload() {
  const imgs = document.getElementsByTagName('img');
  const len = imgs.length;
  // 视口的高度
  const viewHeight = document.documentElement.clientHeight;
  // 滚动条高度
  const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop;
  for (let i = 0; i < len; i  ) {
    const offsetHeight = imgs[i].offsetTop;
    if (offsetHeight < viewHeight   scrollHeight) {
      const src = imgs[i].dataset.src;
      imgs[i].src = src;
    }
  }
}

// 可以使用节流优化一下
window.addEventListener('scroll', lazyload);

实现浅拷贝

浅拷贝是指,一个新的对象对原始对象的属性值进行精确地拷贝,如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值,如果是引用数据类型,拷贝的就是内存地址。如果其中一个对象的引用内存地址发生改变,另一个对象也会发生变化。

(1)Object.assign()

Object.assign()是ES6中对象的拷贝方法,接受的第一个参数是目标对象,其余参数是源对象,用法:Object.assign(target, source_1, ···),该方法可以实现浅拷贝,也可以实现一维对象的深拷贝。

注意:

  • 如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性。
  • 如果该函数只有一个参数,当参数为对象时,直接返回该对象;当参数不是对象时,会先将参数转为对象然后返回。
  • 因为nullundefined 不能转化为对象,所以第一个参数不能为nullundefined,会报错。
代码语言:javascript复制
let target = {a: 1};
let object2 = {b: 2};
let object3 = {c: 3};
Object.assign(target,object2,object3);  
console.log(target);  // {a: 1, b: 2, c: 3}
(2)扩展运算符

使用扩展运算符可以在构造字面量对象的时候,进行属性的拷贝。语法:let cloneObj = { ...obj };

代码语言:javascript复制
let obj1 = {a:1,b:{c:1}}
let obj2 = {...obj1};
obj1.a = 2;
console.log(obj1); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}
obj1.b.c = 2;
console.log(obj1); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}
(3)数组方法实现数组浅拷贝
1)Array.prototype.slice
  • slice()方法是JavaScript数组的一个方法,这个方法可以从已有数组中返回选定的元素:用法:array.slice(start, end),该方法不会改变原始数组。
  • 该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。
代码语言:javascript复制
let arr = [1,2,3,4];
console.log(arr.slice()); // [1,2,3,4]
console.log(arr.slice() === arr); //false
2)Array.prototype.concat
  • concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
  • 该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。
代码语言:javascript复制
let arr = [1,2,3,4];
console.log(arr.concat()); // [1,2,3,4]
console.log(arr.concat() === arr); //false
(4)手写实现浅拷贝
代码语言:javascript复制
// 浅拷贝的实现;

function shallowCopy(object) {
  // 只拷贝对象
  if (!object || typeof object !== "object") return;

  // 根据 object 的类型判断是新建一个数组还是对象
  let newObject = Array.isArray(object) ? [] : {};

  // 遍历 object,并且判断是 object 的属性才拷贝
  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] = object[key];
    }
  }

  return newObject;
}// 浅拷贝的实现;

function shallowCopy(object) {
  // 只拷贝对象
  if (!object || typeof object !== "object") return;

  // 根据 object 的类型判断是新建一个数组还是对象
  let newObject = Array.isArray(object) ? [] : {};

  // 遍历 object,并且判断是 object 的属性才拷贝
  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] = object[key];
    }
  }

  return newObject;
}// 浅拷贝的实现;
function shallowCopy(object) {
  // 只拷贝对象
  if (!object || typeof object !== "object") return;
  // 根据 object 的类型判断是新建一个数组还是对象
  let newObject = Array.isArray(object) ? [] : {};
  // 遍历 object,并且判断是 object 的属性才拷贝
  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] = object[key];
    }
  }
  return newObject;
}

手写 Promise.race

该方法的参数是 Promise 实例数组, 然后其 then 注册的回调方法是数组中的某一个 Promise 的状态变为 fulfilled 的时候就执行. 因为 Promise 的状态只能改变一次, 那么我们只需要把 Promise.race 中产生的 Promise 对象的 resolve 方法, 注入到数组中的每一个 Promise 实例中的回调函数中即可.

代码语言:javascript复制
Promise.race = function (args) {
  return new Promise((resolve, reject) => {
    for (let i = 0, len = args.length; i < len; i  ) {
      args[i].then(resolve, reject)
    }
  })
}

实现prototype继承

所谓的原型链继承就是让新实例的原型等于父类的实例:

代码语言:javascript复制
//父方法
function SupperFunction(flag1){
    this.flag1 = flag1;
}

//子方法
function SubFunction(flag2){
    this.flag2 = flag2;
}

//父实例
var superInstance = new SupperFunction(true);

//子继承父
SubFunction.prototype = superInstance;

//子实例
var subInstance = new SubFunction(false);
//子调用自己和父的属性
subInstance.flag1;   // true
subInstance.flag2;   // false

实现一个call

call做了什么:

  • 将函数设为对象的属性
  • 执行&删除这个函数
  • 指定this到函数并传入给定参数执行函数
  • 如果不传入参数,默认指向为 window
代码语言:javascript复制
// 模拟 call bar.mycall(null);
//实现一个call方法:
Function.prototype.myCall = function(context) {
  //此处没有考虑context非object情况
  context.fn = this;
  let args = [];
  for (let i = 1, len = arguments.length; i < len; i  ) {
    args.push(arguments[i]);
  }
  context.fn(...args);
  let result = context.fn(...args);
  delete context.fn;
  return result;
};

实现节流函数(throttle)

防抖函数原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

// 手写简化版

代码语言:javascript复制
// 节流函数
const throttle = (fn, delay = 500) => {
  let flag = true;
  return (...args) => {
    if (!flag) return;
    flag = false;
    setTimeout(() => {
      fn.apply(this, args);
      flag = true;
    }, delay);
  };
};

适用场景:

  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
  • 缩放场景:监控浏览器resize
  • 动画场景:避免短时间内多次触发动画引起性能问题

手写 Promise.all

1) 核心思路

  1. 接收一个 Promise 实例的数组或具有 Iterator 接口的对象作为参数
  2. 这个方法返回一个新的 promise 对象,
  3. 遍历传入的参数,用Promise.resolve()将参数"包一层",使其变成一个promise对象
  4. 参数所有回调成功才是成功,返回值数组与参数顺序一致
  5. 参数数组其中一个失败,则触发失败状态,第一个触发失败的 Promise 错误信息作为 Promise.all 的错误信息。

2)实现代码

一般来说,Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,如果其中一个接口失败了,多个请求也就失败了,页面可能啥也出不来,这就看当前页面的耦合程度了

代码语言:javascript复制
function promiseAll(promises) {
  return new Promise(function(resolve, reject) {
    if(!Array.isArray(promises)){
        throw new TypeError(`argument must be a array`)
    }
    var resolvedCounter = 0;
    var promiseNum = promises.length;
    var resolvedResult = [];
    for (let i = 0; i < promiseNum; i  ) {
      Promise.resolve(promises[i]).then(value=>{
        resolvedCounter  ;
        resolvedResult[i] = value;
        if (resolvedCounter == promiseNum) {
            return resolve(resolvedResult)
          }
      },error=>{
        return reject(error)
      })
    }
  })
}
// test
let p1 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(1)
    }, 1000)
})
let p2 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(2)
    }, 2000)
})
let p3 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(3)
    }, 3000)
})
promiseAll([p3, p1, p2]).then(res => {
    console.log(res) // [3, 1, 2]
})

0 人点赞