JS手写,既是应对面试的利器,也是自身基础的体现(手写JS内置函数篇)

153 阅读7分钟

为什么要手写代码

可能我们最常见的需要手写代码的地方就是面试了,往往很多时候,聊着聊着面试官突然就说让你实现某某方法。经常见到很多人抱怨,面试造火箭,真的很反感八股文。可是没有办法,面试官需要在短短一个小时或者两小时内判断你的水平,你总得展示点什么。最简单的判断方式之一就是看你实现一些方法,包括但不仅限于手写场景题或简单算法题,一些常用的工具方法,以及一些JS内置库函数。

除此之外,手写一些常见的方法能提升自己的代码掌控能力,测试自己JS基础是否扎实,理解底层的一些实现也更有利于自己写出高质量,少bug的代码,就比如知道forEach的实现原理,就不会在里面写return,更不会想用个变量去接收它的返回值。

我整理了一下自己面试中遇到,以及看各厂面经中出现高频的一些手写题,分为两篇:手写JS内置库函数手写常用工具类函数,此为第一篇,以下是一些常见JS内置库函数的简单实现

手写new操作符

  • 用法:创建一个实例化对象
  • 思路:
  1. 判断传入的 constructor 是否为 function
  2. 创建一个空对象
  3. 将这个空对象的原型指向构造函数的 prototype 属性。
  4. 执行构造函数 并 获取函数的返回值
  5. 判断这个返回值 如果返回的是 Object || Function 类型 就返回该返回值 否则返回创建的对象
/**
 * @param {Function} constructor 构造函数
 * @return {*}
 */
 function myNew(constructor, ...args){
  if(typeof constructor !== 'function'){
    throw Error('constructor is not a function')
  }
  const obj = {}
  obj.__proto__ = constructor.prototype;
  const result = constructor.apply(obj, args);
  return (result && typeof result === 'object') ? result : obj
}

// 使用: const person = myNew(Person, 'Tom', 20);

手写instanceof操作符

  • 用法:instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。
  • 思路:
  1. 通过 Object.getPrototypeOf 获取 obj 的原型
  2. 循环判断 objProtoType 是否和 constructor 的原型相等
  • 如果相等就返回 true
  • 如果不相等 就重新赋值一下 obj 的原型 进入下一次循环
  1. 判断是 objProtoType 是否为空 如果为空就说明不存在 返回 false
/* 
 * @param {Object} left 需要判断的数据
 * @param {Object} right constructor
 * @return {Boolean} 
 */
 function myInstanceof(left, right){
  let proto = Object.getPrototypeOf(left); // 或 left.__proto__ 
  const prototype = right.prototype;
  while(proto){
    if(proto === prototype){
      return true
    }
    proto = proto.__proto__ // 或Object.getPrototypeOf(proto)
  }
  return false
}

手写call函数

call方法在JavaScript中是函数对象的一个方法,它允许你调用一个函数,同时设置函数体内的this指向第一个参数,其余参数将被传递给函数作为它的参数。 思路:

  1. 确定传入的上下文对象
  2. 给上下文对象添加一个属性
  3. 执行函数
  4. 清理并返回结果
Function.prototype.myCall = function (context) {
  context = context || window;
  context.fn = this;  // this指向调用者,如obj.myCall(...data),则this === obj
  const params = [...arguments].splice(1); // 截取下标1之后的参数,因为arguments[0]为传进来的作用域对象
  const result = context.fn(...params); // 执行函数
  return result
}

手写forEach函数

forEach函数的核心原理是遍历数组的所有元素。它按照数组索引的升序依次对数组内的每个元素执行一次提供的函数。

  • 无返回值
  • 不会中断遍历,即使碰到return语句
Array.prototype.myForEach = function (callback) {
  if (!callback || typeof callback !== 'function') {
    throw Error('callback must be a function ')
  }
  for (let i = 0; i < this.length; i++) {
    callback(this[i], i, this) //this指向调用该方法的数组
  }
}

手写map函数

map函数的核心原理是创建一个新数组,这个新数组的元素是通过调用提供的函数对原数组的每个元素进行处理后的结果。和forEach的区别就是多了一个返回值。map方法不会改变原数组,它返回一个新数组

Array.prototype.myMap = function (callback) {
  if (!callback || typeof callback !== 'function') {
    throw Error('callback must be a function ')
  }
  const result = []
  for (let i = 0; i < this.length; i++) {
    result.push(callback(this[i], i, this))
  }
  return result
}

手写filter函数

filter函数的基本原理是根据提供的测试条件(一个函数)来检查数组中的每个元素,然后返回一个包含所有通过测试的元素的新数组。对于数组中的每个元素,filter方法都会执行一次回调函数。如果回调函数对某个元素返回true,则该元素会被包含在新数组中;如果返回false,则不会。filter方法不会改变原数组,它返回一个新数组

