• 原文发布于:2023年1月26日

  • 原文作者:Paul Knulst

  • 通过使用包括 Debounce、Once 和 Memoize 的函数,以及 Pipe、Pick 和 Zip,来提高代码质量!

介绍

  • JavaScript 是一种强大而多功能的编程语言,具有许多内置特性,可以帮助您编写更高效、可维护和易读的代码。

  • 在这篇文章中,我将解释如何使用一些内置特性来创建一些高级函数,以提高性能并使你的代码看起来更加美观。我将介绍 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 作为第一个点。

Partial

Partial 函数在 JavaScript 中类似于 Curry 函数。Curry Partial 的显著区别在于,调用 Partial 函数会立即返回结果,而不是在 Curry 链中返回另一个函数。

function partial(func, ...args) {
  return function partiallyApplied(...moreArgs) {
    return func(...args, ...moreArgs);
  }
}

这个 JavaScript 中的 partial 函数通常需要一个现有的函数、一个或多个输入参数,并返回一个新函数,新函数在调用时以定义时传入的参数和新参数合并在一起调用原始函数。

在下面的使用情况中,将使用前两个参数预先填充 calculate 函数以生成一个具有更可读名称的新函数:

// Define the function that calculates something
function calculate(x, y, z) {
    return (x + y) * z
}

// Create a partially applied version of the function the last argument
const multiply10By = partial(calculate, 8, 2);

// Call the partially applied function with the number of iterations
const result = multiply10By(5);

在这个例子中,通过部分应用通用计算函数并预先填写前两个参数(8 和 2),创建了 multiply10By 函数。 这将创建一个仅需要一个参数的新函数,该参数指定必须对 10 进行的乘法的数量。 此外,它将使代码更易读和理解。

Pipe

管道函数是一种实用函数,用于链接多个函数并将一个函数的输出传递给下一个函数。它类似于Unix 管道操作,通过使用 reduce() ,所有传入的函数将从左到右依次执行:

function pipe(...funcs) {
  return function piped(...args) {
    return funcs.reduce((result, func) => [func.call(this, ...result)], args)[0];
  };
}

理解管道函数,想象你有三个函数:

  • 向字符串添加前缀

  • 向字符串添加后缀

  • 将字符串转换为大写

  • 然后,您可以使用管道函数创建一个新函数,该函数从左到右依次执行,对字符串应用变更。

// Define the functions that add to the string
function addPrefix(str) {
  return "prefix-" + str;
}

function addSuffix(str) {
  return str + "-suffix";
}

function toUppercase(str) {
    return str.toUpperCase()
}

// Create a piped function that applies the three functions in the correct order
const decorated1 = pipe(addPrefix, addSuffix, toUppercase);
const decorated2 = pipe(toUppercase, addPrefix, addSuffix);

// Call the piped function with the input string
const result1 = decorated1("hello");      // PREFIX-HELLO-SUFFIX
const result2 = decorated2("hello");      // prefix-HELLO-suffix

在这个例子中,通过以不同顺序管道执行 addPrefixaddSuffixtoUppercase 函数创建了 decorated1decorated2 函数。创建的新函数可以输入字符串进行调用,以便按给定的顺序应用三个原始函数。由于 pipe 函数提供的顺序不同,因此生成的输出字符串也将不同。

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' }

这个函数将使用 pick() 函数创建一个仅包含 name 和 website 的新对象,而不会暴露 role、password 或 id 给调用者。

Omit

Omit 函数是 Pick 函数的相反,它会从现有对象中删除某些属性。这意味着您可以通过隐藏属性来避免超取数据。如果要隐藏的属性数量小于要选择的属性数量,则可以使用它替换 Pick 函数。

function omit(obj, keys) {
  return Object.keys(obj)
    .filter(key => !keys.includes(key))
    .reduce((acc, key) => {
      acc[key] = obj[key];
      return acc;
    }, {});
}

这个函数有两个参数:

  • obj:原始对象,新对象将从中创建。

  • keys:不应该在新对象中的键数组。

为了创建一个新对象并删除属性,使用 Object.keys() 函数创建原始对象的键数组。然后,使用 filter() 函数删除在 keys 参数中指定的每个键。通过 reduce 函数,将遍历剩余的键,并返回一个新对象,该对象仅由不在 keys 数组中提供的每个键组成。

实际上,如果您有一个大的用户对象,只想删除 id,您可以使用它:

const obj = {
    id: 1,
    name: 'Paul',
    job: 'Senior Engineer',
    twitter: 'https://www.twitter.com/paulknulst',
    website: 'https://www.paulsblog.dev',
};

const selected = omit(obj, ['id']);
console.log(selected); // {name: 'Paul', job: 'Senior Engineer', twitter: 'https://www.twitter.com/paulknulst', website: 'https://www.paulsblog.dev'}

在这个例子中,omit() 函数被用来移除 id 属性,并获取一个仅包含其他属性的对象。这比使用 for 循环,设置 obj.id=undefined 或使用 pick() 并提供所有属性更具可读性。

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 数组组合成单个元组数组。

结束语

在本博客文章中,我涵盖了许多强大且有用的函数,它们帮助编写更高效,可读性更强,更易于维护的 JavaScript 代码。如果正确使用,这些函数将提高代码质量,使项目更易于处理。

重要的是要说明,介绍的这些函数不包含在 JavaScript 语言的核心部分中被实现,但在许多流行的 JavaScript 框架中有被实现,如 underscore.js,lodash 等。

最后,学习在实际软件项目中有效地使用这些函数是一个持续的实践过程。过一段时间后,您将能轻松高效地使用这些函数,使代码更具可读性和可维护性。同时,总体代码质量将得到优化。

最后,您对这些 JavaScript 函数有什么看法?您是否渴望将其应用于您的项目?此外,您对这些函数都有哪些疑问?我很想听到您的想法和回答您的问题。请在评论中分享一切。

谢谢您的阅读,祝你编码愉快!

作者:Runtu4378
链接:https://juejin.cn/post/7198000226770370620
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。