从头开始实现 JavaScript 概念

2025-06-08

从头开始实现 JavaScript 概念

在本文中,我们将从头开始构建几个关键组件,探索 JavaScript 的基本构成要素。在深入探讨这些概念的过程中,我们将运用一系列从基础到复杂的技术,使本次探索对 JavaScript 新手和专业人士都极具价值。

目录

memoize()

任务描述

重新创建该memoize函数(来自“lodash”),通过缓存函数调用结果来优化性能。这确保使用相同参数的重复函数调用通过返回缓存结果而不是重新计算来加快速度。

执行

function customSerializer(entity, cache = new WeakSet()) {
  if (typeof entity !== 'object' || entity === null) {
    return `${typeof entity}:${entity}`;
  }
  if (cache.has(entity)) {
    return 'CircularReference';
  }
  cache.add(entity);

  let objKeys = Object.keys(entity).sort();
  let keyRepresentations = objKeys.map(key =>
    `${customSerializer(key, cache)}:${
      customSerializer(entity[key], cache)
    }`
  );

  if (Array.isArray(entity)) {
    return `Array:[${keyRepresentations.join(',')}]`;
  }

  return `Object:{${keyRepresentations.join(',')}}`;
}


function myMemoize(fn) {
  const cache = new Map();

  return function memoized(...args) {
    const keyRep = args.map(arg => 
      customSerializer(arg)
    ).join('-');
    const key = `${typeof this}:${this}-${keyRep}`;

    if (cache.has(key)) {
      return cache.get(key);
    } else {
      const result = fn.apply(this, args);
      cache.set(key, result);
      return result;
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

实施的关键方面

  1. 缓存机制:它使用Map对象cache来存储函数调用的结果。Map选择该对象是因为其高效的键值配对和检索功能。

  2. 自定义序列化器:该customSerializer函数将函数参数转换为字符串表示形式,用作缓存键。此序列化适用于基本类型、对象(包括嵌套对象)、数组和循环引用。对于对象和数组,它们的键会进行排序,以确保无论属性声明顺序如何,字符串表示形式都保持一致。

  3. 序列化this: 的值this指的是函数所属的对象。在 JavaScript 中,方法的行为会根据调用它们的对象(即调用它们的上下文)而有所不同。这是因为this提供了对上下文对象的属性和方法的访问,并且其值会根据函数的调用方式而变化。

  4. 循环引用:当一个对象通过其属性直接或间接地引用自身时,就会发生循环引用。这种情况可能发生在更复杂的数据结构中,例如, objectA包含对 object 的引用B,而 objectB又直接或间接地引用 object A。处理循环引用对于避免无限循环至关重要。

  5. 使用 进行自动垃圾回收WeakSet:AWeakSet持有对其对象的“弱”引用,这意味着,WeakSet如果没有其他引用,则对象在 中的存在不会阻止其被垃圾回收。此行为在需要临时跟踪对象存在性且不必要地延长其生命周期的情况下尤其有用。由于函数customSerializer可能只需要在序列化过程中标记对象的访问,而无需存储其他数据,因此使用WeakSet可以确保对象不会仅仅因为其在集合中的存在而保持活动状态,从而防止潜在的内存泄漏。

Array.map()

任务描述

重新创建Array.map()以转换函数作为参数的函数。该转换函数将对数组的每个元素执行,并接受三个参数:当前元素、当前元素的索引以及数组本身。

实施的关键方面

  1. 内存预分配new Array(this.length)用于创建预先调整大小的数组,以优化内存分配并通过避免在添加元素时动态调整大小来提高性能。

执行

Array.prototype.myMap = function(fn) {
  const result = new Array(this.length);
  for (let i = 0; i < this.length; i++) {
    result[i] = fn(this[i], i, this);
  }
  return result;
}
Enter fullscreen mode Exit fullscreen mode

Array.filter()

任务描述

重新创建 ,Array.filter()它以谓词函数作为输入,遍历调用它的数组元素,并将谓词应用于每个元素。它返回一个新数组,该数组仅由谓词函数返回的元素组成true

实施的关键方面

  1. 动态内存分配:它动态地将合格元素添加到中filteredArray,使得在少数元素通过谓词函数的情况下该方法的内存效率更高。

执行

Array.prototype.myFilter = function(pred) {
  const filteredArray = [];
  for (let i = 0; i < this.length; i++) {
    if (pred(this[i], i, this)) {
      filteredArray.push(this[i]);
    }
  }
  return filteredArray;
}
Enter fullscreen mode Exit fullscreen mode

Array.reduce()

任务描述

重新创建 ,对数组的每个元素Array.reduce()执行一个reducer函数,最终得到一个输出值。该reducer函数接受四个参数:accumulator、currentValue、currentIndex 和整个数组。

实施的关键方面

  1. initialValuevalueaccumulator和 的startIndex初始化取决于是否initialValue传递了 参数。如果initialValue提供了 (即arguments.length至少为2),则accumulator被设置为 this initialValue,迭代从第 0 个元素开始。否则,如果没有initialValue提供 ,则数组本身的第 0 个元素将用作initialValue

执行

Array.prototype.myReduce = function(callback, initialValue) {
  let accumulator = arguments.length >= 2 
    ? initialValue 
    : this[0];
  let startIndex = arguments.length >= 2 ? 0 : 1;

  for (let i = startIndex; i < this.length; i++) {
    accumulator = callback(accumulator, this[i], i, this);
  }

  return accumulator;
}
Enter fullscreen mode Exit fullscreen mode

bind()

任务描述

重新创建bind()函数,使其允许将对象作为调用原始函数的上下文传递,并传入预先指定的初始参数(如有)。它还应支持使用new运算符,以便在维护正确原型链的同时创建新实例。

执行


Function.prototype.mybind = function(context, ...bindArgs) {
  const self = this;
  const boundFunction = function(...callArgs) {
    const isNewOperatorUsed = new.target !== undefined;
    const thisContext = isNewOperatorUsed ? this : context;
    return self.apply(thisContext, bindArgs.concat(callArgs));
  };

  if (self.prototype) {
    boundFunction.prototype = Object.create(self.prototype);
  }

  return boundFunction;
};
Enter fullscreen mode Exit fullscreen mode

实施的关键方面

  1. 处理new运算符:该语句const isNewOperatorUsed = new.target !== undefined;检查 是否boundFunction通过 运算符被调用为构造函数new。如果new使用了 运算符,thisContext则将 设置为新创建的对象(this),而不是提供的context,从而确认实例化应使用新的上下文,而不是绑定期间提供的上下文。

  2. 原型保存:为了维护原始函数的原型链,mybind有条件地将 的原型设置boundFunction为继承自 的新对象self.prototype。此步骤可确保从 创建的实例boundFunction(用作构造函数时)正确地从原始函数的原型继承属性。此机制保留了预期的继承层次结构并维护了 instanceof 检查。

bind()使用示例new

让我们考虑一个创建代表汽车的对象的简单构造函数:

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
Enter fullscreen mode Exit fullscreen mode

假设我们经常创建Car品牌为“Toyota”的对象。为了提高这个过程的效率,我们可以bind创建一个专门用于丰田汽车的构造函数,并预先填充make参数:

// Creating a specialized Toyota constructor with 'Toyota'
// as the pre-set 'make'
const ToyotaConstructor = Car.bind(null, 'Toyota');

// Now, we can create Toyota car instances
// without specifying 'make'
const myCar = new ToyotaConstructor('Camry', 2020);

// Output: Car { make: 'Toyota', model: 'Camry', year: 2020 }
console.log(myCar);
Enter fullscreen mode Exit fullscreen mode

call()apply()

任务描述

重新创建call()apply()允许使用给定的 this 值和单独提供的参数来调用函数。

执行

Function.prototype.myCall = function(context, ...args) {
  const fnSymbol = Symbol('fnSymbol');
  context[fnSymbol] = this;

  const result = context[fnSymbol](...args);

  delete context[fnSymbol];

  return result;
};

Function.prototype.myApply = function(context, args) {
  const fnSymbol = Symbol('fnSymbol');
  context[fnSymbol] = this;

  const result = context[fnSymbol](...args);

  delete context[fnSymbol];

  return result;
};
Enter fullscreen mode Exit fullscreen mode

实施的关键方面

  1. 属性命名的符号用法:为了防止覆盖上下文对象上潜在的现有属性,或由于名称冲突导致意外行为,我们Symbol使用唯一的符号作为属性名称。这确保了临时属性不会干扰上下文对象的原始属性。

  2. 执行后清理:函数调用执行完成后,添加到上下文对象的临时属性将被删除。此清理步骤至关重要,以避免在上下文对象上留下修改状态。

setInterval()

任务描述

重新创建setIntervalusing setTimeout。该函数应以指定的时间间隔重复调用提供的回调函数。它返回一个函数,调用该函数时会停止该时间间隔。

执行

function mySetInterval(callback, interval) {
  let timerId;

  const repeater = () => {
    callback();
    timerId = setTimeout(repeater, interval);
  };

  repeater();

  return () => {
    clearTimeout(timerId);
  };
}
Enter fullscreen mode Exit fullscreen mode

实施的关键方面

  1. 取消功能:返回的函数mySetInterval提供了一种简单直接的方法来取消正在进行的间隔,而无需在函数范围之外公开或管理计时器 ID。

cloneDeep()

任务描述

重新创建cloneDeep(来自“lodash”)用于对给定输入执行深度复制的函数。该函数应该能够克隆复杂的数据结构,包括对象、数组、映射、集合、日期和正则表达式,并保持每个元素的结构和类型的完整性。

执行

function myCloneDeep(entity, map = new WeakMap()) {
  if (entity === null || typeof entity !== 'object') {
    return entity;
  }

  if (map.has(entity)) {
    return map.get(entity);
  }

  let cloned;
  switch (true) {
    case Array.isArray(entity):
      cloned = [];
      map.set(entity, cloned);
      cloned = entity.map(item => myCloneDeep(item, map));
      break;
    case entity instanceof Date:
      cloned = new Date(entity.getTime());
      break;
    case entity instanceof Map:
      cloned = new Map(Array.from(entity.entries(),
        ([key, val]) => 
        [myCloneDeep(key, map), myCloneDeep(val, map)]));
      break;
    case entity instanceof Set:
      cloned = new Set(Array.from(entity.values(),
        val => myCloneDeep(val, map)));
      break;
    case entity instanceof RegExp:
      cloned = new RegExp(entity.source,
                          entity.flags);
      break;
    default:
      cloned = Object.create(
        Object.getPrototypeOf(entity));
      map.set(entity, cloned);
      for (let key in entity) {
        if (entity.hasOwnProperty(key)) {
          cloned[key] = myCloneDeep(entity[key], map);
        }
      }
  }

  return cloned;
}
Enter fullscreen mode Exit fullscreen mode

实施的关键方面

  1. 循环引用处理:利用 来WeakMap跟踪已访问的对象。如果遇到已克隆的对象,则返回先前克隆的对象,从而有效地处理循环引用并防止堆栈溢出错误。

  2. 特殊对象的处理:区分几种对象类型(Array,,,,),Date确保每种类型都被适当地克隆并保留其特定特征MapSetsRegExp

- **`Array`**: Recursively clones each element, ensuring deep cloning.
- **`Date`**: Copies the date using its numeric value (timestamp).
- **Maps and Sets**: Constructs a new instance, recursively cloning each entry (for `Map`) or value (for `Set`).
- **`RegExp`**: Clones by creating a new instance with the source and flags of the original.
Enter fullscreen mode Exit fullscreen mode
  1. 对象属性的克隆:当输入是普通对象时,它会创建一个与原始对象具有相同原型的对象,然后递归克隆每个自己的属性,确保深度克隆,同时维护原型链。

  2. 效率和性能:利用WeakMap记忆有效地处理具有重复引用和循环的复杂和大型结构,避免冗余克隆,确保最佳性能。

debounce()

任务描述

重新创建该debounce函数(来自“lodash”),该函数允许限制给定回调函数的触发频率。当在短时间内重复调用时,仅会在指定的延迟后执行最后一次调用。

function myDebounce(func, delay) {
  let timerId;
  const debounced = function(...args) {
    clearTimeout(timerId);
    timerId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };

  debounced.cancel = function() {
    clearTimeout(timerId);
    timerId = null;
  };

  debounced.flush = function() {
    clearTimeout(timerId);
    func.apply(this, arguments);
    timerId = null;
  };

  return debounced;
}
Enter fullscreen mode Exit fullscreen mode

实施的关键方面

  1. 取消功能:引入一种.cancel方法,允许外部控制取消任何待执行的去抖动函数。这增加了灵活性,允许根据特定事件或条件取消去抖动函数。

  2. 通过 Flush 立即执行:该.flush方法允许立即执行去抖动函数,忽略延迟。这在需要确保去抖动函数的效果立即生效的场景中非常有用,例如在卸载组件或完成交互之前。

throttle()

任务描述

重新创建该throttle函数(来自“lodash”),以确保给定的回调函数在指定时间间隔内(在本例中是在开始时)最多被调用一次。与防抖不同,节流保证函数以固定的时间间隔执行,从而确保更新的执行,尽管更新速率是可控的。

执行

function myThrottle(func, timeout) {
  let timerId = null;

  const throttled = function(...args) {
    if (timerId === null) {
      func.apply(this, args)
      timerId = setTimeout(() => {
        timerId = null;
      }, timeout)
    }
  }

  throttled.cancel = function() {
    clearTimeout(timerId);
    timerId = null;
  };

  return throttled;
}
Enter fullscreen mode Exit fullscreen mode

实施的关键方面

  1. 取消功能:引入一个.cancel方法,可以清除任何已安排的节流计时器重置。这在清理阶段非常有用,例如在 UI 库/框架中卸载组件,可以防止执行过时并有效地管理资源。

Promise

任务描述

重新创建该类Promise。它是一个专为异步编程设计的结构,允许暂停代码执行,直到异步过程完成。Promise 的核心是代理,该代理的值在创建时不一定已知。它允许您将处理程序与异步操作的最终成功值或失败原因关联起来。这使得异步方法可以像同步方法一样返回值:异步方法不是立即返回最终值,而是返回一个承诺,承诺在未来某个时间点提供该值。它包含Promise处理已完成和已拒绝状态的方法(thencatch),以及无论结果如何都执行代码的方法(finally)。

class MyPromise {
  constructor(executor) {
    ...
  }

  then(onFulfilled, onRejected) {
    ...
  }

  catch(onRejected) {
    ...
  }

  finally(callback) {
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

constructor执行

constructor(executor) {
  this.state = 'pending';
  this.value = undefined;
  this.reason = undefined;
  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 (error) {
    reject(error);
  }
}
Enter fullscreen mode Exit fullscreen mode

constructor实施的关键方面

  1. 状态管理:初始化状态为“待处理”。处理完毕后切换为“已完成”,处理被拒绝后切换为“已拒绝”。
  2. 价值和原因:保存承诺的最终结果(value)或拒绝的原因(reason)。
  3. 处理异步:接受一个executor包含异步操作的函数。它executor接受两个函数,resolve分别是 和reject,调用它们时会将 Promise 转换为相应的状态。
  4. 回调数组:回调队列(onFulfilledCallbacksonRejectedCallbacks)用于在承诺解决或拒绝之前执行延迟操作。

.then执行

resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError(
      'Chaining cycle detected for promise'));
  }
  if (x instanceof MyPromise) {
    x.then(resolve, reject);
  } else {
    resolve(x);
  }
}

then(onFulfilled, onRejected) {
  onFulfilled = typeof onFulfilled === 'function' ? 
    onFulfilled : value => value;

  onRejected = typeof onRejected === 'function' ? 
    onRejected : reason => { throw reason; };

  let promise2 = new MyPromise((resolve, reject) => {
    if (this.state === 'fulfilled') {
      setTimeout(() => {
        try {
          let x = onFulfilled(this.value);
          this.resolvePromise(promise2, x, resolve, reject);
        } catch (error) {
          reject(error);
        }
      });
    } else if (this.state === 'rejected') {
      setTimeout(() => {
        try {
          let x = onRejected(this.reason);
          this.resolvePromise(promise2, x, resolve, reject);
        } catch (error) {
          reject(error);
        }
      });
    } else if (this.state === 'pending') {
      this.onFulfilledCallbacks.push(() => {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value);
            this.resolvePromise(promise2, x, resolve,
              reject);
          } catch (error) {
            reject(error);
          }
        });
      });
      this.onRejectedCallbacks.push(() => {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            this.resolvePromise(promise2, x, resolve,
              reject);
          } catch (error) {
            reject(error);
          }
        });
      });
    }
  });

  return promise2;
}
Enter fullscreen mode Exit fullscreen mode

