• 【读书笔记】【深入理解ES6】#3-函数


    函数形参的默认值

    ES6中的默认参数值

    function makeRequest(url, timeout = 2000, callback = function() {}) {
    
    }
    

    可以为任意参数指定默认值,在已指定默认值的参数后可以继续声明无默认值参数。

    function makeRequest(url, timeout = 2000, callback) {
    
    }
    

    这种情况下,之后当不为第二个参数传入值或者主动为第二个参数传入 undefined 时才会使用 timeout 的默认值

    // 使用 timeout 的默认值
    makeRequest("/foo", undefined, function(body) {
        doSomething(body);
    })
    
    // 使用 timeout 的默认值
    makeRequest("/foo");
    
    // 不使用 timeout 的默认值
    makeRequest("/foo", null, function(body) {
        doSomething(body);
    })
    

    第三个调用需要注意。对于默认参数值,null是一个合法值。
    关于 null 和 undefined 的区别请参照阮一峰的 undefined与null的区别

    默认参数值对 arguments 对象的影响

    ES5非严格模式下,函数命名参数的变化会体现在 arguments 对象中。

    function mixArgs(first, second) {
        console.log(arguments.length);
        console.log(first === arguments[0]);
        console.log(second === arguments[1]);
        first = "c";
        second = "d";
        console.log(first === arguments[0]);
        console.log(second === arguments[1]);
    }
    
    mixArgs("a", "b");
    // 2
    // true
    // true
    // true
    // true
    

    然而在ES5严格模式下,取消了 arguments 对象这个令人感到困惑的行为。
    命名参数与 arguments 对象分离开了。

    function mixArgs(first, second) {
        "use strict"; // 设置为严格模式
        console.log(arguments.length);
        console.log(first === arguments[0]);
        console.log(second === arguments[1]);
        first = "c";
        second = "d";
        console.log(first === arguments[0]);
        console.log(second === arguments[1]);
    }
    
    mixArgs("a", "b");
    // 2
    // true
    // true
    // false
    // false
    

    在ES6中,如果一个函数使用了默认参数值,则无论是否显示定义了严格模式,arguments 对象的行为都将是与ES5严格模式下保持一致。

    function mixArgs(first, second = "b") {
        console.log(arguments.length);
        console.log(first === arguments[0]);
        console.log(second === arguments[1]);
        first = "c";
        second = "d";
        console.log(first === arguments[0]);
        console.log(second === arguments[1]);
    }
    
    mixArgs("a");
    // 1
    // true
    // false
    // false
    // false
    
    mixArgs("a", "b");
    // 2
    // true
    // true
    // false
    // false
    

    默认参数表达式

    默认参数值可以是非原始值传参。

    function getValue() {
        return 5;
    }
    
    function add(first, second = getValue()) {
        return first + second;
    }
    
    console.log(add(1, 1)); // 2
    console.log(add(1)); // 6
    

    上例中,second 的默认值为 getValue() 的返回值。

    默认参数还可以使用先定义的参数作为后定义参数的默认值。

    function add(first, second = first) {
        return first + second;
    }
    
    console.log(add(1, 1)); // 2
    console.log(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
    

    在引用参数默认值时,只允许引用前面参数的值,即先定义的参数不能访问后定义的参数。

    function add(first = second, second) {
        return first + second;
    }
    
    console.log(add(1, 1)); // 2
    console.log(add(undefined, 1)); // 抛出错误
    // Uncaught ReferenceError: second is not defined
    

    因为second比first定义的晚,所以不能作为fist的默认值。
    这里就是所谓默认参数的临时死区(TDZ)。

    Note

    默认参数有自己的作用域和临时死区,其与函数体的作用域是各自独立的,也就是说参数的默认值不可访问函数体内声明的变量。

    处理无命名参数

    JS的函数语法规定,无论函数已定义的命名参数有多少,都不限制调用时传入的实际参数的数量,调用时总是可以传入任意数量的参数。

    ES5中的无命名参数

    JS提供了 arguments 对象来检查函数的所有参数,从而不必定义每一个要用的参数。

    function pick(object) {
        let result = Object.create(null);
    
        // 从第二个参数开始
        for (let i = 1, len = arguments.length; i < len; i++) {
            result[arguments[i]] = object[arguments[i]];
        }
    
        return result;
    }
    
    let book = {
        title: "Understanding ECMAScript 6",
        author: "Nicholas C. Zakas",
        year: 2016
    };
    
    let bookData = pick(book, "author", "year");
    
    console.log(bookData.author); // "Nicholas C. Zakas"
    console.log(bookData.year); // 2016
    

    上例中,pick() 函数返回一个给定对象的副本,包含原始对象的特定子集。

    不定参数

    ES6中提供了不定参数(rest parameters)特性来提供更好的实现方案。

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

    Note

    函数的 length 属性统计的是函数命名参数的数量,不定参数的加入不会影响 length 属性的值。上述 pick 方法的 length 值为1.

    不定参数的使用限制

    1. 每个函数最多只能声明一个不定参数,而且一定要放在所有参数的末尾;
    2. 不定参数不能用于对象字面量 setter 之中。

    不定参数对 arguments 对象的影响

    无论是否使用不定参数,arguments 对象总是包含所有传入函数的参数。

    增强的 Function 构造函数

    使用 Function 构造函数创建函数。

    var add = new Function("first", "second", "return first + second");
    console.log(add(1, 1)); // 2
    

    创建的 add 方法是如下样子的:

    ƒ anonymous(first,second
    /*``*/) {
    return first + second
    }
    

    ES6中增强了该构造函数,使其可以支持默认参数和不定参数。

    var add = new Function("first", "second = first", "return first + second");
    console.log(add(1, 1)); // 2
    console.log(add(1)); // 2
    
    var pickFirst = new Function("...args", "return args[0]");
    console.log(pickFirst(1, 2)); // 1
    

    展开运算符

    在所有新功能中,与不定参数最相似的是展开运算符。
    以Math.max()方法为例,该方法可以接受任意数量的参数并返回最大的那一个。

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

    但该方法不支持数组,如果需要从数组中挑出一个最大的时该怎么做呢?
    ES5中可以使用apply()方法实现该功能。

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

    虽然可以实现该功能,但是难以理解。
    ES6中可以使用展开运算符(...)简化上述示例。

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

    也可以将展开运算符与其它正常传入的参数混合使用。

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

    name 属性

    ES6中所有的函数的name属性都有一个合适的值。可以帮助开发更好的追踪问题。

    function doSomething() {
    
    }
    
    var doAnotherThing = function() {
    
    };
    
    console.log(doSomething.name); // 函数名称 "doSomething"
    console.log(doAnotherThing.name); // 匿名函数的变量的名称 "doAnotherThing"
    

    name 属性的特殊情况

    var doSomething = function doSomethingElse() {
    
    }
    
    var person = {
        get firstName() {
            return "Nicholas";
        },
        sayName: function() {
            console.log(this.name);
        }
    }
    
    console.log(doSomething.name); // "doSomethingElse"
    console.log(person.sayName.name); // "sayName"
    console.log(person.firstName.name); // "get firstName"
    
    • doSomething.name
      函数本身的名字权重更高
    • person.sayName.name
      其值取自对象字面量
    • person.firstName.name
      person.firstName实际上是个getter函数,自动加上了前缀 “get”。
      setter函数也有其前缀“set”。
      另外通过 bind() 函数创建的函数,其名称带有“bound”前缀;
      通过Function构造函数创建的函数,其名称带有“anonymous”前缀。
    var doSomething = function() {
    
    };
    
    console.log(doSomething.bind().name); // "bound doSomething"
    console.log((new Function()).name); // "anonymous"
    

    明确函数的多重用途

    ES5及早期版本中的函数具有多重功能,可以结合new使用,函数内的this值将指向一个新对象,函数最终返回这个新对象。

    function Person(name) {
        this.name = name;
    }
    
    var person = new Person("JiaJia");
    var notAPerson = Person("JiaJia");
    
    console.log(person); // "Person {name: "JiaJia"}"
    console.log(notAPerson); // "undefined"
    console.log(window.name); // "JiaJia"
    

    如果不同new关键字调用Person方法,不仅得不到想要的结果,还会在全局作用创建一个name属性。

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

    使用 instanceof 操作符判断是会否是通过new关键字调用。

    function Person(name) {
        if (this instanceof Person) {
            this.name = name;
        } else {
            throw new Error("必须通过new关键字来调用Person。");
        }
    }
    
    var person = new Person("JiaJia");
    var notAPerson = Person("JiaJia"); // 抛出错误
    

    一般来说上述写法是有效的,但是也有例外情况。
    因为有一种不依赖new关键字的方法也可以将this绑定到person的实例上。

    function Person(name) {
        if (this instanceof Person) {
            this.name = name;
        } else {
            throw new Error("必须通过new关键字来调用Person。");
        }
    }
    
    var person = new Person("JiaJia");
    var notAPerson = Person.call(person, "XKA");
    // 没有抛出错误,但也没有得到想要的对象
    // 实际修改的是person实例的值
    

    元属性(Metaproperty) new.target

    为了解决判断函数是否通过new关键字调用的问题,ES6引入了 new.target 这个元属性。
    元属性是指非对象的属性,其可以提供非对象目标的补充信息。
    当调用函数的[[Construct]]方法时,new.target 被赋值为new操作符的目标,通常是新创建对象实例,也就是函数体内this的构造函数;
    如果调用[[call]]方法,则new.target的值为undefined。

    function Person(name) {
        if (typeof new.target !== "undefined") {
            this.name = name;
        } else {
            throw new Error("必须通过new关键字来调用Person。");
        }
    }
    
    var person = new Person("JiaJia");
    var notAPerson = Person.call(person, "XKA"); // 抛出错误
    

    也可以检查 new.target 是否被某个特定构造函数所调用

    function Person(name) {
        if (typeof new.target === Person) {
            this.name = name;
        } else {
            throw new Error("必须通过new关键字来调用Person。");
        }
    }
    
    function AnotherPerson(name) {
        Person.call(this, name);
    }
    
    var person = new Person("JiaJia");
    var anotherPerson = new AnotherPerson("DLPH"); // 抛出错误
    

    Note

    在函数外使用 new.target 是一个语法错误。

    块级函数

    在代码块中声明的函数。

    "use strict";
    
    if (true) {
        console.log(typeof doSomething); // "function"
        
        function doSomething() {
    
        }
        
        doSomething();
    }
    
    console.log(typeof doSomething); // "undefined"
    

    ES6严格模式下,在定义函数的代码块内,块级函数会被提升至顶部。

    块级函数与let函数表达式类似,一旦执行过程流出了代码块,函数定义立即被移除。
    两者的区别是let定义的函数不会被提升。

    "use strict";
    
    if (true) {
        console.log(typeof doSomething); // 抛出错误
        // Uncaught ReferenceError: doSomething is not defined
        
        let doSomething = function () {
    
        }
        
        doSomething();
    }
    
    console.log(typeof doSomething);
    

    非严格模式下的块级函数

    在ES6的非严格模式下,块级函数会被提升至外围函数或全局作用域的顶部。

    if (true) {
        console.log(typeof doSomething); // "function"
        
        function doSomething() {
    
        }
        
        doSomething();
    }
    
    console.log(typeof doSomething); // "function"
    

    箭头函数

    箭头函数是一种使用箭头(=>)定义函数的新语法,但是它与传统的JS函数有些不同。

    • 没有 this、super、arguments和new.target绑定
    • 不能通过new关键字调用
    • 没有原型
    • 不可以改变 this 的绑定
    • 不支持 arguments 对象
    • 不支持重复的命名参数

    箭头函数语法

    let reflect = value => value;
    
    // 实际相当于
    let reflect = function(value) {
        return value;
    };
    

    如果要传入两个或以上参数,要在参数的两侧添加一对小括号。

    let sum = (num1, num2) => num1 + num2;
    
    // 实际上相当于
    let sum = function(num1, nume) {
        return num1 + num2;
    };
    

    如果没有参数,也要在声明的时候写一组没有内容的小括号。

    let getName = () => "JiaJia";
    
    // 实际上相当于
    let getName = function() {
        return "JiaJia";
    };
    

    如果函数体是多行,则需要用花括号包裹函数体。

    let sum = (num1, num2) => {
        return num1 + num2;
    }
    
    // 实际上相当于
    let sum = function(num1, nume) {
        return num1 + num2;
    };
    

    除了 arguments 对象不能使用外,某种程度上你都可以将花括号里的代码视作传统的函数体定义。

    如果想创建一个空函数,需要写一对没有内容的花括号。

    let doNothing = () => {};
    
    // 实际上相当于
    let doNothing = function() {};
    

    如果想在箭头函数外返回一个对象字面量,则需要将该字面量包裹在小括号里。

    let getTempItem = id => ({ id: id, name: "Temp" });
    
    // 实际上相当于
    let getTempItem = function(id) {
        return {
            id: id,
            name: "Temp"
        };
    };
    

    创建立即执行函数表达式

    let person = ((name) => {
        return {
            getName: function() {
                return name;
            }
        };
    })("JiaJia");
    
    console.log(person.getName()); // "JiaJia"
    

    箭头函数没有this绑定

    箭头函数中没有this绑定,必须通过查找作用域链来决定其值。
    如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this;
    否则this的值会被设置为undefined。

    let PageHandler = {
        id: "123456",
    
        init: function() {
            document.addEventListener("click", event => this.doSomething(event.type), false);
        },
    
        doSomething: function(type) {
            console.log("Handling " + type + " fro " + this.id);
        }
    };
    

    这里addEventListener的第二个参数如果使用匿名函数的形式,则this是当前click事件目标对象(这里是document)的引用。
    而在本例中,this就是PageHandler对象。

    箭头函数缺少正常函数所拥有的property属性,所以不能用它来定义新的类型。
    如果尝试用new关键字调用一个箭头函数,会导致程序抛出错误。

    var MyType = () => {};
    var object = new MyType(); // 抛出错误
    // Uncaught TypeError: MyType is not a constructor
    

    箭头函数中的this值取决于该函数外部非箭头函数的this值,且不能通过call()、apply()或bind()方法来改变this的值。

    箭头函数和数组

    箭头函数的语法简洁,非常适用于数组处理。

    var result = values.sort(function(a, b) {
        return a - b;
    });
    

    可以简化为如下形式

    var result = values.sort((a, b) => a - b);
    

    箭头函数没有arguments绑定

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

    function createArrowFunctionReturningFirstArg() {
        return () => arguments[0];
    }
    
    var arrowFunction = createArrowFunctionReturningFirstArg(5);
    
    console.log(arrowFunction); // 5
    

    尾调用优化

    尾调用指的是函数作为另一个函数的最后一条语句被调用。

    function doSomething() {
        return doSomethingElse(); // 尾调用
    }
    

    在ES5中,尾调用的实现与其它函数调用的实现类似:创建一个新的栈帧(stack frame),将其推入调用栈来表示函数调用。
    也就是说,在循环调用中,每一个未用完的栈帧都会保存在内存中,当调用栈变得过大时会造成程序问题。

    ES6中的尾调用优化

    ES6缩减了严格模式下尾调用栈的大小(非严格模式下不受影响),如果满足以下条件,尾调用不再创建新的栈帧,而是清除并重用当前栈帧。

    • 尾调用不访问当前栈帧的变量(也就是说函数不是一个闭包)
    • 在函数内部,尾调用是最后一条语句
    • 尾调用的结果作为函数返回
    "use strict";
    
    function doSomething() {
        // 可优化
        return doSomethingElse(); // 尾调用
    }
    

    以下形式均无法优化

    "use strict";
    
    function doSomething() {
        // 不可优化
        doSomethingElse();
    }
    
    "use strict";
    
    function doSomething() {
        // 不可优化
        return 1 + doSomethingElse();
    }
    
    "use strict";
    
    function doSomething() {
        // 不可优化
        var result = doSomethingElse();
        return result;
    }
    
    "use strict";
    
    function doSomething() {
        var num = 1,
            func = () => num;
        // 不可优化,该函数是一个闭包
        return func();
    }
    

    如何利用尾优化

    递归函数是主要的应用场景,此时尾调用优化的效果最显著。

    优化前:

    function factorial(n) {
        if (n <= 1) {
            return 1;
        } else {
            // 无法优化,必须在返回之后执行乘法操作
            return n * factorial(n - 1);
        }
    }
    

    优化后:

    function factorial(n, p = 1) {
        if (n <= 1) {
            return 1 * p;
        } else {
            return factorial(n - 1, n * p);
        }
    }
    

    Warning

    通过在谷歌浏览器上测试,好像没有起作用,优化后的代码依然会栈溢出。
    这本书作者在写时,这个特性仍在审核中。估计是该优化没有通过ES6的审核。

  • 相关阅读:
    [LeetCode] 461. Hamming Distance
    [LeetCode] 1503. Last Moment Before All Ants Fall Out of a Plank
    [LeetCode] 271. Encode and Decode Strings
    [LeetCode] 38. Count and Say
    SVN安装及基本操作(图文教程)(超级详细)
    解决ERROR 2003 (HY000): Can't connect to MySQL server on 'localhost:3306' (10061)问题
    雪花算法的原理和实现Java
    XML、XML约束、XML解析、常用的xml解析器(DOM4J)、XPATH
    XML解析之SAX方式解析xml文件
    Javascript面试题
  • 原文地址:https://www.cnblogs.com/Ryukaka/p/7885728.html
Copyright © 2020-2023  润新知