Javascript

函数表达式

2020-08-15  本文已影响0人  25度凉白开

以下内容总结自《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
}

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

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

上一篇下一篇

猜你喜欢

热点阅读