美文网首页
《深入理解ES6》-3-函数-笔记

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

作者: Revontulet | 来源:发表于2017-11-07 16:06 被阅读0次

    函数

    函数形参的默认值

    在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-函数-笔记

          本文链接:https://www.haomeiwen.com/subject/lzejmxtx.html