美文网首页
深入理解ES6:3.函数

深入理解ES6:3.函数

作者: 独木舟的木 | 来源:发表于2019-11-28 09:53 被阅读0次

    Tags:默认参数、不定参数、展开运算符、name 属性、元属性 new.target、箭头函数、尾调用优化、

    函数形参的默认值

    JavaScript 函数的特点:无论在函数定义中声明了多少形参,都可以传入任意数量的参数,也可以在定义函数时添加针对参数数量的处理逻辑,当已定义的形参无对应的传入参数时为其指定一个默认值。

    在 ECMAScript 5 中模拟默认参数

    创建函数,并为参数赋予默认值。

    function makeRequest(url, timeout, callback) {
    
      // timeout、callback 为可选参数
      // 如果未传值,则通过逻辑或操作符 || 为缺失的参数设置默认值
      timeout = timeout || 2000;
      callback = callback || function() {};
    
      // 函数的其余部分
    }
    

    缺陷:如果我们想给 makeRequest 函数的第二个形参 timeout 传入值 0,即使这个值是合法的,也会被视为一个假值,并最终为 timeout 赋值 2000。

    优化方案:通过 typeof 操作符检查参数类型:

    function makeRequest(url, timeout, callback) {
    
      // timeout、callback 为可选参数
      // 如果未传值,默认值参数值为 "undefined"
      timeout =  (typeof timeout !== "undefined") ? timeout : 2000;
      callback = (typeof callback !== "undefined") ? callback : function() {};
    
      // 函数的其余部分
    }
    

    ECMAScript 6 中的默认参数值

    ECMAScript 6 简化了为形参提供默认值的过程,如果没有为参数传入值,则为其提供一个初始值。

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

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

    • 在 ECMAScript 5 非严格模式下,函数命名参数的变化会同步更新到 arguments 对象中。
    • 在 ECMAScript 5 严格模式下,无论参数如何变化,arguments 对象不再随之改变。
    • 在 ECMAScript 6 中,无论是否处于严格模式下,只要一个函数使用了默认参数值arguments 对象都不随参数改变。

    默认参数表达式

    因为默认参数是在函数调用时求值,所以可以使用先定义的参数作为后定义参数的默认值。

    在引用参数默认值时,只允许引用前面参数的值。

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

    处理无命名参数

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

    当传入更少数量的参数时,默认参数值的特性可以有效简化函数声明的代码。

    当传入更多数量的参数时,ECMAScript 6 同样也提供了更好的方案。

    ECMAScript 5 中的无命名参数

    使用 arguments 对象来检查函数的所有参数。

    // 返回一个给定对象的副本,包含原始对象属性的特定子集
    function pick(object) {
      let result = Object.create(null);
    
      // 从第二个参数开始
      for (let i = 1; i < arguments.length; i++) {
        // 将 object 对象的属性值逐个赋值给第二个开始的参数
        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() 函数。

    // 不定参数 keys 包含的是 object 之后传入的所有参数。
    function pick(object, ...keys) {
      let result = Object.create(null);
    
      for (let index = 0; index < keys.length; index++) {
        result[keys[i]] = object[keys[i]];
      }
    
      return result;
    }
    

    不定参数的使用限制

    1. 每个函数最多只能声明一个不定参数,而且一定要放在所有参数的末尾。
    2. 不定参数不能用于对象字面量 setter 之中。
    let object = {
    
      // 语法错误:不可以在 setter 中使用不定参数
      set name(...value) {
        // 执行一些逻辑
      }
    };
    

    不定参数对 arguments 对象的影响

    没有影响

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

    function checkArgs(...args) {
      console.log(args.length); // 2
      console.log(arguments.length); // 2
    
      console.log(args[0], arguments[0]); // a a
      console.log(args[1], arguments[1]); // b b
    }
    
    checkArgs('a', 'b');
    

    增强的 Function 构造函数

    Function 构造函数很少用到,通常我们用它来动态创建新的函数

    // Function 构造函数接受字符串形式的参数,分别为函数的参数和函数体。
    var add = new Function('first', 'second', 'return first + second');
    
    console.log(add(1, 2)); // 3
    

    ECMAScript 6 增强了 Function 构造函数的功能,支持在创建函数时定义默认参数和不定参数。

    定义默认参数:在参数名后添加一个等号及一个默认值

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

    定义不定参数,只需要在最后一个参数前添加...

    var pickFirst = new Function('...args', 'return args[0]');
    
    console.log(pickFirst(1, 2)); // 1
    

    对于 Function 构造函数,新增的默认参数和不定参数特性,使其具备了与声明式创建函数相同的能力。

    展开运算符

    • 不定参数可以让你指定多个各自独立的参数,并通过整合后的数组来访问。
    • 展开运算符可以让你指定一个数组,将它们打散后作为各自独立的参数传入函数。

    JavaScript 内建的 Math.max() 方法可以接受任意数量的参数,并返回值最大的那一个。

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

    如果想从一个数组中挑选出最大的那个值应该怎么做?而 Math.max() 方法又不允许传入数组。

    使用 apply() 方法手动遍历:

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

    使用 ECMAScript 6 中的展开运算符:向 Math.max() 方法中传入一个数组,再在数组前添加不定参数使用符号...

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

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

    let array = [-25, -50, -75, -100];
    
    // 传入限定值 0 ,保证最小值为正数
    console.log(Math.max(...array, 0)); // 0
    

    综上,展开运算符可以简化使用数组给函数传参的编码过程

    name 属性

    ECMAScript 6 为所有函数新增了 name 属性用于辅助辨别函数。

    💡💡💡 函数 name 属性的值不一定引用同名变量,它只是协助调试用的额外信息,所以不能使用 name 属性的值来获取对函数的引用。

    如何选择合适的名称

    // 函数
    function doSomething() {
      // 空函数
    }
    
    // 函数表达式
    var doAnotherThing = function() {
      // 空函数
    };
    
    console.log(doSomething.name); // doSomething,对应声明的函数名
    console.log(doAnotherThing.name); // doAnotherThing,对应被赋值为该匿名函数的变量名
    

    name 属性的特殊情况

    // 函数表达式的名字权重比变量名高
    var doSomething = function doSomethingElse() {
      // 空函数
    };
    
    var person = {
      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); // firstName
    
    • 通过 bind() 函数创建的函数,其名称将带有 “bound” 前缀。
    • 通过 Function 构造函数创建的函数,其名称将是 “anonymous”。

    明确函数的多重用途

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

    JavaScript 函数有两个不同的内部方法:[[Call]][[Construct]]

    当通过 new 关键字调用函数时,执行的是 [[Construct]] 函数,它负责创建一个通常被称作实例的新对象,然后再执行函数体,将 this 绑定到实例上;

    如果不通过 new 关键字调用函数,则执行 [[Call]] 函数,从而直接执行代码中的函数体。

    具有 [[Construct]] 方法的函数被统称为构造函数

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

    在 ECMAScript 5 中,如果想确定一个函数是否通过 new 关键字被调用(或者说,判断该函数是否作为构造函数被调用),最流行的方式是使用 instacneof:

    function Person(name) {
      // 检查 this 值是否是构造函数的实例
      // 因为 [[Construct]] 方法会创建一个 Person 的新实例,并将 this 绑定到新实例上。
      if (this instanceof Person) {
        this.name = name; // 如果通过 new 关键字调用
      } else {
        throw new Error('必须通过 new 关键字来调用 Person');
      }
    }
    
    var person = new Person('Andy');
    var notAPerson = Person('Andy'); // 抛出错误
    // 例外:不依赖 new 方法也可以将 this 绑定到 Person 的实例上
    var stillWorkPerson = Person.call(person, 'Andy'); // 有效!!!
    
    console.log(person); // Person { name: 'Andy' }
    console.log(notAPerson);
    console.log(stillWorkPerson); // undefined
    

    元属性 new.target

    当调用函数的 [[Construct]] 方法时,new.target 被赋值为 new 操作符的目标。通常是新创建对象实例,也就是函数体内 this 的构造函数;

    如果调用 [[Call]] 方法,则 new.target 被赋值为 undefined

    function Person(name) {
      if (typeof new.target !== "undefined") {
        this.name = name; // 如果通过 new 关键字调用
      } else {
        throw new Error('必须通过 new 关键字来调用 Person');
      }
    }
    

    块级函数

    在 ES5 的严格模式下,在代码块内部声明函数时程序会抛出错误。ES6 中则视为块级函数,从而可以在定义该函数的代码块内访问和调用。

    在代码块中,块级函数会被提升至块的顶部,而用 let 定义的函数表达式不会被提升。

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

    箭头函数

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

    箭头函数语法

    • 当箭头函数只有一个参数时,可以直接写参数名,箭头紧跟其后,箭头右侧的表达式被求值后便立即被返回。
    // 一个参数
    let reflect = value => value;
    
    // 实际上相当于
    let reflect = function (value) {
      return value;
    }
    
    • 如果要传入两个或两个以上的参数,要在参数的两侧添加一对小括号。
    // 多个参数
    let sum = (num1, num2) => num1 + num2;
    
    // 实际上相当于
    let sum = function (num1, num2) {
      return num1 + Number;
    }
    
    • 如果函数没有参数,也要在声明的时候写一组没有内容的小括号。
    • 如果函数体由多个表达式组成,需要用花括号{}包裹函数体。
    • 箭头函数没有 this 绑定,必须通过查找作用域链来决定其值。如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this。否则,this 的值会被设置为全局对象。

    尾调用优化

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

    在 ES5 中,尾调用会创建一个新的栈帧(stack frame),而在 ES 6 中,尾调用会清除并重用当前栈帧。

    ES 6 尾调用需要满足的条件:

    • 尾调用不访问当前栈帧的变量(也就是说函数不是一个闭包)。
    • 在函数内部,尾调用是最后一条语句。
    • 尾调用的结果作为函数的返回值。

    尾调用优化的主要应用场景:递归函数

    // 阶乘函数
    function factorial(n) {
      if (n <= 1) {
        return 1;
      } else {
        // 无法优化,因为这里在返回后还执行了乘法操作
        // 也就是说,如果在尾调用返回后还执行了其他操作,即无法得到尾调用优化
        return n * factorial(n - 1);
      }
    }
    
    // 通过默认参数将乘法操作移出 return 语句
    // 用 p 来保存乘法结果,下一次迭代中取出用于计算,不再需要额外的函数调用
    function factorial(n, p = 1) {
      if (n <= 1) {
        return 1 * p;
      } else {
        
        let result = n * p;
        return factorial(n-1, result);
      }
    }
    

    相关文章

      网友评论

          本文标题:深入理解ES6:3.函数

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