.then实施的关键方面

  1. 默认处理程序:将非功能处理程序转换为身份函数(用于实现)或抛出器(用于拒绝),以确保承诺链中的正确转发和错误​​处理。

  2. Promises Chaining:该then方法允许将 Promises 链接起来,从而实现顺序异步操作。它会创建一个新的 Promise( ),该 Promise 依赖于传递给它的回调函数( 、 )promise2的结果。onFulfilledonRejected

  3. 处理解决和拒绝:提供的回调仅在当前 Promise 已解决(无论是已完成还是已拒绝)后调用。x每个回调的结果 ( ) 可能是一个值,也可能是另一个 Promise,它决定了 的解决方式promise2

  4. 防止链接循环resolvePromise函数检查是否promise2与结果()相同x,避免承诺等待自身的循环,从而导致TypeError

  5. 支持 MyPromise 和非 Promise 值:如果结果 ( x) 是 的实例MyPromisethen则使用其解析或拒绝来解决promise2。此功能支持基于 Promise 的操作无缝集成,无论是来自 的实例MyPromise还是原生 JavaScript Promise(假设它们具有相似的行为)。对于非 Promise 值,或者当onFulfilledonRejected只是返回一个值时,promise2会使用该值进行解析,从而支持在 Promise 链中进行简单的转换或分支逻辑。

  6. 异步执行保证onFulfilled:通过延迟和onRejected执行setTimeoutthen确保异步行为。这种延迟保持一致的执行顺序,保证onFulfilledonRejected在执行堆栈清空后被调用。

  7. 错误处理onFulfilled:如果或中发生异常onRejectedpromise2则会因错误而被拒绝,从而允许错误处理通过承诺链传播。