Array.prototype.filter = funtion(callback){
  if (!callback || typeof callback !== 'function') {
    throw Error('callback must be a function ')
  }
  const result = [];
  for (let i = 0; i < this.length; i++) {
    if (cb(this[i])) {
      result.push(this[i])
    }
  }
  return result
}

手写reduce函数

reduce函数的主要原理是对数组中的每个元素应用一个提供的reducer函数(累积器),将数组中的多个值合并成一个单一的结果。对数组中的每个元素(从左到右)执行一次reducer函数,将其结果汇总为单个返回值

  • 思路:
  1. 确定初始值:如果提供了初始值,则使用它;如果没有,使用数组的第一个元素作为初始值。
  2. 迭代数组元素:从数组的第一个元素(或第二个,如果第一个被用作初始值)开始,对每个元素调用reducer函数。
  3. 累积结果:reducer函数的返回值将被作为下一次调用的累积值。
  4. 完成迭代:当数组中的每个元素都被处理过后,返回最终的累积值
Array.prototype.reduce = function (cb, initialValue) {
  let accumulator = initialValue;
  for (let i = 0; i < this.length; i++) {
    accumulator = cb(accumulator, this[i], i, this)
  }
  return accumulator
}

手写promise

这只是一个简化版的Promise实现,包含了Promise的基本结构和核心功能,即then方法和状态转换。该简化版Promise并不包含错误处理和链式调用的完整实现,不过可以展示Promise的基础工作原理。

  • 思路:
  1. 构造函数constructor接收一个执行器函数executor,并初始化状态(state)、值(value)、原因(reason)以及两个回调数组(onFulfilledCallbacksonRejectedCallbacks
  2. 定义resolve和reject函数。 resolvereject是在executor函数内部使用的两个函数,分别用于解决和拒绝Promise
  3. constructor中调用executor函数,并传递resolvereject函数作为参数。
  4. 实现then方法。then方法是Promise的核心,它用于指定解决(fulfilled)和拒绝(rejected)状态下的回调函数。
  5. 处理状态变化。在resolvereject函数中,根据Promise的状态执行相应的回调。
  6. 处理异步回调。在then方法中,如果状态为pending,将回调函数放入对应的数组中。
  7. 执行回调。当Promise解决或拒绝时,执行存储在回调数组中的函数。
class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = null;
    this.reason = null;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
    const resolve = value => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.onFulfilledCallbacks.forEach(fn => fn())
      }
    }
    const reject = reason => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn())
      }
    }
    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }
  function then(onFulfilled, onRejected) {
    if (this.state === 'fulfilled') {
      onFulfilled(this.value);
    }
    if (this.state === 'rejected') {
      onRejected(this.reason);
    }
    if (this.state === 'pending') {
      this.onFulfilledCallbacks.push(() => {
        onFulfilled(this.value)
      })
      this.onRejectedCallbacks.push(() => {
        onRejected(this.reason)
      })
    }
  }
}

// TEST
let promise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('Hello, world!');
  }, 1000);
});

promise.then(value => {
  console.log(value);
}, reason => {
  console.error(reason);
});

手写promise.all函数

Promise.all是一个静态方法,它接收一个Promise实例的数组作为输入,并返回一个新的Promise实例。这个新的Promise在所有输入的Promise都成功解决(resolved)后解决,或者任何一个输入的Promise被拒绝(rejected)后立即拒绝。

Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    let doneCounter = 0;
    const result = [], len = promises.length;
    if(len === 0){
      resolve([])
    }
    for (let i = 0; i < len; i++) {
      Promise.resolve(promises[i]).then(val => {
        doneCounter++;
        result[i] = val;
        if (doneCounter === len) {
          resolve(result)
        }
      }, err => {
        reject(err)
      })
    }
  })
}

手写Symbol.iterator属性

Symbol.iterator属性是一个内置的 Symbol 值,它是默认的迭代器工厂函数。当一个对象需要被迭代时(比如在一个for...of循环中),它的 @@iterator 方法被调用并且返回一个迭代器。这是一个无参数函数,返回一个符合迭代器协议的对象。

const obj = {
  name: 'aa',
  age = 18
}
obj[Symbol.iterator] = function () {
  return {
    next: function () {
      // 此处不能使用Object.keys(obj),获取不到symbol为键的属性
      const objArr = Reflect.ownKeys(obj);
      if (this.index < objArr.length - 1) {
        const key = objArr[this.index];
        this.index++;
        return { //迭代器每次迭代返回格式为 {value:'', done: boolen}
          value: obj[key],
          done: false
        }
      } else {
        return { done: true }
      }
    },
    index: 0
  }
}
console.log(...obj)