前端经典手写面试题汇总

114 阅读5分钟

1.手写new操作符

 function myNew(Fn) {
      if (typeof Fn !== 'function') throw new TypeError('This is not a constructor') // Fn校验
      var args = Array.from(arguments).slice(1) // 取入参
      var obj = {} // 1.创建一个空的简单JavaScript对象(即`  {}  `)
      obj.__proto__ = Fn.prototype // 2.  为步骤1新创建的对象添加属性`  __proto__  `,将该属性链接至构造函数的原型对象
      var res = Fn.call(obj, ...args) // 3.  将步骤1新创建的对象作为this的上下文并传入参数;
      return Object(res) === res ? res : obj // 4.  如果该函数没有返回对象,则返回this。
    }

    var Fn = function (name, age) {
      this.name = name
      this.age = age
    }
    let newobj = myNew(Fn, 'xiaoming', '15')
    console.log(newobj)

2.手写promise

    function myPromise(callback) {
      // 每一个promise对象内部,都会有一个状态信息, 有三个可能值
      // pending 状态 表示等待状态, promise对象的默认状态, 
      // resolve 状态 表示成功状态, 当调用了resolve函数时,状态变成成功状态
      // reject 状态 表示失败状态, 当调用了reject函数时,状态变成失败状态
      // 注意: 状态值只能变化一次,一旦变更为成功或失败状态,则会一致保持这个状态 
      this.state = "pending";  // 初始化状态

      let self = this


      function resolve(data) {
        self.state = "resolve" // 修改状态值为成功
        self.value = data; // 成功时value属性记录成功数据
        self.success(data) // 成功时,调用then函数中成功回调

      }

      function reject(err) {
        self.state = "reject" // 修改状态值为失败
        self.msg = err;  // 失败时用msg属性记录失败信息
        self.fail(err) // 失败时,调用then函数中失败回调
      }
      // MyPromise在创建对象时,其回调函数会直接执行, 所以在构造函数中直接调用, 并传入成功和失败状态对应的函数
      callback(resolve, reject)
    }

    // / then方法是promise对象调用的方法,所以定义到构造函数原型中
    MyPromise.prototype.then = function (success, fail = () => { }) {
      // 由于then函数可以在任意时刻调用, 所以调用then时,promise状态值不确定
      if (this.state == "pending") {
        // 说明此时异步任务还未结束, 还不能调用success或fail, 此时可以把success和fail这个回调函数传入this这个promise对象, 在异步任务结束后调用
        this.success = success;
        this.fail = fail;
      }
      if (this.state == "resolve") {
        // 如果当前状态是成功状态, 则调用then参数中的第一个成功回调
        success(this.value) // 参数传入成功数据
      }
      if (this.state == "reject") {
        // 如果当前状态是失败状态, 则调用then参数中的第二个失败回调
        fail(this.msg) // 参数传入失败信息
      }

      // then函数返回当前promise对象, 用于链式调用
      return this;
    }

3.手写promise.all

 Promise.all = function (iterators) {
      return new Promise((resolve, reject) => {
        if (!iterators || iterators.length === 0) {
          resolve([])
        } else {
          // 计算器,用于判断所有任务是否执行完成
          let count = 0;
          // 结果数组
          let result = [];
          // 执行数组长度
          let len = iterators.length
          for (let i = 0; i < len; i++) {
            // 考虑到iterators[i]可能是普通对象,则统一包装为Promise对象
            Promise.resolve(iterators[i]).then((data) => {
              // 按顺序保存对应的结果
              result[i] = data;
              // 判断++count 的次数是否等于 传入执行数组的长度
              if (++count === len) {
                resolve(result);
              }
            }, (err) => {
              // 任何一个Promise对象执行失败,则调用reject()方法
              reject(err);
            })
          }
        }
      })
    }

4.防抖节流

防抖

   function debounce(fn, wait) {
        let time = null

        return function () {
          let that = this
          args = arguments

          if (time) {
            clear(time)
            time = null
          }

          time = setInterval(() => {
            fn.apply(that, args)
          }, wait)
        }
      }

节流

 function throttle(fn, wait) {

      let curTime = Date.now();

      return function () {
        let that = this,
          args = arguments,
          nowTime = Date.now();
        if (nowTime - curTime > wait) {
          curTime = Date.now();
          fn.apply(that, args)
        }
      }

    }
