# 箭头函数

箭头函数有几个使用注意点:

  • 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。
  • 不可以当作构造函数,也就是说,不可以使用 new 命令,否则会抛出一个错误。
  • 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  • 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。
  • 箭头函数可以让 this 指向固定化,绑定定义时所在的作用域,而不是指向运行时所在的作用域。这种特性很有利于封装回调函数。
  • 除了 this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。
  • 由于箭头函数没有自己的 this,所以当然也就不能用 call()、apply()、bind() 这些方法去改变 this 的指向。

# 普通函数和箭头函数的区别 重点

  • 箭头函数的 this 指向规则(没有 this)
  • 箭头函数的 arguments(没有 arguments)
  • 使用 new 调用箭头函数会报错,即,不能作为构造函数
  • 箭头函数不支持 new.target
  • 箭头函数没有原型
  • 箭头函数没有 super
  • 箭头函数不支持重命名函数参数,普通函数的函数参数支持重命名
  • 箭头函数不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。
  • 箭头函数不能直接用 call()、apply()、bind() 这些方法去改变 this 的指向,但可以间接改变

# 箭头函数的 this 指向规则

箭头函数没有 this,所以需要通过查找作用域链来确定 this 的值。这就意味着如果箭头函数被非箭头函数包含,this 绑定的就是最近一层非箭头函数的 this。因为箭头函数没有 this,所以也不能用 call()、apply()、bind() 这些方法改变 this 的指向,但可以通过修改被继承的普通函数的 this 指向间接修改。

  • 箭头函数没有 prototype(原型),所以箭头函数本身没有 this。
  • 箭头函数的 this 指向在定义的时候继承自外层第一个普通函数的 this。
    • 箭头函数的 this 指向定义时所在的外层第一个普通函数,跟使用位置没有关系。
    • 被继承的普通函数的 this 指向改变,箭头函数的 this 指向会跟着改变
  • 不能直接修改箭头函数的 this 指向。但是,我们可以间接修改箭头函数的指向。去修改被继承的普通函数的 this 指向,然后箭头函数的 this 指向也会跟着改变
  • 箭头函数外层没有普通函数,严格模式和非严格模式下它的 this 都会指向 window(全局对象)

箭头函数实际上可以让 this 指向固定化,绑定 this 使得它不再可变,这种特性很有利于封装回调函数。

Babel 转箭头函数产生的 ES5 代码,就能清楚地说明 this 的指向

// ES6
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

