函数进阶

300 阅读7分钟

this

1 全局环境中的this只想全局对象window
2 函数中的this对象,取决于函数是如何被调用的
简单调用=>window 对象方法调用=>对象本身

      let obj = {
        fn() {
          console.log(this); //obj
          function a() {
            console.log(this); //window
          }
          a();
        },
      };
      obj.fn();

3 call和apply调用 =>第一个参数时指定函数中的this对象

      let obj = {
        fn() {
          console.log(this); //obj
          function a() {
            console.log(this); //obj
          }
          a.apply(this)//a.call(this)
        },
      };
      obj.fn();

4 构造函数调用 => 指向构造函数

        function Person() {
          this.name = "tom";
        }
        let p = new Person();
        console.log(p.name);

函数的双重职能

函数内部有两个不同的方法:[[Call]] 和[[Constructor]] ,使用普通方式调用时,会执行[[Call]] ,使用构造函数调用时,会执行[[Constructor]]
可以使用new.target来避免构造函数当成普通函数使用(instanceof也可以),当以构造函数的方式调用时,new.target指向的是构造函数本身

        function Person(name) {
          if (new.target === Person) {
            this.name = name;
          } else {
            throw new Error("u must use new with Person");
          }
        }
       let p1 = new Person("tom");
       let p2 = Person(); //Uncaught Error: u must use new with Perso

函数的传参方式

函数的参数,是按值传递(call by value),还是按引用传递(call by reference)
按值传递:函数形参的值是调用函数所传入实参的副本
按引用传递:函数形参的值是调用函数所传入实参的引用

基础数据类型

基础数据类型的传参方式是按值传递,这一点毫无疑问

        let a = 1;
        function fn(a) {
          a = 2;
        }
        fn(a);
        console.log(a); //1

引用数据类型

从下面这段代码看,引用类型似乎是按引用传递

        let obj = {
          x: 1,
        };
        function fn(obj) {
          obj.y = 2;
        }
        fn(obj);
        console.log(obj); //{x: 1, y: 2}

但是看下面的代码

        let obj = { x: 1 };
        function fn(obj) {
          obj = 3;
        }
        fn(obj);
        console.log(obj);// {x: 1}

其实,如果是引用类型的参数,传入的是对象引用的副本,他们引用的是同一个对象,第一段代码是在给实参和形参所引用的对象添加新的属性,第二段代码,是将引用地址的副本修改成了3
所以js中函数的参数是按值传递,这个话问题不大,但是有些人不认同这种说话,他们认为是按共享传参(call by sharing),这只是术语上的区别,直到具体是怎么个情况就可以了

函数应用

立即执行函数表达式,闭包,递归,回调,柯里化

立即执行函数表达式

创建了一个新的函数作用域,里面的局部变量和方法不会污染全局作用域

        (function () {
      let page = {
        init() {
          console.log("123");
        },
      };
      page.init();
        })();

ES6中,可以使用{}代替立即执行函数

        {
          let page = {
            init() {
              console.log("123");
            },
          };
          page.init();
        }

闭包

闭包:是指访问了另外一个作用域中的变量的函数
闭包的作用:阻止变量被垃圾回收

  function a() {
          var x = 1;
          return function () {
            return x + 1;
          };
        }
        let b = a();
        console.log(b()); //在运行完a函数之后,里面的变量x并没有被垃圾回收,应为,return出来匿名函数还对他有引用

闭包加立即执行函数的应用;封装(信息隐藏)

下面的代码虽然可以使用getName得到name ,但是也可以直接获取到name

        let obj = {
          name: "tom",
          getName() {
            return this.name;
          },
        };
        console.log(obj.getName(), obj.name); //tom tom

下面的代码则可以做到让别人无妨访问私有属性

      let obj = (function () {
        let name = "tom";
        return {
          getName() {
            return name;
          },
        };
      })();
      console.log(obj.getName());//tom

这在js中是一种很有用的技巧叫做模块模式,它可以把所有不必要暴露在外面的内容都封装在模块内部,对外只提供一些公开方法

递归:在函数中调用函数本身

      function fib(n) {
        if (n <= 2) {
          return 1;
        }
        return fib(n - 1) + fib(n - 2);
      }
      fib(10);

