前端面试手撕题整理
转自https://www.nowcoder.com/discuss/post/398883449182978048
担心以后炸了备份一下。
基础
高频
1.手写 instanceof
// 原理:验证当前类的原型prototype是否会出现在实例的原型链proto上,只要在它的原型链上,则结果都为true function myinstanceOf_(obj, class_name) { // let proto = obj.__proto__; let proto = Object.getPrototypeOf(obj) let prototype = class_name.prototype while (true) { if (proto == null) return false if (proto == prototype) return true // proto = proto.__proto__; proto = Object.getPrototypeOf(proto) } }
2.手写 new 操作符
function myNew(){ //1.创建一个新的对象 let obj = {}; //获得构造函数 let con = [].shift.call(arguments); //2.新对象的隐式原型__proto__链接到构造函数的显式原型prototype obj.__proto__ = con.prototype; //3.构造函数内部的 this 绑定到这个新创建的对象 执行构造函数 let result = con.apply(obj, arguments) //4.如果构造函数没有返回非空对象,则返回创建的新对象 return result instanceof Object ? result:obj; } // test /* function Car(name,price){ this.name = name this.price = price } Car.prototype.run = function() { console.log(this.price); }; var test_create = myNew(Car, 'a', 100000); console.log(test_create) // compare let obj = new Car( 'a', 100000) console.log(obj) */
3.手写 call、apply、bind 函数
- call(thisArg, ...args)
// 给函数的原型添加 _call 方法,使得所有函数都能调用 _call // thisArg 就是要绑定的那个this;...args 扩展操作符传参,适合不定长参数,args是一个数组 Function.prototype._call = function(thisArg,...args){ // 1.获取需要执行的函数 let func = this // 2.将 thisArg 转成对象类型(防止它传入的是非对象类型,例如123数字) thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window // 3.使用 thisArg 调用函数,绑定 this // 评论区大佬提出的改进方法,用 Symbol 创建一个独一无二的属性,避免属性覆盖或冲突的情况 let fn = Symbol() thisArg[fn] = this // thisArg.fn = fn let result = thisArg[fn](...args) delete thisArg[fn] // 4.返回结果 return result }
- apply(thisArg, argsArray)
Function.prototype._apply = function(thisArg,argArray){ // 1.获取需要执行的函数 let fn = this // 2.将 thisArg 转成对象类型(防止它传入的是非对象类型,例如123数字) thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window // 判断一些边界情况 argArray = argArray || [] // 3.使用 thisArg 调用函数,绑定 this thisArg.fn = fn // 将传递过来的数组(可迭代对象)拆分,传给函数 let result = thisArg.fn(...argArray) delete thisArg.fn // 4.返回结果 return result }
- bind(thisArg, ...args)
Function.prototype._call = function(thisArg,...args){ let fn = this thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window thisArg.fn = fn let result = thisArg.fn(...args) delete thisArg.fn return result } // 利用 call 模拟 bind Function.prototype._bind = function(thisArg,...args){ let fn = this // 需要调用的那个函数的引用 // bind 需要返回一个函数 return function(){ return fn._call(thisArg, ...args) } }
4.手写深拷贝
PS:浅拷贝也可以用一样的模板,当然深拷贝考得多
function deepCopy(object) { if (!object || typeof object !== "object") return object; let newObject = Array.isArray(object) ? [] : {}; for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = deepCopy(object[key]); } } return newObject; }
进阶:解决循环引用的深拷贝
function deepClone(obj, hash = new WeakMap()) { if (!object || typeof object !== "object") return object; // 是对象的话就要进行深拷贝,遇到循环引用,将引用存储起来,如果存在就不再拷贝 if (hash.get(obj)) return hash.get(obj); let cloneObj = Array.isArray(object) ? [] : {}; hash.set(obj, cloneObj); for (let key in obj) { if (obj.hasOwnProperty(key)) { // 实现一个递归拷贝 cloneObj[key] = deepClone(obj[key], hash); } } return cloneObj; }
5.手写防抖节流
function debounce(func, delay) { // 这里使用了闭包,所以 timer 不会轻易被销毁 let timer = null // 生成一个新的函数并返回 return function (...args) { // 清空定时器 if (timer) { clearTimeout(timer) } // 重新启动定时器 timer = setTimeout(() => { func.call(this, ...args) }, delay) } }
function throttle(func, delay) { let timer = null // 在 delay 时间内,最多执行一次 func return function (...args) { if (!timer) { timer = setTimeout(() => { func.call(this, ...args) // 完成一次计时,清空,待下一次触发 timer = null }, delay) } } }
6.手写Ajax请求
function ajax(url) { // 创建一个 XHR 对象 return new Promise((resolve,reject) => { const xhr = new XMLHttpRequest() // 指定请求类型,请求URL,和是否异步 xhr.open('GET', url, true) xhr.onreadystatechange = funtion() { // 表明数据已就绪 if(xhr.readyState === 4) { if(xhr.status === 200){ // 回调 resolve(JSON.stringify(xhr.responseText)) } else{ reject('error') } } } // 发送定义好的请求 xhr.send(null) }) }
7.手写数组去重
// 1.Set + 数组复制 fuction unique1(array){ // Array.from(),对一个可迭代对象进行浅拷贝 return Array.from(new Set(array)) } // 2.Set + 扩展运算符浅拷贝 function unique2(array){ // ... 扩展运算符 return [...new Set(array)] } // 3.filter,判断是不是首次出现,如果不是就过滤掉 function unique3(array){ return array.filter((item,index) => { return array.indexOf(item) === index }) } // 4.创建一个新数组,如果之前没加入就加入 function unique4(array){ let res = [] array.forEach(item => { if(res.indexOf(item) === -1){ res.push(item) } }) return res }
进阶:如果数组内有数组和对象,应该怎么去重(此时对象的地址不同,用Set去不了重)
8.手写数组扁平
// 方法1-3:递归 function flat1(array){ // reduce(): 对数组的每一项执行归并函数,这个归并函数的返回值会作为下一次调用时的参数,即 preValue // concat(): 合并两个数组,并返回一个新数组 return array.reduce((preValue,curItem) => { return preValue.concat(Array.isArray(curItem) ? flat1(curItem) : curItem) },[]) } function flat2(array){ let res = [] array.forEach(item => { if(Array.isArray(item)){ // res.push(...flat2(item)) // 如果遇到一个数组,递归 res = res.concat(flat2(item)) } else{ res.push(item) } }) return res } function flat3(array){ // some(): 对数组的每一项都运行传入的函数,如果有一项返回 TRUE,则这个方法返回 TRUE while(array.some(item => Array.isArray(item))){ // ES6 增加了扩展运算符,用于取出参数对象的所有可遍历属性,拷贝到当前对象之中: array = [].concat(...array) console.log(...array) } return array } // 方法4、5:先转成字符串,再变回数组 function flat4(array){ //[1,[2,3]].toString() => 1,2,3 return array.toString().split(',').map(item => parseInt(item)) } function flat5(array){ return array.join(',').split(',').map(item => Number(item)) }
9.手写数组乱序
// 方法1: sort + Math.random() function shuffle1(arr){ return arr.sort(() => Math.random() - 0.5);// } // 方法2:时间复杂度 O(n^2) // 随机拿出一个数(并在原数组中删除),放到新数组中 function randomSortArray(arr) { let backArr = []; while (arr.length) { let index = parseInt(Math.random() * arr.length); backArr.push(arr[index]); arr.splice(index, 1); } return backArr; } // 方法3:时间复杂度 O(n) // 随机选一个放在最后,交换 function randomSortArray2(arr) { let lenNum = arr.length - 1; for (let i = 0; i < lenNum; i++) { let index = parseInt(Math.random() * (lenNum + 1 - i)); [a[index],a[lenNum - i]] = [a[lenNum - i],a[index]] } return arr; }
10.手写 Promise.all()、Promise.race()
PS: 有能力的可以去写下 Promise 和其他的 Promise 方法
function myAll(promises){ // 问题关键:什么时候要执行resolve,什么时候要执行 reject return new Promise((resolve,reject) => { results = [] // 迭代数组中的 Promise,将每个 promise 的结果保存到一个数组里 let counter = 0 for (let i = 0; i < promises.length; i++) { // 如果不是 Promise 类型要先包装一下 // 调用 then 得到结果 Promise.resolve(promises[i]).then(res => { // 这里不能用 push(),因为需要保证结果的顺序。感谢评论区大佬的批评指正 results[i] = res counter++ // 如果全部成功,状态变为 fulfilled if(counter === promises.length){ resolve(results) } },err => { // 如果出现了 rejected 状态,则调用 reject() 返回结果 reject(err) }) } } ) } // 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) }) myAll([p3, p1, p2]).then(res => { console.log(res) // [3, 1, 2] }) // 哪个 promise 状态先确定,就返回它的结果 function myRace(promises) { return new Promise((resolve, reject) => { promises.forEach(promise => { Promise.resolve(promise).then(res => { resolve(res) }, err => { reject(err) }) }) }) }
11.手撕快排
PS: 常见的排序算法,像冒泡,选择,插入排序这些最好也背一下,堆排序归并排序能写则写。万一考到了呢,要是写不出就直接回去等通知了
const _quickSort = array => { // 补全代码 quickSort(array, 0, array.length - 1) // 别忘了返回数组 return array } const quickSort = (array, start, end) => { // 注意递归边界条件 if(end - start < 1) return // 取第一个数作为基准 const base = array[start] let left = start let right = end while(left < right){ // 从右往左找小于基准元素的数,并赋值给右指针 array[right] while(left < right && array[right] >= base) right-- array[left] = array[right] // 从左往右找大于基准元素的数,并赋值给左指针 array[left] while(left < right && array[left] <= base) left++ array[right] = array[left] } // 双指针重合处,将基准元素填到这个位置。基准元素已经事先保存下来了,因此不用担心上面的赋值操作会覆盖掉基准元素的值 // array[left] 位置已经确定,左边的都比它小,右边的都比它大 array[left] = base quickSort(array, start, left - 1) quickSort(array, left + 1, end) return array }
12.手写 JSONP
// 动态的加载js文件 function addScript(src) { const script = document.createElement('script'); script.src = src; script.type = "text/javascript"; document.body.appendChild(script); } addScript("http://xxx.xxx.com/xxx.js?callback=handleRes"); // 设置一个全局的callback函数来接收回调结果 function handleRes(res) { console.log(res); } // 接口返回的数据格式,加载完js脚本后会自动执行回调函数 handleRes({a: 1, b: 2});
13.手写寄生组合继承
PS: 组合继承也要能写出来
function Parent(name) { this.name = name; this.say = () => { console.log(111); }; } Parent.prototype.play = () => { console.log(222); }; function Children(name,age) { Parent.call(this,name); this.age = age } Children.prototype = Object.create(Parent.prototype); Children.prototype.constructor = Children;
14.数组/字符串操作题
可以自己找些基础的练一下,就不一一列举了
15.手写二分查找
// 迭代版 function search(nums, target) { // write code here if(nums.length === 0) return -1 let left = 0,right = nums.length - 1 // 注意这里的边界,有等号 while(left <= right){ let mid = Math.floor((left + right) / 2) if(nums[mid] < target) left = mid + 1 else if(nums[mid] > target) right = mid - 1 else return mid } return -1 } // 递归版 function binary_search(arr, low, high, key) { if (low > high) { return -1; } var mid = parseInt((high + low) / 2); if (arr[mid] == key) { return mid; } else if (arr[mid] > key) { high = mid - 1; return binary_search(arr, low, high, key); } else if (arr[mid] < key) { low = mid + 1; return binary_search(arr, low, high, key); } };
16.手写函数柯里化
function sum(x,y,z) { return x + y + z } function hyCurrying(fn) { // 判断当前已经接收的参数的个数,和函数本身需要接收的参数是否一致 function curried(...args) { // 1.当已经传入的参数 大于等于 需要的参数时,就执行函数 if(args.length >= fn.length){ // 如果调用函数时指定了this,要将其绑定上去 return fn.apply(this, args) } else{ // 没有达到个数时,需要返回一个新的函数,继续来接收参数 return function(...args2) { //return curried.apply(this, [...args, ...args2]) // 接收到参数后,需要递归调用 curried 来检查函数的个数是否达到 return curried.apply(this, args.concat(args2)) } } } return curried } var curryAdd = hyCurry(sum) curryAdd(10,20,30) curryAdd(10,20)(30) curryAdd(10)(20)(30)
17.CSS水平垂直居中
18.CSS画三角形
19.CSS实现两栏和三栏布局
20.手写发布-订阅模式 || Event Bus
中频
1.手写事件委托
2.DOM操作相关题目
3.手写对象扁平化
4.手写数组常见方法 Array.filter/map/fill/reduce
5.手写列表转树
6.日期时间格式化
7.数字千分位
8.URL参数解析
9.手写观察者模式
10.手写 Promise(进阶)
低频
1.手写组合函数
2.手写Object.create()
3.手写Object.is()
4.手写Object.freeze()
5.手写JSON.stringify()
6.大数相加
7.下划线转驼峰
8.CSS清除浮动
场景模拟题
高频
1.实现 sleep 函数
async function test() { console.log('开始') await sleep(4000) console.log('结束') } function sleep(ms) { return new Promise(resolve => { setTimeout(() => { resolve() }, ms) }) } test()
2.setTimeout 实现 setInterval
function setInterval(fn, time){ var interval = function(){ // time时间过去,这个异步被执行,而内部执行的函数正是interval,就相当于进了一个循环 setTimeout(interval, time); // 同步代码 fn(); } //interval被延迟time时间执行 setTimeout(interval,time); }
3.异步循环打印 1,2,3
var sleep = function (time, i) { return new Promise(function (resolve, reject) { setTimeout(function () { resolve(i); }, time); }) }; var start = async function () { for (let i = 1; i <= 3; i++) { let result = await sleep(1000, i); console.log(result); } }; start();
4.循环打印红、黄、绿
function red() { console.log('red'); } function green() { console.log('green'); } function yellow() { console.log('yellow'); } 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 taskRunner = async () => { await task(3000, 'red') await task(2000, 'green') await task(2100, 'yellow') taskRunner() } taskRunner()
进阶
Promise 并发控制、串行执行相关的题目。