When should I use Arrow functions in ECMAScript 6?

后端 未结 9 1615
滥情空心
滥情空心 2020-11-21 05:53

The question is directed at people who have thought about code style in the context of the upcoming ECMAScript 6 (Harmony) and who have already worked with the language.

相关标签:
9条回答
  • 2020-11-21 06:14

    In addition to the great answers so far, I'd like to present a very different reason why arrow functions are in a certain sense fundamentally better than "ordinary" JavaScript functions. For the sake of discussion, let's temporarily assume we use a type checker like TypeScript or Facebook's "Flow". Consider the following toy module, which is valid ECMAScript 6 code plus Flow type annotations: (I'll include the untyped code, which would realistically result from Babel, at the end of this answer, so it can actually be run.)

    export class C {
      n : number;
      f1: number => number; 
      f2: number => number;
    
      constructor(){
        this.n = 42;
        this.f1 = (x:number) => x + this.n;
        this.f2 = function (x:number) { return  x + this.n;};
      }
    }

    Now see what happens when we use the class C from a different module, like this:

    let o = { f1: new C().f1, f2: new C().f2, n: "foo" };
    let n1: number = o.f1(1); // n1 = 43
    console.log(n1 === 43); // true
    let n2: number = o.f2(1); // n2 = "1foo"
    console.log(n2 === "1foo"); // true, not a string!

    As you can see, the type checker failed here: f2 was supposed to return a number, but it returned a string!

    Worse, it seems that no conceivable type checker can handle ordinary (non-arrow) JavaScript functions, because the "this" of f2 does not occur in the argument list of f2, so the required type for "this" could not possibly be added as an annotation to f2.

    Does this problem also affect people who don't use type checkers? I think so, because even when we have no static types, we think as if they're there. ("The first parameters must be a number, the second one a string" etc.) A hidden "this"-argument which may or may not be used in the function's body makes our mental bookkeeping harder.

    Here is the runnable untyped version, which would be produced by Babel:

    class C {
        constructor() {
            this.n = 42;
            this.f1 = x => x + this.n;
            this.f2 = function (x) { return x + this.n; };
        }
    }
    
    let o = { f1: new C().f1, f2: new C().f2, n: "foo" };
    let n1 = o.f1(1); // n1 = 43
    console.log(n1 === 43); // true
    let n2 = o.f2(1); // n2 = "1foo"
    console.log(n2 === "1foo"); // true, not a string!

    0 讨论(0)
  • 2020-11-21 06:15

    I prefer to use arrow functions at all times where access to local this is not needed, because arrow function do not bind their own this, arguments, super, or new.target.

    0 讨论(0)
  • 2020-11-21 06:27

    Arrow functions were created to simplify function scope and solving the this keyword by making it more simpler. They utilize the => syntax, which looks like an arrow.

    Note: It does not replace the existing functions. If you replace every function syntax with arrow functions, its not going to work in all cases.

    Let's have a look at the existing ES5 syntax, If the this keyword were inside an object’s method (a function that belongs to an object), what would it refer to?

    var Actor = {
      name: 'RajiniKanth',
      getName: function() {
         console.log(this.name);
      }
    };
    Actor.getName();
    

    The above snippet would refer to an object and print out the name "RajiniKanth". Let's explore the below snippet and see what would this point out here.

    var Actor = {
      name: 'RajiniKanth',
      movies: ['Kabali', 'Sivaji', 'Baba'],
      showMovies: function() {
       this.movies.forEach(function(movie) {
         alert(this.name + " has acted in " + movie);
       });
      }
    };
    
    Actor.showMovies();
    

    Now what about if the this keyword were inside of method’s function?

    Here this would refer to window object than the inner function as its fallen out of scope. Because this, always references the owner of the function it is in, for this case — since it is now out of scope — the window/global object.

    When it is inside of an object’s method — the function’s owner is the object. Thus the this keyword is bound to the object. Yet when it is inside of a function, either stand alone or within another method, it will always refer to the window/global object.

    var fn = function(){
      alert(this);
    }
    
    fn(); // [object Window]
    

    There are ways to solve this problem in our ES5 itself, let us look into that before diving into ES6 arrow functions on how solve it.

    Typically you would, create a variable outside of the method’s inner function. Now the ‘forEach’ method gains access to this and thus the object’s properties and their values.

    var Actor = {
      name: 'RajiniKanth',
      movies: ['Kabali', 'Sivaji', 'Baba'],
      showMovies: function() {
       var _this = this;
       this.movies.forEach(function(movie) {
         alert(_this.name + " has acted in " + movie);
       });
      }
    };
    
    Actor.showMovies();
    

    using bind to attach the this keyword that refers to the method to the method’s inner function.

    var Actor = {
      name: 'RajiniKanth',
      movies: ['Kabali', 'Sivaji', 'Baba'],
      showMovies: function() {
       this.movies.forEach(function(movie) {
         alert(this.name + " has acted in " + movie);
       }.bind(this));
      }
    };
    
    Actor.showMovies();
    

    Now with ES6 arrow function, we can deal with lexical scoping issue in a simpler way.

    var Actor = {
      name: 'RajiniKanth',
      movies: ['Kabali', 'Sivaji', 'Baba'],
      showMovies: function() {
       this.movies.forEach((movie) => {
         alert(this.name + " has acted in " + movie);
       });
      }
    };
    
    Actor.showMovies();
    

    Arrow functions are more like function statements, except that they bind the this to parent scope. If the arrow function is in top scope, this argument will refer to window/global scope, while an arrow function inside a regular function will have its this argument the same as its outer function.

    With arrow functions this is bound to the enclosing scope at creation time and cannot be changed. The new operator, bind, call, and apply have no effect on this.

    var asyncFunction = (param, callback) => {
      window.setTimeout(() => {
      callback(param);
      }, 1);
    };
    
    // With a traditional function if we don't control
    // the context then can we lose control of `this`.
    var o = {
      doSomething: function () {
      // Here we pass `o` into the async function,
      // expecting it back as `param`
      asyncFunction(o, function (param) {
      // We made a mistake of thinking `this` is
      // the instance of `o`.
      console.log('param === this?', param === this);
      });
      }
    };
    
    o.doSomething(); // param === this? false
    

    In the above example, we lost the control of this. We can solve the above example by using a variable reference of this or using bind. With ES6, it becomes easier in managing the this as its bound to lexical scoping.

    var asyncFunction = (param, callback) => {
      window.setTimeout(() => {
      callback(param);
      }, 1);
    };
    
    var o = {
      doSomething: function () {
      // Here we pass `o` into the async function,
      // expecting it back as `param`.
      //
      // Because this arrow function is created within
      // the scope of `doSomething` it is bound to this
      // lexical scope.
      asyncFunction(o, (param) => {
      console.log('param === this?', param === this);
      });
      }
    };
    
    o.doSomething(); // param === this? true
    

    When not to Arrow functions

    Inside an object literal.

    var Actor = {
      name: 'RajiniKanth',
      movies: ['Kabali', 'Sivaji', 'Baba'],
      getName: () => {
         alert(this.name);
      }
    };
    
    Actor.getName();
    

    Actor.getName is defined with an arrow function, but on invocation it alerts undefined because this.name is undefined as the context remains to window.

    It happens because the arrow function binds the context lexically with the window object... i.e outer scope. Executing this.name is equivalent to window.name, which is undefined.

    Object prototype

    The same rule applies when defining methods on a prototype object. Instead of using an arrow function for defining sayCatName method, which brings an incorrect context window:

    function Actor(name) {
      this.name = name;
    }
    Actor.prototype.getName = () => {
      console.log(this === window); // => true
      return this.name;
    };
    var act = new Actor('RajiniKanth');
    act.getName(); // => undefined
    

    Invoking constructors

    this in a construction invocation is the newly created object. When executing new Fn(), the context of the constructor Fn is a new object: this instanceof Fn === true.

    this is setup from the enclosing context, i.e the outer scope which makes it not assigned to newly created object.

    var Message = (text) => {
      this.text = text;
    };
    // Throws "TypeError: Message is not a constructor"
    var helloMessage = new Message('Hello World!');
    

    Callback with dynamic context

    Arrow function binds the context statically on declaration and is not possible to make it dynamic. Attaching event listeners to DOM elements is a common task in client side programming. An event triggers the handler function with this as the target element.

    var button = document.getElementById('myButton');
    button.addEventListener('click', () => {
      console.log(this === window); // => true
      this.innerHTML = 'Clicked button';
    });
    

    this is window in an arrow function that is defined in the global context. When a click event happens, browser tries to invoke the handler function with button context, but arrow function does not change its pre-defined context. this.innerHTML is equivalent to window.innerHTML and has no sense.

    You have to apply a function expression, which allows to change this depending on the target element:

    var button = document.getElementById('myButton');
    button.addEventListener('click', function() {
      console.log(this === button); // => true
      this.innerHTML = 'Clicked button';
    });
    

    When user clicks the button, this in the handler function is button. Thus this.innerHTML = 'Clicked button' modifies correctly the button text to reflect clicked status.

    References: https://dmitripavlutin.com/when-not-to-use-arrow-functions-in-javascript/

    0 讨论(0)
提交回复
热议问题