《深入理解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);
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355

推荐阅读更多精彩内容

  • 函数参数的默认值 基本用法 在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。 上面代码检查函数l...
    呼呼哥阅读 3,384评论 0 1
  • 第一章 块级作用域绑定 let 和 const 都是不存在提升,声明的都是块级标识符都禁止重声明 每个const声...
    NowhereToRun阅读 1,586评论 0 2
  • 1.函数参数的默认值 (1).基本用法 在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
    赵然228阅读 690评论 0 0
  • 三,字符串扩展 3.1 Unicode表示法 ES6 做出了改进,只要将码点放入大括号,就能正确解读该字符。有了这...
    eastbaby阅读 1,534评论 0 8
  • 一、ES6简介 ​ 历时将近6年的时间来制定的新 ECMAScript 标准 ECMAScript 6(亦称 ...
    一岁一枯荣_阅读 6,072评论 8 25