ES6+ 箭头函数

1. 前言

在编程中使用最多的就是函数,在 ES5 中是用 function 关键字来定义函数的,由于历史原因 function 定义的函数存在一些问题,如 this 的指向、函数参数 arguments 等。

ES6 规定了可以使用 “箭头” => 来定义一个函数,语法更加简洁。它没有自己的 thisargumentssupernew.target,箭头函数表达式更适用于那些本来需要匿名函数的地方,但它不能用作构造函数。

2. this 指向

在 JavaScript 中,要说让人最头疼的知识点中,this 绑定绝对算一个,这是因为 this 的绑定 ‘难以捉摸’,出错的时候还往往不知道为什么,相当反逻辑。下面我们来看一个示例:

var title = "全局标题";
var imooc = {
	title: "慕课网 ES6 Wiki",
	getTitle : function(){
		console.log(this.title);
	}
};
imooc.getTitle();		// 慕课网 ES6 Wiki
var bar = imooc.getTitle;
bar();		// 全局标题

通过上面的小例子的打印结果可以看出 this 的问题,说明 this 的指向是不固定的。

这里简单说明一下 this 的指向,this 指向的是调用它的对象。例子中的 this 是在 getTitle 的函数中的,执行 imooc.getTitle() 这个方法时,调用它的对象是 imooc,所以 this 的指向是 imooc

之后把 imooc.getTitle 方法赋给 bar,这里要注意的是,只是把地址赋值给了 bar ,并没有调用。 而 bar 是全局对象 window 下的方法,所以在执行 bar 方法时,调用它的是 Window 对象,所以这里打印的结果是 window 下的 title——“全局标题”。

TIPS: 上面的示例只是简单的 this 指向问题,还有很多更加复杂的,在面试中经常会被问到,所以还不清楚的同学可以去研究一下 this 的问题。

ES6 为了规避这样的问题,提出了箭头函数的解决方案,在箭头函数中没有自己的 this 指向,所有的 this 指向都指向它的上一层 this ,这样规定就比较容易理解了。下面看使用箭头函数下的 this 指向:

var title = "全局标题";
var imooc = {
	title: "慕课网 ES6 Wiki",
	getTitle : () => {
		console.log(this.title);
	}
};
imooc.getTitle();		// 全局标题
var bar = imooc.getTitle;
bar();		// 全局标题

上面的打印结果可以看出来,所有的 this 指向都指向了 window 对象下的 title,本身的 imooc 对象下没有了 this ,它的上一层就是 window。

3. 语法详解

3.1 基本语法

箭头函数的使用很简单,使用 => 来定义函数,下面对比 ES5 和 ES6 定义函数的对比。

// ES5
var sum = function () {
  // todo
};
// ES6
var sum = () => {
  // todo
}

3.2 有返回值

当函数体内有返回值时,ES6 的箭头函数可以省略大括号:

var sum = (num1, num2) => num1 + num2;

当传递的参数只有一个时,圆括号也可以省略:

var sum = num => num + 10;

下面看个使用 map 求和的例子:

// ES5
[1,2,3].map(function (x) {
  return x * x;
});
// 等同于ES6
[1,2,3].map(x => x * x);

对比 ES5 可以看出箭头函数的简洁表达,更加准确明了。

3.3 返回值是对象

如果函数体返回对象字面量表达式,可以省略大括号,使用圆括号的形式包裹对象。

var getimooc = () => ({a: 1, b: 2});
getimooc()  // {a: 1, b: 2}

3.4 默认参数

在定义函数时,往往需要给参数添加默认值,ES6 中可以直接在圆括号中进行赋值。

var sum = (num1, num2 = 2) => num1 + num2;
console.log(sum(1))   // 3

在使用 function 关键字定义函数时,如果要给传递的参数设置默认参数,只能在函数体内进行赋值操作,ES6 简化了默认参数的赋值过程。

3.5 剩余参数

