作用域和闭包(Closures)

作用域

词法作用域和动态作用域

通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

词法作用域,也叫静态作用域,它的作用域和函数声明层级有关系。动态作用域是在运行时根据程序的调用先后顺序动态的来确定的。

词法作用域(Lexical scoping)

也叫静态作用域(Static Scope),采用词法作用域的变量叫词法变量。词法变量有一个在编译时静态确定的作用域。词法变量的作用域可以是一个函数或一段代码,该变量在这段代码区域内可见(visibility);在这段区域以外该变量不可见(或无法访问)。词法作用域里,取变量的值时,会检查函数定义时的文本环境,捕捉函数定义时对该变量的绑定。

大多数现在程序设计语言都是采用静态作用域规则,如C/C++、C#、Python、Java、JavaScript……

大部分都采用这种作用域方式,简便易懂,以“词法”来决定上下文内容,也就是根据语句的顺序。

动态作用域

相反,采用动态作用域的变量叫做动态变量。只要程序正在执行定义了动态变量的代码段,那么在这段时间内,该变量一直存在;代码段执行结束,该变量便消失。这意味着如果有个函数f,里面调用了函数g,那么在执行g的时候,f里的所有局部变量都会被g访问到。而在静态作用域的情况下,g不能访问f的变量。动态作用域里,取变量的值时,会由内向外逐层检查函数的调用链,并打印第一次遇到的那个绑定的值。显然,最外层的绑定即是全局状态下的那个值。

采用动态作用域的语言有Pascal、Emacs Lisp、Common Lisp(兼有静态作用域)、Perl(兼有静态作用域)。C/C++是静态作用域语言,但在宏中用到的名字,也是动态作用域。

举例说明:

var a = 2;

function foo() {
  console.log(a); // 会输出2还是3?
}

function bar() {
  var a = 3;
  foo();
}

bar();

静态作用域:和函数的声明顺序有关系,那么结果是输出2,因为foo函数和bar函数都被声明在全局下,foo虽然在bar函数中被调用,但是由于词法作用域的规则,foo不会在bar函数中查找变量a,而是会在全局中找a。JavaScript是词法作用域。

动态作用域:那么会返回结果3,因为foo是在bar函数中被调用的,bar中声明了a = 3,那么动态作用域下,会顺着调用foo函数的地方查找a的值。动态作用域就是整个程序运行的时候只有一个env。

总结:

大部分语言采用了静态作用域(词法作用域),以函数声明的地方作为范围。而动态作用域,则是以函数被调用的地方作为作用域范围。这也就引出了两种不同的作用域。

其实,说实话,我不认为理解这两个概念和闭包有多少关系,使用闭包只是想达到变量的隐藏或者读取函数内部变量。而和哪种作用域规则好像没有多少关系。

另外,我还有些困惑,使用闭包到底和函数传参相比较有哪些非常明显的优势?虽然,我也知道它的一些优势:读取函数变量和隐藏变量,但感觉很鸡肋,并不简单易理解、实用并应用广泛。

闭包(Closures)

