侧边栏壁纸

ES6函数的扩展

2023年01月02日 1.6k阅读 0评论 4点赞

函数参数的默认值

基本用法

  • 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允许函数的最后一个参数有尾逗号
  • 在此之前,函数最后一个参数带尾逗号会报错;
  • 该规定也使得函数参数与数组和对象的尾逗号规则可以保持一致;
4

—— 评论区 ——

昵称
邮箱
网址
取消
博主栏壁纸
14 文章数
18 标签数
10 评论量
人生倒计时
舔狗日记