至此,我们已经将函数作为JavaScript中主要的 作用域 机制讨论过了。你可以回想一下典型的function
声明语法是这样的:
function foo() {
// ..
}
虽然从这种语法中看起来不明显,foo
基本上是一个位于外围作用域的变量,它给了被声明的function
一个引用。也就是说,function
本身是一个值,就像42
或[1,2,3]
一样。
这可能听起来像是一个奇怪的概念,所以花点儿时间仔细考虑一下。你不仅可以向一个function
传递一个值(参数值),而且 一个函数本身可以是一个值,它能够赋值给变量,传递给其他函数,或者从其它函数中返回。
因此,一个函数值应当被认为是一个表达式,与任何其他的值或表达式很相似。
考虑如下代码:
var foo = function() {
// ..
};
var x = function bar(){
// ..
};
第一个被赋值给变量foo
的函数表达式称为 匿名 函数表达式,因为它没有“名称”。
第二个函数表达式是 命名的(bar
),它还被赋值给变量x
作为它的引用。命名函数表达式 一般来说更理想,虽然 匿名函数表达式 仍然极其常见。
更多信息参见本系列的 作用域与闭包。
立即被调用的函数表达式(IIFE)
在前一个代码段中,哪一个函数表达式都没有被执行 —— 除非我们使用了foo()
或x()
。
有另一种执行函数表达式的方法,它通常被称为一个 立即被调用的函数表达式 (IIFE):
(function IIFE(){
console.log( "Hello!" );
})();
// "Hello!"
围绕在函数表达式(function IIFE(){ .. })
外部的( .. )
只是一个微妙的JS文法,我们需要它来防止函数表达式被看作一个普通的函数声明。
在表达式末尾的最后的()
—— })();
这一行 —— 才是实际立即执行它前面的函数表达式的东西。
这看起来可能很奇怪,但它不像第一眼看上去那么陌生。考虑这里的foo
和IIFE
之间的相似性:
function foo() { .. }
// `foo` 是函数引用表达式,然后用`()`执行它
foo();
// `IIFE` 是函数表达式,然后用`()`执行它
(function IIFE(){ .. })();
如你所见,在执行它的()
之前列出(function IIFE(){ .. })
,与在执行它的()
之前定义foo
实质上是相同的;在这两种情况下,函数引用都使用立即在它后面的()
执行。
因为IIFE只是一个函数,而函数可以创建变量 作用域,以这样的风格使用一个IIFE经常被用于定义变量,而这些变量将不会影响围绕在IIFE外面的代码:
var a = 42;
(function IIFE(){
var a = 10;
console.log( a ); // 10
})();
console.log( a ); // 42
IIFE还可以有返回值:
var x = (function IIFE(){
return 42;
})();
x; // 42
值42
从被执行的命名为IIFE
的函数中return
,然后被赋值给x
。
闭包
闭包 是JavaScript中最重要,却又经常最少为人知的概念之一。我不会在这里涵盖更深的细节,你可以参照本系列的 作用域与闭包。但我想说几件关于它的事情,以便你了解它的一般概念。它将是你的JS技术结构中最重要的技术之一。
你可以认为闭包是这样一种方法:即使函数已经完成了运行,它依然可以“记住”并持续访问函数的作用域。
考虑如下代码:
function makeAdder(x) {
// 参数 `x` 是一个内部变量
// 内部函数 `add()` 使用 `x`,所以它对 `x` 拥有一个“闭包”
function add(y) {
return y + x;
};
return add;
}
每次调用外部的makeAdder(..)
所返回的对内部add(..)
函数的引用可以记住被传入makeAdder(..)
的x
值。现在,让我们使用makeAdder(..)
:
// `plusOne` 得到一个指向内部函数 `add(..)` 的引用,
// `add()` 函数拥有对外部 `makeAdder(..)` 的参数 `x`
// 的闭包
var plusOne = makeAdder( 1 );
// `plusTen` 得到一个指向内部函数 `add(..)` 的引用,
// `add()` 函数拥有对外部 `makeAdder(..)` 的参数 `x`
// 的闭包
var plusTen = makeAdder( 10 );
plusOne( 3 ); // 4 <-- 1 + 3
plusOne( 41 ); // 42 <-- 1 + 41
plusTen( 13 ); // 23 <-- 10 + 13
这段代码的工作方式是:
- 当我们调用
makeAdder(1)
时,我们得到一个指向它内部的add(..)
的引用,它记住了x
是1
。我们称这个函数引用为plusOne(..)
。 - 当我们调用
makeAdder(10)
时,我们得到了另一个指向它内部的add(..)
引用,它记住了x
是10
。我们称这个函数引用为plusTen(..)
。 - 当我们调用
plusOne(3)
时,它在3
(它内部的y
)上加1
(被x
记住的),于是我们得到结果4
。 - 当我们调用
plusTen(13)
时,它在13
(它内部的y
)上加10
(被x
记住的),于是我们得到结果23
。
如果这看起来很奇怪和令人困惑,不要担心 —— 它确实是的!要完全理解它需要很多的练习。
但是相信我,一旦你理解了它,它就是编程中最强大最有用的技术之一。让你的大脑在闭包中煎熬一会是绝对值得的。在下一节中,我们将进一步实践闭包。
模块
在JavaScript中闭包最常见的用法就是模块模式。模块让你定义对外面世界不可见的私有实现细节(变量,函数),和对外面可访问的公有API。
考虑如下代码:
function User(){
var username, password;
function doLogin(user,pw) {
username = user;
password = pw;
// 做登录的工作
}
var publicAPI = {
login: doLogin
};
return publicAPI;
}
// 创建一个 `User` 模块的实例
var fred = User();
fred.login( "fred", "12Battery34!" );
函数User()
作为一个外部作用域持有变量username
和password
,以及内部doLogin()
函数;它们都是User
模块内部的私有细节,是不能从外部世界访问的。
警告: 我们在这里没有调用new User()
,这是有意为之的,虽然对大多数读者来说那可能更常见。User()
只是一个函数,不是一个要被初始化的对象,所以它只是被一般地调用了。使用new
将是不合适的,而且实际上会浪费资源。
执行User()
创建了User
模块的一个 实例 —— 一个全新的作用域会被创建,而每个内部变量/函数的一个全新的拷贝也因此而被创建。我们将这个实例赋值给fred
。如果我们再次运行User()
,我们将会得到一个与fred
完全分离的新的实例。
内部的doLogin()
函数在username
和password
上拥有闭包,这意味着即便User()
函数已经完成了运行,它依然持有对它们的访问权。
publicAPI
是一个带有一个属性/方法的对象,login
是一个指向内部doLogin()
函数的引用。当我们从User()
中返回publicAPI
时,它就变成了我们称为fred
的实例。
在这个时候,外部的User()
函数已经完成了执行。一般说来,你会认为像username
和password
这样的内部变量将会消失。但是在这里它们不会,因为在login()
函数里有一个闭包使它们继续存活。
这就是为什么我们可以调用fred.login(..)
—— 和调用内部的doLogin(..)
一样 —— 而且它依然可以访问内部变量username
和password
。
这样对闭包和模块模式的简单一瞥,你很有可能还是有点儿糊涂。没关系!要把它装进你的大脑确实需要花些功夫。
以此为起点,关于更多深入细节的探索可以去读本系列的 作用域与闭包。