1.4.4 箭头函数

在箭头函数之前,每个新函数都定义了自己的 this 值,比如构造函数中的 this 值是新对象,比如严格模式下全局作用域里的 this 值是 undefined,比如函数作为对象的方法被调用时,this 值是基础对象等等。但事实证明,对于面向对象的编程风格,这样的处理方式并不理想。

function Person() {
  this.age = 0;
  
  setInterval(function growUp() {
    this.age++;
    console.log(this.age);
  }, 1000);
}

const p = new Person();

以上代码,在非严格模式下,setInterval() 的回调函数 growUp() 会将 this 定义为全局对象,而不是预期的 Person() 构造函数中定义的 this 值——指向新对象。

为了解决这个问题,在 ECMAScript 3/5 中,可以将 this 赋值给一个变量(形成闭包)。比如:

function Person() {
  const self = this;
  self.age = 0;

  setInterval(function growUp() {
    self.age++;
    console.log(self.age);
  }, 1000);
}

const p = new Person();

也可以创建绑定函数,然后将正确的 this 值传递给要调用的函数。比如:

function Person() {
    this.age = 0;

    setInterval((function growUp() {
        this.age++;
        console.log(this.age);
    }).bind(this), 1000); // 绑定
}

const p = new Person();

有了箭头函数之后,这种问题处理起来就简单多了。因为箭头函数没有自己的 this,所以就会直接使用封闭执行上下文(the enclosing execution contexts)里的 this 值。比如:

function Person() {
  this.age = 0;

  setInterval(() => {
    this.age++;
    console.log(this.age);
  }, 1000);
}

const p = new Person();

1. 箭头函数的限制

箭头函数表达式是传统函数表达式的精简替代方案,它有一些限制所以并不适用于所有情况。与传统函数相比,箭头函数的不同之处在于:

  1. 箭头函数没有自己的 thissuper 绑定。进而有以下特性:

    1. 没有 prototype 属性

    2. 不应该用作对象的方法(因为没动态绑定 this 值)

    3. 不能用作构造函数,不能访问 new.target 关键字

    4. 不适用于 call(), apply()bind() 这些实例方法

      • 因为它们三个允许方法在不同的作用域内执行

      • 而箭头函数的 this 值是基于定义箭头函数时的作用域来建立的(自身没绑定 this 值)

  2. 箭头函数没有自己的 arguments 对象

    • 替代方法:剩余参数 (...args) => {}

  3. 箭头函数不能在函数体内使用 yield 关键字(用于暂停和恢复生成器函数)

    • 所以箭头函数不能用作生成器

    • 注:可以在内部嵌套的普通函数里使用 yield

const Foo = () => {};

// 没有 prototype 属性
console.log(Foo.prototype); // undefined

1.1 不应该用作方法

“方法”是一个函数,它作为对象的属性。对象有两种方法:

  • 静态方法:是直接在对象的构造函数上调用的任务

  • 实例方法:是由对象的实例执行的内置任务

当我们说 F 是 O 的方法时,通常意味着 F 使用 O 作为它的 this 绑定。

然而,如果函数属性对 this 值没什么特殊的处理行为,或者函数属性就根本没有动态绑定 this 值(比如箭头函数 => 和绑定函数 .bind()),那么该函数属性一般不会被认为是(对象的)方法。

当箭头函数作为方法使用时,会发生什么?来看几个例子。

1.1.1 普通对象

"use strict";

const obj = {
    i: 10,
    func() {
        console.log(this.i, this);
    },
    arrowFunc: () => console.log(this.i, this)
};
obj.func();
obj.arrowFunc();

以上代码的执行结果,如下:

在严格模式下,全局作用域里的 this 值是 undefined

1.1.2 在 class 中

let a = "global";

class C {
    a = "inside class C";
    func() {
        console.log(this.a);
    }
    arrowFunc = () => {
        console.log(this.a);
    };
}

const c = new C();
c.func();
c.arrowFunc();

const { func, arrowFunc } = c;
func();
arrowFunc();

以上代码的执行结果,如下:

inside class C
inside class C
TypeError: Cannot read properties of undefined (reading 'a')
inside class C

class 的 body 有自己的 this 值。箭头函数作为类字段,它里面的 this 会直接继承周围的 this 值——要么指向类本身(静态字段)要么指向类实例(实例字段)。

在上面的例子中,箭头函数的 this 是指向了类的实例,又因为这里的箭头函数形成了个闭包(而不是普通函数的绑定自己的 this),所以它的 this 值不会根据执行上下文的改变而改变。

1.1.3 等价写法

箭头函数属性通常被称为“自动绑定方法”,它和普通方法的以下用法是等效的:

