Advanced JavaScript Functions to Improve Your Code: From Debounce, Once, and Memoize to Pipe, Pick, and Zip

Photo by Joan Gamell / Unsplash

Advanced JavaScript Functions to Improve Code Quality

Enhance code quality with functions including Debounce, Once, and Memoize, all the way to Pipe, Pick, and Zip!

Paul Knulst  in  Programming Jan 26, 2023 12 min read

Introduction

JavaScript is a powerful and versatile programming language that has many built-in features that can help you write more efficient, maintainable, and readable code.

In this blog post, I will explain how to use some in-built features to create some of the most powerful functions to boost your performance and make your code look much more beautiful. I will cover Debounce, Throttle, Once, Memoize, Curry, Partial, Pipe, Compose, Pick, Omit, and Zip which you can save in a utility file/class to optimize your code quality as a JavaScript developer.

Although the functions are explained using JavaScript, they could be easily implemented in any programming language. Once the concept of the different functions is understood, it can be applied everywhere.

Furthermore, the functions (or concepts) described in this post are often asked in technical interviews.

Whether you are a beginner or an experienced senior developer, these functions will optimize your code and coding experience. They will make working with JavaScript more enjoyable and efficient.

Debounce

A debounce function is a method for preventing a quick series of events from repeatedly activating a function. It works by postponing function execution until a certain period has passed without the event being fired. The Debounce function is a useful solution that could be applied in real-world applications to increase performance by preventing the execution of functions if a user is rapidly clicking the buttons.

The following snippet will show how to implement the debounce function in JavaScript:

function debounce(func, delay) {
  let timeout;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(context, args), delay);
  };
}

In this JavaScript snippet, the debounce function will return a new function that executes the original function after a previously defined delay. If the function is called again, the timeout will be reset, and calling the function will be postponed.

This functionality will be useful if you have a function that updates the layout of a webpage when the window is resized. Without the Debounce function, this function would be called many times in quick succession as the user resizes the window, which can cause performance issues. With the Debounce function, the rate at which the layout is updated can be limited, making the page more responsive and efficient.

This snippet shows how the Debounce function will be used in this use case:

// 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);

In this example, the updateLayout function will be called at most once every 250 milliseconds when the window is resized. This functionality ensures that the layout is only updated 250ms after the user has finished resizing the window, making the webpage more efficient and responsive.

Throttle

The Throttle function is similar to the Debounce function but with slightly different behavior. Instead of limiting the rate at which the function is called, the Throttle function limits the rate at which the function is executed. This means it will forbid executing if the function was invoked before within a given period. It guarantees that a certain function runs at a consistent rate and won't be triggered too often.

The implementation of the Throttle function:

function throttle(func, delay) {
  let wait = false;

  return (...args) => {
    if (wait) {
        return;
    }

    func(...args);
    wait = true;
    setTimeout(() => {
      wait = false;
    }, delay);
  }
}

In this snippet, the throttle function will execute a provided function func, update a wait variable to true, and then starts a timer which will reset the wait parameter after the delay is passed. If the throttle function is called again it will either call the provided function or simply return if the wait parameter is still true.

This Throttle functionality can be used on a webpage if you want to execute a function to update the layout while scrolling. Without a throttle function, this update function will be called multiple times as the user scrolls on the page, resulting in heavy performance issues. Using the throttle function, you can ensure that it will only be executed once every X milliseconds. This will result in more responsive and efficient usability of the webpage.

In the following snippet, you can see how the throttle function can be used:

// 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);

By defining the throttleUpdatedLayout function and specifying a delay of 250 milliseconds, it can be ensured that the updateLayout function will be executed at most once every 250 milliseconds when the window is scrolled. If the event is triggered before the delay is reached, nothing will happen.

Once

The Once function is a method that will prevent executing if already called. This is especially useful while working with event listeners, where you often encounter functions that only should run once. Instead of removing event listeners every time you can use the Once function in JavaScript.

function once(func) {
  let ran = false;
  let result;
  return function() {
    if (ran) return result;
    result = func.apply(this, arguments);
    ran = true;
    return result;
  };
}

For example, you can have a function that sends a request to a server to load some data. With the once() function, you could ensure that the request is not called multiple times if the user keeps clicking the button. This will avoid performance issues.

