手动实现代码相关知识点

428 阅读9分钟

前端面试题系列文章:

【1】「2022」HTML基础知识点

【2】「2022」ECMAScript基础知识点

【3】「2022」CSS基础知识点

【4】「2022」计算机网络基础知识

【5】「2022」计算机网络-HTTP知识点

【6】「2022」浏览器相关知识点

【7】「2022」React相关知识点

【8】「2022」TypeScript相关知识点

【9】「2022」Webpack相关知识点

【10】「2022」代码输出结果相关知识点

【11】「2022」手动实现代码相关知识点

【12】「2022」性能优化相关知识点

【13】「2022」H5相关知识点

X-Mind源文件

x-mind

JavaScript基础

手写 new 操作符

在调用 new的过程中会发生以下四件事情:

  1. 创建一个新的空对象
  2. 给该空对象设置原型,将对象的原型指向为函数的prototype对象
  3. 让函数的this指向这个新对象,执行构造函数的代码(为这个新对象添加属性)
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象
/**
 * @Desc 手写 New 操作符
*/
const myNew = (Cla) => {
  // step1:创建一个空实例对象
  const newObject = {};
  // step2:将空实例对象的 __proto__ 指向函数的 prototype
  newObject.__proto__ = Cla.prototype;
  // step3: 执行构造函数的代码(为新对象添加属性),同时将函数内部的this替换成新对象
  const result = Cla.call(newObject);
  // step4: 对函数执行结果的类型进行判断:如果是简单类型则直接返回新对象,如果是引用类型就返回这个引用类型对象
  return typeof result === 'object' || typeof result === 'function' ? result : newObject
}

function Car() {
  this.name = 'Lamborghini';
  this.prize = 10000000;
  return { name: 'BMW' }
}

const myCar = myNew(Car);

console.log('myCar', myCar);

手写 Promise

首先,在写Promise之前需要了解几个点:Promise 内部有三个状态:pending(初始态)、resolvedrejected,状态一旦改变为resolvedrejected后则不能修改。

  1. 首先Promise的构造函数是立即执行的,并且接受两个能改变内部状态的回调函数:触发rejected状态的函数、触发resolved状态的回调函数
  2. 在执行成功的回调函数和失败的回调函数之前判断Promise内部的状态是否为初始态pending,如果已经改变过状态,则不响应。
  3. Promise原型上有一个 then 方法,接受两个参数,分别是rejected状态的回调函数,resovled状态的回调函数,他们大概率不是立即执行的,所以需要在Promise内部维护一个数组去存储回调函数,等到状态发生改变再批量执行。
function MyPromise(executor){
  const self = this;
  // 内部维护一个状态,初始状态为 pending;
  this.state = PENDING;
  // 内部维护一个value,用来保存 resolve || reject 时的值
  this.value = "";

  this.onResolvedCallbacks = []; // Promise 可能在2s后状态才发生改变,所以需要将回调函数先保存下来
  this.onRejectedCallbacks = []; // Promise 可能在2s后状态才发生改变,所以需要将回调函数先保存下来

  function resolve(value){
    // step2: resolve、reject调用之前必须保证状态没有被改变过,即pending状态
    if (self.state === PENDING) {
      self.state = RESOLVED;
      self.value = value;
      self.onResolvedCallbacks.forEach(fn => {fn(value)})
    }
  };
  function reject(value){
    // step2: resolve、reject调用之前必须保证状态没有被改变过,即pending状态
    if (self.state === PENDING) {
      self.state = REJECTED;
      self.value = value;
      self.onRejectedCallbacks.forEach(fn => {fn(value)})
    }
  };
  // step1: 首先 executor 是立即执行的,接受 resolve,reject 函数作为参数
  try {
    executor(resolve, reject);
  } catch (e) {
    reject(e)
  }
};

// step3: Promise的实例上有一个.then方法,接受两个参数:1. 成功的回调 2. 失败的回调
MyPromise.prototype.then = function(onResolved, onRejected){
  // step4: 大概率会存在这一的情况:我们的.then在执行的时候,状态还没有改变。 我可能在 setTimeout 2s后才改变状态
  if (this.state === PENDING) {
    this.onResolvedCallbacks.push(() => onResolved(this.value));
    this.onRejectedCallbacks.push(() => onRejected(this.value));
  }
  if (this.state === RESOLVED) {
    onResolved(this.value);
  }
  if (this.state === REJECTED) {
    onRejected(this.value);
  }
}

手写 Promise.then

基于上面写的MyPromise,对.then做一些改造:首先.then返回的是一个新的Promise,支持链式调用;并且.then中拿到的回调必须是上一个Promise的成功回调。

  1. then函数中必须返回一个Promise对象
  2. 在上一个Promise完成后,返回一个结果。如果这个结果是个简单的值,就直接调用新的Promiseresolve,让其状态改变。如果返回的结果是个Promise,则需要等它完成之后再触发新Promiseresolve,保证链式调用的顺序