catchfinally实施

static resolve(value) {
  if (value instanceof MyPromise) {
    return value;
  }
  return new MyPromise((resolve, reject) => resolve(value));
}

catch(onRejected) {
  return this.then(null, onRejected);
}

finally(callback) {
  return this.then(
    value => MyPromise.resolve(callback())
              .then(() => value),
    reason => MyPromise.resolve(callback())
              .then(() => { throw reason; })
  );
}
Enter fullscreen mode Exit fullscreen mode

实施的关键方面.catch

  1. 简化的错误处理:.catch方法是 的简写.then(null, onRejected),专注于处理拒绝场景。当只需要拒绝处理程序时,它允许更简洁的语法,从而提高代码的可读性和可维护性。
  2. 支持 Promise 链式调用:由于其内部委托给.then.catch因此返回一个新的 Promise,从而保留了 Promise 链式调用的功能。这允许在错误恢复或错误传播后继续进行链式操作,方法是重新抛出错误或返回一个新的被拒绝的 Promise。
  3. 错误传播:如果onRejected提供了 并且执行时没有错误,则返回的 Promise 会使用 的返回值进行解析onRejected,从而有效地在 Promise 链中实现错误恢复。如果onRejected抛出错误或返回被拒绝的 Promise,则错误会沿着 Promise 链向下传递。

