9

Scope-JS

初次看JavaScript的闭包,感觉并不是很惊艳的感觉,因为怎么看,都像是因为这门语言本身的作用域的关系,才有了闭包这么一说。今天就是从JavaScript本身的作用域来谈谈闭包以及相关的东西。

什么是闭包?

这是在维基百科上的解释:

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

也就是说,如果一个函数引用了他周围环境中的变量,那么,被引用的那个变量即使已经超出了它作用的范围,那么,他也能够继续存在。通常意义上的闭包的意思:将这个function以及它本身的引用的变量用一个函数来将它们封存起来,那么这个包装函数就被称为,闭包函数

当然广义上的闭包,我觉得每个function都可以是,因为它与它引用的那些非local varialblies都构成了闭包的条件。

scope

闭包的产生来自于作用域 scope 的定义。在javascript中,没有块作用域block scope的说法,只有function scope的说法。当在一个函数内部定义了变量以后,那么这个变量在外部是无法看到的。

  • 在ES5中,如果声明变量没有用var ,那么这个变量会被看做是全局变量Global
  • 虽然块作用域这个概念是在ES6,才会正式出现,但是在ES5中除了在try-catch模块中是个例外,其他的地方是没有块作用域的说法的。

作用域链

JS中,只有一个地方有块作用域,那就是 try-catch 表达式中的catch作用域块,也就意味着,这个区域的变量只在这个块中作用,外部不能访问。

在JS中的作用域是有作用域链之称,也就意味着如果,在local variablies当中没有找到相应的变量的话,那么他就会通过一个特殊的__scope,这个 __scope指向定义它的上一层对象的__variables,如果没有找到,以此类推,直到它找到或者链到达全局作用域的__variblies

在进入一个新的作用域的时候,JS会先将作用域内所有的定义了的变量的声明,提升到作用域的最顶端。因为函数是被指定给变量的Function对象,所以它也会被提升。但是情况会因其函数定义的方式的不同而有所不同。这个请参考 MDN-function部分。

函数定义的方式:

  • 函数声明(function declaration)
  • 函数表达式(funciton expression)

retain 作用域

当本地函数被执行完以后,那么将会移出这个函数的内部,而这个函数也将被销毁,但是如果仍有人引用它的话,那么,这个作用域将不会被销毁。如下面的一个例子:

var global_ten = 10;
var sum;
function papa() {
var hundred_more = 100;
sum = function (a, b) {
return a + b + global_ten + hundred_more;
};
return sum(9, 11);
}
papa(); // 130

papa()引用完成之后,那么似乎就可以将它的作用域释放掉了,但是,sum()仍然是一个全局变量,它引用了papa()的作用域,所以在其完成以后,仍然不能将作用域释放掉,预存起来以供使用。

当然了,预存起来的是对作用域的引用,并不是某个值。看一下那个经典的闭包循环问题:

var fns = [];
function definer() {
    for (var i = 0; i < 5; i++) {
        fns.push(function () {
            return i;
        });
    }
}
definer();
fns[0]();
fns[1]();
fns[2]();
fns[3]();
fns[4]();

在这五个函数调用后,你可能期待的是1,2,3,4,5,但是事实上是5,5,5,5,5,这是什么原因来着?变量 i是在definer内部的一个局部变量,所以在这个函数执行完成后,发现还是有东西在引用着变量i(为什么不是直接赋值呢,因为它并不是本地变量而且没有进行复制操作),所以,i是不会被销毁的而且别人也只是对它进行了引用。这样子的话,在前面提到的那个函数执行完成之后,fns对其进行调用的时候,发现引用了definer()函数作用域的变量, 而此时的变量已经被修改为5了,这才导致上面的情况出现。

那么如何解决这个问题呢?总体的解决思路是,将引用的变量i变成本地作用域的东西,所以可以将其中的某些东西替换成下面这个样子:

 fns.push(
        function(){
            function (local_i) {
                return local_i;
            }
        }(i)
);

闭包

先看一道闭包题:

function fun(n,o) {
  console.log(o)
  return {
    fun:function(m){
      return fun(m,n);
    }
  };
}
var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);//undefined,?,?,?
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1);  c.fun(2);  c.fun(3);//undefined,?,?,?
//问:三行a,b,c的输出分别是什么?

在这道题中,比如说第一行,var a = fun(0);首先,这个a 不是fun()函数的引用,它是人家return回来的一个东西,是一个对象,所以在a.fun(1)中,它调用的是第二个fun()函数,那么这个函数返回来的是什么呢,是第一个函数的执行结果,当然也是一个对象啦。但是它输出来的是什么呢,输入的是0。这道题的好处在于,它除了将闭包的概念扯了进来以外,它还将作用域的概念也扯进来了。

这是一道考验理解深度的题目了。关于作用域的部分就是,因为当a.fun(1)的时候,调用的是第二个fun()函数,注意,这里的参数必须要自己加进去,它是不会自己调用这个作用域链的。接下来,这个函数又调用了fun(m,n),正好如上面所讲的那样,因为预留作用域的问题n == 0,所以导致后面的调用中与前面的情况相同。

这倒题的结果就变成了undefined,0,0,0

END

作者的原博客的地址为cn-blog-xxcanghai

作者的原意可能是想来解释闭包的问题来着,但是作为一个局部作用域强迫症的患者,当时我在最后这一部分的传参部分还是比较纠结,不太理解,上面的解释的不清楚的地方可能在于预留作用域在,传参时候的解释。

概览