JavaScript - - By Darren Jones

JavaScript Functions That Define and Rewrite Themselves

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!