《深入理解ES6》-3-函数-笔记

函数

函数形参的默认值

在ES5中模拟默认参数

  • 第一种方式:
    • 缺陷: 如果给num传入值为0, 那么因为被视为false,所以num在函数内为100。
    function aa(num, callback) {
        num = num || 100;
        callback = callback = function() {};
    }
  • 第二种方式:
    • 常见于流行的JS库中
    function aa(num, callback) {
        num = (typeof num !== "undefined") ? num : 100;
        callback = (typeof callback !== "undefined") ? callback : function() {};
    }

ES6中的默认参数

  • 提供一个初始值
    • 例如:
    function aa(num = 100, callback = function() {}) {
        // ...
    }
  • 声明函数时,可以为任意参数设置默认值
  • 在已指定默认值的参数后可以继续声明无默认值的参数
    • 如:function aa(num = 100, callback) {}
    • 此时,只有当 不为第二个参数传值 或 主动为第二个参数传入“undefined”时,第二个参数才会使用默认值
    • 传入null,不会使用第二个参数的默认值,其值最终为null

默认参数对arguments对象的影响

  • ES5中 非严格模式下, 命名参数的变化会被同步更新到arguments对象中
  • ES5的严格模式下,无论参数在函数体内如何变化,都不会影响到arguments对象
  • ES6中,如果一个函数使用了默认参数,那么如论何种模式下,都与严格模式保持一致
    • 默认参数的存在使得arguments对象与命名参数保持分离
    function aa(x, y ="y") {
        console.log(arguments.length);
        console.log(x === arguments[0]);
        console.log(y === arguments[1]);
        x = "x";
        y = "000"
        console.log(x === arguments[0]);
        console.log(y === arguments[1]);
    }
 + 输出结果为:```1, true, false, false, false```
 + 这种特性可以让我们通过arguments对象将参数回复为初始值

默认参数表达式

  • 非原始值传参
    • 只有调用add()函数且不传入第二个参数时才会调用getValue()
    function getValue() {
        return 5
    }
    function add(x, y = getValue()) {
        return x + y;
    }

    console.log(add(1, 1));
    // 2
    console.log(add(1));
    // 6
 + 当使用函数调用结果作为默认参数值时,如果忘记写小括号,最终传入的是函数的引用,而不是函数调用的结果
  • 可以使用先定义的参数作为后定义参数的默认值,但不能用后定义的参数作为先定义参数的默认值
    • 临时死区(TDZ)
    function add(x = y, y) {
        return x + y;
    }

    console.log(add(1, 1));
    // 2
    console.log(add(1));
    // 抛出错误
    // 表示调用add(undefined, 1), 即:
    // let x = y;
    // let y = 1;
    // 此时会报错

默认参数的临时死区

  • 与let声明类似
  • 定义参数时会为每个参数创建一个新的标识符绑定
  • 该绑定在初始化之前不可被引用
  • 如果试图访问,会导致程序抛出错误

处理无命名参数

  • 无论函数已定义的命名参数有多少,调用时都可以传入任意数量的参数
    • 当传入更少的参数时,默认参数值的特性可以有效简化函数声明的代码
    • 当传入更多数量的参数时,需要用到以下ES6的新特性

ES5中的无命名参数

  • 实例:返回一个给定对象的副本,包含院士对象属性的特定子集
  • 模仿了Underscore.js中的pick()方法
    function pick(obj) {
        let result = Object.create(null);
        // 从第二个参数开始
        for (let i = 1, len = arguments.length; i < len; i++) {
            result[arguments[i]] = obj[arguments[i]];
        }
        return result;
    }

    let book = {
        author: "shaun",
        age: 20
    };

    let data = pick(book, "author", "age");
    // shaun
    console.log(data.author);
    // 20
    console.log(data.age);
  • 不足:
    • 不容易发现这个函数可以接受任意数量的参数
    • 因为第一个参数为命名参数且已经被占用,当需要查找需要拷贝的属性名称时,需要从索引1开始遍历arguments对象
    • 可以用ES6的不定参数特性解决

不定参数

  • 在函数的命名参数前加三个点(...)就表明这是一个不定参数
  • 该参数为一个数组,包含"自它之后"传入的所有参数
    • 这个特性使得可以放心遍历keys对象了,没有索引的特殊性
    • 另一个好处是只需看一眼,就能知道函数可以处理的参数数量
  • 通过这个数组名,即可访问到里面的参数
  • 重写pick函数
    function pick(obj, ...keys) {
        let result = Object.create(null);
        for (let i = 1, len = arguments.length; i < len; i++) {
            result[keys[i]] = obj[keys[i]];
        }
        return result;
    }
  • 不定参数的使用限制
    • 每个函数只能声明一个不定参数,而且只能放在所有参数的末尾
    • 不定参数不能用于对象字面量stter之中
      • 因为setter的参数有且只能有一个
    let obj = {
        // 会报错,不可以在setter中使用不定参数
        set name(...value) {
            // to do...
        }
    }
  • 不定参数对arguments的影响
    • 无论是否使用不定参数,当函数被调用时,arguments对象依然包含了所有传入的参数
    function check(...args) {
        console.log(args.length);
        console.log(arguments.length);
        console.log(args.[0], arguments[0]);
        console.log(args.[1], arguments[1]);
    }

    check("a", "b");
    // 输出:
    // 2
    // 2
    // a a
    // b b