class C {
  a = 1;
  constructor() {
    this.method = this.method.bind(this); // 自动绑定 this
  }
  method() {
    console.log(this.a);
  }
}

注意,以上代码的类字段是在实例(而不是原型)上定义的,所以每创建一个实例都会创建一个新的函数引用并分配一个新的闭包,这可能会导致比正常未绑定方法更多的内存使用。

1.2 不能用作构造函数

要在 JavaScript 中调用类的构造函数,通常使用 new 运算符将新对象的引用分配给变量。

构造函数是一种特别的类对象,它可以被实例化。构造函数能实例化一个对象,还能提供对其私有信息的访问。构造函数的概念应用在大多数面向对象的编程语言中。

const Foo = () => {};

// 不能用 new
const foo = new Foo(); // TypeError: Foo is not a constructor

// 不能访问 new.target
const bar = () => new.target; // SyntaxError: new.target expression is not allowed here

通常情况 . 的左侧是对象右侧是属性,但 new.target 里的 new 不是对象,所以 new.target 被称为伪属性。它能让我们检测函数和构造函数是否是用 new 运算符创建的。如果是用 new 运算符调用的,那 new.target 就返回函数或构造函数的引用,否则就返回 undefined

而箭头函数里的 new.target 会直接继承周围作用域里的值从而使其失去原本的含义,所以在箭头函数中它是不能被访问的。

1.3 天然闭包

箭头函数没有绑定自己的 this 值,它会直接继承周围的作用域(会在词法上绑定上下文的 this )。

the value of the enclosing lexical context's this

使用箭头函数的最大好处可能就是使用 setTimeout()EventTarget.addEventListener() 之类的方法,它们通常需要某种闭包、call、apply 或 bind 来确保函数在正确的作用域内执行。

2. 更简短的写法

与普通的函数表达式相比,箭头函数表达式具有更短的语法。

=>,也叫胖(fat)箭头,以区分未来的假设语法 ->

2.1 匿名函数

箭头函数始终是匿名的。将传统的匿名函数改为等价的箭头函数,如下:

// 匿名函数
(function(a) {
  return a + 100;
});

// 第1种:去掉 function 关键字,增加 =>
(a) => {
  return a + 100;
};

// 第2种:去掉函数体的括号 {},去掉 return 关键字(隐含返回)
(a) => a + 100;

// 第3种:去掉参数的括号 ()
a => a + 100;

当参数有多个或者为空时,参数的括号 () 不能省。比如:

// 参数括号不能省
(a, b) => a + b + 100; // 多个参数
() => a + b + 100; // 没有参数

当函数体有多条语句时,函数体的括号 {}return 关键字不能省。比如:

// {} 和 return 不能省:当函数体内有多条语句时
(a, b) => {
  const chuck = 42;
  return a + b + chuck;
};

当函数返回对象字面量表达式时,函数体的括号 {} 不能省。比如:

() => ({ name: "hi" }); // 当返回对象字面量表达式时,{} 不能省
() => { name: "hi" };   // 否则 {} 里的代码会被解析成语句序列
                        // 其中 name 被视为标签,而不是对象字面量的 key

2.2 命名函数

对于传统的命名函数,我们把箭头(函数)表达式视为变量。如下:

// 命名函数
function foo(a) {
  return a + 100;
}

// 改为箭头函数
const foo2 = (a) => a + 100;

2.3 函数参数

箭头函数的参数也支持剩余参数、默认参数和解构赋值。如下:

(a, b, ...r) => expression; // 剩余参数

(a=1, b=2, c) => expression; // 默认参数

([a, b] = [10, 20]) => a + b;  // 解构赋值
({a, b} = {a:10, b:20}) => a + b;

2.4 更多场景

在有些功能模式中,用更简短的函数会更合适。比如:

const fruits = ["strawberry", "apple", "peach", "watermelon"];
// 普通函数
const fruitNameLen = fruits.map(function (s) {   // [10, 5, 5, 10]
    return s.length;
});
// 改成箭头函数
const fruitNameLen2 = fruits.map(s => s.length); // [10, 5, 5, 10]
const arr = [5, 6, 13, 0, 1, 18, 23];
const sum = arr.reduce((a, b) => a + b); // 66
const even = arr.filter(v => v % 2 === 0); // [6, 0, 18]
const double = arr.map(v => v * 2); // [10, 12, 26, 0, 2, 36, 46]
// 更简洁的 promise 链
promise
  .then((a) => {})
  .then((b) => {});
// 视觉上更容易解析的无参数箭头函数
setTimeout(() => {
  console.log('I happen sooner');
  setTimeout(() => {
    // deeper code
    console.log('I happen later');
  }, 1);
}, 1);

3. 主要参考

4. 相关阅读

Last updated