ES6里关于函数的拓展(一)

一、形参默认值

  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);
});

1、触发默认值

  声明函数时,可以为任意参数指定默认值,在已指定默认值的参数后可以继续声明无默认值参数。只有当不为第二个参数传入值或主动为第二个参数传入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,结果触发了默认值;参数等于null,就没有触发默认值

  使用参数默认值时,函数不能有同名参数。

  另外,一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的

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

2、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

3、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");

  即:设置默认参数值的参数与arguments对象的联系断了

  在这个示例中,只给mixArgs()方法传入一个参数,arguments. Iength 的值为 1, arguments[1] 的值为 undefined, first与arguments[0]全等,改变first和second并不会影响arguments对象

4、默认参数表达式

  关于默认参数值,最有趣的特性可能是非原始值传参了。可以通过函数执行来得到默认参数的值

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的默认值

5、临时死区

  默认参数也有临时死区,在这里的参数不可访问。与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(undefined,1) 可表示为
let first = second;
let second = 1;
//在这个示例中,调用add(undefined,1)函数,因为当first初始化时second尚未初始化,所以会导致程序抛出错误,此时second尚处于临时死区中,所有引用临时死区中绑定的行为都会报错

6、形参与自由变量

  下列代码中,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
//类似地,下列代码也报错
//x是形参
let x = 1;
function foo(x = x) {}
foo() // ReferenceError: x is not defined

二、不定参数

  无论函数已定义的命名参数有多少,都不限制调用时传入的实际参数数量,调用时总是可以传入任意数量的参数。当传入更少数量的参数时,默认参数值的特性可以有效简化函数声明的代码;当传入更多数量的参数时,ES6同样也提供了更好的方案。

  ES5:早先,Javascript提供arguments对象来检查函数的所有参数,从而不必定义每一个要用的参数。尽管arguments对象检査在大多数情况下运行良好,但是实际使用起来却有些笨重

  ES6:在ES6中,通过引入不定参数(rest parameters)的特性可以解决这些问题,不定参数也称为剩余参数或rest参数

  在函数的命名参数前添加三个点(...)就表明这是一个不定参数,该参数为一个数组,包含着自它之后传入的所有参数,通过这个数组名即可逐一访问里面的参数

function pick(object, ...keys) {
    let result = Object.create(null);
    for (let i = 0, len = keys.length; i < len; i++) {
        result[keys[i]] = object[keys[i]];
    }
    return result;
}

  在这个函数中,不定参数keys包含的是object之后传入的所有参数,而arguments对象包含的则是所有传入的参数,包括object。这样一来,就可以放心地遍历keys对象了。这种方法还有另一个好处,只需看一眼函数就可以知道该函数可以处理的参数数量

1、不定参数有两条使用限制:

(1)每个函数最多只能声明一个不定参数,而且一定要放在所有参数的末尾

// 语法错误:不能在剩余参数后使用具名参数
function pick(object, ...keys, last) {
    let result = Object.create(null);
    for (let i = 0, len = keys.length; i < len; i++) {
        result[keys[i]] = object[keys[i]];
    }
    return result;
}

(2)不定参数不能在对象字面量的 setter 属性中使用

let object = {
    // 语法错误:不能在 setter 中使用剩余参数
    set name(...value) {
        // 一些操作
    }
};

  之所以存在这条限制,是因为对象字面量setter的参数有且只能有一个。而在不定参数的定义中,参数的数量可以无限多,所以在当前上下文中不允许使用不定参数

2、arguments:

  不定参数的设计初衷是代替JS的arguments对象。起初,在ES4草案中,arguments对象被移除并添加了不定参数的特性,从而可以传入不限数量的参数。但是ES4从未被标准化,这个想法被搁置下来,直到重新引入了ES6标准,唯一的区别是arguments对象依然存在

function checkArgs(n,...args) {
    console.log(args.length);//2
    console.log(arguments.length);//3
    console.log(args);//['b','c']
    console.log(arguments);//['a','b','c']
}
checkArgs("a", "b", "c");

3、应用:不定参数中的变量代表一个数组,所以数组特有的方法都可以用于这个变量

// arguments变量的写法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// 不定参数的写法
const sortNumbers = (...numbers) => numbers.sort();

  上面代码的两种写法,比较后可以发现,不定参数的写法更自然也更简洁。

三、展开运算符

  在所有的新功能中,与不定参数最相似的是展开运算符。不定参数可以指定多个各自独立的参数,并通过整合后的数组来访问;而展开运算符可以指定一个数组,将它们打散后作为各自独立的参数传入函数。JS内建的Math.max()方法可以接受任意数量的参数并返回值最大的那一个

let value1 = 25,
value2 = 50;
console.log(Math.max(value1, value2)); // 50

  如上例所示,如果只处理两个值,那么Math.max()非常简单易用。传入两个值后返回更大的那一个。但是如果想从一个数组中挑选出最大的那个值应该怎么做呢?Math.max()方法不允许传入数组,所以在ES5中,可能需要手动实现从数组中遍历取值,或者使用apply()方法

let values = [25, 50, 75, 100]
console.log(Math.max.apply(Math, values)); // 100

  这个解决方案确实可行,但却让人很难看懂代码的真正意图。

  使用ES6中的展开运算符可以简化上述示例,向Math.max()方法传入一个数组,再在数组前添加不定参数中使用的...符号,就无须再调用apply()方法了。JS引擎读取这段程序后会将参数数组分割为各自独立的参数并依次传入

let values = [25, 50, 75, 100]
// 等价于 console.log(Math.max(25, 50, 75, 100));
console.log(Math.max(...values)); // 100

  使用apply()方法需要手动指定this的绑定,如果使用展开运算符可以使这种简单的数学运算看起来更加简洁

  可以将展开运算符与其他正常传入的参数混合使用。假设限定Math.max()返回的最小值为0,可以单独传入限定值,其他的参数仍然使用展开运算符得到

let values = [-25, -50, -75, -100]
console.log(Math.max(...values, 0)); // 0
//在这个示例中,Math.max()函数先用展开运算符传入数组中的值,又传入了参数0

  展开运算符可以简化使用数组给函数传参的编码过程,在大多数使用apply()方法的情况下展开运算符可能是一个更合适的方案。

原文地址:https://www.cnblogs.com/goloving/p/7881871.html