每天学点ES6-函数
本文翻译自:https://leanpub.com/understandinges6/read#leanpub-auto-objectis,内容有删减,仅作为阅读笔记。
ES6中的函数增加了很多新特性,使得在JavaScript中编程不容易出错,且比以往任何时候还要强大。ES6的函数主要带来了以下扩展:参数的默认值、rest参数、解构参数、扩展(Spread)运算符、name属性、箭头函数等。下面就一一介绍。
参数默认值
在JavaScript中,我们可以给函数传入任意多个参数,而不需要管它实际定义的形参个数。这允许我们定义的函数可以处理参数不同的情况,我们可以为没有指定的参数默认值。在ES5中我们可以经常看到下面这样的代码:
1 | function makeRequest(url, timeout, callback) { |
在这个例子中,timeout和callback都是可选的,因为在没有传相应的参数时,它们有默认值2000和function() {}。我们经常用这种方式来实现默认参数。
ES6中对函数进行了扩展,添加了参数默认值,在没有传入相应参数时,会使用你在定义函数时给定的默认值,而不是像ES5中那样对每个参数还要做一次判断。在ES6中我们可以这样:
1 | function makeRequest(url, timeout = 2000, callback = function() {}) { |
这样只有我们的第一个参数是期望传入的,而其他两个参数都有默认值,这使得函数更加简洁,我们不需要去判断是否某个参数没有被传入,就像timeout = timeout || 2000;这样。当makeRequest()调用时传入了所有的三个参数时,默认参数就不会被使用了。例如:
1 | // uses default timeout and callback |
任何参数有默认值时我们认为它是可选的,而那些没有默认值的参数我们认为是必须的参数。
Rest参数
上面也说到JavaScript中是可以传入任意多个参数,有时候我们没有必要指定所有的参数。在以前我们可以通过arguments函数的所有参数,虽然这在大多数情况下可以工作的很好,但是还是会有一点小累赘。例如:
1 | function sum(first) { |
上面这个函数将所有传入的参数相加,例如我们可以sum(1)或者sum(1, 2, 3, 4)都是可以的。但这个函数我们有几件事情要注意。第一、函数不能明显的看出可以处理一个以上的参数。第二、因为我们指定了一个first参数,那我们就必须从arguments的索引1开始,而不是索引0。当然记住正确的索引并不是很困难,但是还是需要我们关注的一件事。ES6为我们提供了rest参数来解决这个问题。
rest参数由三个.加上参数名字来表示,那个参数名字成为一个数组包含了参数的其余部分。例如,sum()函数可以使用rest参数重写:
1 | function sum(first, ...numbers) { |
在这个版本的函数中,我们可以看到numbers包含了除first参数之外的剩余参数。这意味着我们可以从0开始遍历numbers,而不需要有任何顾虑。我们也可以很容易的看出函数可以处理任意多个参数。
对于rest参数有一个限制就是,在rest参数后不能再跟其他函数,否则会语法错误。例如:
1 | // Syntax error: Can't have a named parameter after rest parameters |
参数解构
在前面我们学过变量解构,解构同样也可以使用在函数的参数中。
通常我们会使用一个options对象作为一个参数来代替传入多个参数,例如:
1 | function setCookie(name, value, options) { |
除了name和value是必须的之外,其他数据没有顺序优先级,这里我们使用了一个options对象来代替,而不是额外的命名参数。这种方式是OK的,但是给我们的函数带来了不透明性。
使用参数解构,之前的函数可以被重写为:
1 | function setCookie(name, value, { secure, path, domain, expires }) { |
这个例子和前面的不同之处在于使用了参数解构取出了必要的数据。这样使得更清楚的知道需要什么样的参数。参数解构和变量解构一样,如果没有传入相应数据时,其值为undefined。
有一点需要注意的是,当我们在上面的函数中没有传入第三个参数时,就像这样:
1 | // Error! |
这个代码是会报错的,在内部执行时其实是像下面这样:
1 | function setCookie(name, value, options) { |
因为变量解构时,当右边的表达式为null或undefined时是会报错的。不过我们可以通过传入一个默认的空对象来解决这个问题:
1 | function setCookie(name, value, { secure, path, domain, expires } = {}) { |
扩展运算符
扩展运算符和rest参数正好相反,rest参数允许我们将多个独立的参数合并成一个数组,而扩展运算符允许我们将一个数组分割,每个元素作为独立参数传入到函数中。我们可以考虑Math.max()方法,我们可以传入任意多个参数,然后返回最大的那个参数值。例如:
1 | let value1 = 25, |
当只有几个参数时我们很好处理,但是当我们的值存在一个数组中时我们应该怎么办呢?在ES5中我们可以通过apply来操作:
1 | let values = [25, 50, 75, 100] |
ES6的扩展运算符使这种情况变得更加简单。你可以通过在数组名前面加上...传入函数中,就像rest参数那样。例如:
1 | let values = [25, 50, 75, 100] |
你也可以混合使用扩展运算符和其他参数。例如:
1 | let values = [-25, -50, -75, -100] |
name属性
ES6为所有函数添加了一个name属性,在ES6的程序中所有的函数的name属性都确保有一个合适的值。例如:
1 | function doSomething() { |
在这段代码中,doSomething()函数的name属性值为doSomething,因为他是一个函数声明。在匿名函数表达式中,doAnotherThing()的name属性值为doAnotherThing。
下面看个更详细的例子:
1 | var doSomething = function doSomethingElse() { |
对于函数名还有几种特殊情况。对于使用bind()创建的函数会在它们的函数名前加上bound前缀。对于使用Function构造函数创建的函数它的名字为anonymous。例如:
1 | var doSomething = function() { |
new.target,[[Call]]和[[Construct]]
在ES5甚至更早,函数服务于双重目的,通过new来调用或没有new。当使用new时,函数内的this值是返回的新对象。例如:
1 | function Person(name) { |
当没有使用new调用Person()时,返回undefined。这里很明显的是这段代码的意图是使用Person和new来创建一个对象。在ES6中,在函数双重角色的困惑上做了一些改变。
第一、规范定义了两个不同的仅在内部使用的方法,每个函数都有:[[Call]]和[[Construct]]。当一个函数没有通过new来调用时,[[Call]]方法会被执行。当一个函数通过new来调用时,[[Construct]]被调用。[[Construct]]方法有责任创建一个新的对象。被称之为new target,然后执行函数体,this的值被设置为new target。有一个[[Construct]]方法的函数被称之为构造函数。
要注意的是不是所有的函数都有
[[Construct]],也不是所有的函数能通过new来调用。箭头函数,将在后面介绍到,没有[[Construct]]方法。
在ES5中,最流行的方式来决定一个函数能否通过new来调用是使用instanceof操作符。例如:
1 | function Person(name) { |
在这里,this的值会被检查是否为构造函数的实例,如果是的话,它继续正常执行。如果不是,就会抛出一个异常。这个能工作是因为[[Construct]]方法创建了Person的一个新实例,并将它赋给了this。不幸的是,这种方式不是完全可信的,this的值可以不通过new的方式也可以为Person的实例,例如:
1 | function Person(name) { |
我们这里通过Person.call(),并且传递person对象作为第一个参数,这就使得无法区分this是通过new创建的还是其他方式。
为了解决这个问题,ES6引入了new.target元属性。当一个函数的[[Construct]]被调用,new.target会成为新创建对象的实例,这个值也会在函数内成为this的值。如果[[Call]]被执行,new.target为undefined。这意味着我们现在可以通过检查new.target是否被定义安全的检查函数是否是通过new的方式被调用。例如:
1 | function Person(name) { |
箭头函数
ES6中新添加了一个箭头函数,就如它的名字,函数通过一种新的语法,使用箭头=>来定义。然而,箭头函数在有些方面和传统的JavaScript函数是不同的:
- 词法的
this绑定:在该函数内的this是通过定义箭头函数的地方决定,而不是使用它的地方决定。 - 不能当作构造函数:箭头函数没有
[[Construct]]方法,因此它不能作为构造函数。当箭头函数与new一起使用时将会抛出异常。 - 不可以改变
this值:在函数内的this的值是不能改变的,它在函数的整个生命周期中保持相同的值。 - 没有
arguments对象:你不能通过arguments对象来获取参数,你必须使用命名参数或者rest参数等。
其中有一些原因来表明这些不同为什么存在。首先,this的绑定在JavaScript是一个常见的错误根源。在一个函数内对this值的跟踪是非常容易丢失的,这可能会导致意想不到的后果。第二,通过限制箭头函数在执行代码时只有单一的this值,JavaScript引擎能更好的优化操作。
语法
箭头函数的语法有多种风格,这取决于你试图完成什么。所有的变种(箭头函数的多种风格)都是以函数参数开始,跟着箭头,跟着函数体。参数和函数体取决于使用可以采取不同的形式。例如,下面的箭头函数接受一个单一参数和简单的返回它:
1 | var reflect = value => value; |
当箭头函数只有一个参数时,我们只需直接使用这个参数而不需要其他的语法。然后箭头函数的右边会被计算和返回。即使我们这没有一个明确的return声明,箭头函数会返回传入的第一个参数。
如果你传入的参数个数大于1时,你就需要圆括号将参数用括号扩起来啦。例如:
1 | var sum = (num1, num2) => num1 + num2; |
sum()函数将两个参数相加并返回结果。不同之处在于我们的参数放在圆括号内,且用,分隔。
需要注意的是,当我们的箭头函数没有参数时,我们必须包含一个空圆括号。例如:
1 | var getName = () => "Nicholas"; |
当你需要提供一个更传统的函数体时,可能包含多个表达式,那么我们可以将这些语句放在花括号{}内,并且定义一个明确的返回值。例如:
1 | var sum = (num1, num2) => { |
当你需要创建一个什么也不做的函数时,我们需要包括花括号,例如:
1 | var doNothing = () => {}; |
还有一点需要注意的是,因为花括号被用于包含函数体,当我们需要从箭头函数返回一个对象直接量时,我们需要将直接量放在圆括号内。例如:
1 | var getTempItem = id => ({ id: id, name: "Temp" }); |
词法的this绑定
在JavaScript中最常见出错的地方就是this在函数内的绑定。因为this的值可以根据调用它的上下文在单个函数内改变,这就可能错误的使用影响某个对象,但你的意图是另外一个对象。考虑以下例子:
1 | var PageHandler = { |
在这段代码中,我们知道this.doSomething中的this对象其实是指向document的,但我们的本意其实是PageHandler对象。如果你试图运行代码,会得到一个错误。你可能会使用bind或var me = this的方式来解决这个问题。例如:
1 | // bind way |
现在代码如你预期的那样执行了,但是我们总会觉得有些奇怪。通过调用bind(this),你实际上是创建了一个新的函数,新函数的this被绑定到了PageHandler。
但是在箭头函数内,它具有隐式this绑定,这意味着箭头函数内的this值总是与定义箭头函数的作用域的this具有相同的值。例如:
1 | var PageHandler = { |
现在this的值和init()内的this值是一样的,它就能用bind的方式实现一样的功能了。
箭头函数被设计为“一次性“函数,所以它不能用于定义新的类型。它和普通的函数不同,它没有prototype属性。如果你尝试在箭头函数上使用new操作符,就会报错。例如:
1 | var MyType = () => {}, |
因为this的值是静态绑定到箭头函数上,你不能通过apply()、call()或bind()的方式来改变this的值。
词法arguments绑定
尽管箭头函数自身没有arguments对象,但是它能获取到包含它的函数的arguments对象。例如:
1 | function createArrowFunctionReturningFirstArg() { |
识别箭头函数
尽管语法不同,箭头函数同样也是函数,它也可以被识别:
1 | var comparator = (a, b) => a - b; |
至此,就是本文的全部内容啦。