定义(摘自MDN

Closures are functions that refer to independent (free) variables. In other words, the function defined in the closure ‘remembers’ the environment in which it was created.

闭包是拥有(指向)独立自由变量的函数。换句话说,定义在闭包中的函数能“记住”它被创建时的环境。

function makeFunc(){
 var x = "Mozilla";
 return function(){
    alert(x);
 }
}
makeFunc()();

If you run this code it will have exactly the same effect as the previous init() example: the string “Mozilla” will be displayed in a JavaScript alert box. What’s different — and interesting — is that the displayName() inner function was returned from the outer function before being executed.

That the code still works may seem unintuitive. Normally, the local variables within a function only exist for the duration of that function’s execution. Once makeFunc() has finished executing, it is reasonable to expect that the x variable will no longer be accessible. Since the code still works as expected, this is obviously not the case.

The solution to this puzzle is that myFunc has become a closure. A closure is a special kind of object that combines two things: a function, and the environment in which that function was created. The environment consists of any local variables that were in-scope at the time that the closure was created. In this case, myFunc is a closure that incorporates(合并) both the displayName function and the “Mozilla” string that existed when the closure was created.

闭包是一个特殊对象(是由函数Function和创建该函数的环境两部分),环境由闭包创建时在作用域中的任何局部变量组成,在我们的例子中,myFunc 是一个闭包,是由匿名函数和闭包创建时存在的 “Mozilla” 字符串形成。

从嵌套函数出发理解闭包

var x = 'global';
function f () {
    var x = 'local';
    function g() {
        alert(x);
    }
    g();
}
f(); // 'local'

当f()调用的时候,作用域链可以理解为由两部分组成,包含f这一调用的调用对象,然后后面是全局对象。此时,查找x的值会先从函数g的调用对象中查找x的值,g中没有,那么接下来去查找外围f调用对象中x的定义,于是找到了x=’local’,那么就会输出x,而不会继续往下查找全局对象了。 如果f中也没定义x的值,那么就会继续查找作用域链后面的全局对象,结果就是global了。如果全局对象中也没定义,那么自然就是undefined。

这一看,词法作用域,检查g函数在哪里定义,然后先看g函数中有没有x变量,若没有,再在g函数声明处同级查找x变量。应该说JS遵循词法作用域所以有这样的结果,若遵循动态作用域,特殊情况下又会有不同的结果。

我们知道了闭包的两个常用用途:

  • 利用闭包可以访问到局部变量

  • 可以把外围作用域中的变量存储在内存中,而不是在函数调用完毕后就销毁

比较一下两个分析:

function makeFunc (x) {
    return function () {return x++}
}
var a = [makeFunc(0), makeFunc(1), makeFunc(2)];
alert(a[0]());
alert(a[1]());
alert(a[2]());

执行结果为0,1,2 ,是严格的词法作用域的正常表现,每次makeFunc调用完毕后,它的调用对象会从作用域链中移除,再没有任何对它的引用,最终通过垃圾收集而完结。

var x = 0;
function makeFunc () {
    return function () {return x++}
}
var a = [makeFunc(), makeFunc(), makeFunc()];
alert(a[0]());
alert(a[1]());
alert(a[2]());

乍一看,结果都是0,1,2,并没有什么区别,可是仔细分析的话,有很大的不同:

只是因为x是全局变量,那么所有的函数调用操作的都是全局下的变量x,也就是三个函数共享了这个变量。分析的话,从前面讲的作用域链分析,从内部函数–>外部函数–>全局下搜寻变量x,直到找到,和前一个不同的是,这里的x是全局变量,嵌套函数完毕,调用对象移除,但是由于他们对象中都没有x,他们的调用对象销毁根本不会影响到x。于是,全局变量x值的改变就这样被保存下来了。

闭包与变量

作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。闭包保存的是整个变量对象,而不是某个特殊的变量。

可以理解为:闭包只有在函数被调用的时候才会按照作用域链向上层查找,那么for循环中的变量 i 是循环结束时的 i 值,而不是循环中的 i 值,因为作用域链上绑定的是 i 对象,而不是动态的绑定 i 的值,它只会存在 i 的最终值!

P.S.:There is not a single closure per function declaration. There is a closure for each call to a function.

function newClosure(someNum, someRef) {
    // Local variables that end up within closure
    var num = someNum;
    var anArray = [1,2,3];
    var ref = someRef;
    return function(x) {
        num += x;
        anArray.push(num);
        console.log('num: ' + num +
            '\nanArray ' + anArray.toString() +
            '\nref.someVar ' + ref.someVar);
      }
}
obj = {someVar: 4};
fn1 = newClosure(4, obj);
fn2 = newClosure(5, obj);
fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4;
fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4;
obj.someVar++;
fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;

总结

缺乏实践的理论都是站不住的,我觉得应该在实践中多去尝试使用,这样才能解决我的困惑。闭包到底好在哪?我不用闭包能不能行?这应该是我在使用时应该多去思考的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值