手写js代码之解析版(持续更新)

112 阅读10分钟

1.promise.all

  • 实现思路

1.由于promise.all只能接受数组,所以需要判断传进来的参数是不是数组

2.如果参数全都是resolve状态,则需要返回这个数组,则需要创建一个变量result存储。如果是需要返回这个数组,为什么不能直接返回参数promises呢?因为all方法返回的是执行了一遍之后的promise对象,而不是参数中还未执行过的promise

3.由于需要计算resolve状态的promise和参数promises是否一致,所以需要创建变量count用于计数

4.前期工作准备完,我们就要开始遍历参数promises了,由于promises里每个元素都是promise对象,因此可以直接调用then方法,如果这个元素resolve的话,count就要+1,并且result也要把这个元素的执行结果存储进去。当count === 参数的长度时,那么就把all返回的promise的状态变成resolve(result)。

5.遍历参数的时候如果遇到reject的话,直接把状态调成reject(item)即可,这里reject的参数只是这个报错的元素

function myPromiseAll(promises){
    return new Promiese((resolve,reject)=>{
        if(Array.isArray(promise)){
        throw new TypeError("promises must be an array")
    }
    let result = []
    let count
    promises.forEach((item,index) => {
        item().then(res=>{
            result[index] = res
            count++
            count == promises.length && reslove(result)
        },rea=>{
            reject(item)
        })
    })
    })
}

2.promise.race

  • 实现思路

1.也是返回一个promise对象,所有的逻辑都写在这个新的promise对象里
2.race是赛跑,传进来的参数哪个跑得快返回的就是哪个,然后开始遍历,这里相当于给每个item都用上了then方法,谁跑得快谁就改变返回的promise对象的状态

function myPromiseRace(promises){
    return new promise((resolve,reject)=>{
        //本身就是看谁跑得快就先执行谁,同步代码的执行顺序就是从上到下,他的执行方式就是按照事件执行顺序定的
        promises.forEach(item => {
            item.then(res=>{
                reslove(res)
            },rea=>{
                reject(rea)
            })
        })
    })
}

3.手写防抖函数

  • 实现思路:函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。

1.首先我们要明白每一个函数的this指向,在下面的注释中已经写清楚

2.为什么在debounce里定义了timer,在其他地方定义不行?
timer是判断这个计时器还是否存在的标识。每次debounce返回的函数是否执行实际代码,要根据上一次的timer的值来执行,在debounce里定义,是为了形成闭包然后缓存timer这个值,让我们知道上一次timer值是否为空

3.clearTimeout(timer)之后,为什么还要timer = null来置空timer值?
因为clearTimeout这是清除了计时器,但是timer是这个计时器的标识符,并没有被删除,需要手动置空