// ES5
// 箭头函数里面根本没有自己的 this,而是引用外层的 this
function foo() {
  var _this = this;

  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 箭头函数的 arguments

箭头函数没有自己的 arguments 对象,这不一定是件坏事,因为箭头函数可以访问外围函数的 arguments 对象。可以通过命名参数或者 rest 参数的形式访问参数。

  • 如果箭头函数的 this 指向 window(全局对象)使用 arguments 会报错,未声明 arguments
  • 箭头函数的 this 指向普通函数时,它的 argumens 继承于该普通函数
  • rest(...扩展符)参数获取函数的多余参数

# new 调用箭头函数

不可以当作构造函数,也就是说,不可以使用 new 命令,否则会抛出一个错误。

JavaScript 函数有两个内部方法:[[Call]][[Construct]]

当通过 new 调用函数时,执行 [[Construct]] 方法,创建一个实例对象,然后再执行函数体,将 this 绑定到实例上。当直接调用的时候,执行 [[Call]] 方法,直接执行函数体。箭头函数并没有 [[Construct]] 方法,不能被用作构造函数,如果通过 new 的方式调用,会报错。

# 不支持 new.target

new.target 是 ES6 新引入的属性,普通函数如果通过 new 调用,new.target 会返回该函数的引用。此属性主要用于确定构造函数是否为 new 调用的。

  • 箭头函数的 this 指向全局对象时,在箭头函数中使用 new.target 会报错
  • 箭头函数的 this 指向普通函数时,它的 new.target 就是指向该普通函数的引用。

# 没有原型

由于不能使用 new 调用箭头函数,所以也没有构建原型的需求,于是箭头函数也不存在 prototype 这个属性。

# 没有 super

连原型都没有,自然也不能通过 super 来访问原型的属性,所以箭头函数也是没有 super 的,不过跟 this、arguments、new.target 一样,这些值由外围最近一层非箭头函数决定。

箭头函数与非箭头函数的区别

  1. 箭头函数本身没有 this。所以需要通过查找作用域链来确定 this 的值。this 绑定的就是最近一层非箭头函数的 this。箭头函数外层如果没有非箭头函数,严格模式和非严格模式下它的 this 都会指向 window(全局对象)。不可以直接修改箭头函数的 this 指向。
  2. 箭头函数本身没有 argument。可以通过扩展符代替 argument 获取函数的多余参数。箭头函数的 this 指向普通函数时,它的 argumens 继承于该普通函数。如果箭头函数的 this 指向 window(全局对象)使用 arguments 会报错,未声明 arguments。
  3. 箭头函数不能通过 new 调用。箭头函数不可以当作构造函数,也就是说,不可以使用 new 命令,否则会抛出一个错误。
  4. 箭头函数不支持 new.target。new.target 会返回该函数的引用。此属性主要用于确定构造函数是否为 new 调用的。箭头函数的 this 指向普通函数时,它的 new.target 就是指向该普通函数的引用。箭头函数的 this 指向全局对象时,在箭头函数中使用 new.target 会报错。
  5. 箭头函数没有原型。由于不能使用 new 调用箭头函数,所以也没有构建原型的需求,于是箭头函数也不存在 prototype 这个属性。
  6. 箭头函数不支持 super。连原型都没有,自然也不能通过 super 来访问原型的属性,所以箭头函数也是没有 super 。
  7. 箭头函数不支持重命名函数参数,普通函数的函数参数支持重命名
  8. 箭头函数不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。

# 尾调用

尾调用(Tail Call) 是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数并返回,且调用发生在当前函数的末尾(即没有其他操作要执行)。

以下三种情况,都不属于尾调用。

// 情况一 在调用 g(x) 之后还有额外的操作(赋值操作),所以这不符合尾调用的定义
function f(x){
  let y = g(x);
  return y;
}

// 情况二 g(x) 的调用结果被加 1 后才返回,
// 这意味着在调用 g(x) 之后还有额外的操作(加法操作),所以这也不符合尾调用的定义
function f(x){
  return g(x) + 1;
}

// 情况三 虽然 g(x) 是在函数 f(x) 的末尾被调用,但没有返回 g(x)
function f(x){
  g(x);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

尾调用之所以与其他调用不同,就在于它的特殊的调用位置。

函数调用会在内存形成一个“调用记录”,又称 调用帧”(call frame),保存调用位置和内部变量等信息。

尾调用优化(Tail call optimization) : 尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。

注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。

注意,目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持。

注意,ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。

  • func.arguments:返回调用时函数的参数。
  • func.caller:返回调用当前函数的那个函数。

尾递归:尾调用自身。

尾递归优化:尾递归之所以需要优化,原因是调用栈太多,造成溢出,那么只要减少调用栈,就不会溢出。怎么做可以减少调用栈呢?就是采用“循环”换掉“递归”。

# 函数的扩展其他特性

ES2017 允许函数的最后一个参数有尾逗号。这样的规定也使得,函数参数与数组和对象的尾逗号规则,保持一致了。

function clownsEverywhere(
  param1,
  param2,
) { /* ... */ }

clownsEverywhere(
  'foo',
  'bar',
);
1
2
3
4
5
6
7
8
9

ES2019 对函数实例的toString()方法做出了修改,明确要求返回一模一样的原始代码。

function /* foo comment */ foo () {}

foo.toString()
// function foo() {} 函数foo的原始代码包含注释,函数名foo和圆括号之间有空格,但是toString()方法都把它们省略了。

function /* foo comment */ foo () {}

foo.toString()
// "function /* foo comment */ foo () {}"
1
2
3
4
5
6
7
8
9

ES2019 允许catch语句省略参数。

try {
  // ...
} catch {
  // ...
}
1
2
3
4
5