温故系列のES6扩展:好用的形参默认值
函数是所有编程语言的重要组成部分,在ES6出现前,JS的函数语法一直没有太大的变化,从而遗留了很多问题,导致实现一些基本的功能经常要编写很多代码。ES6大力度地更新了函数特性,在ES5的基础上进行了许多改进,使用JS编程可以更少出错,同时也更加灵活。
形参默认值
Javascript函数有一个特别的地方,无论在函数定义中声明了多少形参,都可以传入任意数量的参数,也可以在定义函数时添加针对参数数量的处理逻辑,当已定义的形参无对应的传入参数时为其指定一个默认值。
【ES5模拟】在ES5中,一般地,通过下列方式创建函数并为参数设置默认值,下面的方式是我开发中使用的方式,很尴尬
function makeRequest(url, timeout, callback) {
timeout = timeout || 2000;
callback = callback || function() {};
// 函数的剩余部分
}
在这个示例中,timeout和callback为可选参数,如果不传入相应的参数系统会给它们赋予一个默认值。在含有逻辑或操作符的表达式中,前一个操作数的值为false时,总会返回后一个值。对于函数的命名参数,如果不显式传值,则其值默认为undefined。
注意缺陷:因此我们经常使用逻辑或操作符来为缺失的参数提供默认值,然而这个方法也有缺陷,如果我们想给makeRequest函数的第二个形参timeout传入值0,即使这个值是合法的,也会被视为一个false值,并最终将timeout赋值为2000,在这种情况下,更安全的选择是通过typeof检查参数类型,如下所示
function makeRequest(url, timeout, callback) {
timeout = (typeof timeout !== "undefined") ? timeout : 2000;
callback = (typeof callback !== "undefined") ? callback : function() {};
// 函数的剩余部分
}
虽然这种方法更安全,但依然为实现一个基本需求而书写了额外的代码。它代表了一种常见的模式,而流行的 JS 库中都充斥着类似的模式进行默认补全
【ES6默认参数】ES6简化了为形参提供默认值的过程,如果没为参数传入值则为其提供一个初始值
function makeRequest(url, timeout = 2000, callback = function() {}) {
// 函数的剩余部分
}
在这个函数中,只有第一个参数被认为总是要为其传入值的,其他两个参数都有默认值,而且不需要添加任何校验值是否缺失的代码,所以函数代码比较简洁
如果调用make Request()方法时传入3个参数,则不使用默认值
// 使用默认的 timeout 与 callback
makeRequest("/foo");
// 使用默认的 callback
makeRequest("/foo", 500);
// 不使用默认值
makeRequest("/foo", 500, function(body) {
doSomething(body);
});
【触发默认值】声明函数时,可以为任意参数指定默认值,在已指定默认值的参数后可以继续声明无默认值参数
function makeRequest(url, timeout = 2000, callback) {
console.log(url);
console.log(timeout);
console.log(callback);
}
在这种情况下,只有当不为第二个参数传入值或主动为第二个参数传入undefined时才会使用timeout的默认值
[注意]如果传入undefined,将触发该参数等于默认值,null则没有这个效果
function makeRequest(url, timeout = 2000, callback) {
console.log(timeout);
}
makeRequest("/foo");//2000
makeRequest("/foo", undefined);//2000
makeRequest("/foo", null);//null
makeRequest("/foo", 100);//100
上面代码中,timeout参数对应undefined
,结果触发了默认值,y
参数等于null
,就没有触发默认值,使用参数默认值时,函数不能有同名参数
// SyntaxError: Duplicate parameter name not allowed in this context
function foo(x, x, y = 1) {
// ...
}
另外,一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的
let x = 99;
function foo(p = x + 1) {
console.log(p);
}
foo() // 100
x = 100;
foo() // 101
上面代码中,参数p
的默认值是x+1
。这时,每次调用函数foo
,都会重新计算x+1
,而不是默认p
等于100,我在实际开发中很少使用这种方式。
【length属性】指定了默认值以后,函数的length
属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length
属性将失真
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
这是因为length
属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,rest 参数也不会计入length
属性
(function(...args) {}).length // 0
如果设置了默认值的参数不是尾参数,那么length
属性也不再计入后面的参数了
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
【arguments】当使用默认参数值时,arguments对象的行为与以往不同。在ES5非严格模式下,函数命名参数的变化会体现在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
然而,在ES5的严格模式下,取消了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");
这一次更改 first 与 second 就不会再影响 arguments 对象,因此输出结果符合通常的期望
在ES6中,如果一个函数使用了默认参数值,则无论是否显式定义了严格模式,arguments对象的行为都将与ES5严格模式下保持一致。默认参数值的存在使得arguments对象保持与命名参数分离,这个微妙的细节将影响使用arguments对象的方式
// 非严格模式
function mixArgs(first, second = "b") {
console.log(first);//a
console.log(second);//b
console.log(arguments.length);//1
console.log(arguments[0]);//a
console.log(arguments[1]);//undefined
first = 'aa';
arguments[1] = 'b';
console.log(first);//aa
console.log(second);//b
console.log(arguments.length);//1
console.log(arguments[0]);//a
console.log(arguments[1]);//b
}
mixArgs("a");
在这个示例中,只给mixArgs()方法传入一个参数,arguments. Iength 的值为 1, arguments[1] 的值为 undefined, first与arguments[0]全等,改变first和second并不会影响arguments对象
【默认参数表达式】关于默认参数值,最有趣的特性可能是非原始值传参了。可以通过函数执行来得到默认参数的值
function getValue() {
return 5;
}
function add(first, second = getValue()) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 6
在这段代码中,如果不传入最后一个参数,就会调用getvalue()函数来得到正确的默认值。切记,初次解析函数声明时不会调用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
在此示例中,变量value的初始值为5,每次调用getvalue()时加1。第一次调用add(1)返回6,第二次调用add(1)返回7,因为变量value已经被加了1。因为只要调用add()函数就有可能求second的默认值,所以任何时候都可以改变那个值
正因为默认参数是在函数调用时求值,所以可以使用先定义的参数作为后定义参数的默认值
function add(first, second = first) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 2
在上面这段代码中,参数second的默认值为参数first的值,如果只传入一个参数,则两个参数的值相同,从而add(1,1)返回2,add(1)也返回2
function getValue(value) {
return value + 5;
}
function add(first, second = getValue(first)) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 7
在上面这个示例中,声明second=getvalue(first),所以尽管add(1,1)仍然返回2,但是add(1)返回的是(1+6)也就是7
在引用参数默认值的时候,只允许引用前面参数的值,即先定义的参数不能访问后定义的参数
function add(first = second, second) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(undefined, 1)); // 抛出错误
调用add(undefined,1)会抛出错误,因为second比first晚定义,因此其不能作为first的默认值
【临时死区】
在介绍块级作用域时提到过临时死区TDZ,其实默认参数也有同样的临时死区,在这里的参数不可访问。与let声明类似,定义参数时会为每个参数创建一个新的标识符绑定,该绑定在初始化之前不可被引用,如果试图访问会导致程序抛出错误。当调用函数时,会通过传入的值或参数的默认值初始化该参数
function getValue(value) {
return value + 5;
}
function add(first, second = getValue(first)) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 7
调用add(1,1)和add(1)时实际上相当于执行以下代码来创建first和second参数值
// JS 调用 add(1, 1) 可表示为
let first = 1;
let second = 1;
// JS 调用 add(1) 可表示为
let first = 1;
let second = getValue(first);
当初次执行函数add()时,first和second被添加到一个专属于函数参数的临时死区(与let的行为类似)。由于初始化second时first已经被初始化,所以它可以访问first的值,但是反过来就错了
function add(first = second, second) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(undefined, 1)); // 抛出错误
在这个示例中,调用add(1,1)和add(undefined,1)相当于在引擎的背后做了如下事情
// JS 调用 add(1, 1) 可表示为
let first = 1;
let second = 1;
// JS 调用 add(1) 可表示为
let first = second;
let second = 1;
在这个示例中,调用add(undefined,1)函数,因为当first初始化时second尚未初始化,所以会导致程序抛出错误,此时second尚处于临时死区中,所有引用临时死区中绑定的行为都会报错
【形参与自由变量】
下列代码中,y是形参,需要考虑临时死区的问题;而x是自由变量,不需要考虑。所以调用函数时,由于未传入参数,执行y=x,x是自由变量,通过作用域链,在全局作用域找到x=1,并赋值给y,于是y取值1
let x = 1;
function f(y = x) {}
f() // 1
下列代码中,x和y是形参,需要考虑临时死区的问题。因为没有自由变量,所以不考虑作用域链寻值的问题。调用函数时,由于未传入参数,执行y=x,由于x正处于临时死区内,所有引用临时死区中绑定的行为都会报错
let x = 1;
function f(y = x,x) {}
f()// ReferenceError: x is not defined
类似地,下列代码也报错
let x = 1;
function foo(x = x) {}
foo() // ReferenceError: x is not defined
转载自:https://juejin.cn/post/7150862864012541960