函数在接收不定参数时,可以使用剩余运算符把调用函数时传入的参数聚拢起来成为一个参数数组(类似 function 中的 arguments 对象,但 arguments 不是数组,不能直接使用)。

下面是剩余参数的例子:

var fun = (param1, param2, ...rest) => {
  console.log(param1)
  console.log(param2)
  console.log(rest)
};
fun(1, 2, 3, 4, 5);
// 1
// 2
// [3, 4, 5]

4. 没有 this

箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 thissetTimeout 会改变 this 的指向,看下面的示例:

// 在构造函数中
function Person(){
  this.age = 0;
  setTimeout(function(){
    console.log(this);
  }, 1000)
}
var p = new Person();   // Window: {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}

function Person(){
  this.age = 0;
  setTimeout(() => {
    console.log(this);
  }, 1000);
}
var p = new Person();   // Person: {age: 0}

第一个例子中的 setTimeout 的回调函数使用 function 来定义的,从打印的结果可以看出 this 的指向是 window 对象也就是全局作用域。而第二个示例中 setTimeout 的回调函数使用箭头函数来定义,打印的结果可以看到,this 的指向是 Person.

一个实例: 定义为一个构造函数 Person,在函数中定义一个 imooc 对象,使用 function 关键字和箭头函数的方式给 imooc 上添加 getValue 方法,最后返回 imooc 对象,这时候我们来观察 getValue 内的 this 指向问题。

function Person(){
  var imooc = {};
  imooc.num = 10;
  imooc.getValue = () => {
    console.log(this)
  }
  return imooc;
}
var p = new Person();
p.getValue()
// person {} 

上面的示例中,构造函数中 imooc.getValue 方法使用的是箭头函数定义的,所以 getValue 方法不会有 this 的指向,它会根据作用域链向上查找到 Person 构造函数,所以这里的 this 的指向是 Person

function Person(){
  var imooc = {};
  imooc.num = 10;
  imooc.getValue = function() {
    console.log(this)
  }
  return imooc;
}
var p = new Person();
p.getValue()
// {num: 10, getValue: ƒ}   this指向的是 p 的返回值

上面的示例中,构造函数中 imooc.getValue 方法是使用 function 定义的,所以 getValue 中 this 的指向是动态的,指向调用它的那个对象。在 new Person() 时,会返回 imooc 对象赋给实例 ,在使用 p 去调用 getValue()this 的指向就是 p 实例。

总结: 箭头函数的 this 永远指向的是父级作用域。

5. 不绑定 arguments

箭头函数不绑定 Arguments 对象。所以在使用箭头函数定义的函数体内是取不到 arguments 的。

var fun = function() {
  console.log(arguments)
};
fun(1,2,3);  // Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]

var fun = () => {
  console.log(arguments)
};
fun(1,2,3);  // Uncaught ReferenceError: arguments is not defined

上面的示例中,对比两种定义函数的方法可以明显的看出,在箭头函数中去取 arguments 时会报引用错误,没有定义的 arguments

arguments 的主要作用是获取所有调用函数时所需要传入的参数,在箭头函数中使用剩余参数 ...args,在函数内可以直接使用。

function foo(...args) { 
  console.log(args)
}
foo(1);         // [1]
foo(1, 2, 3);   // [1, 2, 3]

6. 其他注意点

6.1 不能用作构造器

箭头函数不能用作构造器,和 new 一起用会抛出错误。

var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor

6.2 没有 prototype 属性

箭头函数没有 prototype 属性。

var Foo = () => {};
console.log(Foo.prototype); // undefined

6.3 不能使用 yield 命令

yield 关键字通常不能在箭头函数中使用,因此箭头函数不能用作 Generator 函数。

7. 小结

本节主要讲解了 ES6 的箭头函数,总结了以下几点:

  • 更短的函数,优雅简洁;
  • 箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this;
  • 不能绑定 arguments, 只能使用 ...args 展开运算来获取当前参数的数组。