关于面试:2022必会的前端手写面试题

38次阅读

共计 11698 个字符,预计需要花费 30 分钟才能阅读完成。

面试题视频解说(高效学习):进入学习

二、题目

1. 防抖节流

这也是一个经典题目了,首先要晓得什么是防抖,什么是节流。

  • 防抖:在一段时间内,事件只会最初触发一次。
  • 节流:事件,依照一段时间的距离来进行触发。
    切实不懂的话,能够去这个大佬的 Demo 地址玩玩防抖节流 DEMO
// 防抖
    function debounce(fn) {
      let timeout = null; 
      return function () {
        // 如果事件再次触发就革除定时器,从新计时
        clearTimeout(timeout);
        timeout = setTimeout(() => {fn.apply(this, arguments);
        }, 500);
      };
    }
    
    // 节流
    function throttle(fn) {
      let flag = null; // 通过闭包保留一个标记
      return function () {if (flag) return; // 当定时器没有执行的时候标记永远是 null
        flag = setTimeout(() => {fn.apply(this, arguments);
           // 最初在 setTimeout 执行结束后再把标记设置为 null(要害)
           // 示意能够执行下一次循环了。flag = null;
        }, 500);
      };
    }
    
复制代码

这道题次要还是考查对 防抖 节流 的了解吧,千万别记反了!

2. 一个正则题

要求写出 区号 + 8 位数字,或者区号 + 非凡号码: 10010/110,两头用短横线隔开的正则验证。区号就是三位数字结尾。

例如 010-12345678

let reg = /^\d{3}-(\d{8}|10010|110)/g
复制代码

这个比较简单,相熟正则的根本用法就能够做进去了。

3. 不应用 a 标签,如何实现 a 标签的性能

// 通过 window.open 和 location.href 办法其实就能够实现。// 别离对应了 a 标签的 blank 和 self 属性
复制代码

4. 不应用循环 API 来删除数组中指定地位的元素(如:删除第三位)写越多越好

这个题的意思就是,不能循环的 API(如 for filter 之类的)。

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// 办法一:splice 操作数组 会扭转原数组 
arr.splice(2, 1)


// 办法二:slice 截取选中元素 返回新数组 不扭转原数组
arr.slice(0, 2).concat(arr.slice(3,))

// 办法三 delete 数组中的元素 再把这个元素给剔除掉
delete arr[2]
arr.join("").replace("empty","").split("")
复制代码

5. 深拷贝

深拷贝和浅拷贝的区别就在于

  • 浅拷贝:对于简单数据类型,浅拷贝只是把援用地址赋值给了新的对象,扭转这个新对象的值,原对象的值也会一起扭转
  • 深拷贝:对于简单数据类型,拷贝后地址援用都是新的,扭转拷贝后新对象的值,不会影响原对象的值

所以关键点就在于对简单数据类型的解决,这里我写了两种写法,第二中比第一种有局部性能晋升

const isObj = (val) => typeof val === "object" && val !== null;

// 写法 1
function deepClone(obj) {
    // 通过 instanceof 去判断你要拷贝的变量它是否是数组(如果不是数组则对象)。// 1. 筹备你想返回的变量(新地址)。const newObj = obj instanceof Array ? [] : {}; // 外围代码。// 2. 做拷贝;简略数据类型只须要赋值,如果遇到简单数据类型就再次进入进行深拷贝,直到所找到的数据为简略数据类型为止。for (const key in obj) {const item = obj[key];
        newObj[key] = isObj(item) ? deepClone(item) : item;
    }

    // 3. 返回拷贝的变量。return newObj;
}




// 写法 2 利用 es6 新个性 WeakMap 弱援用 性能更好 并且反对 Symbol
function deepClone2(obj, wMap = new WeakMap()) {if (isObj(obj)) {
    // 判断是对象还是数组
    let target = Array.isArray(obj) ? [] : {};

    // 如果存在这个就间接返回
    if (wMap.has(obj)) {return wMap.get(obj);
    }

    wMap.set(obj, target);

    // 遍历对象
    Reflect.ownKeys(obj).forEach((item) => {
      // 拿到数据后判断是简单数据还是简略数据 如果是简单数据类型就持续递归调用
      target[item] = isObj(obj[item]) ? deepClone2(obj[item], wMap) : obj[item];
    });

    return target;
  } else {return obj;}
}
复制代码

