面试题视频解说(高效学习):进入学习
二、题目
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;// 写法1function 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弱援用 性能更好 并且反对 Symbolfunction 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");复制代码
提醒:
- script标签算一个宏工作所以最开始就执行了
- 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
的这个函数赋值给变量
- 为什么这里是 执行的 变量getName,而不是函数getName呢。这得归功于
- 执行 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 高
- 能够把 Foo.getName()看成一个整体,因为
- 执行 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到新数组中即可。- 最初再把
数组进行排序
,而后返回新数组就可。