Execute Program

Modern JavaScript: Arrow Function Scoping

Welcome to the Arrow Function Scoping lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • An earlier lesson showed arrow functions, which are often used for callback functions.

  • >
    [1, 2, 3].map(x => x * 2);
    Result:
  • Arrow functions are terse, which is nice. But they have a second big benefit: their scoping rules are easier to understand than regular functions' scoping rules.

  • Regular functions and shorthand methods have confusing this behavior that depends on how they're called. For example, inside of an address object, we can access this.city and this.country.

  • >
    const address = {
    city: 'Paris',
    country: 'France',
    addressFunction() {
    return `${this.city}, ${this.country}`;
    },
    };
    address.addressFunction();
    Result:
  • However, if addressFunction returns another function, now the inner function's this is undefined.

  • >
    const address = {
    city: 'Paris',
    country: 'France',
    addressFunction() {
    return function() {
    return `${this.city}, ${this.country}`;
    };
    },
    };
    address.addressFunction()();
    Result:
  • It's possible to fix this problem with .bind(...), but that's very awkward.

  • >
    const address = {
    city: 'Paris',
    country: 'France',
    addressFunction() {
    const f = function() {
    return `${this.city}, ${this.country}`;
    };
    return f.bind(this);
    },
    };
    address.addressFunction()();
    Result:
  • Fortunately, there's an easier solution: we can use an arrow function. Arrow functions always inherit the scope that they were defined in, including the this value.

  • Here's the example above rewritten with an arrow function. It's one line of code instead of three. No need for any confusing bind calls!

  • >
    const address = {
    city: 'Paris',
    country: 'France',
    addressFunction() {
    /* Inside of this shorthand method, `this` is the containing object,
    * `address`. Arrow functions inherit the scope that they were defined in,
    * so this arrow function's `this` is also the containing object,
    * `address`. */
    return () => `${this.city}, ${this.country}`;
    },
    };
    address.addressFunction()();
    Result:
    'Paris, France'Pass Icon
  • Here's a code problem:

    Finish the volumeFunction shorthand method on the 3D rectangle below. It should return a function. The returned function should calculate and return the 3D rectangle's volume. (Volume is calculated as this.baseArea() * this.height.)

    The function that you return should be an arrow function, which will ensure that it can see the rectangle's this.

    const rectangle3D = {
    width: 3,
    depth: 4,
    height: 5,
    baseArea() { return this.width * this.depth; },
    volumeFunction() { return () => this.baseArea() * this.height; },
    };
    rectangle3D.volumeFunction()();
    Goal:
    60
    Yours:
    60Pass Icon
  • In the problem above, we made volumeFunction a shorthand method. Its this referred to the parent object, rectangle3D. When we reference this.height, we get rectangle3D.height.

  • What if we make volumeFunction itself an arrow function? Unfortunately, that doesn't work!

  • >
    const rectangle3D = {
    width: 3,
    depth: 4,
    height: 5,
    baseArea() { return this.width * this.depth; },
    // This is an arrow function.
    volumeFunction: () => { return () => this.baseArea() * this.height; },
    };

    rectangle3D.volumeFunction()();
    Result:
  • The problem is that the volumeFunction is now an arrow function, which doesn't "know" that it's inside of an object. Arrow functions inherit the scope that they're defined in, which in this case is the global scope. volumeFunction's this refers to the global this. In a browser, that's window; and in Node, it's the global object. Neither of those objects has a .baseArea() or .height, hence the error above!

  • Here's a way to see that more clearly. The next example is the same as the one above, with one difference: we define the arrow function outside of the object, then we assign it to rectangle3D.volumeFunction. We get the same result, but now it's easier to see that volumeFunction doesn't know that it's inside of rectangle3D.

  • >
    const rectangle3D = {
    width: 3,
    depth: 4,
    height: 5,
    baseArea() { return this.width * this.depth; },
    };

    const volumeFunction = () => { return () => this.baseArea() * this.height; };

    rectangle3D.volumeFunction = volumeFunction;

    rectangle3D.volumeFunction()();
    Result:
  • If all of these scoping rules seem confusing, that's because they are! Fortunately, the rule for arrow functions is simple: arrow functions always inherit the scope that they were defined in. This makes them ideal for use as callback functions, where we often want to reference variables in the function or class that created the callback function.

  • Here's a helpful rule that works in most situations.

  • If the function defined is part of an object, and needs to access that object's properties, use a shorthand function.

  • If it's a standalone function or a method that doesn't need to access this, we can define it however we want: as a shorthand method, an arrow function, or using the function keyword. The scoping rules for this don't affect it because it doesn't access this in the first place!

  • This rule is useful because it makes sense even if we forget about JavaScript's surprising this behavior. In every programming language, the difference between functions and methods is that methods are part of an object, whereas functions are defined independently. If it accesses the object's internals, it should be a method. If it doesn't access the internals, it can be a function or a method.