// Promise 如上

MyPromise.prototype.then = function(onResolved, onRejected){

  const self = this;
  // step1: 为了支持Promise的链式调用,Promise.then 会返回一个新的Promise
  return new MyPromise((resolve, reject) => {

    // 封装前一个Promise成功时执行函数
    const onResolvedFn = () => {
      try {
        // 既然是成功的回调,那么当然先执行 onResolved 方法
        const result = onResolved(self.value); // 承前
        // 重点看这里!!! 
        // step2:上一个 onResolved 的结果如果是 Promise 对象 直接调用Promise.then。递归此过程,直到result不为Promise对象
        return result instanceof MyPromise ? result.then(resolve, reject) : resolve(result) // 启后
      } catch(e) {
        reject(e);
      }
    }
    // 封装前一个Promise失败时的执行函数
    const onRejectedFn = () => {
      try {
        const result = onRejected(self.value);
        return result instanceof MyPromise? result.then(resolve, reject) : reject(result);
      } catch(e) {
        console.log('e', e);
      }
    }

    if (this.state === PENDING) {
      this.onResolvedCallbacks.push(onResolvedFn);
      this.onRejectedCallbacks.push(onRejectedFn);
    }
    if (this.state === RESOLVED) {
      onResolvedFn();
    }
    if (this.state === REJECTED) {
      onRejectedFn();
    }
  })
}

promise1.then(
  (value) => {
    console.log('then1 - value', value);
    return new MyPromise((resolve) => {
      resolve(value);
    })
  },
).then(
  (value) => {
    console.log('then2 - value', value);
    value
  }
);

手写Promise.all

  1. 首先Promise.all是绑定在类上的一个静态方法
  2. Promise.all接收一个数组,或者说是具有Iterator接口的对象作为参数
  3. 这个方法返回一个新的Promise
  4. 参数数组中所有的回调成功才是成功,且返回的的顺序和输入顺序保持一致
  5. 参数数组中有一个失败,则触发失败状态,第一个触发失败状态的 Promise 错误信息作为Promise.all的错误信息
MyPromise.all = function (promiseArr) {
  if (!Array.isArray(promiseArr)) {
    console.error("入参必须是数组");
  }
  return new MyPromise((resolve, reject) => {
    let resultCount = 0; // 已经有结果的Promise
    const resolvedArr = []; // 存放Promise resovled结果
    for (let i = 0; i < promiseArr.length; i++) {
      promiseArr[i].then((res) => {
        resolvedArr.push(res);
        resultCount += 1;
        if (resultCount === promiseArr.length) {
          // 所有成功了才成功
          resolve(resolvedArr);
        }
      }, (error) => {
        // 只要有一个失败了就失败
        reject(error);
      });
    }
  });
};

const p1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    // reject('p1 reject');
    resolve('p1 success');
  }, 1000)
})


const p2 = new MyPromise((resolve) => {
  setTimeout(() => {
    resolve('p2 success');
  }, 2000)
})

MyPromise.all([p1, p2]).then((res) => {
  console.log('promise.all success!', res);
}, (res) => {
  console.log('promise.all fail!', res);
})

手写 Promise.race

类比Promise.all,Promise.race只要有一个Promise的状态变为fullfilled | rejected的时候就执行。

MyPromise.race = function(promiseArr){
  if (!Array.isArray(promiseArr)) {
    console.err('入参必须为数组');
  }
  return new MyPromise((resolve, reject) => {
    try {
      for(let i = 0; i < promiseArr.length; i++) {
        promiseArr[i].then((val) => {
          return resolve(val)
        }, (val) => {
          return reject(val)
        })
      }
    } catch(e) {
      return reject(e);
    } 
  })
}

const promiseArr = [Promise.resolve(1), Promise.resolve(2)]
// const promiseArr = [Promise.reject(1), Promise.resolve(2)]

MyPromise.race(promiseArr).then((resovledCB) => {
  console.log('resovledCB', resovledCB);
}, (rejectedCB) => {
  console.log('rejectedCB', rejectedCB);
});

手写防抖函数

函数防抖是指在时间被触发n秒后再执行回调,如果在n秒内事件又被触发,则重新计时。

  1. 首先,我们要知道防抖函数接收一个函数作为要防抖的函数,wait表示防抖的间隔时间,并且返回一个新的函数
  2. 我们利用计时器来保证在事件在一定时间内只触发一次
  3. 每次点击我们都将之前的计时器清除,重新启动一个新的计时器
function debounce(fn, wait) {
  let timerId = null;
  return function() {
    // 保留上下文
    const self = this;
    const args = arguments;
    // 如果有正在计时的定时器,清除该定时器
    if (timerId) {
      clearTimeout(timerId);
      timerId = null;
    }
    // 重新计时
    timerId = setTimeout(() => {
      fn.apply(self, args);
    }, wait)
  }
}

手写节流函数

节流函数是指在n秒内只会执行一次。

