函数表达式

作者: 25度凉白开 | 来源:发表于2020-08-15 19:10 被阅读0次

    以下内容总结自《JS高级程序设计》第三版


    什么是函数表达式?

    函数表达式,是JS中定义函数的一种方式。
    在JS中,共有两种方式定义函数:
    1. 函数声明
    2. 函数表达式
    来看看区别吧:

    // 1. 函数声明
    function f(){
      // 函数体
    }
    
    // 2. 函数表达式
    var f = function(){
      // 函数体
    }
    

    那除了写法不同之外,还有什么区别呢?
    你可能会想到,函数声明的function写在了最开始,而函数表达式没有。没错,这是区别之一,一旦function用在了本行代码的最开始,就代表这是一个函数声明,函数声明还必须要有一个名字(标识符)跟在后面,否则会报错的;
    另外函数声明有一个重要的特征就是函数声明提升,而函数表达式没有。
    下面这段代码展示了这个区别

    // 函数声明
    sayHi()  // 不会报错。
    function sayHi() {
      alert('Hello World!')
    }
    
    // 函数表达式
    sayHi()  // 错误!函数还未定义!
    var sayHi = function() {
      alert('Hello World!')
    }
    

    这便是函数声明提升。如果还有疑惑的话,那再看看下面的代码:

    // 预编译前
    sayHi()
    function sayHi() {
      alert('Hello World')
    }
    
    // 预编译后
    function sayHi() {
      alert('Hello World')
    }
    sayHi()
    

    这便是JS引擎对函数声明做的处理,即函数声明提升。但是很不幸,本章的主角函数表达式,并没有这个待遇。

    不过没关系,在其他方面,函数表达式还是非常有用的。


    一、递归

    先上一段代码

    // 递归函数
    function factorial (num) {    // 函数声明
      if (num < 1) {
        return 1
      } else {
        return num * factorial(num - 1)   // 函数表达式
      }
    }
    

    上面这段代码就是一个递归函数,它既有函数声明,也有函数表达式。这也是函数表达式的用处之一:当作函数返回值

    问题
    上面的函数中虽然调用了自己当作返回值,但是一旦我们进行了下面的骚操作,就完蛋了。

    var anotherFactorial = factorial
    factorial = null
    alert(anotherFactorial(4))  // 出错
    

    这里的执行顺序是这样的:

     1 将 factorial()函数的引用给了anotherFactorial,
     2 然后factorial指向null,
     3 再执行anotherFactorial(),
     4 第一次进去的之后执行的挺好,最后到了返回值的时候,又遇到了factorial() 
     ?嗯?这玩意不是指向null了嘛,我还执行个毛线,直接扔个错误就完事了。
    

    解决方法
    使用 arguments.callee,这个东西指向的使正在执行的函数的指针,所以用它代替函数名就能解决了。
    但是呢,在严格模式下,是不能通过脚本访问arguments.callee的,会报错。那怎么办?使用命名函数表达式的方法来操作

    var factorial = (function f(num) {
      if (num < 1) {
        return 1
      } else {
        return num * f(num - 1)
      }
    })
    

    这里就是给函数加了个括号和函数名,然后赋值给一个变量即可,这个应该没什么难的。倒是 命名函数这个名词要解释一下:

      命名函数是有名字(标识符)的函数表达式
    

    与之对应的有个叫做匿名函数的名词:

      匿名函数是没有名字(标识符)的函数表达式
    

    如上,通俗易懂。其中,匿名函数我们用的更多些。

    二、闭包

    闭包算是JS中最重要的几个概念之一了。在初学闭包概念时,我看过的很多博客和书中,并没有能让我恍然大悟的,这个概念也是慢慢理解的。所以,我会尽可能的将我理解的闭包写出来让大家参考,如有错误,欢迎指正。
    先来说说比较官方的概念

        闭包指有权访问另一个函数作用域中变量的函数。最常见的方式,是在一个函数内部创建另外一个函数。
    

    再说说我的理解:

          我所见到的闭包,大都是嵌套函数的形式,就像上面所说,一个函数里面有一个函数,
          里面的函数有权访问外部函数的变量和方法,内部的函数就叫做闭包。
          ---闭包这个词比较抽象,个人感觉它更像在描述内部函数访问外部变量、方法的这种行为。这句话仅供参考:)
    

    上代码:

    function outSide(par) {
      var a = 1
      var b = function() {
        console.log('outFunc')
      }
      return function inSide() {
        console.log(a,par)
        b()
      }
    }
    
    outSide(123)()   // 1   123
                     // outFunc
    

    可以看到,执行了内部的函数后将外部的函数的变量和方法都访问了一遍。
    是不是感觉很自然。好,那现在我们就更加深入一些。
    要整明白闭包,就要理解函数被调用时发生了什么,那说到函数调用,就得说说作用域链了。不懂什么是作用域链的同学可以先看看《JS高级程序设计》第四章。

       当函数被调用时,会创建一个执行环境及相应的作用域链,然后使用arguments和其他命名参数的值来初始化函数的
       活动对象。但在作用域链中,外部函数的活动对象始终处于第二位,再外层的函数的活动对象处于第三位,直到作用域
       链终点全局执行环境。
    

    通俗的讲,可以形容成一把伞,伞柄一节节可伸缩,手里握着最内层的伞柄,往上一节节的对应一层又一层的外层函数,最终伞面就是全局作用域。当一个闭包结束使用时,就好比伞柄坏了扔掉,但是全局作用域伞面还在,如果产生了一个新的闭包,就安装一个新伞柄。

    注意
    闭包虽然有好处,但是耗内存。因为它带着一大串作用域链盘在内存里,所以谨慎使用闭包。

    1 闭包和变量

    这里需要注意的点是:闭包只能取得外部函数中任何变量的最新值。
    原因:闭包保存的是外部函数这个完整的对象,而不是其中的变量。
    例:

    function out () {
      var result = []
    
      for (var i = 0; i < 10; i++) {
        result[i] = function () {
          return i
        }
      }
      return result
    }
    

    执行完 i == 10 ,result中任何一个元素都保存着同一个 i 变量的引用,所以值都是相同的,而不是对应的索引值。
    如果我们想当场获取i的值,可以使用下面的方法

    function out () {
      var result = []
    
      for (var i = 0; i < 10; i++) {
        result[i] = function (num) {
          return function(){
            return num
          }
        }(i)
      }
      return result
    }
    

    这里又多了一个闭包,而原来的闭包成了一个立即执行函数,作用是立刻获取当前 i 值,它内层的闭包则是返回这个传进来的 i 值,这样就能使每个元素的值对应其索引了。

    2 关于this对象

    在普通函数中,this指向调用它时的对象;
    在ES6的箭头函数中,this指向定义它时的对象。

    这里只说关于普通函数的this。
    还是举个例子

    var name = 'the Window'
    
    var object = {
      name: 'the object',
      getNameFunc: function(){ // 这个函数的this是object
        return function(){     // 这个函数的this是window
          return this.name
        }
      }
    }
    
    alert(object.getNameFunc()())   // 'the Window'(非严格模式下)
    

    外层函数是字面量对象中的方法,所以this指向object;但内层函数只是一个普通函数,它在被调用时不会获取到外层函数的this
    当然,我觉得下面这种理解方式更容易些

    object.getNameFunc()() ====> function(){ return this.name }()
    

    object.getNameFunc()可以等效成后面的写法,而后面的函数this指向肯定是window。
    那怎么才能获取到object当作this呢?
    简单,如下

    var object = {
      name: 'the object',
      getNameFunc: function(){ // 这个函数的this是object
        var that = this
        return function(){     // 这个函数的this是window
          return that.name
        }
      }
    }
    

    将外部函数的this保存下来给内部函数就行了。这样的情况在开发中也很常见。

    3 块级作用域

    在很久以前(ES6之前),js还没有块级作用域的概念,所以,使用了一些手段来达到块级作用域的目的。
    首先来说块级作用域的好处,为什么要用它?

       块级作用域内可以产生私有变量,外界访问不到,一旦执行完,就直接销毁,大大减少内存开销;说的就是你,闭包!
    

    那怎么用呢?
    使用立即执行函数

    (function(){
      // 这里是块级作用域
    })()
    
    // 注意不要写成这样,这样写function在最开始,成了函数声明,会报错
    function(){
      // 。。。
    }()
    

    同样的,闭包也可以放在块级作用域内使用,以减少内存开销。
    题外话
    在《你不知道的js上卷》中有提到
    其实try-catch代码块中的catch,它后面的{}中就是一个块级作用域,感兴趣的同学可以去了解一下。

    三 私有变量

    私有变量是指只有函数内部能访问,外部无法访问到的变量。
    下面的例子我觉得挺好的,来看看

    function MyObject () {
      // 私有变量
      var privateVariable = 10
      // 私有方法
      function privateFunction () {
        return false
      }
    
      // 特权方法
      this.publicMethod = function () {
        privateVariable++
        return privateFunction()
      }
    }
    

    嗯,看完代码应该都懂了,下面来说点特别的。

    1 静态私有变量

    多了两个字: 静态,那区别是什么?
    先上代码:

    (function() {
      // 私有变量
      var privateVariable = 10
      // 私有方法
      function privateFunction () {
        return false
      }
      MyObject = function(){}
      // 特权方法
      MyObject.prototype.publicMethod = function () {
        privateVariable++
        return privateFunction()
      }
    })()
    

    诶?立即执行函数?全局变量MyObject?原型方法?
    看着没太大区别,但是好像鸟枪换炮。
    先是将整体改造成了立即执行函数,又用了一个不带声明关键字的变量MyObject(即全局变量,严格模式下会报错!),最后给MyObject整了个原型方法来访问私有变量和方法。
    不用问,私有变量和方法成了全部实例共享,这就是静态这两个字的含义。

    2 模块模式

    模块模式是为了单例创建私有变量和方法的。
    单例又是什么?是指只有一个实例的对象。
    按照惯例,JS是按照字面量的方式来创建单例对象的。
    先来看看单例

    var singleton = {
      name: 'value',
      method: function () {
        // 。。。
      }
    }
    

    在来看看模块模式做了什么

    var singleton = function(){
      // 私有变量和方法
      var privateVarible = 10
      function privateFunction(){
        return false
      }
      // 公有属性和方法
      return {
        publicProperty: true,
        publicMethod:function(){
          privateVarible ++
          return privateFunction()
        }
      }
    }
    

    模块模式添加了私有变量和方法,并通过return的字面量对象访问私有变量和方法。和单例模式相比,增加了私有的部分。

    应用:
    在需要对单例进行某些初始化,同时又需要维护其私有变量时是很有用的。

    3 增强的模块模式

    听名字就知道比模块模式要强了。那来看来看有多强

    var singleton = function () {
      // 私有属性,方法
      var privateVarible = 10
    
      function privateFunction () {
        return false
      }
    
      // 创建对象
      var object = new CustomType() //  这里的CustomType是可以客制化的(你想放啥放啥)
    
      // 添加公共属性和方法
      object.publicProperty = true 
    
      object.publicMethod = function () {
        privateVarible++
        return privateFunction()
      }
    
      // 返回对象
      return object
    }
    

    增强的地方:把字面量对象改成构造函数,又在实例上添加了属性和方法。

    应用场景:
    单例必须是某个类型的实例,同时还要添加某些属性和方法。

    相关文章

      网友评论

        本文标题:函数表达式

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