美文网首页
函数(下)

函数(下)

作者: NnnLillian | 来源:发表于2019-10-18 19:10 被阅读0次

    上一章节中主要讲解了

    • 处理无命名参数
    • 增强的Function构造函数
    • 展开运算符
    • name属性
    • 明确函数的多重用途
    • 块级函数

    本章节内容

    • 箭头函数
    • 尾调用优化

    箭头函数

    箭头函数有以下几个方面的特点:

    • 没有 this, superm arguments and new.target 绑定。箭头函数中的 this, super, arguments和arguments的值由外围最近一层包含它的非箭头函数定义。
    • 不同通过new关键字调用。箭头函数内部没有 [[construct]]方法, 因此不能当作构造器,使用new操作符;
    • 不存在原型(No prototype),由于不可以通过new关键字调用箭头函数,因而没有构建原型的需求,所以箭头函数不存在prototype这个属性;
    • 不能改变this, 在整个箭头函数生命周期this值保持不变;
    • 不存在arguments对象,不过包含它的函数存在,箭头函数依靠命名参数和rest parameters访问函数的参数;
    • 不能拥有重复的命名参数,ES5只有严格模式下才不允许;

    箭头函数语法

    var f = v => v;
    // 等同于
    var f = function (v) {
      return v;
    };
    
    // 当要传入多参数时,要在参数两个添加一对小括号
    var sum = (num1, num2) => num1 + num2;
    // 等同于
    var sum = function(num1, num2) {
      return num1 + num2;
    };
    
    // 如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分
    var f = () => 5;
    // 等同于
    var f = function () { return 5 };
    
    // 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
    var sum = (num1, num2) => {num1++;  return num1 + num2; }
    // 等同于
    var sum = function(num1, num2) {
      num1++; 
      return num1 + num2;
    };
    
    // 如果想创建一个空函数,需要写一对儿没有内容的花括号
    let getTempItem = () => { };
    // 等同于
    let getTempItem = function() = { };
    
    // 如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。
    let fn = () => void doesNotReturn();
    

    如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。

    // 报错
    let getTempItem = id => { id: id, name: "Temp" };
    
    // 不报错
    let getTempItem = id => ({ id: id, name: "Temp" });
    // 等同于
    let getTempItem = function(id) {
      return {
        id : id,
        name : "Temp"
      };
    };
    

    下面是一种特殊情况,虽然可以运行,但会得到错误的结果。

    let foo = () => { a: 1 };
    foo() // undefined
    

    上面代码中,原始意图是返回一个对象{ a: 1 },但是由于引擎认为大括号是代码块,所以执行了一行语句a: 1。这时,a可以被解释为语句的标签,因此实际执行的语句是1;,然后函数就结束了,没有返回值。

    const full = ({ first, last }) => first + ' ' + last;
    
    // 等同于
    function full(person) {
      return person.first + ' ' + person.last;
    }
    

    箭头函数可以与变量解构结合使用。

    const full = ({ first, last }) => first + ' ' + last;
    
    // 等同于
    function full(person) {
      return person.first + ' ' + person.last;
    }
    

    创建立即执行函数表达式

    当想创建一个与其他程序隔离的作用域时,可以定义一个匿名函数并立即调用,自始至终不保存对该函数的引用。

    let person=function(name){
      
      return{
        getName: function(){
          return name;
        }
      }
    }("Baby");
    
    console.log(person.getName());// "Baby"
    

    立即执行函数表达式创建了一个包含getName()方法的新对象,将参数name作为该对象的一个私有成员返回给函数的调用者
    只要将箭头函数包裹在小括号里,能够实现相同的行为。

    let person=((name) => {
      
      return{
        getName: function(){
          return name;
        }
      };
      
    })("Baby");
    
    console.log(person.getName());// "Baby"
    

    箭头函数没有this绑定

    箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。所以箭头函数可以让this指向固定化。

    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指向固定化,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面。然而,不使用箭头函数的情况下代码会报错。

    let PageHandler = {
    
        id: "123456",
    
        init: function() {
            document.addEventListener("click", function(event) {
                this.doSomething(event.type); // error
            }, false);
        },
    
        doSomething: function(type) {
            console.log("Handling " + type + " for " + this.id);
        }
    };
    // init函数中的this.doSomething,this指向的是函数内部document对象;
    // 而不是PageHandler对象,document对象中不存在doSomething,所以无法正常执行
    

    使用bind()方法显式的将函数的this绑定到PageHandler上来修正这个问题。

    let PageHandler = {
      
      id : "123",
      
      init : function(){
        document.addEventListener("click", (function(event) {
          this.doSomething(event.type);
        }).bind(this), false);
      },
      
      doSomething : function(type) {
        console.log("Handle "+ type + " for " + this.id);
      }
    }
    // 调用bind(this)后创建了一个新函数,它的this被绑定到当前的this,也就是PageHandler
    

    为了避免创建一个额外的函数,可以使用箭头函数。

    let PageHandler = {
    
      id: "123456",
    
      init: function() {
          document.addEventListener("click", 
                  event => this.doSomething(evnet.type), false);
        },
    
      doSomething: function(type) {
          console.log("Handling " + type + " for " + this.id);
      }
    };
    // 此处箭头函数没有自己this,它的this就是其外部函数的this,即init()的this;
    // init为PageHandler的方法,this指向PageHandler对象实例
    

    箭头函数不能使用new

    var MyType = () => {};
    var obj = new MyType(); // Error,不可以通过new关键字调用箭头函数
    

    箭头函数和数组

    诸如sort()、map()、reduce()这些可以接受回调函数的数组方法,可以通过箭头函数简化。

    // 正常函数写法
    [1,2,3].map(function (x) {
      return x * x;
    });
    // 箭头函数写法
    [1,2,3].map(x => x * x);
    
    
    // 正常函数写法
    var result = values.sort(function (a, b) {
      return a - b;
    });
    // 箭头函数写法
    var result = values.sort((a, b) => a - b);
    

    箭头函数没有arguments绑定

    箭头函数没有arguments对象,但是可以使用包含函数中的arguments对象。

    function createArrowFunctionReturningFirstArg() {
        // arguments 为 createArrowFunctionReturningFirstArg中的对象
        return  () => arguments[0]; 
    }
    var arrowFunction = createArrowFunctionReturningFirstArg(10);
    arrFunction(); // 10
    

    箭头函数的辨识方法

    // 箭头函数使用typeof和instanceof操作符调用 与其他函数一样
    var sum = (num1, num2) => num1 + num2;
    
    console.log(typeof sum);// "function"
    console.log(sum instanceof Function);// true
    

    可以在箭头函数上调用call()、apply()、bind()方法,与其他函数不同的是,箭头函数的this不会受到这些方法影响。

    var sum = (num1, num2) => num1 + num2;
    
    console.log(sum.call(null, 1, 2));
    console.log(sum.apply(null, [1, 2]));
    
    var boundSum = sum.bind(null, 1, 2);
    
    console.log(boundSum());
    

    尾调用优化

    尾调用(Tail Call)是指某个函数的最后一步是调用另一个函数。

    function f(x){
      return g(x); // 尾调用
    }
    

    以下三种情况,都不属于尾调用。

    // 情况一,因为调用函数g之后,还有赋值操作,所以不属于尾调用
    function f(x){
      let y = g(x);
      return y;
    }
    
    // 情况二,原因同情况一
    function f(x){
      return g(x) + 1;
    }
    
    // 情况三,原因是没有返回函数g
    function f(x){
      g(x);
    }
    

    尾调用不一定出现在函数尾部,只要是最后一步操作即可

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

    上面代码中,函数m和n都属于尾调用,因为它们都是函数f的最后一步操作。

    ES6中的尾调用优化

    尾调用之所以与其他调用不同,就在于它的特殊的调用位置。

    我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到AB的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。

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

    function f() {
      let m = 1;
      let n = 2;
      return g(m + n);
    }
    f();
    
    // 等同于
    function f() {
      return g(3);
    }
    f();
    
    // 等同于
    g(3);
    

    上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量mn的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除f(x)的调用帧,只保留g(3)的调用帧。

    这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。

    注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。

    function addOne(a){
      var one = 1;
      function inner(b){
        return b + one;
      }
      return inner(a);
    }
    

    上面的函数不会进行尾调用优化,因为内层函数inner用到了外层函数addOne的内部变量one

    利用尾调优化--尾递归

    函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

    递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。

    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:返回调用当前函数的那个函数。
      尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。
    function restricted() {
      'use strict';
      restricted.caller;    // 报错
      restricted.arguments; // 报错
    }
    restricted();
    

    相关文章

      网友评论

          本文标题:函数(下)

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