现代 JavaScript 中最引人注目的功能之一是引入了箭头函数,用 => 来标识。 这种函数有两大优点 – 非常简洁的语法,和更直观的作用域和 this的绑定。 这些优点有时导致箭头函数比其他形式的函数声明更受欢迎。 然而,就像工程中的任何东西一样,箭头函数优点很明显,同时也带来了一些负面的东西。 使用他们的时候需要权衡一下。 学习如何权衡是更好地使用箭头函数的关键。 在本文中,我们将首先回顾箭头函数的工作原理,然后深入研究箭头函数改进代码的示例,最后深入研究箭不建议使用头函数的示例。 JavaScript 箭头函数究竟是什么?这些是匿名函数,它们有自己的特殊语法,接受一定数量的参数,并在其封闭的作用域的上下文(即定义它们的函数或其他代码)中操作。 让我们依次分解这些部分。 箭头函数语法箭头函数具有单一的总体结构,然后在特殊情况下可以通过多种方式简化它们。 核心结构如下所示: (argument1, argument2, ... argumentN) => { // function body}括号内的是参数列表,后跟“胖箭头”(=>),最后是函数体。 这与传统函数非常相似,我们只是省略 function 关键字并在参数后添加一个胖箭头(=>)。 然而,有许多方法可以简化箭头函数。 首先,如果函数体是单个表达式,则可以不使用花括号并将其置于内联中(省略大括号直接将表达式写在一行中)。 表达式的结果将由函数返回。 例如: const add = (a, b) => a + b;其次,如果只有一个参数,你甚至可以省略参数的括号。例如: const getFirst = array => array[0];正如您所看到的,这是一些非常简洁的语法,我们将重点介绍后面的好处。 高级语法有一些高级语法可以了解一下。 首先,如果您尝试使用内联单行表达式语法,但您返回的值是对象字面量。您可能会认为这看起来应该是这样的: (name, description) => {name: name, description: description};问题是这种语法比较含糊不清,容易引起歧义 : 看起来好像你正试图创建一个传统的函数体。 如果你碰巧想要一个对象的单个表达式,请用括号包裹该对象: (name, description) => ({name: name, description: description});封闭的上下文作用域实际上,这意味着 this 和 arguments 都是从它们的父函数继承而来的。 例如,使用和不使用箭头函数比较以下代码: const test = { name: 'test object', createAnonFunction: function() { return function() { console.log(this.name); console.log(arguments); }; }, createArrowFunction: function() { return () => { console.log(this.name); console.log(arguments); }; }};我们有一个简单的 test 对象,有两个方法 – 每个方法都返回一个匿名函数。 不同之处在于第一个方法使用传统函数表达式,而后者中使用箭头函数。 如果我们使用相同参数,在控制台中运行它们,我们会得到完全不一样的结果。 > const anon = test.createAnonFunction('hello', 'world');> const arrow = test.createArrowFunction('hello', 'world'); > anon();undefined{} > arrow();test object{ '0': 'hello', '1': 'world' }第一个匿名函数有自己的函数上下文,因此当您调用它时,test 对象的 this.name 没有可用的引用,也没有创建它时调用的参数。 另一个,箭头函数具有与创建它的函数完全相同的函数上下文,使其可以访问 argumetns 和 test对象。 使用箭头函数改进您的代码传统 lambda 函数的主要用例之一,就是用于遍历列表中的项,现在用 JavaScript 箭头函数实现。 比如你有一个有值的数组,你想去 map 遍历每一项,这时使用箭头函数非常理想: const words = ['hello', 'WORLD', 'Whatever'];const downcasedWords = words.map(word => word.toLowerCase());一个非常常见的例子是提取对象中的某个特定值: const names = objects.map(object => object.name);类似地,当用现代迭代样式取代传统的 for 循环,一般我们使用 forEach 循环,箭头函数能够保持 this 来自于父级,让他们非常直观 类似的,当用 forEach 来替换传统 for循环的时候,实际上箭头函数会直观的保持 this来自于父一级 this.examples.forEach(example => { this.runExample(example);});Promises 和 Promise 链箭头函数的另一个可以使代码更清晰,更直观的地方是管理异步代码。 但是,虽然使用 promises 仍然需要定义在异步代码或调用完成后运行的函数。 这是箭头函数的理想位置,特别是如果您生成的函数是有状态的,同时想引用对象中的某些内容。 例如: this.doSomethingAsync().then((result) => { this.storeResult(result);});对象转换箭头函数的另一个常见且极其强大的用途是封装对象转换。 例如,在 Vue.js 中,有一种通用模式,用于使用 mapState 将 Vuex 存储的各个部分直接包含到 Vue 组件中。 这涉及定义一组 “mappers” ,这些 “mappers” 将从原始的完整的 state 对象转换为提取所涉及组件所需的内容。 这些简单的转换使用箭头函数再合适不过了。比如: export default { computed: { ...mapState({ results: state => state.results, users: state => state.users, }); }}你不应该使用箭头函数的情景在许多情况下,使用箭头函数不是一个好主意。 他们不仅不会帮助你,而且会给你带来一些不必要的麻烦。 第一个是对象的方法。 这是一个函数上下文的例子,这对于我们理解很有帮助。 有一段时间使用 Class(类)属性语法和箭头函数的组合,作为创建“自动绑定方法”的方式,例如, 事件处理程序可以使用,但仍然绑定到类的方法。 这看起来像是这样的: class Counter { counter = 0; handleClick = () => { this.counter++; }}这样,即使 handleClick 由事件处理程序调用,而不是在 Counter 实例的上下文中调用,它仍然可以访问实例的数据。 这种方法的缺点很多,在本文中很好地记录。 虽然使用这种方法确实为您提供了具有绑定函数的快捷方式,但该函数以多种不直观的方式运行,如果您尝试将此对象作为原型进行子类化/使用,则会不利于测试,同时也会产生很多问题。 相反,使用常规函数,如果需要,将其绑定到构造函数中的实例: class Counter { counter = 0; handleClick() { this.counter++; } constructor() { this.handleClick = this.handleClick.bind(this); }}深层的调用链箭头函数可能让你遇到麻烦的另一个地方是,它们被用于许多不同的组合,特别是在函数深层调用链中。 核心原因与匿名函数相同 – 它们给出了非常糟糕的堆栈跟踪。 如果你的函数只是向下一级,比如在迭代器里面,那也不是太糟糕,但是如果你把所有的函数定义为箭头函数,并在它们之间来回调用,你就会陷入困境 遇到一个错误的时候,只是收到错误消息,如: {anonymous}(){anonymous}(){anonymous}(){anonymous}(){anonymous}()有动态上下文的函数箭头函数可能让您遇到麻烦的最后一种情况就是吗, this 是动态绑定的时候。 如果您在这些位置使用箭头函数,那么动态绑定将不起作用,并且你(或稍后使用你的代码的其他人)可能会对事情未按预期执行的原因感到困惑。 一些典型的例子: - 事件处理程序是通过将 this 设置为事件的 currentTarget 属性来调用。
- 如果您仍在使用 jQuery ,则大多数 jQuery 方法将 this 设置为已选择的 dom 元素。
- 如果您正在使用 Vue.js ,则方法和计算函数通常将 this 设置为 Vue 组件。
当然你可以故意使用箭头函数来覆盖这种行为,但特别是在 jQuery 和 Vue 的情况下,这通常会干扰正常运行,让你感到困惑的是为什么看起来与附近其他代码相同的代码不起作用。 总结箭头函数是 JavaScript 语言的一个非常有必要的补充,并且在许多情况下使代码更符合人们的阅读习惯。 然而,像所有其他特性一样,它们有优点和缺点。 我们应该将它们作为我们工具箱中的另一个工具,而不是作为所有函数的全面替代品。
|