function throttle(fn, wait) {
  let canRun = true;
  // 为了防止防抖函数第一次执行的时候 也需等待 wait 秒
  let immediate = true;
  
  return function () {
    if (!canRun) {
      return;
    }
    isFirst = false
    const self = this;
    const args = arguments;
    canRun = false;
    setTimeout(() => {
      fn.apply(self, args);
      canRun = true;
      immediate = false;
    }, immediate ? 0 : wait);
  };
}

手写 call

在手写 call 函数之前,我们要明确两个点:首先 call 函数里的this指的是需要改变指向的函数,第一个参数为this绑定的对象

  1. 直接在函数的原型上加上MyCall属性,所以在使用之前要判断下类型
// 在写MyCall 之前要特别注意两个点: 1. MyCall 里的this指的是需要改变指向的函数  2. 第一个参数为 this 绑定的对象
Function.prototype.MyCall = function(context) {
  // 调用的对象类型必须是 Function 类型
  if (typeof this !== 'function') {
    console.error('调用的对象类型必须是 Function 类型');
  }
  // 获取传入的参数
  const args = [...arguments].slice(1);
  // 判断 context 是否传入,如果没有则为window
  context = context || window;
  // 将要执行的函数绑定为对象的方法
  context.fn = this;
  const result = context.fn(...args);
  delete context.fn;
  return result
}

function getName() {
  this.name = 'zr111';
}

const obj = {name: 'zr'}
getName.call(obj);
console.log('obj', obj);

手写 apply

手写apply和call其实没什么区别,无非是传参的方式不一样罢了。

Function.prototype.MyApply = function(context) {
  // 确保调用对象为函数
  if (typeof this !== 'function') {
      console.error('type error');
      return;
  }
// 获取参数
const args = arguments[1] || [];
// 判断 context 是否传入,没有默认为window
context = context || window;
// 将调用函数设为对象的方法
context.fn = this;
// 使用对象调用函数,并传参
const result = context.fn(...args);
//  删除 fn 方法,并将执行结果返回
return result;
}

之前一直对call和apply的API有所疑问,而且经常记不清哪个传参是直接展开的。其实call是比较早提出来的,后来为了方便开发者传参,又提供了一个apply的方法,两者在使用上还是有区别的,call通常来说性能更好。

参考链接:JavaScript中,有了apply,为什么还要有call?

手写 bind

bind的传参方式和call相同,不同的是bind内部不会立即执行得到结果,而是返回一个函数,让使用者决定在什么时候调用。

Function.prototype.Mybind = function (context) {
  if (typeof context === "function") {
    console.error("type error");
    return;
  }
  // 绑定 this 对象兜底为 window
  context = context || window;
  // 获取参数
  const args = [...arguments].slice(1);
  // 将 this 函数作为 context 的属性
  context.fn = this;
  const returnFn = () => {
    context.fn(...args);
  };
  delete context.fn;
  return returnFn;
};

函数柯里化

函数柯里化的概念和作用就不再介绍了。柯里化后的函数只有在入参满足原函数个数时才会返回执行结果!

  1. 先判断参数个数是否大于原函数所需参数,如果参数个数足够,直接调用
  2. 如果参数不够,就先将当次调用的参数先保存下来,用户在下次调用的时候和下次的参数合并。
function curry(func,...args) {
  // step1: 先判断参数个数是否大于原函数所需参数
  const argsLen = func.length;
  if (args.length >= argsLen) {
    // return func(...args);
    return func.apply(this, args);
  } else {
    // step2: 如果参数不够,就先将当次调用的参数先保存下来,用户在下次调用的时候和下次的参数合并。
    return function(...nextArgs) {
      return curry.apply(this, [func, ...args, ...nextArgs])
    }
  }
}

function log(date, importance, message) {
  console.log((`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`));
}

const curriedLog =  curry(log);
curriedLog(new Date, "WARNING", 'please check you passport !')
curriedLog(new Date())("DEBUG")("there’s some problem happend from your cache");

手写深拷贝

深拷贝主要是对飞简单类型进行递归处理,但是要注意几个细节:

  1. 正确处理正则、Date类型
  2. 保持原型链
  3. 正确处理key为Symbol类型的字段
  4. 对象内的循环引用
function cloneDeep(target, map = new Map()) {
  if (typeof target === "null") {
    return null;
  }
  if (typeof target !== "object") {
    return target;
  }
  // step1: 正确处理 Date 和 RegExp 类型
  if (typeof target.constructor === Date) {
    return new Date(target);
  }
  if (typeof target.constructor === RegExp) {
    return new RegExp(target);
  }
  if (map.has(target)) return map.get(target);
  map.set(target, newTarget);    
  // step2: 保持原型链
  const newTarget = new target.constructor();
  // step3: 正确处理key为`Symbol`类型的字段
  Reflect.ownKeys(target).forEach((key) => {
    newTarget[key] = cloneDeep(target, map);
  });
  return newTarget;
}

数据处理

场景应用