4.let context = this,args = arguments;有什么用,能不能删掉?
可以。但是这里能删掉,因为这里setTimeout使用的是箭头函数,箭头函数的this指向上一层函数作用域,上一层的函数作用域的this指向就是btn,这个情况下可以删除。但如果写成setTimeout(function(){fn.apply(this,arguments)},500)的话,这里的this指向的是window,题目中的这句话就不能删除,因为这是setTimeout内部调用function(){fn.apply(this,arguments)是直接调用,this指向window

5.为什么要用fn.apply(context, args),不能直接fn()?
fn是实际执行函数,this需要指向btn,因为我们业务时需要btn的数据,所以需要强制绑定this

btn.onclick = debounceFn('1''2'const debounceFn = debounce(function(){
        console.log(111)  //这个函数目前在未被调用前,this还不确定,如果直接调用就是指向window,但我们需要显示绑定,不能让其指向window
    },500)

// 函数防抖的实现
function debounce(fn, wait) {
  let timer,context,args
  // console.log(this) 这里的this指向window,因为在调用debounce时,相当于debounce()这样调用,只是括号里存在参数。  
  //这里不是btn调用的,因为在未点击btn时就已经触发debounce事件,点击btn触发的是debounce返回的函数
  return function() {
        context = this,
    //这里的this指向的是btn,因为这是debounce返回的函数,而点击了才可以触发该事件
        args = arguments;

    // 如果此时存在定时器的话,则取消之前的定时器重新记时
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  };
}

其实上面写的那么多,实际的思路很简单,就是如果有定时器,那就清空定时器,然后再让定时器控制执行函数,那么防抖函数的第三个参数就可以实现了。第三个参数是immediate,一旦设置了这个参数,执行的就是另一个逻辑

function debounce(fn,wait,immediate){
    let timer,context,callback,args
    return function(){
        context = this
        args = arguments
        if(timer){clearTimeout(timer)}
        //若immediate的值为true,则一开始就要执行,因此需要把timer取反,然后通过callback来控制函数的执行
        if(immediate){
            callback = !timer
            if(callback){fn.apply(context,args)}
            timer = setTimeout(()=>{
                timer = null
            },wait)
        }else{
            timer = setTimeout(() => {
                fn.apply(context,args)
            }, wait);
        }
    }
}

4.手写节流函数

节流函数是在一定时间内,只触发一次,在同一单位时间内触发,只能有一次生效

function throttle(fn, delay) {
//利用了闭包把开始时间缓存起来
  let curTime = Date.now();

  return function() {
    let context = this,
        args = arguments,
        nowTime = Date.now();

    // 如果两次时间间隔超过了指定时间,则执行函数。
    if (nowTime - curTime >= delay) {
      curTime = Date.now();
      return fn.apply(context, args);
    }
  };
}

5.手写call方法

  • 实现思路:call方法是改变this指向,那么在无法使用call方法改变指向的时候,就可以用this的默认绑定来修改指向

1.call方法是挂载再函数的实例对象上的,所以我们也需要挂载再这上面

2.typeof this是什么意思?
this指向的是实际执行函数,也就是fn,判断这个类型是不是函数,不是就报错,因为call就是修改函数指向,调用的总不能不是函数吧

3.context.fn = this是什么意思
我们已经知道了this指向实际执行函数,而context是一个执行上下文对象,所以 context.fn = this是给context添加一个属性,属性的值就是执行函数,然后在context中调用这个函数,就达成了隐式调用修改this的目的了

4.最后我想聊聊为什么context要写进function的参数中,而其他参数则通过arguments获取
我们知道context是一定要有的,没有就会传进underfined,这有利于我们调用typeof判断。而call的实际参数有多少个我们是不确定的,所以通过arguments获取,再搭配上es6的扩展运算符会方便很多

const fn(){//这是实际执行函数}  
const obj = {}
fn.myCall(obj)

Function.prototype.myCall = function(context){
    if(typeof this !== 'function'){
        throw new Error('type error')
    }
    //如果context没有传入参数,那么就指定为window
    context = context || window
    context.fn = this
    //获得参数
    let args = [...arguments].slice(1)
    let result =  context.fn(...args)
    delete context.fn
    return result
}

6.手写apply方法

  • 实现思路,apply方法和call不同的仅仅是参数需要是一个数组,所以这里的arguments只用看第二个就行了,如果有数组,则通过扩展运算符展开(函数不能传进去数组);没有数组的话直接调用就好了

1.明白了call,apply应该没有什么大问题,聊聊我的收获吧,因为现在声明变量都是用let,let会有块级作用域,所以要在一开始就定义result,而不是在if判断时再定义,这样会导致return的不是我们想要的值

Function.prototype.myApply = function(context) {
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  let result = null;
  // 判断 context 是否存在,如果未传入则为 window
  context = context || window;
  context.fn = this;
  if (arguments[1]) {
    result = context.fn(...arguments[1]);  //不要let result = context.fn(...arguments[1]);不会报错,但是结果不对
  } else {
    result = context.fn();
  }
  // 将属性删除
  delete context.fn;
  return result;
};

7.带有并发限制的promise

题目要求最多只能包含两行的promise,因此就不能使用promise.all和promise.race

思路:

  1. 利用队列先进先出的特性,add操作在队列中插入promise
  2. 判断是否小于执行数量,小于2就不执行
  3. 当一个promise执行完毕的时候,调用他的then方法递归执行runQueue
class Scheduler {
  constructor () {
    this.queue = []
    this.maxCount = 2
    this.runCount = 0
  }
  // promiseCreator执行后返回的是一个Promise
  add(promiseCreator) {
    // 小于等于2,直接执行
    this.queue.push(promiseCreator)
    this.runQueue()
  }

  runQueue () {
    // 队列中还有任务才会被执行
    if (this.queue.length && this.runCount < this.maxCount) {
      // 执行先加入队列的函数
      const promiseCreator = this.queue.shift()
      // 开始执行任务 计数+1    
      this.runCount += 1

      promiseCreator().then(() => {
        // 任务执行完毕,计数-1
        this.runCount -= 1
        this.runQueue()
      })
    }
  }
}

//模仿异步函数
const timeout = time => {
  return new Promise(resolve => {
    setTimeout(resolve, time)
  })
}
  
const scheduler = new Scheduler()
  
const addTask = (time,order) => {
  scheduler.add(() => timeout(time).then(()=>console.log(order)))
}

addTask(1000, '1')
addTask(500, '2')
addTask(300, '3')
addTask(400, '4')

8. 实现js普通数据对象的深度克隆

  1. 判断是数组,遍历每一个元素,遍历第一个的时候通过递归开启深度遍历(回溯算法)
  2. 对象同理
  3. 最后写跳出递归的条件,如果不是数组或者对象,那么直接放回就行
function deepCode(obj){
    if(Array.isArray(obj)){
        const result = []
        // 遍历原始数组,并对每个元素进行递归克隆
        for(let i of obj){
            result.push(deepCode(i))
        }
        return result
    }else if(typeof obj === 'object' && obj !== null){
        let result = {}
        // 遍历原始对象的属性,并对每个属性进行递归克隆
        for(let key in obj){
            result[key] = deepCode(obj[key])
        }
        return result
    }
    else{
        // 如果是基本数据类型或者函数等,直接返回原始值
        return obj
    }
}

const originalArray = [1, 2, { a: 3, b: [4, 5] }]
const cloneArr = deepCode(originalArray)
console.log(cloneArr);

9. var array = [{name:'张三',id:'1'},{name:'李四',id:'2'},{name:'张六',id:'1'}]如果array里的id相同,就去重

  1. 定义一个对象,遍历array,让id作为key,将item作为value存进去
  2. 通过Object.values()获取对象的value组成的数组
var array = [
    { name: '张三', id: '1' },
    { name: '李四', id: '2' },
    { name: '张六', id: '1' }
  ];
  
function remove(obj){
    const result = {}

    obj.forEach(item => {
        result[item['id']] = item
    })

    return Object.values(result)
}
console.log(remove(array));

10.请给string对象定义一个repeatify方法,该方法接受一个整数参数,作为字符串重复次数,最后返回重复指定次数的字符串。例如repeatify(3,'abc'),输出为abcabcabc

String.prototype.repeatify = function(count){
   if(count <= 0){
       return ''
   }else{
       let result = ''
       for(let i = 0;i < count;i++){
           result += this //使用 this 引用当前字符串
       }
       return result
   }
}
console.log('abc'.repeatify(4));

11. 如何获取url中的参数

function getUrlParams() {
    var myHref = window.location.href;
    var args = myHref.split('?');
    if(args[0] == myHref) {
        return '';
    }
    var arr = args[1].split('&');
    var obj = {};
    for(var i = 0; i < arr.length; i++) {
        var arg = arr[i].split('=')
        obj[arg[0]] = arg[1];
    }
    return obj;

12. 实现一个可以设置过期时间的localStorage

  1. 在存入localStorage的时候,将当前时间戳+过期时间戳也存进去
  2. 去除localStorage的时候,判断一下当前时间戳是否在存的时间戳范围内
const customLocalStorage = {
    setItem:function(key,value,time){
        let expiredTime = time * 60 * 1000
        const record = {
            key:value,
            expiredTime :new Data().getTime() + expiredTime
        }
        localStorage.setItem(key,JSON.stringify(record))
    },
    getItem:function(key){
        let value = JSON.parse(localStorage.getItem(key))
        if(new Data().getTime() <= value.expiredTime){
            return value[key]
        }
        return null
    }
}

// 存储一个名为 "myKey" 的值,并设置过期时间为 10 分钟
customLocalStorage.setItem('myKey', 'myValue', 10);

// 获取存储的值
const retrievedValue = customLocalStorage.getItem('myKey');
console.log(retrievedValue); // 输出存储的值

13. 封装一个判断两个对象是否相等的函数

  1. 如果对象的一个value值也是对象的话,那么就要递归调用这个函数,因此要按照递归的想法来想这道题
  2. 递归首先要写出跳出递归的条件,当obj1或者obj2不是Object类型时,就直接return obj1 === obj2
  3. 然后正常写单层逻辑,获取obj1的所有key值组成的数组,如果长度不相同则直接return false
  4. 然后一旦在obj2中找不到key值,或者开始递归的时候返回了false,就直接返回false
  5. 最后一行的return true的作用是,如果递归结束了都没有执行到return false,此时就没有返回值,因此需要加上一个return true
function deepEqual(obj1,obj2){
    if(typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 === null || obj2 === null){
        return obj1 === obj2
    }

    const key1 = Object.keys(obj1)
    const key2 = Object.keys(obj2)

    if(key1.length !== key2.length){
        return false
    }

    for(let key of key1){
        if(!obj2.hasOwnProperty(key) || !deepEqual(obj1[key],obj2[key])){
            return false
        }
    }

    return true
}

console.log(deepEqual({a:1},{a:1}));