• ES6之主要知识点(五)函数


    函数参数的默认值

    作用域

    var x = 1;
    
    function f(x, y = x) {
      console.log(y);
    }
    
    f(2) // 2
    let x = 1;
    
    function f(y = x) {
      let x = 2;
      console.log(y);
    }
    
    f() // 1
    var x = 1;
    function foo(x, y = function() { x = 2; }) {
      var x = 3;
      y();
      console.log(x);
    }
    
    foo() // 3
    x // 1
    var x = 1;
    function foo(x, y = function() { x = 2; }) {
      x = 3;
      y();
      console.log(x);
    }
    
    foo() // 2
    x // 1

    参数变量是默认声明的,所以不能用letconst再次声明。

    使用参数默认值时,函数不能有同名参数

    应用

    function throwIfMissing() {
      throw new Error('Missing parameter');
    }
    
    function foo(mustBeProvided = throwIfMissing()) {
      return mustBeProvided;
    }
    
    foo()
    // Error: Missing parameter

    上面代码的foo函数,如果调用的时候没有参数,就会调用默认值throwIfMissing函数,从而抛出一个错误。

    2.rest 参数

    ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。

    rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

    function push(array, ...items) {
      items.forEach(function(item) {
        array.push(item);
        console.log(item);
      });
    }
    
    var a = [];

    注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

    3.箭头函数

    x => x * x

    上面的箭头函数相当于:

    function (x) {
        return x * x;
    }

    如果参数不是一个,就需要用括号()括起来:

    // 两个参数:
    (x, y) => x * x + y * y
    
    // 无参数:
    () => 3.14
    
    // 可变参数:
    (x, y, ...rest) => {
        var i, sum = x + y;
        for (i=0; i<rest.length; i++) {
            sum += rest[i];
        }
        return sum;
    }

    如果要返回一个对象,就要注意,如果是单表达式,这么写的话会报错:

    // SyntaxError:
    x => { foo: x }

    因为和函数体的{ ... }有语法冲突,所以要改为:

    // ok:
    x => ({ foo: x })

    箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的this是词法作用域,由上下文确定。

    回顾前面的例子,由于JavaScript函数对this绑定的错误处理,下面的例子无法得到预期结果:

    var obj = {
        birth: 1990,
        getAge: function () {
            var b = this.birth; // 1990
            var fn = function () {
                return new Date().getFullYear() - this.birth; // this指向window或undefined
            };
            return fn();
        }

    现在,箭头函数完全修复了this的指向,this总是指向词法作用域,也就是外层调用者obj

    var obj = {
        birth: 1990,
        getAge: function () {
            var b = this.birth; // 1990
            var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象
            return fn();
        }
    };
    obj.getAge(); // 25

    使用注意点

    箭头函数有几个使用注意点。

    (1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

    (2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

    (3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

    (4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

    function Timer() {
      this.s1 = 0;
      this.s2 = 0;
      // 箭头函数
      setInterval(() => this.s1++, 1000);
      // 普通函数
      setInterval(function () {
        this.s2++;
      }, 1000);
    }
    
    var timer = new Timer();
    
    setTimeout(() => console.log('s1: ', timer.s1), 3100);
    setTimeout(() => console.log('s2: ', timer.s2), 3100);
    // s1: 3
    // s2: 0

    上面代码中,Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数)

    ,后者的this指向运行时所在的作用域(即全局对象)。所以,3100毫秒之后,timer.s1被更新了3次,而timer.s2一次都没更新。

     另外,由于箭头函数没有自己的this,所以当然也就不能用call()apply()bind()这些方法去改变this的指向。

    4.绑定 this

    函数绑定运算符是并排的两个冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,

    作为上下文环境(即this对象),绑定到右边的函数上面。

    foo::bar;
    // 等同于
    bar.bind(foo);
    
    foo::bar(...arguments);
    // 等同于
    bar.apply(foo, arguments);
    
    const hasOwnProperty = Object.prototype.hasOwnProperty;
    function hasOwn(obj, key) {
      return obj::hasOwnProperty(key);
    }

    5.尾调用优化

    尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。

    function f(x) {
      if (x > 0) {
        return m(x)
      }
      return n(x);
    }

    尾递归

    function factorial(n) {
      if (n === 1) return 1;
      return n * factorial(n - 1);
    }
    
    factorial(5) // 120

    上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。

    function factorial(n, total) {
      if (n === 1) return total;
      return factorial(n - 1, n * total);
    }
    
    factorial(5, 1) // 120

     严格模式

    ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

    这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。

    • func.arguments:返回调用时函数的参数。
    • func.caller:返回调用当前函数的那个函数。

    尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效

    在非严格模式时候我们可以自己模拟

    蹦床函数(trampoline)可以将递归执行转为循环执行。

    function trampoline(f) {
      while (f && f instanceof Function) {
        f = f();
      }
      return f;
    }

    上面就是蹦床函数的一个实现,它接受一个函数f作为参数。只要f执行后返回一个函数,就继续执行。

    注意,这里是返回一个函数,然后执行该函数,而不是函数里面调用函数,这样就避免了递归执行,从而就消除了调用栈过大的问题。

    然后,要做的就是将原来的递归函数,改写为每一步返回另一个函数。

    function sum(x, y) {
      if (y > 0) {
        return sum.bind(null, x + 1, y - 1);
      } else {
        return x;
      }
    }

    上面代码中,sum函数的每次执行,都会返回自身的另一个版本。

    现在,使用蹦床函数执行sum,就不会发生调用栈溢出。

    trampoline(sum(1, 100000))
    // 100001

    蹦床函数并不是真正的尾递归优化,下面的实现才是。

    function tco(f) {
      var value;
      var active = false;
      var accumulated = [];
    
      return function accumulator() {
        accumulated.push(arguments);
        if (!active) {
          active = true;
          while (accumulated.length) {
            value = f.apply(this, accumulated.shift());
          }
          active = false;
          return value;
        }
      };
    }
    
    var sum = tco(function(x, y) {
      if (y > 0) {
        return sum(x + 1, y - 1)
      }
      else {
        return x
      }
    });
    
    sum(1, 100000)
    // 100001
  • 相关阅读:
    Java中equals和==的区别
    Mybatis中#{}和${}
    AJAX发送带Header的请求
    golang定时任务
    mysql中一条insert语句批量插入多条记录
    golang 从mysql取datetime类型数据
    MySQL 慢查询日志
    mysql日志文件开启及详解:General_log 和 Binlog
    如何在普清的屏上调试CSS样式二倍图背景
    transition、animation在macbook air上图片动画边缘抖动
  • 原文地址:https://www.cnblogs.com/myzy/p/7553023.html
Copyright © 2020-2023  润新知