什么时候应该在ECMAScript 6中使用Arrow函数?

这个问题针对的是那些在即将到来的ECMAScript 6(Harmony)中已经考虑过代码风格并且已经使用该语言的人。

使用() => {}function () {}我们得到了两种非常类似的在ES6中编写函数的方法。 在其他语言中,lambda函数通常是匿名的,但在ECMAScript中,任何函数都可以是匿名的。 这两种类型中的每一种都有独特的使用领域(即,当this需要明确地绑定或明确地不被绑定时)。 在这些领域之间,有大量的符号都可以做的情况。

ES6中的箭头功能至少有两个限制:

  • 不要与new工作
  • 修正了this势必范围在初始化
  • 除了这两个限制之外,箭头函数在理论上可以在几乎任何地方取代常规函数 在实践中使用它们的正确方法是什么? 如果使用箭头功能,例如:

  • “在他们工作的任何地方”,即在任何地方,函数不一定对this变量不可知,我们也不是创建一个对象。
  • 只有“需要的地方”,即事件监听器,超时,需要绑定到某个范围
  • 具有“短”功能但不具有“长”功能
  • 只适用于不包含其他箭头功能的功能
  • 我正在寻找的是在未来版本的ECMAScript中选择适当函数符号的指南。 准则需要清晰明确,以便可以向团队中的开发人员传授知识,并保持一致,以便它不需要不断地从一种函数表示法向另一种函数表示法进行重构。


    前一段时间,我们的团队将其所有代码(一个中等大小的AngularJS应用程序)迁移到使用Traceur Babel编译的JavaScript。 我现在在ES6及以后的版本中使用以下经验法则:

  • 在全局范围和Object.prototype属性中使用function
  • 使用对象构造函数的class
  • 在其他地方使用=>
  • 为什么几乎无处不在使用箭头功

  • 范围安全:当箭头函数一致使用时,所有内容都保证与root用户使用相同的thisObject 。 如果一个标准函数回调和一堆箭头函数混合在一起,那么范围就会变得混乱。
  • 紧凑:箭头功能更易于读取和写入。 (这可能看起来很有见地,所以我会进一步举几个例子)。
  • 清晰度:当几乎所有东西都是箭头功能时,任何常规function立即伸出来定义范围。 开发人员总是可以查找下一个更高级的function语句来查看thisObject是什么。
  • 为什么总是在全局作用域或模块作用域上使用常规函数?

  • 指示不应该访问thisObject的函数。
  • window对象(全局范围)最好是明确地解决。
  • 许多Object.prototype定义存在于全局范围内(认为String.prototype.truncate等),而且这些定义通常必须是function的类型。 始终在全局范围内使用function有助于避免错误。
  • 全局范围内的许多函数都是旧式类定义的对象构造函数。
  • 函数可以被命名为1。 这有两个好处:(1)编写function foo(){}比使用const foo = () => {} - 特别是在其他函数调用之外更不尴尬。 (2)函数名称显示在堆栈轨迹中。 尽管命名每个内部回调将是乏味的,但命名所有公共函数可能是一个好主意。
  • 函数声明被挂起(意味着它们可以在声明之前被访问),这是静态实用函数中的一个有用属性。

  • 对象构造函数

    试图实例化一个箭头函数会抛出一个异常:

    var x = () => {};
    new x(); // TypeError: x is not a constructor
    

    因此,函数对箭头函数的一个关键优势是函数可以作为对象构造函数使用:

    function Person(name) {
        this.name = name;
    }
    

    然而,功能相同的ES Harmony草案类定义几乎一样紧凑:

    class Person {
        constructor(name) {
            this.name = name;
        }
    }
    

    我期望使用前面的符号最终会被阻止。 简单的匿名对象工厂仍然可以使用对象构造函数表示法,其中对象是以编程方式生成的,但不是其他的。

    在需要对象构造函数的地方,应该考虑将函数转换为如上所示的class 。 该语法也适用于匿名函数/类。


    箭头功能的可读性

    坚持常规功能 - 范围安全可能是最好的论点 - 可能是箭头功能比常规功能更不可读。 如果你的代码首先不起作用,那么箭头函数可能看起来并不需要,并且当箭头函数没有被一致地使用时,它们看起来很丑。

    ECMAScript已经发生了很大的变化,因为ECMAScript 5.1为我们提供了函数Array.forEachArray.map和所有这些函数编程特性,这些函数使用了for循环之前会用到的函数。 异步JavaScript已经取得了相当大的进展。 ES6还将发布一个Promise对象,这意味着更多的匿名函数。 功能编程没有回头路可走。 在功能性JavaScript中,箭头函数优于常规函数。

    举例来说,这个(特别令人费解的)代码片段3:

    function CommentController(articles) {
        this.comments = [];
    
        articles.getList()
            .then(articles => Promise.all(articles.map(article => article.comments.getList())))
            .then(commentLists => commentLists.reduce((a, b) => a.concat(b)));
            .then(comments => {
                this.comments = comments;
            })
    }
    

    具有常规功能的同一段代码:

    function CommentController(articles) {
        this.comments = [];
    
        articles.getList()
            .then(function (articles) {
                return Promise.all(articles.map(function (article) { 
                    return article.comments.getList();
                }));
            })
            .then(function (commentLists) {
                return commentLists.reduce(function (a, b) {
                    return a.concat(b); 
                });
            })
            .then(function (comments) {
                this.comments = comments;
            }.bind(this));
    }
    

    虽然任何一个箭头函数都可以被标准函数所取代,但这样做会获得很少的收益。 哪个版本更具可读性? 我会说第一个。

    我认为使用箭头函数或常规函数的问题随着时间的推移会变得不那么重要。 大多数函数将成为类方法,这会剥夺function关键字,或者它们将成为类。 函数将继续用于通过Object.prototype修补类。 同时我建议保留function关键字的任何应该真的是类方法或类。


    笔记

  • 命名的箭头函数已在ES6规范中推迟。 他们可能还会添加未来的版本。
  • 根据规范草案“只要类不使用extend关键字,类声明/表达式就像函数声明一样创建构造函数/原型对”。 一个小的区别是类声明是常量,而函数声明不是。
  • 关于单语句箭头函数中块的注意事项:我喜欢在任何仅为副作用(例如赋值)调用箭头函数的地方使用块。 这样很显然返回值可以被丢弃。

  • 根据该提案,箭头旨在“解决和解决传统Function Expression几个常见问题”。 他们意在改善结合事项this词汇,并提供简洁的语法。

    然而,

  • 人们不能一致地将this词汇绑定在一起
  • 箭头函数语法细腻且不明确
  • 因此,箭头函数为混淆和错误创造了机会,应该从JavaScript程序员的词汇表中排除,并用function替代。

    关于this词汇

    this是有问题的:

    function Book(settings) {
        this.settings = settings;
        this.pages = this.createPages();
    }
    Book.prototype.render = function () {
        this.pages.forEach(function (page) {
            page.draw(this.settings);
        }, this);
    };
    

    箭功能打算解决这个问题,我们需要访问的属性this回调中。 目前已经有几种方法可以做到这一点:人们可以指定this一个变量,使用bind ,或使用可用的第三个参数Array聚集方法。 然而箭头似乎是最简单的解决方法,所以方法可以像这样重构:

    this.pages.forEach(page => page.draw(this.settings));
    

    不过,请考虑一下代码是否使用了像jQuery这样的库,这些库的方法专门用来绑定this 。 现在,有两个this的值来处理:

    Book.prototype.render = function () {
        var book = this;
        this.$pages.each(function (index) {
            var $page = $(this);
            book.draw(book.currentPage + index, $page);
        });
    };
    

    我们必须使用function以便each动态绑定this 。 我们在这里不能使用箭头功能。

    具有多个处理this值也可能会造成混淆,因为它很难知道哪些this作者说的是:

    function Reader() {
        this.book.on('change', function () {
            this.reformat();
        });
    }
    

    作者是否真的打算调用Book.prototype.reformat ? 还是他忘记绑定this ,并打算打电话给Reader.prototype.reformat ? 如果我们改变了处理程序,以箭头的功能,我们将同样不知道作者想动this ,又选择了一个箭头,因为它适合一行:

    function Reader() {
        this.book.on('change', () => this.reformat());
    }
    

    有人可能会说:“箭头有时候可能是错误的功能吗?也许,如果我们很少需要动态化this值,那么大部分时间使用箭头仍然可以。”

    但问问自己:“调试代码并发现错误的结果是由'边缘案例'引起的'是'值得'吗?”“我宁愿避免大部分时间都会遇到麻烦,但是100%的时间。

    有一个更好的方法:始终使用function (因此this总是可以动态绑定的),并且始终通过变量引用this 。 变量是词汇和假设许多名称。 this分配给变量将使您的意图清晰:

    function Reader() {
        var reader = this;
        reader.book.on('change', function () {
            var book = this;
            book.reformat();
            reader.reformat();
        });
    }
    

    此外,总是将this分配给一个变量(即使有一个this或没有其他功能),可以确保即使在代码改变后,他的意图仍然保持清晰。

    此外,动态this并不例外。 jQuery在超过5000万个网站上使用(截至2016年2月撰写)。 这里有其他的API绑定this动态:

  • 摩卡(昨天下载约120k)通过this方式公开了测试方法。
  • Grunt(昨天约63k下载)通过this暴露了构建任务的方法。
  • Backbone(昨天下载约22k)定义了访问this方法。
  • 事件API(如DOM)通过this引用EventTarget
  • 修补或扩展的原型API引用具有this实例。
  • (通过http://trends.builtwith.com/javascript/jQuery和https://www.npmjs.com统计。)

    你很可能需要动态this绑定了。

    词法this有时预期,但有时没有; 只是作为一个动态this有时预期,但有时没有。 值得庆幸的是,有一种更好的方式,它总是产生并传达预期的约束。

    关于简洁语法

    箭头函数成功地为函数提供了“较短的语法形式”。 但是这些较短的功能会让你更成功吗?

    x => x * x “比function (x) { return x * x; }更容易阅读” function (x) { return x * x; } function (x) { return x * x; } ? 也许是这样,因为它更可能产生一行简短的代码。 适应戴森的阅读速度和线条长度对从屏幕阅读效果的影响,

    中等长度的行(每行55个字符)似乎支持正常和快速的有效读取。 这产生了最高水平的理解。 。 。

    对条件(三元)操作符和单行if语句进行类似的证明。

    但是,您是否真的在撰写提案中提到的简单数学函数? 我的域名不是数学的,所以我的子程序很少如此优雅。 相反,我通常会看到箭头函数打破了列限制,并且由于编辑器或样式指南而换行到另一行,从而导致戴森定义的“可读性”无效。

    有人可能会说,“如果可能的话,使用简短版本的短版本怎么样?” 但是现在一种文体规则与语言约束相矛盾:“尽量使用最短函数符号,记住有时候只有最长的符号才会按预期结合this 。” 这种混合使箭头特别容易被误用。

    箭头函数语法有许多问题:

    const a = x =>
        doSomething(x);
    
    const b = x =>
        doSomething(x);
        doSomethingElse(x);
    

    这两个函数在语法上都是有效的。 但doSomethingElse(x); 不在b的主体中,它只是一个缩进,顶级的陈述。

    当扩展到块形式时,不再有隐式return ,哪一个可以忘记恢复。 但是这种表达可能只是为了产生副作用,所以谁知道明确的return是否有必要进行?

    const create = () => User.create();
    
    const create = () => {
        let user;
        User.create().then(result => {
            user = result;
            return sendEmail();
        }).then(() => user);
    };
    
    const create = () => {
        let user;
        return User.create().then(result => {
            user = result;
            return sendEmail();
        }).then(() => user);
    };
    

    可以将其作为休息参数来解析为扩展运算符:

    processData(data, ...results => {}) // Spread
    processData(data, (...results) => {}) // Rest
    

    分配可以与默认参数混淆:

    const a = 1;
    let x;
    const b = x => {}; // No default
    const b = x = a => {}; // "Adding a default" instead creates a double assignment
    const b = (x = a) => {}; // Remember to add parens
    

    块看起来像对象:

    (id) => id // Returns `id`
    (id) => {name: id} // Returns `undefined` (it's a labeled statement)
    (id) => ({name: id}) // Returns an object
    

    这是什么意思?

    () => {}
    

    作者是否打算创建一个no-op,或者返回一个空对象的函数? (考虑到这一点,我们是否应该放置{ after => ?我们是否应该仅限于表达式语法?这将进一步减少箭头的频率。)

    =>看起来像<=>=

    x => 1 ? 2 : 3
    x <= 1 ? 2 : 3
    
    if (x => 1) {}
    if (x >= 1) {}
    

    要立即调用箭头函数表达式,必须将()在外面,但把()在里面是有效的,并可能是故意的。

    (() => doSomething()()) // Creates function calling value of `doSomething()`
    (() => doSomething())() // Calls the arrow function
    

    虽然,如果写(() => doSomething()()); 为了编写立即调用的函数表达式,根本不会发生任何事情。

    考虑到上述所有情况,很难说箭头函数“更容易理解”。 人们可以学习使用这种语法所需的所有特殊规则。 是不是真的值得吗?

    function的语法无疑是一般化的。 单独使用function意味着语言本身可以防止编写混淆的代码。 要编写在所有情况下都应该在语法上理解的过程,我选择function

    关于指导方针

    您请求的指导方针需要“清晰”和“一致”。 使用箭头函数最终将导致语法上有效的,逻辑上无效的代码,这两个函数形式交织在一起,有意义且任意。 因此,我提供以下内容:

    ES6功能符号指南:

  • 始终使用function创建程序。
  • 始终this分配给一个变量。 不要使用() => {}

  • 创建箭头函数是为了简化函数scope并通过使其更简单来解决this关键字。 他们利用=>语法,它看起来像一个箭头。

    注意:它不会取代现有的功能。 如果用箭头函数替换每个函数语法,则不会在所有情况下都能正常工作。

    让我们来看看现有的ES5语法,如果this关键字位于对象的方法(属于某个对象的函数)内,它会引用什么?

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

    上面的代码片段会引用一个object并打印出名字"RajiniKanth" 。 我们来看看下面的代码片段,看看这里指出的是什么。

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

    那么如果this关键字在method's function呢?

    这里指的是window object不是inner function因为它已经超出了scope 。 因为this总是引用它所在的函数的所有者,对于这种情况 - 因为它现在超出了作用域 - 窗口/全局对象。

    当它位于object方法的内部时 - function的所有者就是对象。 因此,这个关键字绑定到对象。 然而,当它位于函数内部时,无论是单独存在还是在另一个方法中,它都会引用window/global对象。

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

    在我们的ES5有许多方法可以解决这个问题,让我们在深入研究ES6箭头功能之前先看看它是如何解决这个问题的。

    通常你会在方法的内部函数外创建一个变量。 现在, 'forEach'方法可以访问this object's属性及其值。

    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();
    

    使用bind将引用方法的this关键字附加到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();
    

    现在使用ES6箭头函数,我们可以更简单地处理lexical scoping问题。

    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更像函数语句,只是它们将此bindparent scope 。 如果arrow function is in top scope ,则this参数将引用window/global scope ,而常规函数内的箭头函数将其参数与其外部函数相同。

    arrow功能this势必给封闭scope在创建时,不能改变。 新操作符,绑定,调用和应用对此没有影响。

    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
    

    在上面的例子中,我们失去了对此的控制。 我们可以通过使用的可变参考解决上面的例子中this或使用bind 。 随着ES6,它成为管理越容易this作为其绑定到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
    

    何时不用箭头功能

    在对象文字中。

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

    Actor.getName是用箭头函数定义的,但在调用时,它警告未定义,因为this.nameundefined因为上下文保持到window

    发生这种情况是因为箭头函数与window object (即外部范围)进行词汇绑定。 执行this.name相当于window.name ,它是未定义的。

    对象原型

    prototype object上定义方法时适用同样的规则。 而不是使用箭头函数来定义sayCatName方法,它会带来不正确的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
    

    调用构造函数

    this在构造调用中是新创建的对象。 当执行新的Fn()时, constructor Fn的上下文是一个新对象: this instanceof Fn === true

    this是从封闭的上下文中设置的,也就是外部范围,它不会将其分配给新创建的对象。

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

    用动态上下文回调

    Arrow函数在声明上静态地绑定context ,并且不可能使其变为动态的。 将事件监听器附加到DOM元素是客户端编程中的一项常见任务。 一个事件触发处理函数,并将其作为目标元素。

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

    this是在全局上下文中定义的箭头函数中的窗口。 当发生点击事件时,浏览器尝试使用按钮上下文调用处理函数,但箭头函数不会更改其预定义的上下文。 this.innerHTML相当于window.innerHTML ,没有任何意义。

    你必须应用一个函数表达式,它允许根据目标元素来改变它:

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

    当用户点击按钮时,处理函数中的这个按钮就是按钮。 因此, this.innerHTML = 'Clicked button'正确修改按钮文本以反映点击状态。

    参考文献:https://rainsoft.io/when-not-to-use-arrow-functions-in-javascript/

    链接地址: http://www.djcxy.com/p/51697.html

    上一篇: When should I use Arrow functions in ECMAScript 6?

    下一篇: Anonymous function returns class properties? PHP