箭头函数与常规函数

在 JavaScript 中,可以使用许多的方法声明函数。首先通常方法是使用 function 关键字:

// 函数声明
function greet(who) {
  return `Hello, ${who}!`;
}
// 函数表达式
const greet = function (who) {
  return `Hello, ${who}`;
};

我将函数声明和函数表达式称之为_常规函数_。

第二种方式是在 ES2015 中加入的_箭头函数语法_:

const greet = (who) => {
  return `Hello, ${who}!`;
};

面对两种声明函数的常规与箭头语法,你是否使用其中的一种替代另一种呢?这是个有意思的问题。

在这篇文章中,我将展示这两者之间的主要不同,让你能够在需要时选择正确的语法。

#this 的值

#常规函数

在 JavaScript 常规函数中,this 的值(又称为执行上下文)是动态的。

动态的上下文意味着 this 的值依赖于该函数是如何被引用的。在 JavaScript 中,有 4 种方式引用常规函数。

在_简单引用_中 this 的值等价于全局对象(或者 undefined,如果该函数运行在严格模式下):

function myFunction() {
  console.log(this);
}

// 简单引用
myFunction(); // 输出全局对象(window)

在_方法引用_中 this 的值为拥有该方法的对象:

const myObject = {
  method() {
    console.log(this);
  },
};
// 方法引用
myObject.method(); // 输出 "myObject"

在使用 myFunc.call(context, arg1, ..., argN)myFunc.apply(context, [arg1, ..., argN]) 的_间接引用_中 this 的值等价于第一个参数:

function myFunction() {
  console.log(this);
}

const myContext = { value: 'A' };

myFunction.call(myContext); // 输出 { value: 'A' }
myFunction.apply(myContext); // 输出 { value: 'A' }

在_构造函数引用_中使用 new 关键字的 this 等价于新创建的实例:

function MyFunction() {
  console.log(this);
}

new MyFunction(); // 输出一个 MyFunction 的实例

#箭头函数

箭头函数内部 this 的行为与常规函数的 this 行为有很多的不同。

无论怎样或在任意地方被执行,箭头函数内部 this 的值总是等价于它外部的函数的 this 值。一句话,箭头函数绑定 this 解析。换句话说,箭头函数不能声明它自己的执行上下文。

在下面的例子,myMethod() 是在函数 callback() 外部的箭头函数:

const myObject = {
  myMethod(items) {
    console.log(this); // 输出 "myObject"
    const callback = () => {
      console.log(this); // 输出 "myObject"
    };
    items.forEach(callback);
  },
};

myObject.myMethod([1, 2, 3]);

箭头函数 callback() 内部的 this 值等价于外部函数 myMethod()this 值。

this 绑定解析是箭头函数一个巨棒的功能。当在方法内部使用回调函数时可以确认箭头函数不声明它自己的 this:不再需要使用 const self = thiscallback.bind(this) 之类的变通技巧。

#构造函数

#常规函数

如同上面章节所见,常规函数很容易创建构建函数。举例来说,创建 Car() 函数的实例 car:

function Car(color) {
  this.color = color;
}

const redCar = new Car('red');
redCar instanceof Car; // => true

Car 是一个常规函数,并且使用 new 关键字,它创建属于 Car 类型的新实例。

#箭头函数

一件重要的事是 this 绑定解析的箭头函数不能被用于构造函数。

如果尝试在箭头函数前面使用 new 关键字,JavaScript 将抛出一个错误:

const Car = (color) => {
  this.color = color;
};

const redCar = new Car('red');
// TypeError: Car is not a constructor

#arguments 对象

#常规函数

在常规函数的内部,arguments 是一个特别的类数组对象,该对象包含函数引用的参数列表。

让我们引用 2 个参数运行 myFunction 函数:

function myFunction() {
  console.log(arguments);
}

myFunction('a', 'b'); // 输出 { 0: 'a', 1: 'b'}

类数组对象 arguments 包含函数运行参数:'a''b''

#箭头函数

注意,在箭头函数内部没有特殊的 arguments 对象。

再次(与 this 值相同),按 arguments 词法解析对象:箭头函数中访问 arguments 从外部函数一致。

让我们尝试在箭头函数内部访问 arguments

function myRegularFunction() {
  const myArrowFunction = () => {
    console.log(arguments);
  };

  myArrowFunction('c', 'd');
}

