本篇要讲述的是JavaScript的局部作用域,以及与之相对的函数调用对象,还是闭包的使用。
前情回顾
前面讲到,JavaScript有全局作用域和局部作用域之分。做为一门脚步语言,JavsScript要嵌入宿主运行,用于表示宿主的对象,通常就等同于全局对象,比如浏览器中的window
对象,或者NodeJS中的global
对象。而局部作用域,则是函数运行时产生的临时对象,所以局部作用域也是临时作用域。
前面还讲到,JavaScript里面一切皆属性。不管是定义的变量也好,还是定义的函数也好,都是某个对象的属性。如果我们再代码最上层定义了一个var a = 0
,那么全局对象中就多了一个叫做a
的属性,其值为0
。如果,var a = 0
出现在一个函数的定义中,那么这个函数在运行时所产生的临时对象中就多了一个叫做a
,值为0
的属性。
函数调用对象
我们可以给函数调用时产生的临时对象取一个名字,叫做函数调用对象
(execution context)。那么当一个函数被调用时,会生成一个函数调用对象
,这个对象的属性集合就是这个函数运行时候的局部作用域。如果当前的函数中调用了另外一个函数,那么就会产生一个嵌套的函数调用对象
,如下所示:
函数调用 /-------------\ 函数调用 /------------\
-------> | 函数调用对象 | ------> | 函数调用对象 |
\-------------/ \-------------/
如上图所示,在函数中嵌套调用函数的时候,就会产生一个调用链。每个函数在调用的时候,都可以访问与之对应的函数调用对象的属性集合(局部作用域),以及全局对象的属性集合。那这边存在一个问题,函数在调用的时候,能否回溯调用链,访问先前的函数调用对象呢? 答案是不一定,要分情况考虑。如果是函数的定义是分开的,则不行,比如下面的例子:
function g() {
console.log("g()")
}
function f() {
g()
}
以上的例子中,函数调用链是: f() -> g()
,但是g()并不能访问f()的函数调用对象的属性集合。
函数定义的嵌套
有一种情况能够使被调用的函数能够访问先前的函数调用对象,那就是把函数定义嵌套到另一函数中:
function f() {
var a = 1;
function g() {
console.log(a)
}
g()
}
f() // 输出1
当一个函数嵌套定义在另一个函数中的时候。父函数的作用域对子函数开放。所以,上面的代码中,g()
中的语句console.log(a)
打印的是父函数的变量a
。而且这跟嵌套的层级没有关系,如下面代码所示:
function f() {
var a = 1;
function g() {
function h() {
console.log(a)
}
h()
}
g()
}
f() // 输出1
返回嵌套的函数
因为函数体也是一种对象,我们甚至可以返回嵌套的函数体,以便稍后调用,如下所示:
function f() {
var a = 1;
function g() {
console.log(a)
}
return g
}
h = f()
h() // 输出 1
当一个函数返回的时候,按道理它的局部作用域应该消失才对,但是被嵌套的函数所使用的那些属性却继续存在,所以上面的h()
的运行结果是1,也就是f()的函数调用对象的属性a
的值。
对于这种现象,当作用域销毁之后,其某些属性还可以访问,我们叫做闭包(Closure)。可以认为,那些被嵌套的子函数所访问的属性被挪到了一个新的作用域,这个作用域叫做闭包作用域。
只有从函数返回的函数才会有闭包作用域,而闭包作用域可以看作是这些嵌套函数的私密空间,可以用来存储一些私密的信息,比如计数信息:
function f() {
var counter = 0
function g() {
counter += 1
console.log(counter)
}
return g
}
incr = f()
incr() // 输出 1
incr() // 输出 2
incr() // 输出 3
如果不使用闭包,我们则需要把计数器信息保存到别的地方,比如根据How to use static variables in a Javascript function所示的例子,把计数信息保存到函数本身的属性中:
function foo() {
if( typeof foo.counter == 'undefined' ) {
foo.counter = 0;
}
foo.counter++;
console.log(foo.counter)
}
foo(); // 输出 1
foo(); // 输出 2
foo(); // 输出 3
上面方法的问题在于,函数本身的属性在函数体之外也可以修改,不够私密,比如:
foo.counter = 100
foo(); // 输出 101
闭包的实现
关于闭包是如何实现的,下面两篇文章提供了一些线索:
一般来说,函数在执行的时候,局部作用域中的变量是存储在栈上(stack)的,当函数调用结束的时候,局部作用域会从栈上弹出,从而消解。但是闭包要求在函数调用结束时依然可以访问局部作用域中的变量,这就要求变量具有更长的生存期。一般的做法是把这些变量放到堆(heap)上。比如下面的代码:
function f() {
var counter = 0
function g() {
counter += 1
console.log(counter)
}
return g
}
JavaScript解释器发现counter
这个变量在嵌套函数中被访问,于是乎就把它放在堆上,而不是栈上。当函数f()
退出时,在堆上的counter
依然可以被访问。那counter
会不会永远存在,不会被销毁呢?不用担心,JavaScript自带有垃圾收集功能。在上面的例子中,counter
在g
的闭包中,当没有人访问g
了,g
会被释放,随之对应的闭包内容也会被释放。
一个更复杂的例子
下面的例子不仅返回了函数,还返回另外一个对象:
function f1() {
var o = {
a: 1,
b: 2,
};
var f = function () {
console.log(o.a)
}
return [o, f]
}
ret = f1()
x = ret[0]
y = ret[1]
y() // 输出1
x.a += 1
y() // 输出2
Python中闭包的实现
脚本语言一般都可以嵌套函数定义,一般也都有闭包的概念,但是实现细节上会有所差异。Python最初的实现,父函数局部作用域中的变量对于嵌套函数是只读的:
def f1():
a = 1
def x(): a += 1; print(a)
def y(): print(a)
return (x,y)
x, y = f1()
x() # 出现错误 UnboundLocalError: local variable 'a' referenced before assignment
y() # 输出 1
Python3中引入了一个关键字nonlocal
,可以指明局部变量对于嵌套函数并非是只读的:
def f1():
a = 1
def x(): nonlocal a; a += 1; print(a)
def y(): print(a)
return (x,y)
x, y = f1()
x() # 输出 2
y() # 输出 2
参考链接
- Understanding JavaScript Closures: A Practical Approach
- W3school JavaScript Closures
- How do JavaScript closures work under the hood
- JavaScript Closures Explained
- How to implement closures
- The JavaScript languageAdvanced working with functions
- Why aren’t python nested functions called closures?
- Lexical scopingEmulating private methods with closures
- I never understood JavaScript closures
- Secrets of JavaScript closures
- Master the JavaScript Interview: What is a Closure?