Without the once() function, you would remove the click listener instantly after the request is sent to prevent sending the request again.

Applying the once() function to any code will look like this:

// 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);

In this example, the requestSomeData function will be called once, even if the user clicks the button multiple times.

Memoize

Memoize is a JavaScript function, that is used to cache the results of a given function to prevent calling computationally expensive routines multiple times with the same arguments.

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

This memoize() function will cache the result of a given function and uses the arguments as key to retrieve the result if called again with the same arguments.

Now, if you have a function that performs a complex calculation that is based on an input variable, you can use the memoize() function to cache the results and retrieve them instantly if called multiple times with the same input.

To see the benefits of the memoize() function, you can use it to calculate the Fibonacci numbers:

// 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')

In this example, the fibonacci() function will be converted into a memoizedFibonacci function. Then the memoized() function will be called, and the execution time will be logged to the console.

The output will look like this:

Advanced JavaScript Functions to Improve Your Code Quality: From Debounce, Once, and Memoize to Pipe, Pick, and Zip

Although the second call only calculates the Fibonacci number of 29 it took much longer than calculating the Fibonacci number of 30 a second time because it was cached by the memoize() function.

Curry

The Curry function (also known as Currying) is an advanced JavaScript function used to create a new function from an existing one by "pre-filling" some of its arguments. Currying is often used when working with functions that take multiple arguments and transform them into functions that take some arguments because the other will always stay the same.

Using the Curry function has several benefits:

  • It helps to avoid using the same variable again and again
  • It makes code more readable
  • It divides functions into multiple smaller functions that can handle one responsibility
function curry(func, arity = func.length) {
  return function curried(...args) {
    if (args.length >= arity) return func(...args);
    return function(...moreArgs) {
      return curried(...args, ...moreArgs);
    };
  };
}

This Curry function takes another function (func) and an optional argument arity that defaults to the length of func's arguments. It returns a new function (curried) that can be called with a arity number of arguments. If not all arguments have been supplied, it returns a new function that can be called with more arguments until all required arguments have been provided. When all arguments are supplied, the original function (func) is called, and its result will be returned.

To understand the benefits of the Curry function, you could think of a method to calculate the distance between two points in a plane. Using a Curry function, you can create a new function that will only require one of these points, making it easier to use.

The following snippet will show how the previously defined curry function is used to optimize the readability of the implementation:

// 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);

In this example, a curried version of the distance function is created (distanceFromOrigin) by using the curry function and passing distance as the first argument and 3 as the second argument (arity). Also, it will call the curried function with 0,0 as the first two arguments.

The resulting function distanceFromOrigin is now a new function that needs only two arguments, and will always use 0,0 as the first point.

Partial

The Partial function in JavaScript is similar to the Curry function. The significant difference between Curry and Partial is that a call to a Partial function returns the result instantly instead of returning another function down the currying chain.

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

The partial function in JavaScript typically takes an existing function, one or more input arguments, and returns a new function that calls the original function with the additional arguments passed in when the new function is called.

In the following use case, a calculate function will be pre-filled with the first two arguments to generate a new function with a more readable name:

// 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);

In this example, the multiply10By function is created by partially applying the generic calculate function and pre-filling the first two arguments with 8 and 2. This will create a new function multiply10By that only requires one argument specifying the amount of multiplication of 10 that has to be done. Also, it will make the code more readable and understandable.

Pipe

The Pipe function is a utility function used to chain multiple functions and pass the output of one to the next one in the chain. It is similar to the Unix pipe operator and will apply all functions left-to-right by using the JavaScript reduce() function:

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

To understand the pipe function, imagine you have three functions:

  • add a Prefix to a String
  • add a Suffix to a String
  • convert a String to Uppercase

Then you can use the pipe function to create a new function that will apply every single one from left to right to a String.

// 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

In this example, the decorated1 and decorated2 functions are created by piping the addPrefix, addSuffix, and toUppercase functions in different orders. The new functions, that are created can be called with the input string to apply the three original ones in the given order. The resulting output strings will be different because the order provided in the pipe function is different.

Compose

The Compose function is the same as the Pipe function, but it will use reduceRight to apply all functions:

function compose(...funcs) {
  return function composed(...args) {
    return funcs.reduceRight((result, func) => [func.call(this, ...result)], args)[0];
  };
}

