loading...

Mastering Hard Parts of JavaScript: Closure III

internettradie profile image Ryan Ameri ・5 min read

Exercise 11

Create a function dateStamp that accepts a function and returns a function. The returned function will accept however many arguments the passed-in function accepts, and return an object with a date key that contains a timestamp with the time of invocation, and an output key that contains the result from invoking the passed-in function. HINT: You may need to research how to access information on Date objects.

function dateStamp() {}
const stampedMultBy2 = dateStamp((n) => n * 2);
console.log(stampedMultBy2(4));
// => should log { date: (today's date), output: 8 }
console.log(stampedMultBy2(6));
// => should log { date: (today's date), output: 12 }

Solution 11

function dateStamp(func) {
  const logTime = {};
  function stamping(input) {
    logTime.date = new Date();
    logTime.output = func(input);
    return logTime;
  }
  return stamping;
}

Another way of giving our function memory, except here instead of counting how many times our function has been called, we are keeping track of when it was called. Since our function needs to have a memory, it needs to have a persistent object in its outside scope, i.e., closure. This object then gets a date attribute that is set to the when the function is invoked, and an output attribute that is set to the return value of the original parameter with the second function's parameter as its argument.

We should be feeling pretty confident about giving our functions memory now, and that basically is the gist of closure.

Exercise 12

Create a function censor that accepts no arguments. censor will return a function that will accept either two strings, or one string. When two strings are given, the returned function will hold onto the two strings as a pair, for future use. When one string is given, the returned function will return the same string, except all instances of first strings (of saved pairs) will be replaced with their corresponding second strings (of those saved pairs).

function censor() {}
const changeScene = censor();
changeScene("dogs", "cats");
changeScene("quick", "slow");
console.log(changeScene("The quick, brown fox jumps over the lazy dogs."));
// => should log 'The slow, brown fox jumps over the lazy cats.'

Solution 12

function censor() {
  const phrases = new Map();
  function actualFn(...args) {
    if (args.length === 2) {
      phrases.set(args[0], args[1]);
    } else {
      let input = args[0];
      for (let [key, value] of phrases) {
        let regex = new RegExp(key, "g");
        input = input.replace(regex, value);
      }
      return input;
    }
  }
  return actualFn;
}

Now our function is getting a little bit more interesting, but when you break this exercise down, it is still very much doing the same things we've been practicing in the previous exercises, i.e., we need to have memory of some sort, and our function needs to have different behaviour based on the number of arguments passed.

For this exercise, I decided to use a Map() for the memory part but an object could also be used. I use rest parameters to capture all the arguments passed to the inside function and then check the array's size to see how many arguments there were. If two arguments were passed, we store them in our phrases map and we're done. If only one argument was passed, we use string.prototype.replace() and replace everything in our string that matches the previously stored values in our phrases map.

I wanted to use the new String.prototype.replaceAll() but at the time of this writing that's not still widely supported (for example it's not supported in the version of node.js 14 that I'm using to run my exercises). Once support for replaceAll() becomes more widespread, we can use that and we wouldn't need to construct a regex.

Exercise 13

There's no such thing as private properties on a JavaScript object! But, maybe there are? Implement a function createSecretHolder(secret) which accepts any value as secret and returns an object with ONLY two methods. getSecret() which returns the secret setSecret() which sets the secret

function createSecretHolder() {}
const obj = createSecretHolder(5);
console.log(obj.getSecret());
// => returns 5
obj.setSecret(2);
console.log(obj.getSecret());
// => returns 2

Solution 13

function createSecretHolder(secret) {
  let num = secret;
  const obj = {
    getSecret() {
      return num;
    },
    setSecret(n) {
      num = n;
    },
  };
  return obj;
}

Ha! An interesting method of implementing getter and setters! We'll cover these in more detail in Chapter 4, Classes and the Prototype, but here we are seeing how they such getters and setters can be implemented behind the scenes because classes in JS are (mostly) syntatic sugar.

I also believe that you now can (kind of) set private properties on an object in JS in the form of private class fields which was added in ES2019 (if these two paragraphs don't make any sense to you, don't worry, you should still be able to solve the exercise!)

So how did I solve this? Instead of returning a function, here I returned an object. However our object has two methods one is the getter which doesn't receive any parameters and instead returns the value of the num variable stored in its outside scope. The other is a setter which which just changes that value. Because the object is persistent and retains its values, this acts similar to how a normal getter and setter act in OOP languages such as Java.

Exercise 14

Write a function, callTimes, that returns a new function. The new function should return the number of times it’s been called.

function callTimes() {}
let myNewFunc1 = callTimes();
let myNewFunc2 = callTimes();
console.log(myNewFunc1()); // => 1
console.log(myNewFunc1()); // => 2
console.log(myNewFunc2()); // => 1
console.log(myNewFunc2()); // => 2

Solution 14

function callTimes() {
  let counter = 0;
  function insideFn() {
    counter++;
    return counter;
  }
  return insideFn;
}

Compared to some of our recent exercises, this is rather simple again, but it's good practice to remember how to count the number of times a function has been called. Good demonstration that we have access to the COVE (the outside variables) and can both retrieve them or change them.

Exercise 15

Create a function russianRoulette that accepts a number (let us call it n), and returns a function. The returned function will take no arguments, and will return the string 'click' the first n - 1 number of times it is invoked. On the very next invocation (the nth invocation), the returned function will return the string 'bang'. On every invocation after that, the returned function returns the string 'reload to play again'.

function russianRoulette() {}
const play = russianRoulette(3);
console.log(play());
// => should log 'click'
console.log(play());
// => should log 'click'
console.log(play());
// => should log 'bang'
console.log(play());
// => should log 'reload to play again'
console.log(play());
// => should log 'reload to play again'

Solution 15

function russianRoulette(num) {
  let count = 0;
  function closureFn() {
    count++;
    if (count < num) return "click";
    else if (count === num) return "bang";
    else return "reload to play again";
  }
  return closureFn;
}

Russian Roulette sounds scary, but this really is a variation of the same problem we've been solving in these past few exercises: count the number of times a function has been called, and perform different task based on that. Here what we do also depends on the parameter that was originally passed to the function (the num).

The flexibility and power of closure should be quite apparent here. To implement this functionality using a traditional OOP language such as Java would require quite a bit more lines of code.

Posted on by:

internettradie profile

Ryan Ameri

@internettradie

Re-discovering frontend development. Professional translator & interpreter. Amateur powerlifter. BLM. He/him/his

Discussion

pic
Editor guide