10.5 默认参数值
    在ECMAScript5.1及以前,实现默认参数的一种常用方式:
    检测某个参数是否等于undefined
    如果是则意味着没有传这个参数,那就给它赋一个值

    1. function makeKing(name) {
    2. name = (typeof name !== 'undefined') ? name : 'Henry';
    3. return `国王${name}三世`;
    4. }
    5. console.log(makeKing()); // 国王Henry三世
    6. console.log(makeKing('Louis')); // 国王Louis三世

    ECMAScript 6之后不用这么麻烦,因为它支持显式定义默认参数。
    与前面代码等价的ES6写法,在函数定义中的参数后面用=,就可以为参数赋一个默认值:

    1. function makeKing(name = 'Henry') {
    2. return `国王${name}三世`;
    3. }
    4. console.log(makeKing()); // 国王Henry三世
    5. console.log(makeKing('Louis')); // 国王Louis三世

    给参数传undefined相当于没有传值,不过这样可以利用多个独立的默认值:

    1. function makeKing(name = 'Henry', numerals = '三世') {
    2. return `国王${name}${numerals}`;
    3. }
    4. console.log(makeKing()); // 国王Henry三世
    5. console.log(makeKing('Louis')); // 国王Louis三世
    6. console.log(makeKing( undefined, '六世')); // 国王Henry六世

    使用默认参数时,arguments对象的值不反映参数的默认值,只反映传给函数的参数。
    跟ES5严格模式一样,修改命名参数也不会影响arguments对象,它始终以调用函数时传入的值为准:

    1. function makeKing(name = 'Henry') {
    2. name = 'Louis';
    3. return `国王${arguments[0]}`;
    4. }
    5. console.log(makeKing()); // 国王undefined
    6. console.log(makeKing('Louis')); // 国王Louis

    默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值:

    1. let romanNumerals = ['一世','二世','三世','四世','五世','六世'];
    2. let ordinality = 0;
    3. function getNumerals() {
    4. // 每次调用后递增
    5. return romanNumerals[ordinality++];
    6. }
    7. function makeKing(name = 'Henry', numerals = getNumerals()) {
    8. return `国王${name}${numerals}`;
    9. }
    10. console.log(makeKing()); // 国王Henry一世
    11. console.log(makeKing('Louis', '十六世')); // 国王Louis十六世
    12. console.log(makeKing()); // 国王Henry二世
    13. console.log(makeKing()); // 国王Henry三世

    函数的默认参数,只有在函数被调用时,才会求值,不会在函数定义时求值。
    计算默认值的函数,只有在调用函数,但未传相应参数时,才会被调用。
    箭头函数也可以这样使用默认参数,但在只有一个参数时,必须使用括号而不能省略:

    1. let makeKing = (name = 'Henry') => `国王${name}`;
    2. console.log(makeKing()); //国王Henry

    默认参数作用域与暂时性死区
    因为在求值默认参数时可以定义对象,也可以动态调用函数,所以函数参数肯定是在某个作用域中求值的。
    给多个参数定义默认值实际上跟使用let关键字顺序声明变量一样。
    来看下面的例子:

    1. function makeKing(name = 'Henry', numerals = '八世') {
    2. return `国王${name}${numerals}`;
    3. }
    4. console.log(makeKing()); // 国王Henry八世
    5. // 这里的默认参数会按照定义它们的顺序依次被初始化。可以依照如下示例想象一下这个过程:
    6. function makeKing() {
    7. let name = 'Henry';
    8. let numerals = '八世';
    9. return `国王${name}${numerals}`;
    10. }
    11. console.log(makeKing()); // 国王Henry八世

    因为参数是按顺序初始化的,所以后定义默认值的参数可以引用先定义的参数

    1. function makeKing(name = 'Henry', numerals = name) {
    2. return `国王${name} ${numerals}`;
    3. }
    4. console.log(makeKing()); // 国王Henry Henry

    参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的。
    像这样就会抛出错误:

    1. function makeKing(name = numerals, numerals = '八世') {
    2. return `国王${name} ${numerals}`;
    3. }
    4. console.log(makeKing());
    5. // Uncaught ReferenceError: Cannot access 'numerals' before initialization

    参数也存在于自己的作用域中,它们不能引用函数体的作用域:

    1. function makeKing(name = numerals, numerals = defaultNumeral) {
    2. let defaultNumeral = '八世';
    3. return `国王${name} ${numerals}`;
    4. }
    5. console.log(makeKing());
    6. // Uncaught ReferenceError: Cannot access 'numerals' before initialization

    10.6 参数扩展与收集
    ECMAScript 6新增了扩展操作符,使用它可以非常简洁地操作和组合集合数据。
    扩展操作符最有用的场景:函数定义中的参数列表
    在这里,它可以充分利用这门语言的弱类型,及参数长度可变的特点。
    扩展操作符既可以用于调用函数时传参,也可以用于定义函数参数。
    10.6.1 扩展参数
    在给函数传参时,有时候可能不需要传一个数组,而是要分别传入数组的元素。
    假设有如下函数定义,它会将所有传入的参数累加起来:

    1. let values = [1,2,3,4];
    2. function getSum() {
    3. let sum = 0;
    4. for (let i = 0; i < arguments.length; ++i) {
    5. sum += arguments[i];
    6. }
    7. return sum;
    8. }

    这个函数希望将所有加数逐个传进来,然后通过迭代arguments对象来实现累加。
    如果不使用扩展操作符,想把定义在这个函数这面的数组拆分,那么就得求助于apply()方法:

    1. console.log(getSum.apply(null, values)); // 10

    但在ECMAScript 6中,可通过扩展操作符极为简洁地实现这种操作。
    对可迭代对象应用扩展操作符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入。
    比如,使用扩展操作符可以将前面例子中的数组像这样直接传给函数:

    1. console.log(getSum(...values)); // 10

    因为数组的长度已知,所以在使用扩展操作符传参的时候,并不妨碍在其前面或后面再传其他的值,包括使用扩展操作符传其他参数:

    1. console.log(getSum(-1, ...values)); // 9
    2. console.log(getSum(...values, 5)); // 15
    3. console.log(getSum(-1, ...values, 5)); // 14
    4. console.log(getSum(...values, ...[5,6,7])); // 28

    对函数中的arguments对象而言,它并不知道扩展操作符的存在,而是按照调用函数时传入的参数接收每一个值
    arguments对象只是消费扩展操作符的一种方式。在普通函数和箭头函数中,也可以将扩展操作符用于命名参数,当然同时也可以使用默认参数:

    1. function getProduct(a,b,c = 1) {
    2. return a*b*c;
    3. }
    4. let getsum = (a,b,c = 0) => {
    5. return a+b+c;
    6. }
    7. console.log(getProduct(...[1,2])); // 2
    8. console.log(getProduct(...[1,2,3])); // 6
    9. console.log(getProduct(...[1,2,3,4])); // 6
    10. console.log(getsum(...[0,1])); // 1
    11. console.log(getsum(...[0,1,2])); // 3
    12. console.log(getsum(...[0,1,2,3])); // 3

    10.6.2 收集参数
    在构思函数定义时,可以使用扩展操作符把不同长度的独立参数组合为一个数组
    这有点类似arguments对象的构造机制,只不过收集参数的结果会得到一个Array实例。

    1. function getSum(...values) {
    2. // 顺序累加values中的所有值
    3. // 初始的总和为0
    4. return values.reduce((x,y) => x+y, 0);
    5. }
    6. console.log(getSum(1,2,3)); // 6

    收集参数的前面如果还有命名参数,则只会收集其余的参数;如果没有则会得到空数组。因为收集参数的结果可变,所以只能把它作为最后一个参数
    箭头函数虽然不支持arguments对象,但支持收集参数的定义方式,因此也可以实现与使用arguments一样的逻辑

    1. let getSum = (...values) => {
    2. return values.reduce((x,y) => x+y, 0);
    3. }
    4. console.log(getSum(1,2,3)); // 6

    10.7 函数声明与函数表达式
    JavaScript引擎在加载数据时,对函数声明和函数表达式,是区别对待的。
    JavaScript引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。
    而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。
    来看下面的例子:

    1. console.log(sum(10, 10));
    2. function sum(num1, num2) {
    3. return num1 + num2;
    4. } // 20

    代码可以正常运行,因为函数声明,会在任何代码执行之前,先被读取,并添加到执行上下文。这个过程叫作函数声明提升(function declaration hoisting)
    在执行代码时,JavaScript引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部。
    因此即使函数定义出现在调用它们的代码之后,引擎也会把函数声明提升到顶部。
    如果把前面代码中的函数声明改为等价的函数表达式,那么执行的时候就会出错

    1. console.log(sum(10, 10));
    2. let sum = function(num1, num2) {
    3. return num1 + num2;
    4. }
    5. // Uncaught ReferenceError: Cannot access 'sum' before initialization

    代码之所以会出错,是因为这个函数定义包含在一个,变量初始化语句中,而不是函数声明中。这意味着代码如果没有执行到加粗的那一行,那么执行上下文中就没有函数的定义。
    这并不是因为使用let而导致的,使用var关键字也会碰到同样的问题

    1. console.log(sum(10, 10));
    2. var sum = function(num1, num2) {
    3. return num1 + num2;
    4. }
    5. // Uncaught TypeError: sum is not a function

    除了函数什么时候真正有定义这个区别之外,这两种语法是等价的。
    注:在使用函数表达式初始化变量时,也可以给函数一个名称,比如let sum =function sum(){}
    10.8 函数作为值
    因为函数名在ECMAScript中就是变量,所以函数可以用在任何可以使用变量的地方。
    不仅可以把函数作为参数传给另一个函数,还可以在一个函数中返回另一个函数。
    从一个函数中返回另一个函数也可以,而且非常有用。

    1. function callSomeFunction(someFunc, someArg) {
    2. return someFunc(someArg);
    3. }
    4. function add10(num) {
    5. return num + 10;
    6. }
    7. let result1 = callSomeFunction(add10, 10);
    8. console.log(result1);
    9. function getGreeting(name) {
    10. return '你好,' + name;
    11. }
    12. let result2 = callSomeFunction(getGreeting, 'Amy');
    13. console.log(result2);
    14. // 20
    15. // 你好,Amy

    callSomeFunction()函数是通用的,第一个参数传入的是什么函数都可以,而且它始终返回调用作为第一个参数传入的函数的结果。
    注:如果是访问函数而不是调用函数,那就必须不带括号
    所以传给callSomeFunction()的必须是add10和getGreeting,而不能是它们的执行结果。
    从一个函数中返回另一个函数也是可以的,而且非常有用。