回调 :同步回调 异步回调

同步回调

      function a(n, func) {
        ++n;
        func(n);
      }
      a(1, function (n) {
        console.log(n);
      });

柯里化函数

将使用多个参数的一个函数,转换成一系列使用一个参数的函数,并且返回接受余下参数且返回结果的函数
示例 实现生成唯一ID 的函数,要求
1 可以传入一个起始ID ,之后返回的id从这个值开始算。
2还可以传入一个数字 ,返回的值是:上次的id加上这个数字,如果没有传入数字,返回的结果返回上次的id+1

      let fn = function (id) {
        return function (num) {
          return num ? id + num : id + 1;
        };
      };
      let curry = fn("999");
      console.log(curry(), curry(3)); //9991 9993

不难发现柯里化需要使用闭包的技术
除了上述的应用外,还有其他的应用,比如函数式编程 关于函数式编程的解释可以自行搜索

函数式编程

      function not(fn) {
        return function () {
          let result = fn.apply(this, arguments);
          return !result;
        };
      }
      function even(x) {
        return x % 2 === 0;
      }
      let odd = not(even);
      console.log([1, 3, 5, 7].every(odd)); //true

箭头函数

箭头函数
箭头函数和普通函数的区别
1 没有自己的this,super,arguments和new.target,他们都是距离箭头函数最近的非箭头函数的绑定
2 不能使用new 来调用
3 没有原型对象
4 内部的this无法改变(就是说无法通过call,apply,bind来改变内部的this指向)
5 形参名称不能重复

尾调用优化

首先了解一下什么是尾调用

在执行某一个函数时,如果最后一步是调用一个另外一个函数,并且被调用的函数的返回值,直接被当前函数返回就是尾调用(tail call) 在了解尾调用之前需要了解没有尾调用优化是什么样子的,需要了解函数的执行过程,就拿下面的函数举例

      function g(x) {
        return x;   
      }
      function f(x) {
        const y = x + 1;
        return g(y); 
      }
      console.log(f(2)); 

初始化时,此时js引擎的栈中保存着这些信息,由于g在f之前声明,所以在栈的下面 当代码执行到第C行时,调用函数f ,会创建一个frame ,首先这个freame会要保存回到c行的地址,然后是分配f函数的参数,然后进入f函数的函数体,此时栈中的信息如下 当代码执行到b行时,同样会创建一个frame ,他会保存回到b行的地址,然后时分配给g参数,然后进入g的函数体,此时栈中的信息如下 当代码执行到a行时,他返回了x,之后就会移除函数g的frame ,然后回到第b行, 接下来就是把g返回的值,返回给f ,然后删除f的frame , 最后代码执行C,输出3
在es6之前的函数调用就是这么执行的,可以发现,函数f的frame始终没有变化,他唯一的作用是记录回到C的信息
栈就是内存,是非常宝贵的,能销毁就要及时销毁,也就是说,创建函数g的frame的时候,就可以把函数f的frame销毁了,同时把回到c行的信息,保存到函数g的frame中去
所以尾调用优化,就是优化的栈的利用率
下面是尾调用的示意图,创建函数g的fream的同时,移除函数f的frame,并且把回到第c行的信息保存到g的frame中

尾调用要符合三个要求

1 尾调用不需要访问当前stack frame中的变量,也就是没有闭包
2 返回尾调用处时,不要做其他的事情
3 尾调用的放回值,直接返回给调用它的所在函数的调用者

尾递归

递归时,如果函数调用本身 时是一个尾调用,则称之为尾递归
下面代码不是尾递归,所以会堆栈溢出,造成网页卡死

      function fib(n) {
        if (n === 1 || n === 2) {
          return 1;
        }
        return fib(n - 1) + fib(n - 2);
      }
      console.log(fib(100));//堆栈溢出

用下面这段代码就可以避免网页卡死
巧妙的把前两项之和通过a+b实现

      function fib(n, a = 0, b = 1) {   
        if (n === 0) return 1;
        if (n === 1) return b;
        return fib(n - 1, b, a + b);
      }
      console.log(fib(100)) //354224848179262000000