07.ES6函数的扩展

    科技2025-01-10  12

    函数参数的默认值

    基本用法

    ES6之前只能采用变通的方法为函数参数指定默认值: // ES6之前变通的方法为函数参数指定默认值 function fn1(x,y){ /* y = y || "World"; // 短路运算 return x + y; 该方法并不完美,y传入的值为false时无法正确设置默认值*/ // 判断y是否等于false if(typeof y === "undefined"){ y = "World"; } return x + y ; } let res1 = fn1("hello"); // helloWorld ES6允许为函数参数设置默认值,直接写在参数定义的后面即可; // ES6 可以直接将参数默认值写在参数定义的后面 function fn2(x = "name",y = "peanut"){ console.log(x + ":" + y); } fn2(); // name:peanut fn2("name","china"); // name:china 参数变量是默认声明的,不能再使用let、const再次声明; function test(x = 5){ let x = 1; // 错误 const x = 2; // 错误 } 使用默认参数时,不能存在同名参数;参数默认值是惰性求值(用到了才会计算表达式然后赋值);使用默认值的几个好处: 代码简洁;能一眼看出哪些参数是可以省略的;有利于代码优化,即时未来的版本彻底拿掉这个参数,也不会导致代码无法运行;

    与解构赋值默认值结合使用

    参数默认值可以和解构赋值结合使用: // 解构赋值 + 参数默认值 function test({x,y = 1}) { console.log(x,y); } test({}); // undefined 1 test({x : "num"}) //num 1 test({x : 111 , y : 222}); //111 222 /* 注意 : 参数使用的是对象的解构赋值,如果传入的参数不是对象,那么就无法生成对应的x、y 参数 此时会报错 */ test();

    双重默认值

    在某些情况下,如果参数使用对象解构,那么该参数就不可省略,如果需要省略该参数,就必须使用双重默认值;见以下代码案例: function fetch(url,{body = '' , method = 'GET' , headers = {}}) { console.log(method); } fetch('www.peanut.run',{}); // GET // 省略第二个参数会报错 fetch('www.peanut.run'); // error

    ==========>

    可以使用双重默认值,使得第二个参数省略时函数也能正常运行:

    function fetch(url,{method = "GET"} = {}) { console.log(method); } fetch('www.peanut.run', {}); // GET // 此时可以省略第二个参数 fetch('www.peanut.run'); // GET

    参数默认值的位置

    通常情况下,设置了默认值的参数应该是函数的尾参数;有默认值的参数如果不是尾参数,那么该参数可以省略,但是其后面的参数是不可以省略的; // 有默认值的参数应该是尾参数 function myFn(x,y = 1,z) { console.log(x,y,z); } myFn(11,,2); // 错误,默认值参数不是尾参数 myFn(11); // 自身可省略,其后面的所有参数都不能省略,否则后面的参数都是undefined,无法正确赋值

    函数的length属性

    不指定参数默认值,函数的length属性返回的是函数参数的个数; // 不指定参数默认值,函数的length属性返回的是函数参数的个数 function fl_1(name,age,sex) {}; // 3 指定参数默认值后(如果不是尾参数),默认值参数及其后面的所有参数都将不被计入到length属性中;尾参数指定默认值,尾参数自身不计入length; // 指定参数默认值后,默认值参数及其后面的所有参数都将不被计入到length属性中 function fl_2(name,age = 18,sex) {}; // 1 age sex不计 rest参数也不会被计入; // rest也不会被计入 function fl_3(name,age,...parm) {}; // 2

    作用域

    设置函数参数的默认值之后,一旦函数进行声明初始化时,参数就会形成一个独立的作用域,这个作用域会在函数初始化结束后消失; // 设置默认值,函数声明初始化时参数会形成独立作用域 let x = 1; function fun1(y = x) { let x = 3; // 内部的变量对参数作用域没有影响,参数作用域只会到外部作用域查找 console.log(y); } fun1(2); // 2 fun1(); // 1 不管参数的默认值是一个值还是一个函数,其生成作用域查找变量时,都只会到函数外部的作用域中查找; var p = 1; function foo(p,y = function(){ p = 2; }){ var p = 3; y(); console.log(p); } foo(); // 3

    参数默认值的应用

    设置某一个参数不得省略,如果被省略就抛出错误; // 指定某个参数不可省略,如果省略就抛出错误 function error() { throw new Error("参数不可省略"); } function must(z = error()) {}; must(); // "参数不可省略" 将参数默认值设置为undefined,表明该参数可以被省略; // 将某个参数默认值设为undefined 表明该参数不可省略 function must1(opt = undefined){ console.log(opt); } must1(1); // 1 must1(); // undefined

    rest参数

    ES6引入,形式为“…变量名”,用于获取函数的多余参数;功能与arguments对象类似,区别==arguments对象是伪数组,rest参数则是一个数组,可以使用数组特有的所有方法;rest参数必须是尾参数,rest参数之后不能有其他参数;rest参数不包含在函数的length属性中; // rest作用与arguments对象类似 // rest参数是数组,arguments对象是伪数组 let test1 = (...nums) =>{ const arr = nums; // rest参数支持数组所有的方法 arr.push(99); console.log(arr); console.log(this.length); // 0 } test1(1,2,3,4,5); // [1, 2, 3, 4, 5, 99]

    严格模式

    ES5开始就可以使用“use strict”指定严格模式;ES2016对此做了一点修改,只要函数参数使用了默认值、解构赋值或者扩展运算符,函数内部就不允许显式的设置严格模式; // 声明严格模式 "use strict"; function myFn(a = 1) { "use strict"; // 错误 } 不能在函数内部声明严格模式的原因:在执行代码时,函数参数内部的代码优先于函数内部的代码执行,但是函数内的严格模式会应用与函数内部和函数参数,这就导致出现不合理的情况,函数从内部才能得知是否要严格模式执行,但是函数参数优先于函数内部执行;

    name属性

    name属性返回函数名;ES6之后,匿名函数的name属性也会返回函数名(之前是返回空字符串);将具名函数赋值给变量,name属性返回的仍是函数名; // ES6之后 匿名函数也会返回函数名 const f = function () {}; console.log(f.name); // f es5返回空字符串 // 具名函数赋值给变量,仍返回函数名 const ff = function myFn() {}; console.log(ff.name); // myFn

    箭头函数

    基本用法

    ES6允许用箭头(=>)定义函数; () => {}; 箭头前圆括号代表函数参数,函数只需要一个参数时可以省略该圆括号; // 1个返回值 1个参数 let fn1 = a => a; console.log(fn1(1)); 代码块部分只有一个返回值时,可以省略大括号,直接将要返回的结果写在箭头后面; // 多个参数 代码块多条代码 let fn2 = (aa,bb) =>{ let cc = aa + bb; console.log(cc); } fn2(1,2);// 3 大括号会被解释为代码块,因此返回一个对象时需要在大括号外部嵌套一个圆括号; let fn3 = (ID) => ({id : ID , name : "peanut"}); console.log(fn3(12)); // {id: 12, name: "peanut"} 可以和函数参数的解构赋值结合使用; let fn4 = ({first,last}) => { return first + " " + last; } let res1 = fn4({first : "宝",last : "鸡"}); console.log(res1); // 宝 鸡 // 等同于ES5写法: let fn5 = function (person) { return person.first + " " + person.last; } 箭头函数用处之一就是简化回调函数; // 简化回调函数 // ES5写法: const arr = [1,2,3]; arr.map(function(x){ return x * x; }) // ES6箭头函数 arr.map(x => x*x);

    注意事项

    箭头函数有以下几点需要注意: 函数体内的this对象就是函数定义时所在的对象,而不是调用函数的那个对象;箭头函数不能当做构造函数使用,即不能使用new关键字(报错);不可以使用arguments对象,使用rest参数代替;不可以使用yield命令,因此箭头函数不能使用Generator函数; // 箭头函数中的this是固定的 function foo() { setTimeout(() =>{ console.log('id:', this.id); },100); } var id = 12; foo.call({id : 21}); // 21 指向函数定义时的对象 用处:箭头函数可以让this指向固定化,这一特性很适合封装回调函数; // 箭头函数能使this指向固定化 适合封装回调函数 // DOM事件的回调函数封装在一个函数中 var handler = { id : '123', init : function () { document.addEventListener('click', event => this.doSomeThing(event.type),false); }, doSomeThing : function (type) { console.log('Handler' + type + 'for' + this.id); } } handler.init(); // Handlerclickfor123

    为什么箭头函数不能用作构造函数?

    因为箭头函数根本没有自己的this对象,所以不能用作构造函数;没有自己的this对象,因此箭头函数中的this引用的是其外部的this对象;

    箭头函数没有自己的this对象,自然也不能用call()、bind()、apply()方法修改this指向;

    同this一样,以下三个参数也是指向箭头函数外部的对应变量,箭头函数内部并不存在:

    arguments;super;new.target;

    箭头函数可以嵌套使用;

    // 多重嵌套函数 es5写法: function insert(value) { return { into: function (array) { return { after: function (afterValue) { array.splice(array.indexOf(afterValue) + 1, 0, val return array; } }; } }; } // ES6写法 let insert = value => ({into : array => ({after : afterValue => { array.splice(array.indexOf(afterValue + 1 , 0 , value)); return array; }})})

    绑定this(了解即可)

    ES7提出的一个提案:“函数绑定”运算符,用于取代call、bind、apply方法;函数绑定运算符是两个并排的冒号“::”,左边是一个对象,右边是一个函数;该运算符会自动将左边的对象作为上下文执行环境(this),绑定到右边的函数上;

    尾调用优化

    什么是尾调用?

    尾调用是函数式编程的一个重要概念,指某个函数的最后一步是调用另一个函数; function f(x){ return g(x); } 以下三种情况不属于尾调用: // 以下三种情况不属于尾调用 // 1.调用其他函数后进行复制操作并返回结果 function f(x){ let y = g(x); return y; } // 2.调用后还有其他操作 function h(x){ return i(x) + 1; } // 3.调用后未定义返回值 function k(x) { l(x); } // =====>等同于以下代码 function k(x) { l(x); return undefined; } 尾调用不一定出现在函数末尾: // 尾调用不一定出现在函数尾部 function f(x) { if(x > 0){ return m(x); // 尾调用 } return n(x); // 尾调用 }

    尾调用优化

    1.首先搞清楚调用帧和调用栈的概念:

    函数调用会在内存形成一个调用帧,用来保存调用位置和内部变量等信息;假设在A函数内部调用B函数,那么A函数的调用帧上方还会形成一个B的调用帧。等待B运行结束,将结果返回到A,B的调用帧才会消失;以此类推,如果B的内部还调用C,那么A的上方除了B的调用帧还会出现C的调用帧…;所有的调用帧就形成一个调用栈;

    2.尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,直接用内层函数的调用帧取代外层函数即可;

    尾调用优化:即只保留内层函数的调用帧。如果所有的函数都是尾调用,那么完全可以做到每次执行时调用帧只有一个,将大大提升性能,这也是尾调用优化的意义所在。

    如下所示:

    // 尾调用优化的目标是使得每一次调用都只有一个调用帧 function f() { let m = 1; let n = 2; return g(m + n); // 调用g之后,f的调用帧就不需要了 } f(); // 优化后的形式: function f() { return g(3); } f(); // 等同于: g(3); 只有不再用到外层函数的内部变量,内层函数的调用帧才能完全取代外层函数的调用帧,否则无法实现“尾调用优化”: function addOne(a) { var one = 1; function inner(b) { return b + one; // 无法尾调用优化 inner函数内部引用了外部函数的one变量 } return inner(a); }

    尾递归

    定义:函数调用自身就称为递归,尾调用自身就称为尾递归;递归非常消耗内存,在特定情况下需要保存许多调用帧,这就导致很容易出现栈溢出错误;但是对于尾调用来说,只存在一个调用帧,就不可能出现栈溢出; // 尾调用优化后的尾递归函数 function fac(n,total = 1) { if(n === 1){ return total; } return fac(n - 1,n * total); } fac(5,1);

    严格模式

    尾调用优化仅在严格模式下开启,正常模式下是无效的;原因:正常模式下由于arguments(返回调用时函数的参数)和caller(返回调用当前函数的函数)变量的存在,这两个变量可以追踪函数的调用栈;尾调用优化时,会修改函数的调用帧,上面的参数也会失真,严格模式能禁用这两个变量,因此尾调用优化仅在严格模式下生效; // 尾调用优化仅在严格模式下生效 function test() { "use strict"; test.arguments; // 报错 test.caller; // 报错 }

    非严格模式下尾递归优化的实现

    正常模式下,如果调用栈过多就会造成栈溢出,在不支持的环境下,如何实现尾调用优化呢?答案是减少调用栈即可;蹦床函数可以将递归执行转换为循环执行: // 蹦床函数 function trampoline(f){ while(f && f instanceof Function){ f = f(); } return f; }

    以上代码就是一个蹦床函数的实现,它接收一个参数f,只要f执行后就返回一个函数,就继续执行;简单说就是只返回一个函数,而不是在函数内部调用函数,避免了递归执行,消除调用栈过大;

    如以下例子所示: function sum(x,y){ if(y > 0){ return sum(x + 1,y -1); }else{ return x; } } sum(1,10000); sum(1,100000); // 报错 栈溢出 // 利用蹦床函数改写===> function betterSum(x,y) { if(y > 0){ return betterSum.bind(null,x + 1 , y - 1); // 绑定this,传入参数但是不执行 }else{ return x; } } betterSum(1,1000000); 蹦床函数真正意义上并没有实现尾递归,真正实现应该是以下形式: // 尾递归优化的真正实现 function tco(f) { var active = false; // 表示激活状态 var temp_arr = []; // 保存参数f var value; // tco函数返回值 return function accumulator(){ temp_arr.push(f); // 将参数加入到数组尾部 if(!active){ active = true; while(temp_arr.length){ f = f.apply(this,temp_arr.shift()); // 将上面传入数组的f删除并返回赋值给新的f } active = false; return value; } } } var sum_new = tco(function (x,y){ if(y > 0){ return sum(x + 1,y -1); }else{ return x; } }); sum_new(1,1000000);

    函数参数的尾逗号

    ES2017允许函数的最后一个参数有尾逗号;在此之前,函数最后一个参数带尾逗号会报错;该规定也使得函数参数与数组和对象的尾逗号规则可以保持一致;
    Processed: 0.014, SQL: 8