简洁性

箭头函数在初步了解阶段,你可以发现大部分都是在说箭头函数的简洁性,说它是一个更短的函数。

var elements = [
  'Hydrogen',
  'Helium',
  'Lithium',
  'Beryllium'
];

elements.map(function(element) {
  return element.length;
});

elements.map(element => element.length);

确实,在代码书写上使用它使得代码更加简洁明了,但是它并不是万能的,在一些复杂的地方,箭头函数不见得就会很合适。

var dollabillsyall = (strings, ...values) =>
    strings.reduce((s, v, idx) => {
        if (idx > 0) {
            if (typeof values[idx - 1] == "number") {
                // 看,这里也使用了插入字符串字面量!
                s += `$${values[idx-1].toFixed( 2 )}`;
            } else {
                s += values[idx - 1];
            }
        }
        return s + v;
    }, "");

这个例子中,我们使用箭头函数只是去掉了return、function关键词和函数外层的{},对于这段代码来说,可读性真的有改善吗?

实际上我认为,由于省略了return和函数外层{},在一定程度上模糊了函数内部主体,以往只需要看到return strings.reduce就不需要往上看了,现在我们必须看到=>才明白dollabillsyall函数的返回值,当代码更加复杂的时候带来的阅读阻碍也会增加,特别是习惯了通过function关键词来进行断句的人。

虽然不是一条严格的规律,但我认为 => 箭头函数转变带来的可读性提升与被转化函数的长度负相关。这个函数越长,=> 带来的好处就越小;函数越短,=> 带来的好处就越大。

我认为更合理的做法是只在确实需要简短的在线函数表达式的时候才采用 =>,而对于那些一般长度的函数则无需改变。

重点 this

过度关注箭头函数的简洁性,使我们忽略了它的一个重要细节,就是this的行为绑定,实际上箭头函数的主要设计目的就是为了以特定的方式改变 this 的行为特性,解决 this 相关编码的一个特殊而又常见的痛点。

This被证明是令人厌烦的面向对象风格的编程

在以前:

function Person() {
  var that = this;
  that.age = 0;

  setInterval(function growUp() {
    // 回调引用的是`that`变量,其值是预期的对象。
    that.age++;
  }, 1000);
}

我们常常在函数作用域中创建一个that用于存储this变量,这样在后续的函数使用时,通过作用域链获取到它,从而做一些操作。

这种做法太痛苦了,利用箭头函数我们可以更加方便的实现这样的功能。

function Person(){
  this.age = 0;

  setInterval(() => {
    this.age++; // |this| 正确地指向 p 实例
  }, 1000);
}

var p = new Person();

其原理是:箭头函数本身不会创建自己this,他只会继承自己的作用域链的上一层的this

我们来看个例子:

const obj1 = {
    value: "abc",
};

const obj2 = {
    value: "def",
    getArrowFn: function() {
        return () => console.log(this.value);
    },
};

const arrowFn = obj2.getArrowFn();
arrowFn.call(obj1); // 输出 'def'

再看这个例子:

const obj1 = {
    value: "abc",
};

const obj2 = {
    value: "def",
    getArrowFn: function() {
        return () => console.log(this.value);
    },
};

obj1.getArrowFn = obj2.getArrowFn;
obj1.getArrowFn()(); //输出 'abc'

我举这两个例子,主要是就是要反驳一个观点,就是箭头函数的this就是定义时所在的对象

定义时按正常理解,就是书写时,我在写这个箭头函数的时候,this就已经确定了,那么我再回过头来看看第二个例子,obj1.getArrowFn = obj2.getArrowFn很明显是运行时的,所以这种说法是片面的(并不准确)。

如果想要理解箭头函数的this怎么确定的,我们就要更加深入JavaScript运行原理去寻找答案了。

我总结一下就是:

JavaScript运行需要经过三个主要阶段:解析 -> 编译 -> 执行;在之前的文章中我说了,this是存在于执行上下文的,但是这不是this最开始出现的阶段,在JavaScript解析阶段会有两个步骤:词法分析、语法分析;在词法分析时其实就已经有this了,但是这个时候this可以理解为是一个占位,此时this并不知道它是谁,只有在执行阶段才能确定是谁。

但是在箭头函数中就略有不同,在词法分析阶段,它的this就已经关联了,它会关联上外层的this,如果我们把this比作一个引用类型就很好理解了,箭头函数持有的是外部的this引用,这样当外部的this确认后,箭头函数的this也就确认了。

这也能说明我上述的两个例子,第一个例子运行getArrowFn时,this就已经明确是obj2了,所以当我们运行箭头函数时,得到的也是obj2的value;第二个例子运行getArrowFn时,getArrowFn是通过obj1调用,this也自然是指向它,所以箭头函数输出的事obj1的value

所以我们可以明确的知道,箭头函数的this在分析阶段就会关联上外层this(定义时),但是只有在运行时才能知道this是谁。

使用箭头函数规则

  • 如果你有一个简短单句在线函数表达式,其中唯一的语句是 return 某个计算出的值,且这个函数内部没有 this 引用,且没有自身引用(递归、事件绑定 / 解绑定),且不会要求函数执行这些,那么可以安全地把它重构为 => 箭头函数。
  • 如果你有一个内层函数表达式,依赖于在包含它的函数中调用 var self = this hack 或者 .bind(this) 来确保适当的 this 绑定,那么这个内层函数表达式应该可以安全地转换为 => 箭头函数。
  • 如果你的内层函数表达式依赖于封装函数中某种像 var args = Array.prototype.slice.call(arguments) 来保证 arguments 的词法复制,那么这个内层函数应该可以安全地转换为 => 箭头函数。
  • 所有的其他情况——函数声明、较长的多语句函数表达式、需要词法名称标识符(递归等)的函数,以及任何不符合以上几点特征的函数——一般都应该避免 => 函数语法。
分类: 你不知道的JavaScript 标签: this作用域词法箭头函数简洁

评论

全部评论 2

  1. 李努力
    李努力
    Google Chrome Windows 10
    其实你反驳的那个观点,方法是用function声明的啊,function的this就是谁调用,this指向哪个对象啊,你那两个方法只是返回了一个箭头函数
    1. 木灵鱼儿
      木灵鱼儿
      FireFox Windows 10
      @李努力没有,早期去搜箭头函数的教程,会看到一些人说箭头函数的this就是声明时确定的

目录