Function Polyfills in JavaScript: Enhancing Compatibility with Older Environments
JavaScript provides powerful function-related methods, but not all browsers or environments support them natively. Features like call()
, apply()
, bind()
, as well as performance optimization techniques like debounce()
, throttle()
, memoize()
, and once()
are essential in modern development. However, these methods may be missing in older environments. In this blog, we’ll explore how to implement polyfills for these function methods, ensuring that your code is compatible across all browsers and offers better control and performance in your applications.
Polyfill for Function.prototype.call
Let’s take an example to understand how the call
method works with Function:
const person = {
firstName: 'Alex',
lastName: 'Smith',
fullName: function(title, profession) {
return `${title} ${this.firstName} ${this.lastName}, ${profession}`;
}
};
const person2 = { firstName: 'Murali', lastName: 'Singh' };
const greeting = person.fullName.call(person2, 'Mr.', 'Software Engineer');
console.log(greeting); // Output: "Mr. Murali Singh, Software Engineer"
As you can see, we’re calling the fullName
function by passing the person2
object along with the required arguments, such as title
and profession
. This, in turn, executes the fullName
function, setting this
to the person2
object and return whatever the fullName
function returns.
Now, let's go ahead and create the callPolyfill
.
Function.prototype.callPolyfill = function(thisArg = {}, ...args) {
if(!this) {
throw new TypeError("Function.protoype.callPolyfill is applied on null or undefiend");
}
let originalFunction = this;
let newFunctionName = new Symbol('callFunction')
thisArg[newFunctionName] = originalFunction;
let value = thisArg[newFunctionName](...args);
delete thisArg[newFunctionName];
return value;
}
1. What does it take?
thisArg
: The value thatthis
should refer to inside the function when it’s executed. IfthisArg
isnull
orundefined
, it defaults to the empty object....args
: The arguments that are passed to the function.
2. What does it return?
- After executing the function with the specified
thisArg
and arguments, the polyfill returns whatever the original function returns.
3. How does it perform?
Temporary Property: To simulate the
call
behavior, it temporarily adds a new property to thethisArg
object, using a uniqueSymbol
to avoid any conflicts.Function Execution: The function is then assigned to temporary property of
thisArg
and then the function is called by passing arguments (...args
) .Cleanup: The temporary property is removed to avoid polluting the
thisArg
object.Return the result: Finally, the polyfill returns the result of the function call.
Let’s apply the same function from the example to our callPolyfill
and see the result:
const greetingPolyfill = person.fullName.callPolyfill(person2, 'Mr.', 'Software Engineer');
console.log(greetingPolyfill); // Output: "Mr. Murali Singh, Software Engineer"
Polyfill for Function.prototype.bind
Let’s take an example to understand how the bind
method works with Function:
function fullName(title, profession) {
return `${title} ${this.firstName} ${this.lastName}, ${profession}`;
}
const person = {
firstName: 'Murali',
lastName: 'Singh'
};
const greeting = fullName.bind(person, "Mr.", "Software Engineer");
console.log(greeting()) // Output: Mr. Murali Singh, Software Engineer
As you can see, we are binding the fullName
function to the person
object and passing the required arguments, such as title
and profession
. This returns a new function with the specified context and arguments.
Now, let's go ahead and create the bindPolyfill
.
Function.prototype.bind = function(thisArg = {}, ...args) {
if(!this) {
throw new TypeError("Function.protoype.bindPolyfill is applied on null or undefiend");
}
let newFunctionName = new Symbol("bindFunction");
thisArg[newFunctionName] = this;
return function(...newArgs) {
const result = thisArg[newFunctionName](...args, ...newArgs);
delete thisArg[newFunctionName];
return result;
}
}
Function.prototype.bind = function(thisArg = {}, ...args) {
if(!this) {
throw new TypeError("Function.protoype.bindPolyfill is applied on null or undefiend");
}
const functionToBind = this;
return function(...newArgs) {
const result = functionToBind.apply(thisArg, [...args, ...newArgs]);
// OR
// return functionToBind.call(thisArg, ...args, ...newArgs);
};
};
1. What does it take?
thisArg
: The value to which thethis
keyword should refer when the bound function is called....args
: An array of arguments that will be prepended to any arguments passed to the returned function when invoked.
2. What does it return?
- A new function that, when called, invokes the original function with the provided
thisArg
and combined arguments (args
+newArgs
).
3. How does it perform?
It stores the original function (
this
) infunctionToBind
.There are two possible strategies for invoking the original function:
With a Symbol approach: It temporarily adds the original function to
thisArg
using a uniqueSymbol
, then creates a new function. When the new function is called, it invokes the original function withapply
(orcall
), passing inthisArg
and the combined arguments, and cleans up by deleting the temporary property fromthisArg
.Without a Symbol approach: It directly calls the original function with
apply
(orcall
), passing inthisArg
and the combined arguments when the new function is invoked. This approach is simpler and avoids using any temporary properties onthisArg
.
Let’s apply the bindPolyfill
in some example and see the result:
const person = {
firstName: 'Murali',
lastName: 'Singh'
};
function greet(greeting, punctuation) {
return `${greeting}, ${this.firstName} ${this.lastName}${punctuation}`;
}
const greetingMessage = greet.bindPolyfill(person, ['Hello', '!']);
console.log(greetingMessage()); // Output: Hello, Murali Singh!
const numbers = [1, 2, 3, 4, 5];
function sumAll(additionalNumber) {
return this.reduce((acc, num) => acc + num, 0) + additionalNumber;
}
const sum=sumAll.bindPolyfill(numbers,[10]);
console.log(totalSum()); // Output: 25 (1 + 2 + 3 + 4 + 5 + 10)
Polyfill for Function.prototype.apply
We will take the same example that we took in call
method to understand how the apply
method works with Function:
const person = {
firstName: 'Alex',
lastName: 'Smith',
fullName: function(title, profession) {
return `${title} ${this.firstName} ${this.lastName}, ${profession}`;
}
};
const person2 = { firstName: 'Murali', lastName: 'Singh' };
const greeting = person.fullName.apply(person2, ['Mr.', 'Software Engineer']);
console.log(greeting); // Output: "Mr. Murali Singh, Software Engineer"
As you can see, the only difference between call
and apply
is how the arguments are passed to the function. With apply
, we pass the arguments as an array, whereas with call
, we spread the arguments individually.
Now, let's go ahead and create the applyPolyfill
.
Function.prototype.applyPolyfill = function(thisArg = {}, args = []) {
if(!this) {
throw new TypeError("Function.protoype.applyPolyfill is applied on null or undefiend");
}
if(!Array.isArray(args)) {
throw new TypeError("The second argument must be an array.");
}
let originalFunction = this;
let newFunctionName = new Symbol("applyFunction");
thisArg[newFunctionName] = originalFunction;
let result = thisArg[newFunctionName](...args);
delete thisArg[newFunctionName];
return result;
};
1. What does it take?
thisArg
: The value to be used asthis
when the original function is called.args
: An array (or array-like object) of arguments that will be passed to the original function when it is called.
2. What does it return?
- The result of calling the original function with
thisArg
as thethis
context andargs
as the arguments.
3. How does it perform?
Check if
this
is callable: The firstif
statement ensures thatthis
is a function, because.apply
can only be used on functions. If it’s not callable, an error is thrown.Storing the Original Function:
let originalFunction = this;
stores the current function that we’re callingapply
on.Unique Property Name:
let newFunctionName = Symbol("applyFunction");
creates a unique symbol to be used as the temporary method name onthisArg
. This avoids any naming collisions with existing properties on the object.Attach Function to
thisArg
: The function is added as a property onthisArg
(the object we want to bindthis
to) using the unique property name.Call the Function:
thisArg[newFunctionName](...args)
invokes the function withthisArg
as the context andargs
as the arguments.Cleanup: After the function is called, the temporary property is deleted from
thisArg
to avoid polluting it.Return the Result: Finally, the result of calling the function is returned.
Let’s apply the applyPolyfill
in some example and see the result:
const person = {
firstName: 'Murali',
lastName: 'Singh'
};
function greet(greeting, punctuation) {
return `${greeting}, ${this.firstName} ${this.lastName}${punctuation}`;
}
const result = greet.applyPolyfill(person, ['Hello', '!']);
console.log(result); // Output: Hello, Murali Singh!
function findMax() {
return Math.max.apply(null, arguments);
}
const numbers = [1, 3, 5, 7, 2, 8, 6];
const maxNumber = findMax.applyPolyfill({}, numbers);
console.log(maxNumber); // Output: 8
Polyfill for Function.prototype.debounce
Debouncing is a technique in programming that helps improve the performance of web applications by controlling the frequency at which time-consuming tasks are triggered. If a task is triggered too often—like when a user types quickly or rapidly clicks a button—it can lead to performance issues.Debouncing provides a solution by limiting how frequently a function can be executed.
Now, let's go ahead and create the debouncePolyfill
.
Function.prototype.debouncePolyfill = function(delay, ...args) {
if (!this) {
throw new TypeError("Function.protoype.debouncePolyfill is applied on null or undefiend");
}
let originalFunction = this;
let timeout;
return function(...newArgs) {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
originalFunction(...args, ...newArgs);
}, delay);
}
}
1. What does it take?
delay
: The delay in milliseconds before the function is executed after the last call....args
: Initial arguments to be passed to the function.
2. What does it return?
- A debounced function that delays the execution of the original function until after
delay
milliseconds have passed since the last time it was invoked.
3. How does it perform?
It uses
setTimeout
to delay the function call andclearTimeout
to cancel any pending calls if the function is called again within thedelay
period.The debounced function executes with both the initial arguments (
args
) and any new arguments passed (newArgs
).
Let’s apply the debouncePolyfill
in some example and see the result:
// Dummy list of fruits to simulate a search
const fruits = [
"Apple", "Banana", "Cherry", "Date", "Grape", "Lemon",
"Mango", "Orange", "Peach", "Pear", "Pineapple", "Plum",
"Strawberry", "Watermelon"
];
function searchFruits(query) {
console.log(`Searching for: "${query}"`);
const filteredResults = fruits.filter(fruit => fruit.toLowerCase().includes(query.toLowerCase()));
console.log("Search Results:", filteredResults);
}
// Create a debounced version of the search function
const debouncedSearch = searchFruits.debouncePolyfill(500); // 500ms debounce delay
// Simulate typing in the search input field
const simulateUserTyping = (queries) => {
queries.forEach((query, index) => {
setTimeout(() => {
debouncedSearch(query);
}, index * 200); // Typing every 200ms
});
};
// Simulate a user typing search queries
simulateUserTyping(["Ap", "Ban", "Ch", "Pea", "Or"]);
// Output :
// Searching for: "Or"
// Search Results: [ 'Orange' ]
Polyfill for Function.prototype.throttle
Throttling limits the frequency at which a function is executed, ensuring it runs at most once in a specified time interval. It’s useful for optimizing performance in scenarios involving rapid, continuous events like scrolling or resizing.
Now, let's go ahead and create the throttlePolyfill
.
Function.prototype.throttlePolyfill = function(delay, ...args) {
if(!this) {
throw new TypeError(`Function.prototype.throttlePolyfill is applied on null or undefiend`);
}
let originalFunction = this;
let lastExecutedTime = 0;
return function(...newArgs) {
if(Date.now() - lastExecutedTime >= delay) {
lastExecutedTime = Date.now();
originalFunction(...args, ...newArgs);
}
}
}
1. What does it take?
delay
: The time in milliseconds to wait before the function can be executed again....args
: Arguments that are pre-applied to the original function
2. What does it return?
- A throttled function that throttles the execution of the original function, ensuring it only runs once every
delay
milliseconds.
3. How does it perform?
- It tracks the time of the last execution and only invokes the original function if the
delay
has passed since the last call. If not, it ignores the call.
Let’s apply the throttlePolyfill
in some example and see the result:
function logScrollEvent(event) {
console.log(`Scrolling at ${new Date().toLocaleTimeString()}`);
}
// Create a throttled version of the scroll event handler
const throttledScrollHandler = logScrollEvent.throttlePolyfill(2000);
// Simulate a scroll event happening at different times
setInterval(() => {
throttledScrollHandler();
}, 1000);
// Output:
// Scrolling at 12:30:01 PM
// Scrolling at 12:30:03 PM
// Scrolling at 12:30:05 PM
Polyfill for Function.prototype.memoize
Memoization helps by storing the results of expensive function calls and reusing them when the same inputs occur again. This avoids redundant calculations, making your code more efficient.
Now, let's go ahead and create the memoizePolyfill
.
Function.prototype.memoizePolyfill = function(thisArg={}, ...args) {
if (!this) {
throw new TypeError(`Function.prototype.memoizePolyfill is applied on null or undefiend`);
}
let result = {}; // This will store cached results
let originalFunction = this; // Store a reference to the original function
return function(...newArgs) {
let params = [...args, ...newArgs];
params = JSON.stringify(params);
if (!result[params]) {
let functionCallResult = originalFunction.call(thisArg, ...args, ...newArgs);
// or originalFunction.apply(thisArg, [...args, ...newArgs])
result[params] = functionCallResult;
}
return result[params]; // Return the computed or cached result
};
}
1. What does it take?
thisArg
(default is{}
): The value ofthis
to be used inside the original function when it is called....args
:The initial arguments passed tomemoize()
. These are the arguments that are combined with the subsequent ones passed to the returned memoized function.
2. What does it return?
- A memoized function is returned.
3. How does it perform?
When the memoized function is first called with a set of arguments, it checks if the result for those arguments has already been computed and stored in the
result
object (cache).If the result is cached, it returns the cached value.
If not, it calls the original function, stores the result in the cache, and then returns it.
The cache key is the stringified version of the combined arguments (
args
+newArgs
), which ensures that the function works correctly even when the arguments are complex objects or arrays.
Let’s apply the memoizePolyfill
in some example and see the result:
// A simple function that calculates the sum of two numbers
function add(a, b) {
console.log(`Calculating ${a} + ${b}...`); // This will help us visualize when the function is being called
return a + b;
}
// Memoize the add function
const memoizedAdd = add.memoizePolyfill({}); // {} is the `thisArg` (optional)
console.log(memoizedAdd(3, 4)); // First call, will calculate and cache the result 7
console.log(memoizedAdd(3, 4)); // Second call, will return cached result 7
console.log(memoizedAdd(5, 6)); // First call for a new set of numbers, will calculate and cache the result 11
console.log(memoizedAdd(5, 6)); // Second call, will return cached result 11
Polyfill for Function.prototype.once
This implementation of Function.prototype.once
is designed to allow a function to be executed only once. It will remember the result of the first execution, and return that result for all subsequent calls, without running the function again.
Function.prototype.oncePolyfill = function(thisArg={}, ...args) {
if (!this) {
return new TypeError(`Function.prototype.once is applied on null or undefiend`);
}
let originalFunction = this;
let result;
return function(...newArgs) {
if (originalFunction) {
result = originalFunction.apply(thisArg, [...args, ...newArgs]);
// or result = originalFunction.call(thisArg, ...args, ...newArgs);
originalFunction = null; // Set original function to null to prevent further execution
}
return result; // Return the result of the first call
};
}
1. What does it take?
thisArg
(default is{}
): The value ofthis
to be used inside the original function when it is called....args
: An array of arguments that are passed whenonce()
is invoked, which are passed as the first set of arguments to the original function.
2. What does it return?
- It returns a new function.
3. How does it perform?
First call: When the new function returned by
once()
is invoked, it checks iforiginalFunction
is still available. If it is, it runs it and stores the result.Subsequent calls: Once the function has been executed once, it sets
originalFunction
tonull
, ensuring that the function doesn’t run again on further invocations. It simply returns the result from the first call.
Let’s apply the oncePolyfill
in some example and see the result:
Let's say you are building a user registration system. You want to send a welcome email to the user, but you only want the email to be sent once, no matter how many times the user refreshes the page or triggers the function.
function sendUserWelcomeEmail(userName){
return `Welcome email send to ${userName}`;
}
const sendOnce=sendUserWelcomeEmail.oncePolyfill({},"Murali Singh");
// OR
// const sendOnce=sendUserWelcomeEmail.once({});
// sendOnce("Murali Singh");
console.log(sendOnce()); // Output : Welcome email send to Murali Singh
console.log(sendOnce("Alex Smith")); // Output : Welcome email send to Murali Singh
Implementing polyfills for function methods like call()
, apply()
, bind()
, debounce()
, throttle()
, memoize()
, and once()
ensures your code is more flexible and performs better across different environments. By adding these polyfills, you can leverage modern JavaScript techniques, even in older browsers, enhancing both the functionality and efficiency of your web applications.