实施的关键方面.finally

  1. 始终执行:.finally方法确保callback无论 Promise 是完成还是拒绝,都会执行提供的内容。这对于需要在异步操作之后执行的清理操作特别有用,无论其结果如何。
  2. 返回值保存:虽然callbackin.finally不接收任何参数(与 in.then或不同.catch),但 Promise 的原始完成值或拒绝原因会被保留并在链中传递。 from 返回的 Promise.finally会以相同的值或原因被解决或拒绝,除非callback本身导致 Promise 被拒绝。
  3. 错误处理和传播:如果callback成功执行,则 返回的 Promise.finally将以与原始 Promise 相同的方式进行处理。但是,如果callback抛出错误或返回被拒绝的 Promise,则 返回的 Promise.finally将被拒绝并返回新的错误,从而允许在 Promise 链中拦截错误并更改拒绝原因。

EventEmitter

任务描述

重新创建EventEmitter允许实现观察者模式的类,使对象(称为“发射器”)能够发出命名事件,从而调用先前注册的监听器(或“处理程序”)。这是 Node.js 中处理异步事件的关键组件,广泛用于发送信号和管理应用程序的状态和行为。实现自定义事件监听器EventEmitter需要创建用于注册事件监听器、触发事件和移除监听器的方法。

class MyEventEmitter {
  constructor() {
    this.events = {};
  }

