JavaScript十大手撕代码

JavaScript十大手撕代码

JavaScript中有很多API贼好用(同时也是面试的高频题目),省下了很多工夫,你知道它的原理吗?这篇文章对它们做一个总结。

一、手撕instanceof

  • instanceof的原理:通过判断对象的原型是否等于构造函数的原型来进行类型判断
  • 代码实现:
const myInstanceOf=(Left,Right)=>{
  if(!Left){
    return false
  }
  while(Left){
    if(Left.__proto__===Right.prototype){
      return true
    }else{
      Left=Left.__proto__
    }
  }
  return false
}

//验证
console.log(myInstanceOf({},Array));  //false

二、手撕call,apply,bind

call,apply,bind是通过this的显示绑定修改函数的this指向

1. call

call的用法:a.call(b) -> 将a的this指向b
我们需要借助隐式绑定规则来实现call,具体实现步骤如下:
往要绑定的那个对象(b)上挂一个属性,值为需要被调用的那个函数名(a),在外层去调用函数。

function foo(x,y){
  console.log(this.a,x+y);
}

const obj={
  a:1
}

Function.prototype.myCall=function(context,...args){
  if(typeof this !== 'function')  return new TypeError('is not a function')
  const fn=Symbol('fn'//使用Symbol尽可能降低myCall对其他的影响
  context[fn]=this  //this指向foo
  const res=context[fn](...args)  //解构,调用fn
  delete context[fn]  //不要忘了删除obj上的工具函数fn
  return res  //将结果返回
}

//验证
foo.myCall(obj,1,2)   //1,3

2. apply

apply和call的本质区别就是接受的参数形式不同,call接收零散的参数,而apply以数组的方式接收参数,实现思路完全一样,代码如下:

function foo(x,y){
  console.log(this.a,x+y);
}

const obj={
  a:1
}

Function.prototype.myApply=function(context,args){
  if(typeof this !== 'function')  return new TypeError('is not a function')
  const fn=Symbol('fn'//尽可能降低myCall对其他的影响
  context[fn]=this
  context[fn](...args)
  delete context[fn]
}

//验证
foo.myApply(obj,[1,2])  //1,3

3. bind

bind和call,apply的区别是会返回一个新的函数,接收零散的参数
需要注意的是,官方bind的操作是这样的:

  • 当new了bind返回的函数时,相当于new了foo,且new的参数需作为实参传给foo
  • foo的this.a访问不到obj中的a
function foo(x,y,z){
  this.name='zt'
  console.log(this.a,x+y+z);
}

const obj={
  a:1
}


Function.prototype.myBind=function(context,...args){
  
  if(typeof this !== 'function')  return new TypeError('is not a function')

  context=context||window

  let _this=this
  
  return function F(...arg){
    //判断返回出去的F有没有被new,有就要把foo给到new出来的对象
    if(this instanceof F){
      return new _this(...args,...arg) //new一个foo
    }
    _this.apply(context,args.concat(arg))  //this是F的,_this是foo的  把foo的this指向obj用apply
  }
}

//验证
const bar=foo.myBind(obj,1,2)
console.log(new bar(3));   //undefined 6  foo { name: 'zt' }

三、手撕深拷贝

这篇文章中详细记录了实现过程?【js手写】浅拷贝与深拷贝

四、手撕Promise

思路:

  • 我们知道,promise是有三种状态的,分别是pending(异步操作正在进行), fulfilled(异步操作成功完成), rejected(异步操作失败)。我们可以定义一个变量保存promise的状态。
  • resolve和reject的实现:把状态变更,并把resolve或reject中的值保存起来留给.then使用
  • 要保证实例对象能访问.then,必须将.then挂在构造函数的原型上
  • .then接收两个函数作为参数,我们必须对所传参数进行判断是否为函数,当状态为fulfilled时,onFulfilled函数触发,并将前面resolve中的值传给onFulfilled函数;状态为rejected时同理。
  • 当在promise里放一个异步函数(例:setTimeout)包裹resolve或reject函数时,它会被挂起,那么当执行到.then时,promise的状态仍然是pending,故不能触发.then中的回调函数。我们可以定义两个数组分别存放.then中的两个回调函数,将其分别在resolve和reject函数中调用,这样保证了在resolve和reject函数触发时,.then中的回调函数即能触发。

代码如下:

const PENDING = 'pending'
const FULFILLED = 'fullfilled'
const REJECTED = 'rejected'

function myPromise(fn{
  this.state = PENDING
  this.value = null
  const that = this
  that.resolvedCallbacks = []
  that.rejectedCallbacks = []

  function resolve(val{
    if (that.state == PENDING) {
      that.state = FULFILLED
      that.value = val
      that.resolvedCallbacks.map((cb)=>{
        cb(that.value)
      })
    }
  }
  function reject(val{
    if (that.state == PENDING) {
      that.state = REJECTED
      that.value = val
      that.rejectedCallbacks.map((cb)=>{
        cb(that.value)
      })
    }
  }

  try {
    fn(resolve, reject)
  } catch (error) {
    reject(error)
  }

}

myPromise.prototype.then = function (onFullfilled, onRejected{
  const that = this
  onFullfilled = typeof onFullfilled === 'function' ? onFullfilled : v => v
  onRejected= typeof onRejected === 'function' ? onRejected : r => { throw r }

  if(that.state===PENDING){
    that.resolvedCallbacks.push(onFullfilled)
    that.resolvedCallbacks.push(onRejected)
  }
  if (that.state === FULFILLED) {
    onFullfilled(that.value)
  }
  if (that.state === REJECTED) {
    onRejected(that.value)
  }


//验证   ok ok
let p = new myPromise((resolve, reject) => {
  // reject('fail')
  resolve('ok')
})

p.then((res) => {
  console.log(res,'ok');
}, (err) => {
  console.log(err,'fail');
})

五、手撕防抖,节流

这篇文章中详细记录了实现过程?面试官:什么是防抖和节流?如何实现?应用场景?

六、手撕数组API

1. forEach()

思路:

  • forEach()用于数组的遍历,参数接收一个回调函数,回调函数中接收三个参数,分别代表每一项的值、下标、数组本身。
  • 要保证数组能访问到我们自己手写的API,必须将其挂到数组的原型上

代码实现:

const arr = [
  { name'zt'age18 },
  { name'aa'age19 },
  { name'bb'age18 },
  { name'cc'age21 },
]

//代码实现
Array.prototype.my_forEach = function (callback{
  for (let i = 0; i < this.length; i++) {
    callback(this[i], i, this)
  }
}

//验证
arr.my_forEach((item, index, arr) => {      //111  111
  if (item.age === 18) {
    item.age = 17
    return   
  }
  console.log('111');
})

2. map()

思路:

  • map()也用于数组的遍历,与forEach不同的是,它会返回一个新数组,这个新数组是map接收的回调函数返回值
    代码实现:
const arr = [
  { name'zt'age18 },
  { name'aa'age19 },
  { name'bb'age18 },
  { name'cc'age21 },
]

Array.prototype.my_map=function(callback){
  const res=[]
  for(let i=0;i<this.length;i++){
    res.push(callback(this[i],i,this))
  }
  return res
}

//验证
let newarr=arr.my_map((item,index,arr)=>{
  if(item.age>18){
    return item
  }
})
console.log(newarr);  
//[
 // undefined,
 // { name: 'aa', age: 19 },
 // undefined,
 // { name: 'cc', age: 21 }
//]

3. filter()

思路:

  • filter()用于筛选过滤满足条件的元素,并返回一个新数组

代码实现:

const arr = [
  { name'zt'age18 },
  { name'aa'age19 },
  { name'bb'age18 },
  { name'cc'age21 },
]

Array.prototype.my_filter = function (callback{
  const res = []
  for (let i = 0; i < this.length; i++) {
    callback(this[i], i, this) && res.push(this[i])
  }
  return res
}

//验证
let newarr = arr.my_filter((item, index, arr) => {
  return item.age > 18
})
console.log(newarr);   [ { name'aa'age19 }, { name'cc'age21 } ]

4. reduce()

思路:

  • reduce()用于将数组中所有元素按指定的规则进行归并计算,返回一个最终值
  • reduce()接收两个参数:回调函数、初始值(可选)。
  • 回调函数中接收四个参数:初始值 或 存储上一次回调函数的返回值、每一项的值、下标、数组本身。
  • 若不提供初始值,则从第二项开始,并将第一个值作为第一次执行的返回值

代码实现:

const arr = [
  { name'zt'age18 },
  { name'aa'age19 },
  { name'bb'age18 },
  { name'cc'age21 },
]

Array.prototype.my_reduce = function (callback,...arg{
  let pre,start=0
  if(arg.length){
    pre=arg[0]
  }
  else{
    pre=this[0]
    start=1
  }
  for (let i = start; i < this.length; i++) {
    pre=callback(pre,this[i], i, this)   
  }
  return pre
}

//验证
const sum = arr.my_reduce((pre, current, index, arr) => { 
  return pre+=current.age
},0)  
console.log(sum);  //76

5. fill()

思路:

  • fill()用于填充一个数组的所有元素,它会影响原数组 ,返回值为修改后原数组
  • fill()接收三个参数:填充的值、起始位置(默认为0)、结束位置(默认为this.length-1)。
  • 填充遵循左闭右开的原则
  • 不提供起始位置和结束位置时,默认填充整个数组

代码实现:

Array.prototype.my_fill = function (value,start,end{
  if(!start&&start!==0){
    start=0
  }
  end=end||this.length
  for(let i=start;i<end;i++){
    this[i]=value
  }
  return this
}

//验证
const arr=new Array(7).my_fill('hh',null,3)  //往数组的某个位置开始填充到哪个位置,左闭右开
console.log(arr);   //[ 'hh', 'hh', 'hh', <4 empty items> ]

6. includes()

思路:

  • includes()用于判断数组中是否包含某个元素,返回值为 true 或 false
  • includes()提供第二个参数,支持从指定位置开始查找

代码实现:

const arr = ['a''b''c''d''e']

Array.prototype.my_includes = function (item,start{
  if(start<0){start+=this.length}
  for (let i = start; i < this.length; i++) {
    if(this[i]===item){
      return true
    }
  }
  return false
}

//验证
const flag = arr.my_includes('c',3)  //查找的元素,从哪个下标开始查找
console.log(flag); //false

7. join()

思路:

  • join()用于将数组中的所有元素指定符号连接成一个字符串

代码实现:

const arr = ['a''b''c']

Array.prototype.my_join = function (s = ','{
  let str = ''
  for (let i = 0; i < this.length; i++) {
    str += `${this[i]}${s}`
  }
  return str.slice(0, str.length - 1)
}

//验证
const str = arr.my_join(' ')
console.log(str);  //a b c

8. find()

思路:

  • find()用于返回数组中第一个满足条件元素,找不到返回undefined
  • find()的参数为一个回调函数

代码实现:

const arr = [
  { name'zt'age18 },
  { name'aa'age19 },
  { name'bb'age18 },
  { name'cc'age21 },
]

Array.prototype.my_find = function (callback{
  for (let i = 0; i < this.length; i++) {
    if(callback(this[i], i, this)){
      return this[i]
    }

  }
  return undefined
}

//验证
let j = arr.my_find((item, index, arr) => {  
  return item.age > 19   
})
console.log(j);   //{ name: 'cc', age: 21 }

9. findIndex()

思路:

  • findIndex()用于返回数组中第一个满足条件索引,找不到返回-1
  • findIndex()的参数为一个回调函数

代码实现:

const arr = [
  { name'zt'age18 },
  { name'aa'age19 },
  { name'bb'age18 },
  { name'cc'age21 },
]

Array.prototype.my_findIndex = function (callback{
  for (let i = 0; i < this.length; i++) {
    if(callback(this[i], i, this)){
      return i
    }
  }
  return -1
}


let j = arr.my_findIndex((item, index, arr) => {  
  return item.age > 19
})
console.log(j);  //3

10. some()

思路:

  • some()用来检测数组中的元素是否满足指定条件。
  • 有一个元素符合条件,则返回true,且后面的元素会再检测。

代码实现:

const arr = [
  { name'zt'age18 },
  { name'aa'age19 },
  { name'bb'age18 },
  { name'cc'age21 },
]

Array.prototype.my_some = function (callback{
  for (let i = 0; i < this.length; i++) {
    if(callback(this[i], i, this)){
      return true
    }
  }
  return false
}

//验证
const flag = arr.some((item, index, arr) => {
  return item.age > 20
})
console.log(flag);  //true

11. every()

思路:

  • every() 用来检测所有元素是否都符合指定条件。
  • 有一个不满足条件,则返回false,后面的元素都会再执行。

代码实现:

const arr = [
  { name'zt'age18 },
  { name'aa'age19 },
  { name'bb'age18 },
  { name'cc'age21 },
]

Array.prototype.my_every = function (callback{
  for (let i = 0; i < this.length; i++) {
    if(!callback(this[i], i, this)){
      return false
    }
  }
  return true
}

//验证
const flag = arr.my_every((item, index, arr) => {
  return item.age > 16
})
console.log(flag);  //true

七、数组去重

1. 双层for循环 + splice()

let arr = [11'1''1'22232]
function unique(arr{
    for (let i = 0; i < arr.length; i++) {
        for (let j = i + 1; j < arr.length; j++) {
            if (arr[i] === arr[j]) {
                arr.splice(j, 1)
                j--  //删除后j向前走了一位,下标需要减一,避免少遍历一位
            }
        }
    }
    return arr
 }
 
console.log(unique(arr)) //[ 1, '1', 2, 3 ]

2. 排序后做前后比较

let arr = [11'1''1'22232]

function unique(arr{
  let res = []
  let seen //记录上一次比较的值
  let newarr=[...arr]  //解构出来,开辟一个新数组
  newarr.sort((a,b)=>a-b)  //sort会影响原数组  n*logn
  for (let i = 0; i < newarr.length; i++) {
    if (newarr[i]!==seen) {
      res.push(newarr[i])
    }
    seen=newarr[i]
  }
  return res
}

console.log(unique(arr)) //[ 1, '1', 2, 3 ]

3. 借助include

let arr = [11'1''1'22232]

function unique(arr{
  let res = []
  for (let i = 0; i < arr.length; i++) {
      if(!res.includes(arr[i])){
        res.push(arr[i])
      }
  }
  return res
}

console.log(unique(arr)) //[ 1, '1', 2, 3 ]

4. 借助set

let arr = [11'1''1'22232]
const res1 = Array.from(new Set(arr));
console.log(res1);   //[ 1, '1', 2, 3 ]

八、数组扁平化

1. 递归

let arr1 = [12, [34, [5],6]]

function flatter(arr{
  let len = arr.length
  let result = []
  for (let i = 0; i < len; i++) {  //遍历数组每一项
    if (Array.isArray(arr[i])) {   //判断子项是否为数组并拼接起来
      result=result.concat(flatter(arr[i]))//是则使用递归继续扁平化
    }
    else {
      result.push(arr[i])  //不是则存入result
    }
  }
  return result
}

console.log(flatter(arr1))  //[ 1, 2, 3, 4, 5, 6 ]

2. 借助reduce (本质也是递归)

let arr1 = [12, [34, [5],6]]

const flatter = arr => {
  return arr.reduce((pre, cur) => {
    return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
  }, [])
}
console.log(flatter(arr1))  //[ 1, 2, 3, 4, 5, 6 ]

3. 借助正则

let arr1 = [12, [34, [5],6]]

const res = JSON.parse('[' + JSON.stringify(arr1).replace(/\[|\]/g'') + ']');
console.log(res)  //[ 1, 2, 3, 4, 5, 6 ]

九、函数柯里化

思路:

  • 函数柯里化是只传递给函数一部分参数调用它,让它返回一个函数去处理剩下的参数
  • 传入的参数大于等于原始函数fn的参数个数,则直接执行该函数,小于则继续对当前函数进行柯里化,返回一个接受所有参数(当前参数和剩余参数) 的函数

代码实现:

const my_curry = (fn, ...args) => 
  args.length >= fn.length 
  ? fn(...args) 
  : (...args1) => curry(fn, ...args, ...args1);
   
function adder(x, y, z{
    return x + y + z;
}
const add = my_curry(adder);
console.log(add(123));  //6
console.log(add(1)(2)(3));  //6
console.log(add(12)(3));  //6
console.log(add(1)(23));  //6

十、new方法

思路:

  • new方法主要分为四步:
    (1) 创建一个新对象
    (2) 将构造函数中的this指向该对象
    (3) 执行构造函数中的代码(为这个新对象添加属性
    (4) 返回新对象
function _new(obj, ...rest){
    // 基于obj的原型创建一个新的对象
    const newObj = Object.create(obj.prototype);

    // 添加属性到新创建的newObj上, 并获取obj函数执行的结果.
    const result = obj.apply(newObj, rest);

    // 如果执行结果有返回值并且是一个对象, 返回执行的结果, 否则, 返回新创建的对象
    return typeof result === 'object' ? result : newObj;
}

最后

再给我们的辅导服务打个广告,我们目前有面试全流程辅导、简历指导、模拟面试、零基础辅导和付费咨询等增值服务,大厂前端专家一对一辅导。

辅导服务推出了近 2 年的时间,已助力超过 200 + 的同学找到心仪的工作,感兴趣的伙伴可以联系小助手(微信号:interview-fe)了解详情哦~


原文:https://juejin.cn/post/7253260410664419389