这道题次要是的计划就是,递归加数据类型的判断

如是简单数据类型,就递归的再次调用你这个拷贝办法 直到是简略数据类型后能够进行间接赋值

6. 手写 call bind apply

call bind apply 的作用都是能够进行批改 this 指向

  • call 和 apply 的区别在于参数传递的不同
  • bind 区别在于最初会返回一个函数。
// call
    Function.prototype.MyCall = function (context) {if (typeof this !== "function") {throw new Error('type error')
      }
      if (context === null || context === undefined) {// 指定为 null 和 undefined 的 this 值会主动指向全局对象(浏览器中为 window)
        context = window
      } else {
        // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
        context = Object(context)
      }

      // 应用 Symbol 来确定惟一
      const fnSym = Symbol()

      // 模仿对象的 this 指向
      context[fnSym] = this

      // 获取参数
      const args = [...arguments].slice(1)

      // 绑定参数 并执行函数
      const result = context[fnSym](...args) 

      // 革除定义的 this
      delete context[fnSym]

      // 返回后果 
      return result
    } 
    
    
    // call 如果能明确的话 apply 其实就是改一下参数的问题
    // apply
    Function.prototype.MyApply = function (context) {if (typeof this !== "function") {throw new Error('type error')
      }

      if (context === null || context === undefined) {// 指定为 null 和 undefined 的 this 值会主动指向全局对象(浏览器中为 window)
        context = window
      } else {
        // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
        context = Object(context) 
      }


      // 应用 Symbol 来确定惟一
      const fnSym = Symbol()
      // 模仿对象的 this 指向
      context[fnSym] = this

      // 获取参数
      const args = [...arguments][1]

      // 绑定参数 并执行函数 因为 apply 传入的是一个数组 所以须要解构
      const result = arguments.length > 1 ? context[fnSym](...args) : context[fnSym]()

      // 革除定义的 this
      delete context[fnSym]

      // 返回后果  // 革除定义的 this
      return result
    }
    
    
    
    // bind
    Function.prototype.MyBind = function (context) {if (typeof this !== "function") {throw new Error('type error')
      }

      if (context === null || context === undefined) {// 指定为 null 和 undefined 的 this 值会主动指向全局对象(浏览器中为 window)
        context = window
      } else {
        // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
        context = Object(context) 
      }

      // 模仿对象的 this 指向
      const self = this

      // 获取参数
      const args = [...arguments].slice(1)
        
      // 最初返回一个函数 并绑定 this 要思考到应用 new 去调用,并且 bind 是能够传参的
      return function Fn(...newFnArgs) {if (this instanceof Fn) {return new self(...args, ...newFnArgs)
        }
            return self.apply(context, [...args, ...newFnArgs])
        }
    }
复制代码

7. 手写实现继承

这里我就只实现两种办法了,ES6 之前的寄生组合式继承 和 ES6 之后的 class 继承形式。

/**
    * es6 之前  寄生组合继承 
    */
    {function Parent(name) {
        this.name = name
        this.arr = [1, 2, 3]
      }

      Parent.prototype.say = () => {console.log('Hi');
      }

      function Child(name, age) {Parent.call(this, name)
        this.age = age
      }

      //  外围代码 通过 Object.create 创立新对象 子类 和 父类就会隔离
      // Object.create:创立一个新对象,应用现有的对象来提供新创建的对象的__proto__ 
      Child.prototype = Object.create(Parent.prototype)
      Child.prototype.constructor = Child
    }
    
    
    
    /**
    *   es6 继承 应用关键字 class
    */
     {
      class Parent {constructor(name) {
          this.name = name
          this.arr = [1, 2, 3]
        }
      }
      class Child extends Parent {constructor(name, age) {super(name)
          this.age = age
        }
      }
    }
复制代码

补充一个小常识,ES6 的 Class 继承在通过 Babel 进行转换成 ES5 代码的时候 应用的就是 寄生组合式继承。

继承的办法有很多,记住下面这两种根本就能够了!

8. 手写 new 操作符

首先咱们要晓得 new 一个对象的时候他产生了什么。

其实就是在外部生成了一个对象,而后把你的属性这些附加到这个对象上,最初再返回这个对象。