  on(eventName, listener) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(listener);
  }

  once(eventName, listener) {
    const onceWrapper = (...args) => {
      listener.apply(this, args);
      this.off(eventName, onceWrapper);
    };
    this.on(eventName, onceWrapper);
  }

  emit(eventName, ...args) {
    const listeners = this.events[eventName];
    if (listeners && listeners.length) {
      listeners.forEach((listener) => {
        listener.apply(this, args);
      });
    }
  }

  off(eventName, listenerToRemove) {
    if (!this.events[eventName]) {
      return;
    }
    const filterListeners = 
      (listener) => listener !== listenerToRemove;
    this.events[eventName] = 
      this.events[eventName].filter(filterListeners);
  }
}
Enter fullscreen mode Exit fullscreen mode

EventEmitter实施的关键方面

  1. EventListener 注册.on向指定事件的侦听器数组中添加侦听器函数,如果该事件名称的数组尚不存在,则创建一个新数组。

  2. 一次性事件监听器.once注册一个监听器,该监听器调用一次后会自动移除。它将原始监听器包装在一个函数 ( onceWrapper) 中,该函数在执行后也会移除包装器,确保监听器只触发一次。

  3. 发出事件.emit触发事件,并使用提供的参数调用所有已注册的监听器。它将参数应用于每个监听器函数,从而允许将数据传递给监听器。

  4. 移除事件监听器.off从事件的监听器数组中移除特定的监听器。如果移除后该事件没有任何监听器,则可以将其保留为空数组,或者根据需要进行进一步清理(本实现中未显示)。

鏂囩珷鏉ユ簮锛�https://dev.to/antonzo/implementing-javascript-concepts-from-scratch-4623
PREV
用 JavaScript 轻松删除重复元素!😵 GenAI LIVE!| 2025 年 6 月 4 日
NEXT
11-20 TypeScript 项目的自定义实用程序类型