本篇文章我将解释如何使用一些内置特性来创建一些高级函数,以提高性能并使你的代码看起来更加美观。我将介绍 Debounce、Throttle、Once、Memoize、Curry、Partial、Pipe、Compose、Pick、Omit 和 Zip,您可以将它们保存在 utils 文件/类中,以优化您的代码质量。
尽管这些函数使用 JavaScript 实现,但它们可以很容易地在任何编程语言中实现。一旦理解了这些函数的概念,就可以在任何语言中应用。
此外,本文所描述的函数(或概念)经常在技术面试中被问到。
无论您是初学者还是经验丰富的高级开发人员,这些函数都将优化您的代码和编程体验。它们将令你在使用 JavaScript 时更加愉快和高效。
Debounce
Debounce 函数是一种防止系列事件被快速重复调用的方法。它的机制是推迟函数执行,直到一定时间段过去,事件未被触发,才工作。Debounce 函数是一种有用的解决方案,可以应用于现实世界中,防止用户快速点击按钮并多次执行函数,从而提高性能。
以下代码片段将展示如何在 JavaScript 中实现 debounce 函数:
function debounce(func, delay) {
let timeout;
return function() {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), delay);
};
}
在这个 JavaScript 代码片段中,debounce
函数将返回一个新函数,在先前定义的 delay
后执行原始函数。如果该函数再次被调用,timeout
将被重置,函数的调用将被推迟。
如果您有一个在窗口调整大小时更新网页布局的函数,则此功能将很有用。如果没有 Debounce 函数,当用户调整窗口大小时,此函数将被快速连续调用多次,这可能会导致性能问题。使用 Debounce 函数,可以限制布局更新的频率,使页面更具响应性和效率。
以下代码片段显示了在这种情况下使用 Debounce 函数的方法:
// Define the function that updates the layout
function updateLayout() {
// Update the layout...
}
// Create a debounced version of the function
const debouncedUpdateLayout = debounce(updateLayout, 250);
// Listen for window resize events and call the debounced function
window.addEventListener("resize", debouncedUpdateLayout);
在这个例子中,当窗口调整大小时,updateLayout
函数最多每 250 毫秒被调用一次。此功能确保布局仅在用户完成调整窗口大小后的 250ms 后被更新,使网页更有效和响应。
Throttle
Throttle 函数与 Debounce 函数类似,但行为略有不同。它不是限制函数调用的速率,而是限制函数执行的速率。这意味着如果在上一段给定时间内调用了函数,它将禁止执行。它保证某个函数以一致的速率运行,不会过于频繁地触发。
Throttle 函数的实现如下:
function throttle(func, delay) {
let wait = false;
return (...args) => {
if (wait) {
return;
}
func(...args);
wait = true;
setTimeout(() => {
wait = false;
}, delay);
}
}
在这个代码片段中, throttle
函数会执行提供的 func
函数,将 wait
变量更新为 true
,然后启动一个计时器,在 delay
过后重置 wait
参数。 如果再次调用 throttle
函数,它在 wait
参数仍然为 true
时直接返回,反之调用提供的函数。
如果您要在滚动时更新布局,可以在网页上使用 Throttle 功能。 如果没有 throttle
函数,当用户在页面上滚动时,此更新函数将被多次调用,导致严重的性能问题。 使用 throttle
函数,您可以确保每 X 毫秒只执行一次。 这将导致网页更响应更有效的可用性。
在下面的代码片段中,您可以看到如何使用 throttle
函数:
// Define the function that updates the layout
function updateLayout() {
// Update the layout...
}
// Create a throttled version of the function
const throttledUpdateLayout = throttle(updateLayout, 250);
// Listen for window scroll events and call the throttled function
window.addEventListener("scroll", throttledUpdateLayout);
通过定义 hrottleUpdatedLayout
函数并指定 250 毫秒的延迟,可以确保窗口滚动时,updateLayout
函数最多每 250 毫秒执行一次。如果事件在达到延迟之前被触发,则不会发生任何事情。
Once
Once 函数是一种在已经调用过后将防止再次执行的方法。特别适用于处理仅应该运行一次的事件监听器,而不是在每次都移除事件监听器,您可以在 JavaScript 中使用 Once 函数。
function once(func) {
let ran = false;
let result;
return function() {
if (ran) return result;
result = func.apply(this, arguments);
ran = true;
return result;
};
}
例如,您可以拥有一个向服务器发送请求以加载数据的函数。使用 once()
函数,您可以确保请求不会多次调用,即使用户不断点击按钮。这将避免性能问题。
如果没有 once()
函数,您需要在请求发送后立即删除单击监听器,以防再次发送请求。
在任何代码中应用 once()
函数将如下所示:
// Define the function that sends the request
function requestSomeData() {
// Send the request...
}
// Create a version of the function that can only be called once
const sendRequestOnce = once(sendRequest);
// Listen for clicks on a button and call the "once" function
const button = document.querySelector("button");
button.addEventListener("click", sendRequestOnce);
在这个例子中,即使用户多次点击按钮,requestSomeData
函数也只会被调用一次。
Memoize
Memoize是 一个 JavaScript 函数,用于缓存给定函数的结果,以防止多次使用相同参数调用计算代价高昂的例程。
function memoize(func) {
const cache = new Map();
return function() {
const key = JSON.stringify(arguments);
if (cache.has(key)) {
return cache.get(key);
}
const result = func.apply(this, arguments);
cache.set(key, result);
return result;
};
}
这个 memoize()
函数将缓存给定函数的结果,并使用参数作为键来检索如果在同一参数下再次调用时的结果。
现在,如果您有一个基于输入变量的复杂计算的函数,您可以使用 memoize()
函数来缓存结果并在多次使用相同输入时立即检索它们。
为了看到 memoize()
函数的好处,您可以使用它来计算斐波那契数列:
// Define the function that performs the calculation
function fibonacci(n) {
if (n < 2)
return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// Create a memoized version of the function
const memoizedFibonacci = memoize(fibonacci);
// Call the memoized function with multiple input values
console.time('total')
console.time('sub1')
const result1 = memoizedFibonacci(30);
console.timeEnd('sub1')
console.time('sub2')
const result2 = memoizedFibonacci(29);
console.timeEnd('sub2')
console.time('sub3')
const result3 = memoizedFibonacci(30);
console.timeEnd('sub3')
console.timeEnd('total')
在这个例子中,fibonacci()
函数将被转换成 memoizedFibonacci
函数。然后调用 memoized()
函数,并将执行时间记录到控制台。
输出将如下所示:
尽管第二次调用(sub2)仅计算斐波那契数字 29,看起来时间比第二次计算(sub3)斐波那契数字 30 更长,因为它(sub3)被 memoize()
函数缓存了。
Curry
Curry 函数(也称为 Currying)是一个高级的 JavaScript 函数,用于通过“预填充”一些参数来从现有函数创建一个新函数。当处理带有多个参数的函数时,常常使用 Curry 函数将它们转换为仅需局部参数的函数,因为其他参数将始终保持不变。
使用 Curry 函数有几个好处:
- 它有助于避免多次使用同一个变量
- 它使代码更具可读性
- 它将函数划分为多个较小的函数,符合单一职责
function curry(func, arity = func.length) {
return function curried(...args) {
if (args.length >= arity) return func(...args);
return function(...moreArgs) {
return curried(...args, ...moreArgs);
};
};
}
这个 Curry 函数接收另一个函数 (func
) 和一个可选的 arity
参数,它默认值为 func
参数的长度。它返回一个新函数 (curried
),可以使用 arity
个参数调用。如果调用时未提供所有参数,则返回一个新函数,可以记录后续调用时的参数,直到提供了所有必需参数。当提供了所有参数时,调用原始函数 (func
),并返回其结果。
为了理解 Curry 函数的好处,你可以想象一种计算平面上两点间距离的方法。使用 Curry 函数,您可以创建一个新函数,它仅需要其中一个点,使其更容易使用。
以下片段将显示如何使用之前定义的 curry 函数来优化实现的可读性:
// Define the function that calculates the distance between two points
function distance(x1, y1, x2, y2) {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}
// Create a curried version of the function that only requires one of the points
const distanceFromOrigin = curry(distance, 3)(0, 0);
// Call the curried function with the other point
const d1 = distanceFromOrigin(1, 1);
const d2 = distanceFromOrigin(2, 2);
在这个例子中,通过使用 curry
函数将 distance
作为第一个参数, 3
作为第二个参数(arity
),创建了距离函数的柯里化版本(distanceFromOrigin
)。另外,它将使用 0,0
和前两个参数一起调用柯里化函数。
现在,函数 distanceFromOrigin
是一个新函数,只需要两个参数,并始终使用 0,0
作为第一个点。
Compose
组合函数与管道函数相同,但它将使用 reduceRight
应用所有函数:
function compose(...funcs) {
return function composed(...args) {
return funcs.reduceRight((result, func) => [func.call(this, ...result)], args)[0];
};
}
这将导致相同的功能,但函数是从右到左依次执行的。
Pick
Pick 函数在 JavaScript 中用于从对象中选择特定值。它是通过从提供的项目中选择某些属性来创建一个新对象的方法。它是一种函数编程技术,允许从任何对象中提取属性的子集(如果该属性可用)。
这是 Pick 函数的实现:
function pick(obj, keys) {
return keys.reduce((acc, key) => {
if (obj.hasOwnProperty(key)) {
acc[key] = obj[key];
}
return acc;
}, {});
}
这个函数有两个参数:
obj
:原始对象,新对象将根据它来创建。keys
:要选择传输到新对象中的键数组。
要创建新对象,使用 reduce()
方法来遍历键数组并将它们与原始对象的属性进行比较。如果存在值,它将被添加到 reduce 函数的累加器对象中,该对象已使用 {}
进行初始化。
在 reduce 函数的最后,累加器对象是一个新的对象,仅包含在 keys
数组中的指定属性。
如果您想避免超取数据,此函数非常有用。使用 Pick 函数,您可以从数据库中检索任何对象,然后仅 pick()
所需的属性并将它们返回给调用者。
const obj = {
id: 1,
name: 'Paul',
password: '82ada72easd7',
role: 'admin',
website: 'https://www.paulsblog.dev',
};
const selected = pick(obj, ['name', 'website']);
console.log(selected); // { name: 'Paul', website: 'https://www.paulsblog.dev' }
Zip
Zip 函数,它将每个元素数组与另一个元素数组进行匹配,并用于将多个数组合并为单个二维数组。生成的数组将包含来自每个数组的相应元素。通常,当处理来自多个源的数据并需要以某种方式合并或关联时使用此功能。
与 Python 不同,JavaScript 没有预先提供 Zip 函数。但是,实现很容易。
function zip(...arrays) {
const maxLength = Math.max(...arrays.map(array => array.length));
return Array.from({ length: maxLength }).map((_, i) => {
return Array.from({ length: arrays.length }, (_, j) => arrays[j][i]);
});
}
这个 JavaScript 代码片段将创建一个新的二维数组,其中每个子数组由提供的数组的元素组成。这意味着,原始数组的每个元素和另一个原始数组的相同索引的元素,根据索引合并成若干个子数组。
例如,您可以有三个数组:
- 包含各个点的 x 坐标
- 包含各个点的 y 坐标
- 包含各个点的 z 坐标
如果没有 zip 函数,您将手动循环数组并配对 x、y 和 z 元素。但是,通过使用 zip 函数,您可以传递原始数组并生成新的 (x、y、z) 元组数组。
// Define the arrays that contain the coordinates
const xCoordinates = [1, 2, 3, 4];
const yCoordinates = [5, 6, 7, 8];
const zCoordinates = [3, 6, 1, 7];
// Create a zipped array of points
const points = zip(xCoordinates, yCoordinates, zCoordinates);
// Use the zipped array of points
console.log(points); // [[1, 5, 3], [2, 6, 6], [3, 7, 1], [4, 8, 7]]
在这个例子中,zip
函数用于将 xCoordinates
、yCoordinates
和 zCoordinates
数组组合成单个元组数组。