常见手撕题

103 阅读8分钟

基础

高频

手写new 操作符

  1. 创建一个新的空对象;
  2. 将对象的原型指向构造函数的原型对象,obj.__proto__ = Person.prototype
  3. 让函数的this指向这个对象,执行构造函数(为这个新对象添加属性);
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象;如果是引用类型,返回这个引用类型的对象。
 function Person(name, age) {
   this.name = name
   this.age = age
   return 123 //返回值为值类型,则new操作返回新创建的对象
   //return {a:1},若构造函数返回值为引用类型,则new操作直接返回该引用类型
 }
 ​
 const _new = function (constructor, ...args) {
   const obj = {}
   // obj.__proto__ = constructor.prototype
   Object.setPrototypeOf(obj,constructor.prototype)
   const res = constructor.call(obj, ...args)
   return res instanceof Object ? res : obj
 }
 ​
 const obj = _new(Person, '小明', 18)
 console.log(obj.name); // 小明
 console.log(obj.age); // 18

深拷贝与浅拷贝

 // 深拷贝
 function deepClone(obj) {
   if (!obj || typeof obj !== 'object') return obj
   let newObj = Array.isArray(obj) ? [] : {}
   for (let key in obj) {
     //对象原型上的东西不应该进行拷贝
     if (obj.hasOwnProperty(key)) {
       newObj[key] = deepClone(obj[key])
     }
   }
   return newObj
 }
 // 浅拷贝

数组扁平化

 // 测试数组
 const arr = [1, [2, 3], [4, 5, [6, 7]], 8, 9];
 // 转换函数
 function flat(arr) {
   // 遍历传入的 arr 的每一项
   const result = arr.map(item => {
     if (Array.isArray(item)) {
       return flat(item)
     }
     return item
   })
   return [].concat(...result)
 }
 ​
 const flatArr = flat(arr);
 console.log(flatArr);
 // [1, 2, 3, 4, 5, 6, 7, 8, 9]
 // 测试数组
 const arr = [1, [2, 3], [4, 5, [6, 7]], 8, 9];
 // 转换函数
 function flat(arr) {
   while (arr.some(item => Array.isArray(item))) {  // some返回boolean值
     arr = [].concat(...arr)
   }
   return arr
 }
 ​
 const flatArr = flat(arr);
 console.log(flatArr);
 // [1, 2, 3, 4, 5, 6, 7, 8, 9]

扁平数组结构转为树形结构

 let flatArr = [   { id: 1, title: '标题1', pid: 0 },   { id: 2, title: '标题2', pid: 0 },   { id: 3, title: '标题2-1', pid: 2 },   { id: 4, title: '标题3-1', pid: 3 },   { id: 5, title: '标题4-1', pid: 4 },   { id: 6, title: '标题2-2', pid: 2 }, ]
 ​
 function convert(list) {
   const result = []
   const map = list.reduce((pre, curr) => {
     pre[curr.id] = curr
     return pre
   }, {})
   for (let item of list) {
     if (item.pid === 0) {
       result.push(item)
       continue
     }
     if (item.pid in map) {
       const parent = map[item.pid]
       parent.children = parent.children || []
       parent.children.push(item)
     }
   }
   return result
 }
 console.log(convert(flatArr))

树形结构转为扁平数组结构

 function flatten(data) {
   return data.reduce((pre, curr) => {
     // concat
     const { id, title, pid, children = [] } = curr
     return pre.concat([{ id, title, pid }], flatten(children))
   }, [])
 }
 let flattenRes = flatten(res)
 console.log(flattenRes);

手写call bind apply

 Function.prototype.myCall = function (thisArg, ...args) {
   // 当我们thisArg传入的是一个非真值的对象时,thisArg指向window
   thisArg = thisArg || window
   // 3.使用 thisArg 调用函数,绑定 this
   // 用 Symbol 创建一个独一无二的属性,避免属性覆盖或冲突的情况
   const fn = Symbol()
   thisArg[fn] = this
   // 执行thisArg.fn,并返回返回值
   // args本身是剩余参数,搭配的变量是一个数组,数组解构后就可以一个个传入函数中
   const res = thisArg[fn](...args)
   // 删除该方法以避免对传入对象造成污染
   delete thisArg[fn]
   return res
 }
 // 区别就是这里第二个参数直接就是个数组
 Function.prototype.myApply = function (thisArg, args) {
   thisArg = thisArg || window
   const fn = Symbol()
   thisArg[fn] = this
   // args本身是个数组,所以我们需要解构后一个个传入函数中
   const res = thisArg[fn](...args)
   // 执行完借用的函数后,删除掉,留着过年吗?
   delete thisArg[fn]
   return res
 }
 Function.prototype.myBind = function (thisArg, ...outArgs) {
   // 处理边界条件
   thisArg = thisArg || {}
   const fn = Symbol()
   thisArg[fn] = this
   // 返回一个函数
   return function (...innerArgs) {
     // outArgs和innerArgs都是一个数组,解构后传入函数
     const res = thisArg[fn](...outArgs, ...innerArgs)
     // delete thisArg[fn] 这里千万不能销毁绑定的函数,否则第二次调用的时候,就会出现问题。
     return res
   }
 }

数组去重

 // 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
 }

数组乱序

// 蛋老师
function shuffle4(arr) {
  const len = arr.length
  for (let i = len - 1; i >= 0; i--) {
    const index = Math.floor(Math.random() * i)
    [arr[index], arr[i]] = [arr[i], arr[index]]
  }
  return arr
}

// 方法1: sort + Math.random()
function shuffle1(arr) {
  return arr.sort(() => Math.random() - 0.5)
}

