从头开始实现 JavaScript 概念
在本文中,我们将从头开始构建几个关键组件,探索 JavaScript 的基本构成要素。在深入探讨这些概念的过程中,我们将运用一系列从基础到复杂的技术,使本次探索对 JavaScript 新手和专业人士都极具价值。
目录
memoize()
Array.map()
Array.filter()
Array.reduce()
bind()
call()
,apply()
setInterval()
cloneDeep()
debounce()
throttle()
Promise
EventEmitter
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;
}
};
}
实施的关键方面
-
缓存机制:它使用
Map
对象cache
来存储函数调用的结果。Map
选择该对象是因为其高效的键值配对和检索功能。 -
自定义序列化器:该
customSerializer
函数将函数参数转换为字符串表示形式,用作缓存键。此序列化适用于基本类型、对象(包括嵌套对象)、数组和循环引用。对于对象和数组,它们的键会进行排序,以确保无论属性声明顺序如何,字符串表示形式都保持一致。 -
序列化
this
: 的值this
指的是函数所属的对象。在 JavaScript 中,方法的行为会根据调用它们的对象(即调用它们的上下文)而有所不同。这是因为this
提供了对上下文对象的属性和方法的访问,并且其值会根据函数的调用方式而变化。 -
循环引用:当一个对象通过其属性直接或间接地引用自身时,就会发生循环引用。这种情况可能发生在更复杂的数据结构中,例如, object
A
包含对 object 的引用B
,而 objectB
又直接或间接地引用 objectA
。处理循环引用对于避免无限循环至关重要。 -
使用 进行自动垃圾回收
WeakSet
:AWeakSet
持有对其对象的“弱”引用,这意味着,WeakSet
如果没有其他引用,则对象在 中的存在不会阻止其被垃圾回收。此行为在需要临时跟踪对象存在性且不必要地延长其生命周期的情况下尤其有用。由于函数customSerializer
可能只需要在序列化过程中标记对象的访问,而无需存储其他数据,因此使用WeakSet
可以确保对象不会仅仅因为其在集合中的存在而保持活动状态,从而防止潜在的内存泄漏。
Array.map()
任务描述
重新创建Array.map()
以转换函数作为参数的函数。该转换函数将对数组的每个元素执行,并接受三个参数:当前元素、当前元素的索引以及数组本身。
实施的关键方面
- 内存预分配:
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;
}
Array.filter()
任务描述
重新创建 ,Array.filter()
它以谓词函数作为输入,遍历调用它的数组元素,并将谓词应用于每个元素。它返回一个新数组,该数组仅由谓词函数返回的元素组成true
。
实施的关键方面
- 动态内存分配:它动态地将合格元素添加到中
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;
}
Array.reduce()
任务描述
重新创建 ,对数组的每个元素Array.reduce()
执行一个reducer
函数,最终得到一个输出值。该reducer
函数接受四个参数:accumulator、currentValue、currentIndex 和整个数组。
实施的关键方面
initialValue
value:accumulator
和 的startIndex
初始化取决于是否initialValue
传递了 参数。如果initialValue
提供了 (即arguments.length
至少为2
),则accumulator
被设置为 thisinitialValue
,迭代从第 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;
}
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;
};
实施的关键方面
-
处理
new
运算符:该语句const isNewOperatorUsed = new.target !== undefined;
检查 是否boundFunction
通过 运算符被调用为构造函数new
。如果new
使用了 运算符,thisContext
则将 设置为新创建的对象(this
),而不是提供的context
,从而确认实例化应使用新的上下文,而不是绑定期间提供的上下文。 -
原型保存:为了维护原始函数的原型链,
mybind
有条件地将 的原型设置boundFunction
为继承自 的新对象self.prototype
。此步骤可确保从 创建的实例boundFunction
(用作构造函数时)正确地从原始函数的原型继承属性。此机制保留了预期的继承层次结构并维护了 instanceof 检查。
bind()
使用示例new
让我们考虑一个创建代表汽车的对象的简单构造函数:
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
假设我们经常创建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);
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;
};
实施的关键方面
-
属性命名的符号用法:为了防止覆盖上下文对象上潜在的现有属性,或由于名称冲突导致意外行为,我们
Symbol
使用唯一的符号作为属性名称。这确保了临时属性不会干扰上下文对象的原始属性。 -
执行后清理:函数调用执行完成后,添加到上下文对象的临时属性将被删除。此清理步骤至关重要,以避免在上下文对象上留下修改状态。
setInterval()
任务描述
重新创建setInterval
using setTimeout
。该函数应以指定的时间间隔重复调用提供的回调函数。它返回一个函数,调用该函数时会停止该时间间隔。
执行
function mySetInterval(callback, interval) {
let timerId;
const repeater = () => {
callback();
timerId = setTimeout(repeater, interval);
};
repeater();
return () => {
clearTimeout(timerId);
};
}
实施的关键方面
- 取消功能:返回的函数
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;
}
实施的关键方面
-
循环引用处理:利用 来
WeakMap
跟踪已访问的对象。如果遇到已克隆的对象,则返回先前克隆的对象,从而有效地处理循环引用并防止堆栈溢出错误。 -
特殊对象的处理:区分几种对象类型(
Array
,,,,),Date
以确保每种类型都被适当地克隆并保留其特定特征Map
。Sets
RegExp
- **`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.
-
对象属性的克隆:当输入是普通对象时,它会创建一个与原始对象具有相同原型的对象,然后递归克隆每个自己的属性,确保深度克隆,同时维护原型链。
-
效率和性能:利用
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;
}
实施的关键方面
-
取消功能:引入一种
.cancel
方法,允许外部控制取消任何待执行的去抖动函数。这增加了灵活性,允许根据特定事件或条件取消去抖动函数。 -
通过 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;
}
实施的关键方面
- 取消功能:引入一个
.cancel
方法,可以清除任何已安排的节流计时器重置。这在清理阶段非常有用,例如在 UI 库/框架中卸载组件,可以防止执行过时并有效地管理资源。
Promise
任务描述
重新创建该类Promise
。它是一个专为异步编程设计的结构,允许暂停代码执行,直到异步过程完成。Promise 的核心是代理,该代理的值在创建时不一定已知。它允许您将处理程序与异步操作的最终成功值或失败原因关联起来。这使得异步方法可以像同步方法一样返回值:异步方法不是立即返回最终值,而是返回一个承诺,承诺在未来某个时间点提供该值。它包含Promise
处理已完成和已拒绝状态的方法(then
,catch
),以及无论结果如何都执行代码的方法(finally
)。
class MyPromise {
constructor(executor) {
...
}
then(onFulfilled, onRejected) {
...
}
catch(onRejected) {
...
}
finally(callback) {
...
}
}
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);
}
}
constructor
实施的关键方面
- 状态管理:初始化状态为“待处理”。处理完毕后切换为“已完成”,处理被拒绝后切换为“已拒绝”。
- 价值和原因:保存承诺的最终结果(
value
)或拒绝的原因(reason
)。 - 处理异步:接受一个
executor
包含异步操作的函数。它executor
接受两个函数,resolve
分别是 和reject
,调用它们时会将 Promise 转换为相应的状态。 - 回调数组:回调队列(
onFulfilledCallbacks
,onRejectedCallbacks
)用于在承诺解决或拒绝之前执行延迟操作。
.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;
}
.then
实施的关键方面
-
默认处理程序:将非功能处理程序转换为身份函数(用于实现)或抛出器(用于拒绝),以确保承诺链中的正确转发和错误处理。
-
Promises Chaining:该
then
方法允许将 Promises 链接起来,从而实现顺序异步操作。它会创建一个新的 Promise( ),该 Promise 依赖于传递给它的回调函数( 、 )promise2
的结果。onFulfilled
onRejected
-
处理解决和拒绝:提供的回调仅在当前 Promise 已解决(无论是已完成还是已拒绝)后调用。
x
每个回调的结果 ( ) 可能是一个值,也可能是另一个 Promise,它决定了 的解决方式promise2
。 -
防止链接循环:
resolvePromise
函数检查是否promise2
与结果()相同x
,避免承诺等待自身的循环,从而导致TypeError
。 -
支持 MyPromise 和非 Promise 值:如果结果 (
x
) 是 的实例MyPromise
,then
则使用其解析或拒绝来解决promise2
。此功能支持基于 Promise 的操作无缝集成,无论是来自 的实例MyPromise
还是原生 JavaScript Promise(假设它们具有相似的行为)。对于非 Promise 值,或者当onFulfilled
或onRejected
只是返回一个值时,promise2
会使用该值进行解析,从而支持在 Promise 链中进行简单的转换或分支逻辑。 -
异步执行保证
onFulfilled
:通过延迟和onRejected
的执行setTimeout
,then
确保异步行为。这种延迟保持一致的执行顺序,保证onFulfilled
和onRejected
在执行堆栈清空后被调用。 -
错误处理
onFulfilled
:如果或中发生异常onRejected
,promise2
则会因错误而被拒绝,从而允许错误处理通过承诺链传播。
catch
和finally
实施
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; })
);
}
实施的关键方面.catch
:
- 简化的错误处理:该
.catch
方法是 的简写.then(null, onRejected)
,专注于处理拒绝场景。当只需要拒绝处理程序时,它允许更简洁的语法,从而提高代码的可读性和可维护性。 - 支持 Promise 链式调用:由于其内部委托给
.then
,.catch
因此返回一个新的 Promise,从而保留了 Promise 链式调用的功能。这允许在错误恢复或错误传播后继续进行链式操作,方法是重新抛出错误或返回一个新的被拒绝的 Promise。 - 错误传播:如果
onRejected
提供了 并且执行时没有错误,则返回的 Promise 会使用 的返回值进行解析onRejected
,从而有效地在 Promise 链中实现错误恢复。如果onRejected
抛出错误或返回被拒绝的 Promise,则错误会沿着 Promise 链向下传递。
实施的关键方面.finally
:
- 始终执行:该
.finally
方法确保callback
无论 Promise 是完成还是拒绝,都会执行提供的内容。这对于需要在异步操作之后执行的清理操作特别有用,无论其结果如何。 - 返回值保存:虽然
callback
in.finally
不接收任何参数(与 in.then
或不同.catch
),但 Promise 的原始完成值或拒绝原因会被保留并在链中传递。 from 返回的 Promise.finally
会以相同的值或原因被解决或拒绝,除非callback
本身导致 Promise 被拒绝。 - 错误处理和传播:如果
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);
}
}
EventEmitter
实施的关键方面
-
EventListener 注册
.on
:向指定事件的侦听器数组中添加侦听器函数,如果该事件名称的数组尚不存在,则创建一个新数组。 -
一次性事件监听器
.once
:注册一个监听器,该监听器调用一次后会自动移除。它将原始监听器包装在一个函数 (onceWrapper
) 中,该函数在执行后也会移除包装器,确保监听器只触发一次。 -
发出事件
.emit
:触发事件,并使用提供的参数调用所有已注册的监听器。它将参数应用于每个监听器函数,从而允许将数据传递给监听器。 -
移除事件监听器
.off
:从事件的监听器数组中移除特定的监听器。如果移除后该事件没有任何监听器,则可以将其保留为空数组,或者根据需要进行进一步清理(本实现中未显示)。