增强的Function构造函数

  • Function构造函数是来动态创建新函数的方法
  • 接受字符串形式的参数作为函数的参数和函数体(最后一项默认为函数体)
    var add = new Function("x", "y", "return x + y");
    // 2
    console.log(add(1, 1));
  • ES6中支持定义默认参数和不定参数
    • 默认参数
    var add = new Function("x", "y = 2", "return x + y");
    // 2
    console.log(add(1, 1));
    // 3
    console.log(add(1));
 + 不定参数(只能在最后一个参数前加...)
    var pick = new Function("...args", "return args[0]");
    // 1
    console.log(pick(1, 2));

展开运算符

  • 与不定参数的区别
    • 不定参数可以让你指定多个各自独立的参数,并通过数组来访问
    • 展开运算符可以让你指定一个数组,将它们打散后作为各自独立的参数传入函数
  • ES5 实例
    let value = [25, 50, 75, 100];
    // 100
    console.log(Math.max.apply(Math, value));
  • ES6 实例
    let value = [25, 50, 75, 100];
    // 等价于
    // console.log(Math.max(25, 50, 75, 100));
    // 100
    console.log(Math.max(...value));
  • 可以将展开运算符与其他正常传入的参数混合使用
    let value = [25, -50, -75, -100];
    let value2 = [-25, -50, -75, -100];
    // 25
    console.log(Math.max(...value, 0));
    // 0
    console.log(Math.max(...value2, 0));
  • 大多数使用apply()方法的情况下,展开运算符都可能是一个更适合的方案

name属性

  • 辨别函数对调试和追踪难以解读的栈记录
  • ES6 为所有函数新增了name属性

如何选择合适的名称

  • name属性都有一个合适的值
    function dosth() {
        //
    }
    var doelse function() {
        //
    };
    // dosth
    dosth.name;
    // doelse
    doelse.name;

name属性的特殊情况

  • 特殊情况
    var dosth = function doelse() {
        //
    };
    var person = {
        get firstName() {
            return "shaun";
        },
        sayName: function() {
            console.log(this.name);
        }
    }
    // 由于函数表达式的名字比函数本身被赋值的变量的权重高
    // doelse
    dosth.name;
    // 取值对象字面量
    // sayName
    person.sayName.name;
    // getter和setter,都会有前缀
    // get firstName
    person.firstName.name;
 + 其他两种前缀
    var dosth = function() {
        //
    };
    // bound dosth
    dosth.bind().name;
    // anonymous
    (new Function()).name;
  • 函数的name属性的值不一定引用同名变量,只是协助调试用的额外信息
  • 所以不能使用name属性的值来获取对于函数的引用

明确函数的多重用途

  • 当通过new关键字调用函数时,执行的是[[construct]]函数
    • 负责创建一个实例新对象
    • 然后再执行函数体
    • 将this绑定到实例上
  • 不通过new调用时,执行的是[[call]]函数
    • 直接执行函数体
  • 具有[[construct]]方法的函数被统称为构造函数
  • 没有[[construct]]方法的函数不能通过new来调用

在ES5中判断函数被调用的方法

  • ES5 确定一个函数是否通过new关键字被调用,最流行用instanceof
    • 缺点:不完全可靠,例如call
    function person() {
        if (this instanceof person) {
            this.name = name;
        } else {
            throw new Error("msg")
        }
    }
    // 成功执行
    var person = new Preson("shaun");
    // 抛错
    var notPerson = Preson("shaun");
    // 也被成功执行
    var notPerson2 = Preson.call(person, "shaun");

元属性(Metaproperty) new.target

  • 此特性可以解决判断是否通过new调用的问题
  • 当调用函数的[[construct]]方法时,new.target被赋值为new操作符的目标
  • 当调用函数的[[call]]方法时,new.target被赋值为undefined
  • 在函数体外使用new.target是一个语法错误

块级函数

  • ES5 中不能在例如if语句中创建函数
  • ES6 中可以,但是只能在代码块中调用此函数,代码块结束执行后,此函数将不再存在

块级函数的使用场景

  • 严格模式下
    • let 声明的变量不会提升到代码块顶部
    • 声明的函数会提升到代码块顶部
  • 非严格模式下
    • 函数直接提升到外围函数或全局作用域的顶部