// 方法2:时间复杂度 O(n^2)
// 随机拿出一个数(并在原数组中删除),放到新数组中
function shuffle3(arr) {
  const backArr = []
  while (arr.length) {
    const index = parseInt(Math.random() * arr.length)
    backArr.push(arr[index])
    arr.splice(index, 1)
  }
  return backArr
}

手写防抖&&节流

 // 防抖函数
 /**
  * 防抖函数  一个需要频繁触发的函数,在规定时间内,只让最后一次生效,前面的不生效
  * @param fn要被节流的函数
  * @param delay规定的时间
  */
 function debounce(fn, delay = 200) {
   let timer = null
   return function (...args) {
     if (timer) {
       // 清除上一次的延时器
       clearTimeout(timer)
       timer = null
     } else {
       // 对第一次输入立即执行
       fn.apply(this, args)
     }
     // 重新设置新的延时器
     timer = setTimeout(() => {
       // 修正this指向问题
       fn.apply(this, args)
     }, delay)
   }
 }
 // 节流函数
 /**
  * 节流函数 一个函数执行一次后,只有大于设定的执行周期才会执行第二次。有个需要频繁触发的函数,出于优化性能的角度,在规定时间内,只让函数触发的第一次生效,后面的不生效。
  * @param fn要被节流的函数
  * @param delay规定的时间
  */
 function throttle(fn, delay) {
   let pre = 0
   let timer = null
   return function (...args) {
     const now = Date.now()
     // 如果时间差超过了规定时间间隔
     if (now - pre > delay) {
       pre = now
       fn.apply(this, args) //this指向本匿名函数
     } else {
       // 如果在规定时间间隔内,则后续事件会直接清除
       if (timer) {
         clearTimeout(timer)
         timer = null
       }
       // 最后一次事件会触发
       timer = setTimeout(() => {
         pre = now
         fn.apply(this, args)
       }, delay)
     }
   }
 }

手写 instanceof

 function myInstanceof(obj, className) {
   let pointer = obj
   // 判断构造函数的 prototype 对象是否在对象的原型链上
   while (pointer) {
     if (pointer === className.prototype) {
       return true
     }
     pointer = pointer.__proto__
   }
   return false
 }

函数柯里化的实现

函数柯里化指的是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

 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)

实现AJAX请求

AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。

创建AJAX请求的步骤:

  • 创建一个 XMLHttpRequest 对象。
  • 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
  • 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
  • 当对象的属性和监听函数设置完成后,最后调用 sent 方法来向服务器发起请求,可以传入参数作为发送的数据体。
 function ajax(url) {
   // 创建一个 XHR 对象
   return new Promise((resolve, reject) => {
     const xhr = new XMLHttpRequest()
     // 指定请求类型,请求URL,和是否异步
     xhr.open('GET', url, true)
     xhr.onreadystatechange = function () {
       // 表明数据已就绪
       if (xhr.readyState === 4) {
         if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
           // 回调
           resolve(JSON.stringify(xhr.responseText))
         }
         else {
           reject('error')
         }
       }
     }
     // 发送定义好的请求
     xhr.send(null)
   })
 }

手写 Promise.all()

 function promiseAll(promises) {
   return new Promise((resolve, reject) => {
     const promiseResult = []
     // 成功执行的次数
     let fullCount = 0
     if (promises.length === 0) {
       resolve(promiseResult)
     }
     for (let i = 0; i < promises.length; i++) {
       // 不是promise,需要包装一层
       Promise.resolve(promises[i]).then((res) => {
         // 这里不能用 push(),因为需要保证结果的顺序
         promiseResult[i] = res
         fullCount++
         // 如果全部成功,状态变为 fulfilled
         if (fullCount === promises.length) {
           resolve(promiseResult)
         }
       }).catch(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)
 })
 promiseAll([p3, p1, p2]).then(res => {
   console.log(res) // [3, 1, 2]
 })

Promise.race()

 // 哪个 promise 状态先确定,就返回它的结果
 function myRace(promises) {
     return new Promise((resolve, reject) => {
         promises.forEach(promise => {
             Promise.resolve(promise).then(res => {
                 resolve(res)
             }, err => {
                 reject(err)
             })
         })
     })
 }

手写 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});

寄生式组合继承

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;
 function Person(name) {
   this.name = name;
 }
 ​
 Person.prototype.sayName = function() {
   console.log("My name is " + this.name + ".");
 };
 ​
 function Student(name, grade) {
   Person.call(this, name);
   this.grade = grade;
 }
 ​
 Student.prototype = Object.create(Person.prototype);
 Student.prototype.constructor = Student;
 ​
 Student.prototype.sayMyGrade = function() {
   console.log("My grade is " + this.grade + ".");
 };

组合继承

发布订阅模式

 ​

观察者模式

 ​

快速排序

PS: 常见的排序算法,像冒泡,选择,插入排序这些最好也背一下,堆排序归并排序能写则写。万一考到了呢,要是写不出就直接回去等通知了

 function quickSort(arr) {
   if (arr.length <= 1) return arr
   let pivotIndx = Math.floor(arr.length / 2)
   let pivot = arr.splice(pivotIndx, 1)[0]
   let left = []
   let right = []
   for (let i = 0; i < arr.length; i++) {
     if (arr[i] < pivot) {
       left.push(arr[i])
     } else {
       right.push(arr[i])
     }
   }
   return [...quickSort(left), pivot, ...quickSort(right)]
 }

冒泡排序

 function bubbleSort(arr) {
   for (let i = 0; i < arr.length - 1; i++) {
     for (let j = 0; j < arr.length - 1 - i; j++) {
       if (arr[j] > arr[j + 1]) {
         [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
       }
     }
   }
   return arr
 }

选择排序

插入排序

生成区间内随机整数

中频

低频

场景模拟题

高频