function throttle(fn,delay) {
      let timeout
      return function() {
        let args = arguments;//注意如果要传参的话 这句不能省略
        if(!timeout){
          timeout = setTimeout(()=>{
            timeout = null;
            fn.apply(this,args)
          },delay)
        }
      }
    }

5.call和apply手写

call

  function myCall(context) {


      if (Object.prototype.toString.call(this) !== "[object,function]") {
        console.error("type error");
      }
      let args = [...arguments].slice(1),
        result = null;
      context = context || window;
      // 将调用函数设为对象的方法

      // 将调用函数设为对象的方法
      context.fn = this;
      // 调用函数
      result = context.fn(...args);
      // 将属性删除
      delete context.fn;
      return result;


    }

apply

  function myApply(context) {

      if (Object.prototype.toString.call(this) !== "[object,function]") {
        console.error("type error");
      }
      let res = null
      var context = context || window
      context.fn = this
      //判断有没有传入第二个参数数组
      if (arguments[1]) {
        res = context.fn(...arguments[1])
      } else {
        res = context.fn()
      }
      delete context.fn
      return res

    }

6.函数柯里化

   function createCurry(func, args) {

      var arity = func.length;
      var args = args || [];

      return function () {
        var _args = [].slice.call(arguments);
        [].push.apply(_args, args);

        // 如果参数个数小于最初的func.length,则递归调用,继续收集参数
        if (_args.length < arity) {
          return createCurry.call(this, func, _args);
        }

        // 参数收集完毕,则执行func
        return func.apply(this, _args);
      }

    }
   

7.深拷贝

   function deepClone(obj) {
      //判断拷贝的要进行深拷贝的是数组还是对象,是数组的话进行数组拷贝,对象的话进行对象拷贝
      var objClone = Array.isArray(obj) ? [] : {};
      //进行深拷贝的不能为空,并且是对象或者是
      if (obj && typeof obj === "object") {
        for (key in obj) {
          if (obj.hasOwnProperty(key)) {
            if (obj[key] && typeof obj[key] === "object") {
              objClone[key] = deepClone1(obj[key]);
            } else {
              objClone[key] = obj[key];
            }
          }
        }
      }
      return objClone;
    }

8.数组去重


   let arr = [1, 2, 1, 2, 11, 1]
    // 1)

    function noRepeat() {
      Array.from(new Set(arr))
    }



    // 2)

    function noRepeat(arr) {
      for (var i = 0; i < arr.length - 1; i++) {
        for (var j = i + 1; j < arr.length; j++) {
          if (arr[i] === arr[j]) {
            arr.splice(j, 1);
            j--;
          }
        }
      }
      return arr;
    }
    // 3)

    function noRepeat(arr) {
      for (var i = 0; i < arr.length; i++) {
        for (var j = 0; j < arr.length; j++) {
          if (arr[i] == arr[j] && i != j) {//将后面重复的数删掉
            arr.splice(j, 1);
          }
        }
      }
      return arr;
    }

7.继承(es5)

function Parent() {
      this.name = '无法传参';
      this.loves = ['敲代码', '摄影']
    }

    Parent.prototype.getName = function () {
      console.log(this.name);
    }
    Parent.prototype.getLoves = function () {
      console.log(this.loves);
    }

    function Child(age) {
      this.age = age
    }

    // 原型链继承
    //     优点
    // 1、父类方法可以复用
    // 缺点
    // 1、父类的所有 引用属性(如:loves) 会被所有子类共享,当其中一个子类的引用属性被修改后,会影响其他子类
    // 2、子类型实例不能给父类型构造函数传参

    Child.prototype = new Parent()
    Child.prototype.getAge = function () {
      console.log(this.age);
    }

    // 构造函数继承
    // 优势
    // 1、可以在子类构造函数中向父类传参数
    // 2、父类的引用属性不会被共享
    // 缺点
    // 1、子类不能访问父类原型 User.prototype 上定义的方法,因此所有方法属性都写在构造函数中,每次创建实例都会初始化



    function Child(name, age) {
      // 子类继承父类的属性
      Parent.call(this, name);
      // 实例属性
      this.age = age
    }
    Child.prototype.getAge = function () {
      console.log(this.age);
    }


    // 组合继承


    function Child(name, age) {
      // 子类继承父类的属性
      Parent.call(this, name);
      // 实例属性
      this.age = age
    }

    Child.prototype = new Parent()

    Child.prototype.getAge = function () {
      console.log(this.age);
    }


