至此,我们已经将函数作为JavaScript中主要的 作用域 机制讨论过了。你可以回想一下典型的function声明语法是这样的:

  1. function foo() {
  2. // ..
  3. }

虽然从这种语法中看起来不明显,foo基本上是一个位于外围作用域的变量,它给了被声明的function一个引用。也就是说,function本身是一个值,就像42[1,2,3]一样。

这可能听起来像是一个奇怪的概念,所以花点儿时间仔细考虑一下。你不仅可以向一个function传递一个值(参数值),而且 一个函数本身可以是一个值,它能够赋值给变量,传递给其他函数,或者从其它函数中返回。

因此,一个函数值应当被认为是一个表达式,与任何其他的值或表达式很相似。

考虑如下代码:

  1. var foo = function() {
  2. // ..
  3. };
  4. var x = function bar(){
  5. // ..
  6. };

第一个被赋值给变量foo的函数表达式称为 匿名 函数表达式,因为它没有“名称”。

第二个函数表达式是 命名的bar),它还被赋值给变量x作为它的引用。命名函数表达式 一般来说更理想,虽然 匿名函数表达式 仍然极其常见。

更多信息参见本系列的 作用域与闭包

立即被调用的函数表达式(IIFE)

在前一个代码段中,哪一个函数表达式都没有被执行 —— 除非我们使用了foo()x()

有另一种执行函数表达式的方法,它通常被称为一个 立即被调用的函数表达式 (IIFE):

  1. (function IIFE(){
  2. console.log( "Hello!" );
  3. })();
  4. // "Hello!"

围绕在函数表达式(function IIFE(){ .. })外部的( .. )只是一个微妙的JS文法,我们需要它来防止函数表达式被看作一个普通的函数声明。

在表达式末尾的最后的() —— })();这一行 —— 才是实际立即执行它前面的函数表达式的东西。

这看起来可能很奇怪,但它不像第一眼看上去那么陌生。考虑这里的fooIIFE之间的相似性:

  1. function foo() { .. }
  2. // `foo` 是函数引用表达式,然后用`()`执行它
  3. foo();
  4. // `IIFE` 是函数表达式,然后用`()`执行它
  5. (function IIFE(){ .. })();

如你所见,在执行它的()之前列出(function IIFE(){ .. }),与在执行它的()之前定义foo实质上是相同的;在这两种情况下,函数引用都使用立即在它后面的()执行。

因为IIFE只是一个函数,而函数可以创建变量 作用域,以这样的风格使用一个IIFE经常被用于定义变量,而这些变量将不会影响围绕在IIFE外面的代码:

  1. var a = 42;
  2. (function IIFE(){
  3. var a = 10;
  4. console.log( a ); // 10
  5. })();
  6. console.log( a ); // 42

IIFE还可以有返回值:

  1. var x = (function IIFE(){
  2. return 42;
  3. })();
  4. x; // 42

42从被执行的命名为IIFE的函数中return,然后被赋值给x

闭包

闭包 是JavaScript中最重要,却又经常最少为人知的概念之一。我不会在这里涵盖更深的细节,你可以参照本系列的 作用域与闭包。但我想说几件关于它的事情,以便你了解它的一般概念。它将是你的JS技术结构中最重要的技术之一。

你可以认为闭包是这样一种方法:即使函数已经完成了运行,它依然可以“记住”并持续访问函数的作用域。

考虑如下代码:

  1. function makeAdder(x) {
  2. // 参数 `x` 是一个内部变量
  3. // 内部函数 `add()` 使用 `x`,所以它对 `x` 拥有一个“闭包”
  4. function add(y) {
  5. return y + x;
  6. };
  7. return add;
  8. }

每次调用外部的makeAdder(..)所返回的对内部add(..)函数的引用可以记住被传入makeAdder(..)x值。现在,让我们使用makeAdder(..)

  1. // `plusOne` 得到一个指向内部函数 `add(..)` 的引用,
  2. // `add()` 函数拥有对外部 `makeAdder(..)` 的参数 `x`
  3. // 的闭包
  4. var plusOne = makeAdder( 1 );
  5. // `plusTen` 得到一个指向内部函数 `add(..)` 的引用,
  6. // `add()` 函数拥有对外部 `makeAdder(..)` 的参数 `x`
  7. // 的闭包
  8. var plusTen = makeAdder( 10 );
  9. plusOne( 3 ); // 4 <-- 1 + 3
  10. plusOne( 41 ); // 42 <-- 1 + 41
  11. plusTen( 13 ); // 23 <-- 10 + 13

这段代码的工作方式是:

  1. 当我们调用makeAdder(1)时,我们得到一个指向它内部的add(..)的引用,它记住了x1。我们称这个函数引用为plusOne(..)
  2. 当我们调用makeAdder(10)时,我们得到了另一个指向它内部的add(..)引用,它记住了x10。我们称这个函数引用为plusTen(..)
  3. 当我们调用plusOne(3)时,它在3(它内部的y)上加1(被x记住的),于是我们得到结果4
  4. 当我们调用plusTen(13)时,它在13(它内部的y)上加10(被x记住的),于是我们得到结果23

如果这看起来很奇怪和令人困惑,不要担心 —— 它确实是的!要完全理解它需要很多的练习。

但是相信我,一旦你理解了它,它就是编程中最强大最有用的技术之一。让你的大脑在闭包中煎熬一会是绝对值得的。在下一节中,我们将进一步实践闭包。

模块

在JavaScript中闭包最常见的用法就是模块模式。模块让你定义对外面世界不可见的私有实现细节(变量,函数),和对外面可访问的公有API。

考虑如下代码:

  1. function User(){
  2. var username, password;
  3. function doLogin(user,pw) {
  4. username = user;
  5. password = pw;
  6. // 做登录的工作
  7. }
  8. var publicAPI = {
  9. login: doLogin
  10. };
  11. return publicAPI;
  12. }
  13. // 创建一个 `User` 模块的实例
  14. var fred = User();
  15. fred.login( "fred", "12Battery34!" );

函数User()作为一个外部作用域持有变量usernamepassword,以及内部doLogin()函数;它们都是User模块内部的私有细节,是不能从外部世界访问的。

警告: 我们在这里没有调用new User(),这是有意为之的,虽然对大多数读者来说那可能更常见。User()只是一个函数,不是一个要被初始化的对象,所以它只是被一般地调用了。使用new将是不合适的,而且实际上会浪费资源。

执行User()创建了User模块的一个 实例 —— 一个全新的作用域会被创建,而每个内部变量/函数的一个全新的拷贝也因此而被创建。我们将这个实例赋值给fred。如果我们再次运行User(),我们将会得到一个与fred完全分离的新的实例。

内部的doLogin()函数在usernamepassword上拥有闭包,这意味着即便User()函数已经完成了运行,它依然持有对它们的访问权。

publicAPI是一个带有一个属性/方法的对象,login是一个指向内部doLogin()函数的引用。当我们从User()中返回publicAPI时,它就变成了我们称为fred的实例。

在这个时候,外部的User()函数已经完成了执行。一般说来,你会认为像usernamepassword这样的内部变量将会消失。但是在这里它们不会,因为在login()函数里有一个闭包使它们继续存活。

这就是为什么我们可以调用fred.login(..) —— 和调用内部的doLogin(..)一样 —— 而且它依然可以访问内部变量usernamepassword

这样对闭包和模块模式的简单一瞥,你很有可能还是有点儿糊涂。没关系!要把它装进你的大脑确实需要花些功夫。

以此为起点,关于更多深入细节的探索可以去读本系列的 作用域与闭包