function myNew(fn, ...args) {
  // 基于原型链 创立一个新对象
  let newObj = Object.create(fn.prototype)

  // 增加属性到新对象上 并获取 obj 函数的后果
  let res = fn.call(newObj, ...args)

  // 如果执行后果有返回值并且是一个对象, 返回执行的后果, 否则, 返回新创建的对象
  return res && typeof res === 'object' ? res : newObj;
}
复制代码

9. js 执行机制 说出后果并说出 why

这道题考查的是,js 的工作执行流程,对宏工作和微工作的了解

console.log("start");

setTimeout(() => {console.log("setTimeout1");
}, 0);

(async function foo() {console.log("async 1");

  await asyncFunction();

  console.log("async2");

})().then(console.log("foo.then"));

async function asyncFunction() {console.log("asyncFunction");

  setTimeout(() => {console.log("setTimeout2");
  }, 0);

  new Promise((res) => {console.log("promise1");

    res("promise2");
  }).then(console.log);
}

console.log("end");
复制代码

提醒:

  1. script 标签算一个宏工作所以最开始就执行了
  2. async await 在 await 之后的代码都会被放到微工作队列中去

开始执行

  • 最开始碰到 console.log(“start”); 间接执行并打印出start
  • 往下走,遇到一个 setTimeout1 就放到 宏工作队列
  • 碰到立刻执行函数 foo,打印出async 1
  • 遇到 await 梗塞队列,先 执行 await 的函数
  • 执行 asyncFunction 函数,打印出asyncFunction
  • 遇到第二个 setTimeout2,放到宏工作队列
  • new Promise 立刻执行,打印出promise1
  • 执行到 res(“promise2”) 函数调用,就是 Promise.then。放到微工作队列
  • asyncFunction 函数就执行结束,把前面的打印 async2 会放到 微工作队列
  • 而后打印出立刻执行函数的 then 办法foo.then
  • 最初执行打印end
  • 开始执行微工作的队列 打印出第一个promise2
  • 而后打印第二个async2
  • 微工作执行结束,执行宏工作 打印第一个setTimeout1
  • 执行第二个宏工作 打印setTimeout2
  • 就此,函数执行结束

画工不好,能了解到意思就行😭。看看你们的想法和答案是否和这个流程统一

10. 如何拦挡全局 Promise reject,但并没有设定 reject 处理器 时候的谬误

这道题我是没写进去,最开始想着 trycatch 但这个并不是全局的。

后续查了材料才发现 是用一个 window 下面的办法

// 应用 Try catch 只能拦挡 try 语句块外面的
try {new Promise((resolve, reject) => {reject("WTF 123");
  });
} catch (e) {console.log("e", e);
  throw e;
}

// 应用 unhandledrejection 来拦挡全局谬误(这个是对的)window.addEventListener("unhandledrejection", (event) => {event && event.preventDefault();
  console.log("event", event);
});
复制代码

11. 手写实现 sleep

这个我只通过了一种办法实现,就是刚刚咱们在下面 js 执行流程中我有提过。await 会有异步梗塞的意思

还有一个办法是我在网上找到的办法,通过齐全梗塞过程的办法来实现 这个有点吊

// 应用 promise 配合 await 的异步办法来实现 sleep
    {(async () => {console.log('start');
        await sleep(3000)
        console.log('end');

        function sleep(timer) {
          return new Promise(res => {setTimeout(() => {res()
            }, timer);
          })
        }
      })();}

    // 办法二 这是齐全梗塞过程来达到 sleep
    {(async () => {console.log('start');
        await sleep(3000)
        console.log('end');

        function sleep(delay) {let t = Date.now();
          while (Date.now() - t <= delay) {continue;}
        };
      })()}
复制代码

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

有限链式调用实现的关键在于 对象的 toString 办法: 每个对象都有一个 toString() 办法,当该对象被示意为一个文本值时,或者一个对象以预期的字符串形式援用时主动调用

也就是我在调用很屡次后,他们的后果会 存在 add 函数中的 sum 变量上 ,当我 alert 的时候 add 会 主动调用 toString办法 打印出 sum, 也就是最终的后果

13. 两个数组中齐全独立的数据

就是找到仅在两个数组中呈现过一次的数据

var a = [1, 2, 4], b = [1, 3, 8, 4]
const newArr = a.concat(b).filter((item, _, arr) => {return arr.indexOf(item) === arr.lastIndexOf(item)
})
复制代码