myRegularFunction('a', 'b');
// 输出 { 0: 'a', 1: 'b' }

箭头函数 myArrowFunction() 引用参数 c, d。然而,arguments 对象依然等价于 myRegularFunction() 的参数:a, b

如果想要直接访问箭头函数的参数,可以使用扩展参数功能:

function myRegularFunction() {
  const myArrowFunction = (...args) => {
    console.log(args);
  };

  myArrowFunction('c', 'd');
}

myRegularFunction('a', 'b');
// 输出 { 0: 'c', 1: 'd' }

...args 扩展参数聚合箭头函数执行时参数:{ 0: 'c', 1: 'd' }

#内部返回 return

#常规函数

仅能通过 return expression 表达式声明函数的返回结果值:

function myFunction() {
  return 42;
}

myFunction(); // => 42

如果缺失 return 声明,或者在 return 声明之后没有表达式,常规函数将返回 undefined

function myEmptyFunction() {
  42;
}

function myEmptyFunction2() {
  42;
  return;
}

myEmptyFunction(); // => undefined
myEmptyFunction2(); // => undefined

#箭头函数

你可以在箭头函数中使用与常规函数相同的方式返回值,但有一种例外用法。

如果箭头函数包含一个表达式,并且删除了函数的括号包裹,那么表达式结果被直接返回。称之为行内箭头函数

const increment = (num) => num + 1;

increment(41); // => 42

increment() 箭头函数仅包含一个表达式 num + 1。箭头函数不需要使用 return 关键字直接返回该表达式。

#方法

#常规函数

在类中常规函数使用通常的方式声明方法。

在下面的类 Hero 中,方法 logName() 使用常规函数声明:

class Hero {
  constructor(heroName) {
    this.heroName = heroName;
  }

  logName() {
    console.log(this.heroName);
  }
}

const batman = new Hero('Batman');

通常来说,常规函数作为方法在使用。有时,需要提供方法作为回调函数,举例来说 setTimeout 或者事件监听器。在这些用例中,可能遭遇不同访问 this 的方式。

让我们使用 logName() 方法作为 setTimeout 的回调函数:

setTimeout(batman.logName, 1000);
// after 1 second logs "undefined"

1 秒后,undefined 在控制台中被打印出来。setTimeout 简单引用 logName(这时 this 是全局对象)。

让我们手动绑定 this 的值到正确的上下文:

setTimeout(batman.logName.bind(batman), 1000);
// after 1 second logs "Batman"

batman.logName.bind(batman) 绑定 thisbatman 实例。现在可以确定该方法没有失去上下文。

手动绑定这个需要大量的代码,尤其是当你有很多方法的时候。还有一个更好的方法:将箭头函数作为类字段。

#箭头函数

感谢 Class fields 提案,现在可以使用箭头函数在类内部作为方法。

现在,与常规函数不同,方法声明使用箭头函数绑定 this 解析到类的实例。我们使用箭头函数作为字段:

class Hero {
  constructor(heroName) {
    this.heroName = heroName;
  }

  logName = () => {
    console.log(this.heroName);
  };
}

const batman = new Hero('Batman');

现在可以使用 batman.logName 作为回调函数而无需手动绑定 thislogName() 方法内部的 this 值总是类的实例:

setTimeout(batman.logName, 1000);
// after 1 second logs "Batman"

#总结

了解常规函数与箭头函数之间的不同有助于在需要时选择正确的语法。

常规函数内部的 this 值是动态的并且依赖于引用。但是箭头函数内部的 this 绑定等价于外部函数。

arguments 对象包含了常规函数中的参数列表。而箭头函数则相反,不定义参数(但你可以使用扩展参数 ...args 轻松访问箭头函数的参数)。

如果箭头函数有一个表达式,那么即使不使用 return 关键字,该表达式也会隐式返回。

最后但同样重要的是,你可以在类内部使用箭头函数语法定义方法。箭头方法将 this 值绑定到类的实例。

无论箭头方法如何被调用,this 值总是等于类实例,这在方法被用作回调时非常有用。


原文链接:https://wenjun.me/2020/05/differences-between-arrow-and-regular-functions.html

本文链接:https://segmentfault.com/a/1190000023892802


wenyiweb
58 声望2 粉丝