8.继承(es6)

  class Parent {
      constructor(name) {
        this.name = name;
        this.loves = ['敲代码', '摄影']
      }
      getName() {
        console.log(this.name)
      };
      getLoves() {
        console.log(this.loves)
      };
    }

    class Child extends Parent {
      constructor(name, age) {
        super(name);  // 调用父类的构造函数
        this.age = age;
      };
      getAge() {
        console.log(this.age)
      };
    }

9.实现add(1)(2)==3

    // 题意的答案
   const add = (num1) => (num2)=> num2 + num1;
   
   
   // 我自己整了一个加强版 可以无限链式调用 add(1)(2)(3)(4)(5)....
   function add(x) {
      // 存储和
      let sum = x;
       
      // 函数调用会相加,然后每次都会返回这个函数本身
      let tmp = function (y) {
        sum = sum + y;
        return tmp;
      };
      
      // 对象的toString必须是一个方法 在方法中返回了这个和
      tmp.toString = () => sum
      return tmp;
   }
   
   alert(add(1)(2)(3)(4)(5))

10.数组转化为tree

    let arr = [
      { id: 0, name: '1', parent: -1, childNode: [] },
      { id: 1, name: '1', parent: 0, childNode: [] },
      { id: 99, name: '1-1', parent: 1, childNode: [] },
      { id: 111, name: '1-1-1', parent: 99, childNode: [] },
      { id: 66, name: '1-1-2', parent: 99, childNode: [] },
      { id: 1121, name: '1-1-2-1', parent: 112, childNode: [] },
      { id: 12, name: '1-2', parent: 1, childNode: [] },
      { id: 2, name: '2', parent: 0, childNode: [] },
      { id: 21, name: '2-1', parent: 2, childNode: [] },
      { id: 22, name: '2-2', parent: 2, childNode: [] },
      { id: 221, name: '2-2-1', parent: 22, childNode: [] },
      { id: 3, name: '3', parent: 0, childNode: [] },
      { id: 31, name: '3-1', parent: 3, childNode: [] },
      { id: 32, name: '3-2', parent: 3, childNode: [] }
    ]

    function arrToTree(arr, parentId) {
       // 判断是否是顶层节点,如果是就返回。不是的话就判断是不是自己要找的子节点
      const filterArr = arr.filter(item => {
        return parentId === undefined ? item.parent === -1 : item.parent === parentId
      })
       
      // 进行递归调用把子节点加到父节点的 childNode里面去
      filterArr.map(item => {
        item.childNode = arrToTree(arr, item.id)
        return item
      })
       
      return filterArr
    }
    
    arrToTree(arr)

11.手写instanceof

function instanceOf1(obj, fun) {
    //判断数据类型
    if (typeof obj !== 'object' || obj === null || typeof fun !== 'function') {
        return false
    }
    //实现循环查找
    let proto = obj.__proto__
    while (true) {
        //到达原型链尽头,也就是查找循环终止的条件
        if (proto === null) return false
        if (proto === fun.prototype) return true
        //obj的原型不是fun的prototype的时候,沿着原型链继续向上寻找
        proto = proto.__proto__
    }
}

eventLoop 手写题

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}
console.log('script start')
setTimeout(function () {
    console.log('settimeout')
})
async1()
new Promise(function (resolve) {
    console.log('promise1')
    resolve()
}).then(function () {
    console.log('promise2')
})
console.log('script end')

  1. 执行整段代码,遇到 console.log('script start') 直接打印结果,输出 script start
  2. 遇到定时器了,它是宏任务,先放着不执行
  3. 遇到 async1(),执行 async1 函数,先打印 async1 start,下面遇到await怎么办?先执行 async2,打印 async2,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码
  4. 跳到 new Promise 这里,直接执行,打印 promise1,下面遇到 .then(),它是微任务,放到微任务列表等待执行
  5. 最后一行直接打印 script end,现在同步代码执行完了,开始执行微任务,即 await下面的代码,打印 async1 end
  6. 继续执行下一个微任务,即执行 then 的回调,打印 promise2
  7. 上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印 settimeout

所以最后的结果是:script startasync1 startasync2promise1script endasync1 endpromise2settimeout