This will result in the same functionality, but the functions are applied from right to left.

Pick

The Pick function in JavaScript is used to select specific values from an object. It is a way to create a new object by selecting certain properties from a provided project. It is a functional programming technique that allows extracting a subset of properties from any object if the properties are available.

Here's the implementation of the Pick function:

function pick(obj, keys) {
  return keys.reduce((acc, key) => {
    if (obj.hasOwnProperty(key)) {
      acc[key] = obj[key];
    }
    return acc;
  }, {});
}

This function takes two parameters:

  • obj: Original object where the new object will be created from
  • keys: Array of keys to select into the new object.

To create a new object the function will use the reduce() method to iterate the keys and compare them to the original object's properties. If a value is present, it will be added to the accumulator object of the reduce function, which was initialized with {}.

At the end of the reduce function, the accumulator object will be the new object and contains only the specified properties that were in keys array.

This function is useful if you want to avoid over-fetching data. With the Pick function, you can retrieve any object from the database and then only pick() needed properties and return them to the caller.

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

This function will use the pick() function to create a new object only containing name and website which can be returned to the caller without exposing the role, the password, or the id

Omit

The Omit function is the opposite of the Pick function, as it will remove certain properties from an existing object. This means you can avoid over-fetching by hiding properties. It can be used as a replacement for the Pick function if the amount of properties to hide is smaller than the number of properties to pick.

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

This function takes two parameters:

  • obj: Original object where the new object will be created from
  • keys: Array of keys that won't be in the new object.

To create a new object and remove the properties the Object.keys() function is used to create an array of keys for the original object. Then JavaScript filter() function will remove every key that was specified in the keys argument. With the reduce function, the remaining keys will be iterated, and a new object is returned that only consist of every key not provided in the keys array.

In practice, you can use it if you have a large user object where you only want to remove the 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'}

In this example, the omit() function is used to remove the id property and retrieve an object which will make your code more readable than using a for loop, setting obj.id = undefined or using pick() and supplying every attribute to pick.

Zip

The Zip function is a JavaScript function that matches each passed array of elements to another array element and is used to combine multiple arrays into a single array of tuples. The resulting array will contain the corresponding elements from each array. Often, this functionality is used when working with data from multiple sources that need to be merged or correlated in some way.

Unlike Python, JavaScript does not provide the Zip function out of the box. But, the implementation is easy:

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]);
  });
}

This JavaScript snippet will create a new array of arrays, where every subarray is composed of the elements of the provided arrays. This means, that every element of the original array will be mapped to another element from another original array at the same index.

For example, you could have three arrays that:

  1. contains the x coordinate
  2. contains the y coordinate
  3. contains the z coordinate

Without a zip function, you would manually loop through the arrays and pair the x, y, and z elements. But, by using the zip function, you can pass the original arrays and generate a new array of (x, y, z) tuples.

// 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]]

In this example, the zip function is used to combine the xCoordinates, yCoordinates, and zCoordinates arrays into a single array of tuples.

Closing Notes

During this blog post, I covered many powerful and useful functions, that help to write more efficient, readable, and maintainable JavaScript code. If applied correctly, these functions will boost your code quality and make it easier to work with the project.

It's important to say that the explained JavaScript functions are not part of the core JavaScript language but are implemented in several popular JavaScript frameworks like underscore.js, lodash, etc.

Finally, learning to use these functions effectively in real-life software projects is an ongoing process that needs practice. But, after some time, you will use these functions with ease and efficiency, making your code more readable and maintainable. Also, overall code quality will be optimized.

Finally, what do you think about these JavaScript functions? Are you eager to apply them to your project? Also, do you have any questions regarding any of the functions? I would love to hear your thoughts and answer your questions. Please share everything in the comments.

Feel free to connect with me on Medium, LinkedIn, Twitter, and GitHub.

Thank you for reading, and happy coding!


☕

🙌 Support this content

If you like this content, please consider supporting me. You can share it on social media, buy me a coffee, or become a paid member. Any support helps.

See the contribute page for all (free or paid) ways to say thank you!

Thanks! 🥰

By Paul Knulst

I'm a husband, dad, lifelong learner, tech lover, and Senior Engineer working as a Tech Lead. I write about projects and challenges in IT.