javascript作用域链与闭包

1.作用域链
javasrcipt的作用域链是很重要的一个环节,如果此处理解不好,在日后编写javascript就会造成很多不必要的麻烦。当然如果能够很好的理解它,那么javascript的闭包也能很好的理解了。在理解作用域链之前,首先要理解什么是作用域。
作用域:直白点说,作用域就是变量和函数的访问范围,并且控制着变量和函数的可见性和生命周期。那么在javascript中,作用域主要分为两种:
a)全局作用域
b)局部作用域
在其他的语言中,比如c、c++、java等,在条件语句或者循环语句(即花括号之内)之内的每一段代码都有各自的作用域,而且变量在声明代码段外是不可见的,称之为块级作用域。但是在javascript中是没有块级作用域的,只有函数级作用域。请牢记这一点。我们可以看看下面的例子。

function fun(){
  if(true){
    var arg1 = '参数1';
  }
  alert(arg1)
  for(var i=0;i<10;i++){
    //什么也不做
  };
  alert(i)
}


执行上面代码后就应该明白了,javascript并没有块级作用域。

变量没有在函数内声明或者声明的时候没有带var就是全局变量,拥有全局作用域,window对象的所有属性拥有全局作用域;在代码任何地方都可以访问,函数内部声明并且以var修饰的变量就是局部变量,只能在函数体内使用,函数的参数虽然没有使用var但仍然是局部变量。
           

var a=3; //全局变量
function fn(b){ //局部变量
    c=2; //全局变量
    var d=5; //局部变量
    function subFn(){
        var e=d; //父函数的局部变量对子函数可见
        for(var i=0;i<3;i++){
            console.write(i);
        }
        alert(i);//3, 在for循环内声明,循环外function内仍然可见,没有块作用域
    }
}
alert(c); //在function内声明但不带var修饰,仍然是全局变量                

 
只要是理解了JavaScript没有块作用域,简单的JavaScript作用域很好理解,还有一点儿容易让初学者迷惑的地方是,JavaScript虽然是解释执行,但也不是按部就班逐句解释执行的,在真正解释执行之前,JavaScript解释器会预解析代码,将变量、函数声明部分提前解释,这就意味着我们可以在function声明语句之前调用function,这多数人习以为常,但是对于变量的与解析乍一看会很奇怪

console.log(a); //undefined
var a=3;
console.log(a); //3
console.log(b); //Uncaught ReferenceError: b is not defined

上面代码在执行前var a=3; 的声明部分就已经得到预解析(但是不会执行赋值语句),所以第一次的时候会是undefined而不会报错,执行过赋值语句后会得到3,上段代码去掉最后一句。

针对简单的作用域总结一下:
1.没有块级作用域,只有函数级作用域。
2.JavaScript解释器首先在当前作用域中搜索是否有该变量的定义,如果有,就是用这个变量;如果没有就到父域中寻找该变量.以此类推,直到最顶级作用域,仍然没有找到就抛出异常"变量未定义"。
3.全局执行环境是最外层的一个执行环境,在web浏览器中全局执行环境是window对象,因此所有全局变量和函数都是作为window对象的属性而创建的。每个函数都有自己的执行环境,当执行流进入一个函数的时候,函数的环境会被推入一个函数栈中,而在函数执行完毕后执行环境出栈并被销毁,保存在其中的所有变量和函数定义随之销毁,控制权返回到之前的执行环境中,全局的执行环境在应用程序退出(浏览器关闭)才会被销毁。

如果只是这样那么JavaScript作用域问题就很简单了,然而由于函数子函数导致的问题使作用域不止这样简单。所以执行环境或者说运行期上下文登场了,即:执行环境(execution context)定义了变量或函数有权访问的其它数据,决定了它们的各自行为。每个执行环境都有一个与之关联的变量对象(variable object, VO),执行环境中定义的所有变量和函数都会保存在这个对象中,解析器在处理数据的时候就会访问这个内部对象。

理解上面的内容后,再来理解作用域链
当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain,不简称sc)来保证对执行环境有权访问的变量和函数的有序访问。作用域第一个对象始终是当前执行代码所在环境的变量对象(VO)

        function a(x,y){
            var b=x+y;
            return b;
        }



在函数a创建的时候它的作用域链填入全局对象,全局对象中有所有全局变量



如果执行环境是函数,那么将其活动对象(activation object, AO)作为作用域链第一个对象,第二个对象是包含环境,下一个是包含环境的包含环境,以此类推。

function a(x,y){
    var b=x+y;
    return b;
}
var tatal=a(5,10);

 
这时候 var total=a(5,10);语句的作用域链如下


在函数运行过程中标识符的解析是沿着作用域链一级一级搜索的过程,从第一个对象开始,逐级向后回溯,直到找到同名标识符为止,找到后不再继续遍历,找不到就报错。

下面以绘制的方法深入理解一下作用域链。
1 绘制规则:
1) 作用域链就是对象的数组
2) 全部script是0级链,每个对象占一个位置
3) 凡是看到函数延伸一个链出来,一级级展开
4) 访问首先看当前函数,如果没有定义往上一级链检查
5) 如此往复,直到0级链
2 举例
var num = 10;
var func1 = function() {
 var num = 20;
 var func2 = function() {
  var num = 30;
  alert(num);
 };
 func2();
};
var func2 = function() {
 var num = 20;
 var func3 = function() {
  alert(num);
 };
 func3();
};
func1();
func2();