最终进去的后果是 [2,3,8],原理其实很简略:合并两个数组,而后查找数组的 第一个呈现的索引 最初一个呈现的索引 是否统一就能够判断是否是独立的数据了。

14. 判断齐全平方数

就是判断一个数字能不能被开平方,比方 9 的开平方是 3 是对的。5 没法开平方就是错的。

var fn = function (num) {return num ** 0.5 % 1 == 0};
复制代码

原理就是,开平方后判断是否是正整数就行了

15. 函数执行 说出后果并说出 why

function Foo() {getName = function () {console.log(1);
  };
  return this;
}

Foo.getName = function () {console.log(2);
}

Foo.prototype.getName = function () {console.log(3);
}

var getName = function () {console.log(4);
}

function getName() {console.log(5)
}

Foo.getName();

getName();

Foo().getName()

getName();

new Foo.getName(); 

new Foo().getName()

new new Foo().getName()
复制代码

这道题其实就是看你对作用域的关系的了解吧

执行后果:

  • 执行 Foo.getName(), 执行 Foo 函数对象上的的静态方法。 打印出 2
  • 执行 getName(),就是执行的 getName 变量的函数。打印 4

    • 为什么这里是 执行的 变量 getName,而不是函数 getName 呢。这得归功于js 的预编译
    • js 在执行之前进行预编译,会进行 函数晋升 变量晋升
    • 所以函数和变量都进行晋升了,然而 函数申明的优先级最高 ,会被晋升至 以后作用域最顶端
    • 当在执行到前面的时候会导致 getName 被从新赋值,就会把执行后果为4 的这个函数赋值给变量
  • 执行 Foo().getName()调用 Foo 执行后返回值上的 getName 办法。 Foo 函数执行了,外面会给 里面的 getName 函数从新赋值,并返回了 this。也就是执行了 this.getName。所以打印出了 1
  • 执行 getName(),因为上一步,函数被从新赋值。所以这次的后果和上次的后果是一样的,还是为1
  • 执行 new Foo.getName(),这个 new 其实就是 new 了 Foo 下面的 静态方法 getName 所以是2。当然如果你们在这个函数外面打印 this 的话,会发现指向的是一个新对象 也就是 new 进去的一个新对象

    • 能够把 Foo.getName()看成一个整体,因为 这里 . 的优先级比 new 高
  • 执行 new Foo().getName(),这里函数执行 new Foo() 会返回一个对象,而后调用这个 对象原型上的 getName 办法,所以后果是 3
  • 执行 new new Foo().getName(), 这个和上一次的后果是一样,上一个函数调用后并咩有返回值,所以在进行 new 的时候也没有意义了。最终后果也是3

16. 原型调用面试题 说出后果并说出 why

function Foo() {Foo.a = function () {console.log(1);
  };
  this.a = function () {console.log(2);
  };
}

Foo.prototype.a = function () {console.log(4);
};

Function.prototype.a = function () {console.log(3);
};


Foo.a();

let obj = new Foo();
obj.a();
Foo.a();
复制代码

执行后果:

  • 执行 Foo.a()Foo 自身目前并没有 a 这个值,就会通过 __proto__ 进行查找,然而

    ,所以输入是 3
  • new 实例化了 Foo 生成对象 obj,而后调用 obj.a(),然而在 Foo 函数外部给这个 obj 对象附上了 a 函数。所以后果是2。如果在外部没有给这个对象赋值 a 的话,就会去到原型链查找 a 函数,就会打印 4.
  • 执行 Foo.a(),在上一步中 Foo 函数执行,外部给 Foo 自身赋值函数 a ,所以这次就打印1

17. 数组分组改成减法运算

这个题的意思就是 [5, [[4, 3], 2, 1]] 变成 (5 - ((4 - 3) - 2 - 1)) 并执行。且 不能应用 eval()

办法一:既然不能用 eval,那咱们就用 new Function 吧🤭

办法二:当然办法一有点违反了题意,所以还有第二种办法