箭头函数

  • 没有this, super, arguments, new.target的绑定
    • 这些值由外围最近一层非箭头函数决定
  • 不能通过new调用
    • 因为没有[[construct]]方法
  • 没有原型
  • 不可以改变this的绑定
    • 在函数生命周期内都不会变
  • 不支持重复的命名参数
  • 也有一个name属性

箭头函数语法

  • 传入一个参数
    let fn = val => val;
  • 传入2个以上的参数
    let sum = (x, y) => x + y;
  • 不传参数
    let name = () => "shaun";
  • 由多个表达式组成的函数体要用{}包裹,并显式定义一个返回值
    let sum = (x, y) => { return x + y; };
  • 除了arguments对象不可用外,某种程度上都可以将花括号内的代码视作传统的函数体
  • 创建一个空函数,仍需要写一对没有内容的花括号
  • 如果想在箭头函数外返回一个对象字面量,需要将该对象字面量包裹在小括号内(为了将其与函数体区分开)
    let getId = id => ({ id: id, name: "shaun" });
    // 相当于
    let getId = function(id) {
        return {
            id: id,
            name: "shaun"
        };
    };

创建立即执行函数表达式

  • 函数的一个流行使用方式是创建IIFE
    • 定义一个匿名函数并立即调用
    • 自始至终不保存对该函数的引用
    • 可以用作创建一个与其他程序隔离的作用域
    let person = function(name) {
        return {
            getName: function() {
                return name;
            }
        };
    }("shaun");
    // shaun
    console.log( person.getName());
  • 将箭头函数包裹在小括号内,但不把("shaun")包裹在内
    let person = ((name) => {
        return {
            getName: function() {
                return name;
            }
        };
    })("shaun");
    // shaun
    console.log( person.getName());

箭头函数没有this绑定

  • 箭头函数的this值取决于该函数外部非箭头函数的this值
    • 箭头函数内没有this绑定,需要通过查找作用域链来决定其值
    • 否则会被设置为undefined

箭头函数和数组

  • 箭头函数适用于数组处理
  • 实例:排序
    var value = [1, 2, 3, 9, 8];
    // 老办法
    var result = value.sort(function(a, b) {
        return a - b;
    })
    // 新办法
    var result2 = value.sort((a, b) => a - b); 
  • 诸如sort(), map(), reduce()这些可以接受回掉函数的数组办法
  • 都可以通过箭头函数语法简化编码过程,减少编码量

箭头函数没有argument绑定

  • 箭头函数没有自己的arguments对象
  • 但是无论在哪个上下文中执行,箭头函数始终可以访问外围函数的arguments对象

箭头函数的辨识方法

  • 同样可以辨识出来
    var result = (a, b) => a - b;
    // function
    console.log(typeof result); 
    // true
    console.log(result instanceof Function);
  • 仍然可以在箭头函数上调用call(), apply(), bind()方法,但有区别:
    var result = (a, b) => a + b;
    // 3
    console.log(result.call(null, 1, 2)); 
    // 3
    console.log(result.call(null, 1, 2)); 

    var boundResult = result.bind(null, 1, 2);
    // bind后的方法不用传参
    // 3
    console.log(boundResult());

尾调用优化

  • 尾调用指的是函数作为另一个函数的最后一条语句被调用
  • 如此会创建一个新的栈帧:stack frame
  • 在循环调用中,每一个未用完的栈帧都会被保存在内存中
    function dosth() {
        // 尾调用
        return doelse();
    }

ES6中的尾调用优化

  • ES6 缩减了严格模式下尾调用栈的大小
  • 非严格模式下不受影响
  • 满足以下条件,尾调用不再创建新的栈帧,而是清除并重用当前栈帧
    • 尾调用不访问当前栈帧的变量(即函数不是闭包)
    • 在函数内部,尾调用是最后一条语句
    • 伟嗲用的结果作为函数值返回

如何利用尾调用优化

  • 递归函数是其应用最常见的场景,尾调用优化效果最显著
    function fn(n) {
        if (n <= 1) {
            return 1;
        } else {
            return n * fn(n - 1);
        }
    } 
  • 上面的代码中,递归调用前执行了乘法,因而当前版本的阶乘函数不能被引擎优化

  • 当n是一个非常大的数时,调用栈的尺寸就会不断增长并存在最终导致溢出的风险

  • 优化方法:

    • 首先确保乘法不在函数调用后执行
      • 这里使用默认参数来将乘法移除return语句
    function fn(n, p = 1) {
        if (n <= 1) {
            return 1 * p;
        } else {
            let result = n * p;
            // 优化后
            return fn(n - 1, result);
        }
    }

你可能感兴趣的:(《深入理解ES6》-3-函数-笔记)