10.5 默认参数值
在ECMAScript5.1及以前,实现默认参数的一种常用方式:
检测某个参数是否等于undefined
如果是则意味着没有传这个参数,那就给它赋一个值
function makeKing(name) {name = (typeof name !== 'undefined') ? name : 'Henry';return `国王${name}三世`;}console.log(makeKing()); // 国王Henry三世console.log(makeKing('Louis')); // 国王Louis三世
ECMAScript 6之后不用这么麻烦,因为它支持显式定义默认参数。
与前面代码等价的ES6写法,在函数定义中的参数后面用=,就可以为参数赋一个默认值:
function makeKing(name = 'Henry') {return `国王${name}三世`;}console.log(makeKing()); // 国王Henry三世console.log(makeKing('Louis')); // 国王Louis三世
给参数传undefined相当于没有传值,不过这样可以利用多个独立的默认值:
function makeKing(name = 'Henry', numerals = '三世') {return `国王${name}${numerals}`;}console.log(makeKing()); // 国王Henry三世console.log(makeKing('Louis')); // 国王Louis三世console.log(makeKing( undefined, '六世')); // 国王Henry六世
使用默认参数时,arguments对象的值不反映参数的默认值,只反映传给函数的参数。
跟ES5严格模式一样,修改命名参数也不会影响arguments对象,它始终以调用函数时传入的值为准:
function makeKing(name = 'Henry') {name = 'Louis';return `国王${arguments[0]}`;}console.log(makeKing()); // 国王undefinedconsole.log(makeKing('Louis')); // 国王Louis
默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值:
let romanNumerals = ['一世','二世','三世','四世','五世','六世'];let ordinality = 0;function getNumerals() {// 每次调用后递增return romanNumerals[ordinality++];}function makeKing(name = 'Henry', numerals = getNumerals()) {return `国王${name}${numerals}`;}console.log(makeKing()); // 国王Henry一世console.log(makeKing('Louis', '十六世')); // 国王Louis十六世console.log(makeKing()); // 国王Henry二世console.log(makeKing()); // 国王Henry三世
函数的默认参数,只有在函数被调用时,才会求值,不会在函数定义时求值。
计算默认值的函数,只有在调用函数,但未传相应参数时,才会被调用。
箭头函数也可以这样使用默认参数,但在只有一个参数时,必须使用括号而不能省略:
let makeKing = (name = 'Henry') => `国王${name}`;console.log(makeKing()); //国王Henry
默认参数作用域与暂时性死区
因为在求值默认参数时可以定义对象,也可以动态调用函数,所以函数参数肯定是在某个作用域中求值的。
给多个参数定义默认值实际上跟使用let关键字顺序声明变量一样。
来看下面的例子:
function makeKing(name = 'Henry', numerals = '八世') {return `国王${name}${numerals}`;}console.log(makeKing()); // 国王Henry八世// 这里的默认参数会按照定义它们的顺序依次被初始化。可以依照如下示例想象一下这个过程:function makeKing() {let name = 'Henry';let numerals = '八世';return `国王${name}${numerals}`;}console.log(makeKing()); // 国王Henry八世
因为参数是按顺序初始化的,所以后定义默认值的参数可以引用先定义的参数
function makeKing(name = 'Henry', numerals = name) {return `国王${name} ${numerals}`;}console.log(makeKing()); // 国王Henry Henry
参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的。
像这样就会抛出错误:
function makeKing(name = numerals, numerals = '八世') {return `国王${name} ${numerals}`;}console.log(makeKing());// Uncaught ReferenceError: Cannot access 'numerals' before initialization
参数也存在于自己的作用域中,它们不能引用函数体的作用域:
function makeKing(name = numerals, numerals = defaultNumeral) {let defaultNumeral = '八世';return `国王${name} ${numerals}`;}console.log(makeKing());// Uncaught ReferenceError: Cannot access 'numerals' before initialization
10.6 参数扩展与收集
ECMAScript 6新增了扩展操作符,使用它可以非常简洁地操作和组合集合数据。
扩展操作符最有用的场景:函数定义中的参数列表
在这里,它可以充分利用这门语言的弱类型,及参数长度可变的特点。
扩展操作符既可以用于调用函数时传参,也可以用于定义函数参数。
10.6.1 扩展参数
在给函数传参时,有时候可能不需要传一个数组,而是要分别传入数组的元素。
假设有如下函数定义,它会将所有传入的参数累加起来:
let values = [1,2,3,4];function getSum() {let sum = 0;for (let i = 0; i < arguments.length; ++i) {sum += arguments[i];}return sum;}
这个函数希望将所有加数逐个传进来,然后通过迭代arguments对象来实现累加。
如果不使用扩展操作符,想把定义在这个函数这面的数组拆分,那么就得求助于apply()方法:
console.log(getSum.apply(null, values)); // 10
但在ECMAScript 6中,可通过扩展操作符极为简洁地实现这种操作。
对可迭代对象应用扩展操作符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入。
比如,使用扩展操作符可以将前面例子中的数组像这样直接传给函数:
console.log(getSum(...values)); // 10
因为数组的长度已知,所以在使用扩展操作符传参的时候,并不妨碍在其前面或后面再传其他的值,包括使用扩展操作符传其他参数:
console.log(getSum(-1, ...values)); // 9console.log(getSum(...values, 5)); // 15console.log(getSum(-1, ...values, 5)); // 14console.log(getSum(...values, ...[5,6,7])); // 28
对函数中的arguments对象而言,它并不知道扩展操作符的存在,而是按照调用函数时传入的参数接收每一个值
arguments对象只是消费扩展操作符的一种方式。在普通函数和箭头函数中,也可以将扩展操作符用于命名参数,当然同时也可以使用默认参数:
function getProduct(a,b,c = 1) {return a*b*c;}let getsum = (a,b,c = 0) => {return a+b+c;}console.log(getProduct(...[1,2])); // 2console.log(getProduct(...[1,2,3])); // 6console.log(getProduct(...[1,2,3,4])); // 6console.log(getsum(...[0,1])); // 1console.log(getsum(...[0,1,2])); // 3console.log(getsum(...[0,1,2,3])); // 3
10.6.2 收集参数
在构思函数定义时,可以使用扩展操作符把不同长度的独立参数组合为一个数组
这有点类似arguments对象的构造机制,只不过收集参数的结果会得到一个Array实例。
function getSum(...values) {// 顺序累加values中的所有值// 初始的总和为0return values.reduce((x,y) => x+y, 0);}console.log(getSum(1,2,3)); // 6
收集参数的前面如果还有命名参数,则只会收集其余的参数;如果没有则会得到空数组。因为收集参数的结果可变,所以只能把它作为最后一个参数
箭头函数虽然不支持arguments对象,但支持收集参数的定义方式,因此也可以实现与使用arguments一样的逻辑
let getSum = (...values) => {return values.reduce((x,y) => x+y, 0);}console.log(getSum(1,2,3)); // 6
10.7 函数声明与函数表达式
JavaScript引擎在加载数据时,对函数声明和函数表达式,是区别对待的。
JavaScript引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。
而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。
来看下面的例子:
console.log(sum(10, 10));function sum(num1, num2) {return num1 + num2;} // 20
代码可以正常运行,因为函数声明,会在任何代码执行之前,先被读取,并添加到执行上下文。这个过程叫作函数声明提升(function declaration hoisting)。
在执行代码时,JavaScript引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部。
因此即使函数定义出现在调用它们的代码之后,引擎也会把函数声明提升到顶部。
如果把前面代码中的函数声明改为等价的函数表达式,那么执行的时候就会出错
console.log(sum(10, 10));let sum = function(num1, num2) {return num1 + num2;}// Uncaught ReferenceError: Cannot access 'sum' before initialization
代码之所以会出错,是因为这个函数定义包含在一个,变量初始化语句中,而不是函数声明中。这意味着代码如果没有执行到加粗的那一行,那么执行上下文中就没有函数的定义。
这并不是因为使用let而导致的,使用var关键字也会碰到同样的问题
console.log(sum(10, 10));var sum = function(num1, num2) {return num1 + num2;}// Uncaught TypeError: sum is not a function
除了函数什么时候真正有定义这个区别之外,这两种语法是等价的。
注:在使用函数表达式初始化变量时,也可以给函数一个名称,比如let sum =function sum(){}
10.8 函数作为值
因为函数名在ECMAScript中就是变量,所以函数可以用在任何可以使用变量的地方。
不仅可以把函数作为参数传给另一个函数,还可以在一个函数中返回另一个函数。
从一个函数中返回另一个函数也可以,而且非常有用。
function callSomeFunction(someFunc, someArg) {return someFunc(someArg);}function add10(num) {return num + 10;}let result1 = callSomeFunction(add10, 10);console.log(result1);function getGreeting(name) {return '你好,' + name;}let result2 = callSomeFunction(getGreeting, 'Amy');console.log(result2);// 20// 你好,Amy
callSomeFunction()函数是通用的,第一个参数传入的是什么函数都可以,而且它始终返回调用作为第一个参数传入的函数的结果。
注:如果是访问函数而不是调用函数,那就必须不带括号
所以传给callSomeFunction()的必须是add10和getGreeting,而不能是它们的执行结果。
从一个函数中返回另一个函数也是可以的,而且非常有用。