var newArr = [5, [[4, 3], 2, 1]]

    // 1. 取巧
    // 转为字符串
    let newStringArr = `${JSON.stringify(newArr)}`
    // 循环批改括号和减号
    let fn = newStringArr.split("").map((el) => {switch (el) {
        case "[":
          return '('
        case "]":
          return ')'
        case ",":
          return '-'
        default:
          return el
      }
    }).join("")
    // 最终通过 new Function 调用能够了!new Function("return" + fn)()
    
    
    // 2. 办法二 
    function run(arr) {return arr.reduce((pre, cur) => {let first = Array.isArray(pre) ? run(pre) : pre
        let last = Array.isArray(cur) ? run(cur) : cur
        return first - last
      })
    }
    run(nweArr)
复制代码
  • 办法一的原理就很简略,转成字符串循环批改括号和减号在进行拼接。最终 通过 new Function 调用 就能够了
  • 办法二的意思就是通过 reduce 进行一个递归调用 的意思。如果右边 不是数组 就能够减去左边的,但如果 左边是数组的话,就要把左边的数组先进行减法运算。也是就减法括号运算的的优先级.

18. 手写数组的 flat

const flat = function (arr, deep = 1) {
      // 申明一个新数组
      let result = []
      
      arr.forEach(item => {if (Array.isArray(item) && deep > 0) {
          // 层级递加
          // deep--  来自评论区的大佬斧正:deep - 1
          // 应用 concat 链接数组  
          result = result.concat(flat(item, deep - 1))
        } else {result.push(item)
        }
      })
      return result
    }
复制代码
  • 原理就是,先在外部生成一个新数组,遍历原来的数组
  • 当原数组内 存在数组 并且层级 deep 大于等于 1 时 进行递归, 如果 不满足这个条件就能够间接 push 数据到新数组
  • 递归同时要先把层级缩小,而后通过 concat 链接递归进去的数组
  • 最终返回这个数组就能够了

19. 数组转为 tree

最顶层的parent 为 -1,其余的 parent 都是为 上一层节点的 id

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)
复制代码
  • 这道题也是利用递归来进行的,在最开始会进行 是否是顶层节点的判断
  • 如果是就间接返回,如果不是则 判断是不是本人要增加到父节点的子节点
  • 而后再一层一层把节点退出进去
  • 最初返回这个对象

20. 合并数组并排序去重

题意就是,我有两个数组,把他们 两个合并 。而后并 去重 去重的逻辑是哪儿边的反复次数更多,我就留下哪儿边的。

比方上面的数组中,一边有两个数字 5 另一半有三个数字 5 。则我须要 留下三个数字 5 去掉两个数字 5 。周而复始,最初失去的后果在进行排序。

  • 数组一:[1, 100, 0, 5, 1, 5]
  • 数组二:[2, 5, 5, 5, 1, 3]
  • 最终的后果:[0, 1, 1, 2, 3, 5, 5, 5, 100]
// 判断呈现次数最多的次数
    function maxNum(item, arr) {
      let num = 0;
      arr.forEach(val => {item === val && num++})

      return num
    }

    function fn(arr1, arr2) {
      // 应用 Map 数据类型来记录次数
      let obj = new Map();

      // 合并数组并找出最多的次数, 并以键值对寄存到 Map 数据类型
      [...arr1, ...arr2].forEach(item => {let hasNum = obj.get(item)
        let num = 1
        if (hasNum) {num = hasNum + 1}
        obj.set(item, num)
      })

      // 寄存合并并去重之后的数组
      let arr = []
      // 遍历 Map 数据类型 而后把次数最多的间接 push 到新数组
      for (const key of obj.keys()) {if (obj.get(key) > 1) {for (let index = 0; index < Math.max(maxNum(key, arr1), maxNum(key, arr2)); index++) {arr.push(key)
          }
        } else {arr.push(key)
        }
      }

    // 最初进行排序
      return arr.sort((a, b) => a - b)
    }
复制代码
  • 这个题的思路其实就是,我先把 两个数组合并起来
  • 并以 键值对的形式寄存到 Map 数据类型 , 键就是数据,而值就是这个数据呈现的次数
  • 生成一个新数组,用来 寄存合并之后的数组
  • 遍历这个 Map 数据类型 , 如果这个数据 呈现的次数大于一 ,那么就去 寻找两个数组中谁呈现的次数更多 ,把 呈现次数更多的这个数据,循环 push 到新数组中 。如果 呈现次数等于一,那就间接 push 到新数组中即可。
  • 最初再把 数组进行排序,而后返回新数组就可。

正文完
 0