- 你不知道的JavaScript(下卷)
- (美)凯尔·辛普森
- 2128字
- 2020-08-29 01:58:06
2.5 作为值的函数
到目前为止,我们已经介绍了JavaScript中作为主要作用域机制的函数。回忆一下典型的函数声明语法是怎样的:
function foo() { // .. }
虽然从这个语法上看可能不是很明显,但foo基本上就是一个外层作用域中的一个变量,这个作用域赋予被声明函数一个引用。也就是说,这个函数本身是一个值,就像42或者[1,2,3]一样。
这个概念乍听起来可能很奇怪,你需要花点时间来理解它。不仅你可以向函数传入值(参数),函数本身也可以作为值赋给变量或者向其他函数传入,又或者从其他函数传出。
因此,应该将函数值视为一个表达式,与其他的值或者表达式类似。
考虑:
var foo = function() { // .. }; var x = function bar(){ // .. };
第一个赋给变量foo的函数表达式被称为是匿名的,因为这个函数表达式没有名称。
第二个函数表达式是已命名的(bar),即使它的引用赋值给了变量x。虽然匿名函数表达式的使用仍然极为广泛,但通常更需要已命名函数表达式。
要想获取更多信息,参见本系列中的《你不知道的JavaScript(上卷)》第一部分。
2.5.1 立即调用函数表达式
在前面的代码片段中,两个函数表达式都没有运行——如果加上foo()或者x(),那么就可以执行了。
还有另一种方法可以执行函数表达式,这种方法通常被称为立即调用函数表达式(immediately invoked function expression, IIFE):
(function IIFE(){ console.log( "Hello! " ); })(); // "Hello! "
(function IIFE(){ .. })函数表达式外面的( .. )就是JavaScript语法能够防止其成为普通函数声明的部分。
表达式最后的()(即 })();这一行)实际上就表示立即执行前面给出的函数表达式。
这看起来可能有点奇怪,但实际上并不像初看上去那么诡异。思考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
以上执行的IIFE命名函数返回了值42,并被赋给了x。
2.5.2 闭包
闭包是JavaScript中一个非常重要,且经常被误解的概念。这里不作深入介绍,可以参见本系列中的《你不知道的JavaScript(上卷)》第一部分。但我将会解释相关的几个要点,以帮助你理解一般概念。这将会是你JavaScript技巧集中最重要的技术之一。
你可以将闭包看作“记忆”并在函数运行完毕后继续访问这个函数作用域(其变量)的一种方法。
考虑:
function makeAdder(x) { // 参数x是一个内层变量 // 内层函数add()使用x,所以它外围有一个“闭包” function add(y) { return y + x; }; return add; }
每次调用外层makeAdder(..)返回的、指向内层add(..)函数的引用能够记忆传入makeAdder(..)的x值。现在,我们来使用makeAdder(..):
// plusOne获得指向内层add(..)的一个引用 // 带有闭包的函数在外层makeAdder(..)的x参数上 var plusOne = makeAdder( 1 ); // plusTen获得指向内层add(..)的一个引用 // 带有闭包的函数在外层makeAdder(..)的x参数上 var plusTen = makeAdder( 10 ); plusOne( 3 ); // 4 <--1 + 3 plusOne( 41 ); // 42 <--1 + 41 plusTen( 13 ); // 23 <--10 + 13
我们来详细说明一下这段代码是如何执行的。
(1) 调用makeAdder(1)时得到了内层add(..)的一个引用,它会将x记为1。我们将这个函数引用命名为plusOne()。
(2) 调用makeAdder(10)时得到了内层add(..)的另一个引用,它会将x记为10,我们将这个函数引用命名为plusTen()。
(3) 调用plusOne(3)时,它会向1(记住的x)加上3(内层y),从而得到结果4。
(4) 调用plusTen(13)时,它会向10(记住的x)加上13(内层y),从而得到结果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()函数运行完毕之后,函数doLogin()也保持着对它们的访问权。
publicAPI是带有一个属性/方法login的对象,login是对内层函数doLogin()的一个引用。当我们从User()返回publicAPI时,它就变成了我们命名为fred的那个实例。
此时,外层的函数User()已经运行完毕。我们通常认为像username和password这样的内层变量也就随之消失了。但上述示例并不会这样,因为login()函数的内部有一个可以使得它们依然保持活跃的闭包。
这就是我们可以调用fred.login(..)的原因,这等同于调用内层doLogin(..),并且fred. login(..)仍然可以访问内层变量username和password。
这里只是对闭包和模块模式的匆匆一瞥,它们的某些细节很可能还是有点令人迷惑。没关系!让你的大脑完全理解它还需要一些努力。
从现在开始,阅读本系列《你不知道的JavaScript(上卷)》第一部分,这有助于你更进一步地理解这些知识。