下面分析一下这段代码:
-> 首先整段代码是一个全局作用域,可以标记为0级作用域链,那么就有一个数组
var link_0 = [ num, func1, func2 ];// 这里用伪代码描述
-> 在这里func1和func2都是函数,因此引出两条1级作用域链,分别为
var link_1 = { func1: [ num, func2 ] };// 这里用伪代码描述
var link_1 = { func2: [ num, func3 ] };// 这里用伪代码描述
-> 第一条1级链衍生出2级链
var link_2 = { func2: [ num ] };// 这里用伪代码描述
-> 第二条1级链中没有定义变量,是一个空链,就表示为
var link_2 = { func3: [ ] };
-> 将上面代码整合一下,就可以将作用域链表示为:
复制代码 代码如下:

// 这里用伪代码描述
var link = [ // 0级链
 num,
 { func1 : [ // 第一条1级链
  num,
  { func2 : [ // 2级链
   num
  ] }
 ]},
 { func2 : [ // 第二条1级链
  num,
  { func3 : [] }
 ]}
];


有了这个作用域链的图,那么就可以非常清晰的了解访问变量是如何进行的:
在需要使用变量时,首先在当前的链上寻找变量,如果找到就直接使用,不会向上再找;如果没有找到,那么就向上一级作用域链寻找,直到0级作用域链.


2.闭包
概念:闭包是指有权访问另一个函数作用域中的变量的函数。
创建闭包的常见方式就是在一个函数内部创建另一个函数。

当内部函数在定义它的作用域的外部被引用时,就创建了该内部函数的一个闭包。这种情况下我们称既不是内部函数局部变量,也不是其参数的变量为自由变量,称外部函数的调用环境为封闭闭包的环境。从本质上讲,如果内部函数引用了位于外部函数中的变量,相当于授权该变量能够被延迟使用。因此,当外部函数调用完成后,这些变量的内存不会被释放(最后的值会保存),闭包仍然需要使用它们。
        

    function outerFn() {
            var outerVar = 0;
           
            function innerFn() {
                outerVar++;
                console.log("outerVar = " + outerVar + "<br/>");
            }
            return innerFn;
        }
        var fnRef = outerFn();
        fnRef();
        fnRef();
        var fnRef2 = outerFn();
        fnRef2();
        fnRef2();

 
闭包之间的交互

当存在多个内部函数时,很可能出现意料之外的闭包。我们定义一个递增函数,这个函数的增量为2

function outerFn() {
            var outerVar = 0;
            function innerFn1() {
                outerVar++;
                console.log("Inner function 1   outerVar = " + outerVar + "<br/>");
            }

            function innerFn2() {
                outerVar += 2;
                console.log("Inner function 2    outerVar = " + outerVar + "<br/>");
            }
            return { "fn1": innerFn1, "fn2": innerFn2 };
        }
        var fnRef = outerFn();
        fnRef.fn1();
        fnRef.fn2();
        fnRef.fn1();
        var fnRef2 = outerFn();
        fnRef2.fn1();
        fnRef2.fn2();
        fnRef2.fn1();

我们映射返回两个内部函数的引用,可以通过返回的引用调用任一个内部函数,结果:

Inner function 1    outerVar = 1
Inner function 2    outerVar = 3
Inner function 1    outerVar = 4

Inner function 1    outerVar = 1
Inner function 2    outerVar = 3
Inner function 1    outerVar = 4

innerFn1和innerFn2引用了同一个局部变量,因此他们共享一个封闭环境。当innerFn1为outerVar递增一时,久违innerFn2设置了outerVar的新的起点值,反之亦然。我们也看到对outerFn的后续调用还会创建这些闭包的新实例,同时也会创建新的封闭环境,本质上是创建了一个新对象,自由变量就是这个对象的实例变量,而闭包就是这个对象的实例方法,而且这些变量也是私有的,因为不能在封装它们的作用域外部直接引用这些变量,从而确保了了面向对象数据的专有性。


解惑
现在我们可以回头看看开头写的例子就很容易明白为什么第一种写法每次都会alert 4了。

for (var i = 0; i < 4; i++) {
           spans[i].onclick = function() {
               alert(i);
           }
       }

 
上面代码在页面加载后就会执行,当i的值为4的时候,判断条件不成立,for循环执行完毕,但是因为每个span的onclick方法这时候为内部函数,所以i被闭包引用,内存不能被销毁,i的值会一直保持4,直到程序改变它或者所有的onclick函数销毁(主动把函数赋为null或者页面卸载)时才会被回收。这样每次我们点击span的时候,onclick函数会查找i的值(作用域链是引用方式),一查等于4,然后就alert给我们了。而第二种方式是使用了一个立即执行的函数又创建了一层闭包,函数声明放在括号内就变成了表达式,后面再加上括号括号就是调用了,这时候把i当参数传入,函数立即执行,num保存每次i的值。



备注:在方法的方法中调用this的话默认指向的都是全局作用域,即window
如下例子

var a = {

  b:function(){
    alert(this);
    (function(_this){
      alert(this);
      alert(_this);
    })(this);
  }
}
a.b();
原文地址:https://www.cnblogs.com/screw/p/5124540.html