Key Takeaways
- JavaScript’s dynamic nature allows for a function to define and even redefine itself. This is achieved by assigning an anonymous function to a variable that carries the same name as the function. This concept is known as the Lazy Definition Pattern and can be used for initialization code that is needed only during the first function invocation.
- If a function is assigned to another variable before its first invocation and subsequent redefinition, the new variable will maintain the original function definition and not be rewritten. However, any properties set on the function prior to its redefinition will be lost.
- The technique of rewriting functions can be combined with feature detection to create more efficient functions, a concept known as init-time branching. This allows for the function to be optimized for the browser being used, checking for feature support only during the first function call.
The following is a short extract from our new book, JavaScript: Novice to Ninja, 2nd Edition, written by Darren Jones. It’s the ultimate beginner’s guide to JavaScript. SitePoint Premium members get access with their membership, or you can buy a copy in stores worldwide.
The dynamic nature of JavaScript means that a function is able to not only call itself, but define itself, and even redefine itself. This is done by assigning an anonymous function to a variable that has the same name as the function.
Consider the following function:
function party(){
console.log('Wow this is amazing!');
party = function(){
console.log('Been there, got the T-Shirt');
}
}
This logs a message in the console, then redefines itself to log a different message in the console. When the function has been called once, it will be as if it was defined like this:
function party() {
console.log('Been there, got the T-Shirt');
}
Every time the function is called after the first time, it will log the message “Been there, got the T-Shirt”:
party();
<< 'Wow this is amazing!'
party();
<< 'Been there, got the T-Shirt'
party();
<< 'Been there, got the T-Shirt'
If the function is also assigned to another variable, this variable will maintain the original function definition and not be rewritten. This is because the original function is assigned to a variable, then within the function, a variable with the same name as the function is assigned to a different function. You can see an example of this if we create a variable called beachParty
that is assigned to the party()
function before it is called for the first time and redefined:
function party(){
console.log('Wow this is amazing!');
party = function(){
console.log('Been there, got the T-Shirt');
}
}
const beachParty = party; // note that the party function has not been invoked
beachParty(); // the party() function has now been redefined, even though it hasn't been called explicitly
<< 'Wow this is amazing!'
party();
<< 'Been there, got the T-Shirt'
beachParty(); // but this function hasn't been redefined
<< 'Wow this is amazing!'
beachParty(); // no matter how many times this is called it will remain the same
<< 'Wow this is amazing!'
Losing Properties
Be careful: if any properties have previously been set on the function, these will be lost when the function redefines itself. In the previous example, we can set a music
property, and see that it no longer exists after the function has been invoked and redefined:
function party() {
console.log('Wow this is amazing!');
party = function(){
console.log('Been there, got the T-Shirt');
}
}
party.music = 'Classical Jazz'; // set a property of the function
party();
<< "Wow this is amazing!"
party.music; // function has now been redefined, so the property doesn't exist
<< undefined
This is called the Lazy Definition Pattern and is often used when some initialization code is required the first time it’s invoked. This means the initialization can be done the first time it’s called, then the function can be redefined to what you want it to be for every subsequent invocation.
Init-Time Branching
This technique can be used with the feature detection that we discussed in the last chapter to create functions that rewrite themselves, known as init-time branching. This enables the functions to work more effectively in the browser, and avoid checking for features every time they’re invoked.
Let’s take the example of our fictional unicorn
object that’s yet to have full support in all browsers. In the last chapter, we looked at how we can use feature detection to check if this is supported. Now we can go one step further: we can define a function based on whether certain methods are supported. This means we only need to check for support the first time the function is called:
function ride(){
if (window.unicorn) {
ride = function(){
// some code that uses the brand new and sparkly unicorn methods
return 'Riding on a unicorn is the best!';
}
} else {
ride = function(){
// some code that uses the older pony methods
return 'Riding on a pony is still pretty good';
}
}
return ride();
}
After we’ve checked whether the window.unicorn
object exists (by checking to see if it’s truthy), we’ve rewritten the ride()
function according to the outcome. Right at the end of the function, we call it again so that the rewritten function is now invoked, and the relevant value returned. One thing to be aware of is that the function is invoked twice the first time, although it becomes more efficient each subsequent time it’s invoked. Let’s take a look at how it works:
ride(); // the function rewrites itself, then calls itself
<< 'Riding on a pony is still pretty good'
Once the function has been invoked, it’s rewritten based on the browser’s capabilities. We can check this by inspecting the function without invoking it:
ride
<< function ride() {
return 'Riding on a pony is still pretty good';
}
This can be a useful pattern to initialize functions the first time they’re called, optimizing them for the browser being used.
Recursive Functions
A recursive function is one that invokes itself until a certain condition is met. It’s a useful tool to use when iterative processes are involved. A common example is a function that calculates the factorial of a number:
function factorial(n) {
if (n === 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}
This function will return 1
if 0
is provided as an argument (0 factorial is 1), otherwise it will multiply the argument by the result of invoking itself with an argument of one less. The function will continue to invoke itself until finally the argument is 0
and 1
is returned. This will result in a multiplication of 1, 2, 3 and all the numbers up to the original argument.
Another example from the world of mathematics is the Collatz conjecture. This is a problem that is simple to state, but, so far, has not been solved. It involves taking any positive integer and following these rules:
If the number is even, divide it by two
If the number is odd, multiply it by three and add one
For example, if we start with the number 18, we would have the following sequence:
18, 9, 28, 14, 7, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1, 4, 2, 1, …
As you can see, the sequence becomes stuck in a loop at the end, cycling through “4,2,1”. The Collatz Conjecture states that every positive integer will create a sequence that finishes in this loop. This has been verified for all numbers up to 5 × 2⁶⁰, but there is no proof it will continue to be true for all the integers higher than this. To test the conjecture, we can write a function that uses recursion to keep invoking the function until it reaches a value of 1
(because we want our function to avoid being stuck in a recursive loop at the end!):
function collatz(n, sequence=[n]) {
if (n === 1){
return `Sequence took ${sequence.length} steps. It was ${sequence}`;
}
if (n%2 === 0) {
n = n/2;
} else {
n = 3*n + 1;
}
return collatz(n,[...sequence,n]);
}
This function takes a number as a parameter, as well as another parameter called sequence
, which has a default value of an array containing the first parameter. The second parameter is only used when the function calls itself recursively.
The first thing the function does is tests to see if n
has a value of 1. If it does, the function returns a message to say how many steps it took. If it hasn’t reached 1, it checks if the value of n
is even (in which case it divides it by 2), or odd, in which case it multiplies by 3 and then adds 1. The function then calls itself, providing the new value of n
and the new sequence as arguments. The new sequence is constructed by placing the old sequence and the value of n
inside a new array and applying the spread operator to the old sequence.
Let’s see what happens to the number 18:
collatz(18);
<< 'Sequence took 21 steps. It was 18,9,28,14,7,22,11,34,17,52,26,13,40,20,10,5,16,8,4,2,1'
As you can see, it takes 21 steps, but eventually it ends up at 1.
Have a go at using the function and see if you can find a value above 5 × 2⁶⁰ that doesn’t end at 1 — you’ll be famous if you do!
Frequently Asked Questions (FAQs) about JavaScript Functions that Define and Rewrite Themselves
How Can I Create a Function Dynamically in JavaScript?
In JavaScript, you can create functions dynamically using the Function constructor. This constructor takes two arguments: a string containing a comma-separated list of argument names, and a string containing the function body. For example, you can create a function that adds two numbers like this:var add = new Function('a', 'b', 'return a + b');
console.log(add(1, 2)); // Outputs: 3
This method allows you to define functions dynamically, but it’s generally not recommended because it’s less efficient and more error-prone than declaring functions normally.
What Does It Mean for a Function to Define Itself in JavaScript?
In JavaScript, a function can define itself, meaning it can modify its own code while it’s running. This is possible because functions in JavaScript are first-class objects, which means they can be passed around, returned from other functions, and even modified. Here’s an example of a function that redefines itself:function foo() {
foo = function() {
console.log('This is the new function');
};
}
The first time you call foo()
, it redefines itself. The next time you call foo()
, it will execute the new code.
How Can I Rewrite a Function in JavaScript?
In JavaScript, you can rewrite a function by simply assigning a new function to the same variable. Here’s an example:var foo = function() {
console.log('This is the original function');
};
foo = function() {
console.log('This is the new function');
};
When you call foo()
, it will execute the new function, not the original one. This is because the variable foo
now points to the new function.
What Are the Advantages and Disadvantages of Defining Functions Dynamically in JavaScript?
Defining functions dynamically in JavaScript can provide flexibility, as you can create and modify functions on the fly based on your program’s needs. However, it also has several disadvantages. It’s less efficient than declaring functions normally, as the JavaScript engine can’t optimize the function ahead of time. It’s also more error-prone, as any mistakes in the function body string won’t be caught until the function is executed.
Can I Use Arrow Functions to Define and Rewrite Functions in JavaScript?
Yes, you can use arrow functions to define and rewrite functions in JavaScript. Arrow functions provide a more concise syntax and have some differences in how they handle this
and other special keywords. Here’s an example of defining and rewriting an arrow function:let foo = () => {
console.log('This is the original function');
};
foo = () => {
console.log('This is the new function');
};
When you call foo()
, it will execute the new function, not the original one.
Darren loves building web apps and coding in JavaScript, Haskell and Ruby. He is the author of Learn to Code using JavaScript, JavaScript: Novice to Ninja and Jump Start Sinatra.He is also the creator of Nanny State, a tiny alternative to React. He can be found on Twitter @daz4126.