每天学点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; |
至此,就是本文的全部内容啦。