ECMAScript6函数
函数
前言
ECMAScript6大力度的更新了函数特性,在ECMAScript5的基础上进行了许多改进。内容较多,所以对于相对较浅显的内容我会简单举个例子说明下带过,重难点再作相应的解析。
函数形参的默认值
我们都知道JavaScript函数有一个特别的地方,即无论在函数中声明了多少形参,都可以传入任意数量的参数,也可以在定义函数时添加针对参数数量的处理逻辑,当已定义的形参无传入参数时赋予其默认值。
function request(url, timeout, callback) {
timeout = timeout || 2000;
callback = callback || function () { };
//.....
}
其中表达的意思也很明确,timeout和callback为可选参数,如果不传入对应的参数,我们为其赋予默认值。在含有逻辑或操作符的表达式中,前一个操作符为假值时,总会返回后一个值。对于函数的命名参数,如果不显示传值,则其值为undefined。因此示例中使用逻辑或操作符也存在问题,因为0也为假值,即使我们给timeout形参传入0,他也会被赋值为2000,尽管它是一个合法的参数。
function request(url, timeout, callback) {
timeout = (typeof timeout !== "undefined") ? timeout : 2000;
callback = (typeof callback !== "undefined") ? callback : function () { };
//.....
}
所以在以前的JavaScript中为函数的形参赋予默认值的常见做法是这样的,这种方法也代表了一种常见的模式,在流行的JavaScript库中均使用类似的模式进行默认补全。不难看出,即便是这种非常基础的操作,我们仍需要写很多额外的代码。
ECMAScript6中的参数默认值
ECMAScript6简化了为形参提供默认值的过程,我们来将上面的示例改写
function request(url, timeout = 2000, callback = function () { }) {
//.....
}
简洁明了,在这个函数中,只有第一个参数被认为是总要传入值的,其他两个参数都有默认值,而且不需要添加任何校验的代码,所以函数体会更加的小。不过有一点不同的是,在已指定默认值的参数后可以继续声明无默认值的参数。
function request(url, timeout = 2000, callback) {
//.....
}
在这种情况下,只有当不为第二个参数传入值或主动为第二个参数传入undefined
时才会使用timeout的默认值。
//使用timeout默认值
request("/user",undefined,function(body){
})
//不使用timeout默认值
request("/user",null,function(body){
})
null也是一个合法值。
默认参数值对arguments对象的影响
当使用默认参数时,arguments对象的行为与以往不同。在ECMAScript5的非严格模式下,函数命名参数的变化会体现在arguments对象中。
function mixArgs(first, second) {
console.log(first === arguments[0]); //true
console.log(second === arguments[1]); //true
first = "c";
second = "d";
console.log(first === arguments[0]); //true
console.log(second === arguments[1]); //true
}
mixArgs("a", "b")
在非严格模式下,命名参数的变化会同步更新到arguments对象中,所以当first和second被赋予新值时,arguments[0]和arguments[1]相应的也就更新了,最终所有的全等比较结果为true。然而,在ECMAScript5严格模式下,取消了arguments对象的这个令人感到困惑的行为,无论参数如何变化,arguments对象不再随之改变。
function mixArgs(first, second) {
"use strict"
console.log(first === arguments[0]); //true
console.log(second === arguments[1]); //true
first = "c";
second = "d";
console.log(first === arguments[0]); //false
console.log(second === arguments[1]); //false
}
mixArgs("a", "b")
在ECMAScript6中,如果一个函数使用了默认参数值,则无论是否显示定义了严格模式,arguments对象的行为都将与ECMAScript5严格模式下保持一致。
默认参数表达式
关于默认参数值,除了给予一个给定的默认值外,还有另外一个有趣的特性,我们还可以使用非原始值传参。举个例子,我们可以通过函数执行来得到默认参数的值,就像这样
function getValue() {
return 5;
}
function add(first, second = getValue()) {
return first + second;
}
console.log(add(1, 1)); //2
console.log(add(1)); //6
这里需要注意的是,初次解析函数声明时不会调用getValue()方法,只有当调用add()函数且不传入第二个参数时才会调用,我们稍微改动一下上面的例子
let value = 5;
function getValue() {
return value++;
}
function add(first, second = getValue()) {
return first + second;
}
console.log(add(1, 1)); //2
console.log(add(1)); //6
console.log(add(1)); //7
注意,当使用函数调用结果作为默认参数值时,如果忘记写小括号,例如,second = getValue,则最终传入的是对函数的引用,而不是函数调用的结果。
正是因为默认参数是在函数调用时求值,所以可以使用先定义的参数作为后定义参数的默认值。
function add(first, second = first) {
return first + second;
}
console.log(add(1)); //2
console.log(add(1, 2)); //3
更进一步,我们可以将参数first传入一个函数来获取参数second的值,就像这样
function getValue(value) {
return value + 5;
}
function add(first, second = getValue(first)) {
return first + second;
}
console.log(add(1)); //7
console.log(add(1, 2)); //3
但是要注意一点,在引用默认参数值的时候,只允许引用前面参数的值,即先定义的参数不能访问后定义的参数。接下来我将解释一下其原因。
默认参数的临时死区
在讲解let和const的时候我们说过临时死区(TDZ),其实默认参数也有同样的临时死区,在这里的参数不可访问。与let声明类似,定义参数时会为每个参数创建一个新的标识符绑定,该绑定在初始化前不可被引用。当调用函数时,会通过传入的值或默认的参数值初始化该参数。
我们拿上面的这个例子来示范:
function getValue(value) {
return value + 5;
}
function add(first, second = getValue(first)) {
return first + second;
}
console.log(add(1)); //7
console.log(add(1, 2)); //3
调用add(1)和add(1,2)时实际上相当于执行以下代码来创建first和second的参数值:
//调用add(1)
let first = 1;
let second = getValue(first);
//调用add(1,2)
let first = 1;
let second = 2;
当初次执行函数add()时,绑定first和second被添加到一个专属于函数参数的临时死区(与let的行为类似)。由于初始化second时first已经被初始化,所以它可以访问first的值,但是反过来就错了。因此不难理解为什么在引用默认参数值的时候,只允许引用前面参数的值。
函数参数有自己的作用域和临时死区,其与函数体的作用域是各自独立的,也就是说参数的默认值不可访问函数体内声明的变量。
处理无命名参数
上面讲述了当传入更少数量的参数时,默认参数值的特性可以有效的简化函数声明的代码,当传入更多数量的参数时,ECMAScript6同样提供了更好的方案。这个比较简单,就直接举例说明一下了。
ECMAScript5中的无命名参数
我们先来看一下在ECMAScript5中的做法:
function pick(object) {
let result = Object.create(null);
console.log(arguments);
//{"0":{"title":"ECMAScript6","author":"Zakas","year":"2016"},"1":"author","2":"year"}
//从第二个参数开始
for (let i = 1; i < arguments.length; i++) {
result[arguments[i]] = object[arguments[i]];
}
return result;
}
let book = {
title: "ECMAScript6",
author: "Zakas",
year: "2016"
}
let bookData = pick(book, "author", "year");
console.log(bookData.author); //Zakas
console.log(bookData.year); //2016
以上做法存在几个需要注意的地方:首先,并不是很容易能发现这个函数可以接受任意数量的参数;其次,因为第一个参数为命名参数且已被使用,当你要查找需要拷贝的属性名称时,必须得从索引1而不是索引0开始遍历arguments对象。
不定参数
ECMAScript6中采用不定参数
在函数的命名参数前加上三个...就表明这是一个不定参数,该参数为一个数组,包含着自它之后传入的所有参数,通过这个数组名即可逐一访问里面的参数。我们来重写上面的例子
function pick(object, ...keys) {
let result = Object.create(null);
for (let i = 0; i < keys.length; i++) {
result[keys[i]] = object[keys[i]];
}
return result;
}
console.log(pick.length); //1
其结果与之前的一样,但是更简洁明了了不是吗。
函数的length属性统计的是函数命名参数的数量,不定参数的加入不会影响length属性的值。
不定参数有两条使用限制:
- 每个函数最多只能声明一个不定参数,并且一定要放在所有参数的末尾
- 不定参数不能用于对象字面量setter之中
增强的Function构造函数
Function构造函数是JavaScript中很少被用到的一部分,通常我们用它来动态创建新的函数。ECMAScript6增强了Function构造函数的功能,支持在创建函数定义默认参数和不定参数,其写法与在普通函数中一致。
var add = new Function("first", "second = first", "return first + second")
console.log(add(1)); //2
console.log(add(1, 2)); //3
var pick = new Function("...args", "return args[0]");
console.log(pick(1, 2, 3)); //1
展开运算符
展开运算符在实际工作中经常用到。其与不定参数最为相似,不定参数可以让你指定多个各自独立的参数,并通过整合后的数组来访问;而展开运算符可以让你指定一个数组,将它们打散后作为各自独立的参数传入函数。我们直接来举个例子,Math.max()方法可以接受任意数量的参数并返回值最大的那一个,但是它不允许传入数组,让我们分别看下两种做法:
ECMAScript5:
let values = [20, 39, 1, 33, 58];
console.log(Math.max.apply(null, values)) //58
ECMAScript6展开运算符:
let values = [20, 39, 1, 33, 58];
console.log(Math.max(...values)); //58
//等价于
console.log(Math.max(20, 39, 1, 33, 58))
使用ECMAScript6中的展开运算符就可以简化上述示例。JavaScript引擎读取这段程序后会将参数数组分割为各自独立的参数并依次传入。展开运算符也可以与其他正常传入的参数混合使用。
let values = [-22, -33, -12, -64, -29];
console.log(Math.max(...values, 0)); //0
展开运算符可以简化使用数组给函数传参的编码过程,将来你可能会发现,在大多数使用apply()方法的情况下展开运算符可能是一个更合适的方案。
//todo...