理解JavaScript(3):作用域、作用域链和闭包

作用域和作用域链

  作用域和作用域链是JavaScript函数调用的基础。深入理解作用域和作用域,有助于我们提高编程水平。当然,如果你理解了作用域和作用域链,也就可以轻易的掌握闭包。下面我就来说说我的理解。

作用域

  JavaScript只有两种作用域全局作用域局部作用域,JavaScript中没有java、c++中的块级作用域。

  • 全局作用域

    1 最外层定义的函数和变量,所以使用时最好使用匿名函数将脚本包裹,避免污染全局变量(如果插件比较多会出现命名冲突)
(function (obj) {
    var a = {
        name: 'oldMan';
        age: 24;
    } 
    var temp = 1; // 函数执行完毕temp会被销毁
    a.age += temp;
    obj.a = a; // 要保留的对象挂在到window对象上
})(window);

  2 函数内部没有用var声明的变量

  • 局部作用域

    只在固定的代码片段才能访问到一般情况下函数的外部不能访问函数的临时变量(闭包除外)
function add () {
    var b = 0;
    b++;
}
function add () 
for(var i = 0; i < 1; i++) {
    var a = 0;
    a++;
}
console.log(a, i); // a = 1, i = 1
console.log(b); // Uncaught ReferenceError: b is not defined

作用域链

定义:根据函数内部可以访问外部变量的机制,用链式查找的方式决定哪些数据能够被访问,以及访问的数据内容是什么。

为了充分的了解作用域链,我们先来聊聊什么是执行环境和执行顺序

  • 执行环境

    执行环境也分为全局执行环境和函数执行环境。

    1.在web浏览器中,全局环境是最外围的执行环境,全局执行环境被认为是window对象,因此所有的全局变量和函数都作为window对象的属性和方法创建的。

    2.js只有函数作用域,每个函数在声明的时候都会产生一个执行环境,也就是函数执行环境,同时,js会为每个执行环境关联一个变量对象。环境中定义的所有函数都保存在这个对象之中

  • 执行顺序

    js的执行顺序是根据函数的调用来决定,当一个函数被调用,该环境变量的变量对象就被压入一个环境栈,而在函数执行之后,栈将该函数的变量对象弹出,把控制权交给之前的执行环境变量对象。下面用图和代码结合说明;
var scope = "global";
function fn2 () {return scope;}
function fn1 () {return scope;}

fn1();
fn2();

以下图片来源于网络侵权删
image
说了这么多我们终于可以说说作用链到底如何形成的了。

  • 作用域链的形成
    当某个函数第一次调用时,就会创建一个执行环境以及相应的作用域链,并把作用域链赋值给一个特殊的内部属性。然后使用this,arguments(arguments 在全局环境中不存在)和其他命名参数的值来初始化函数的活动对象。当前执行环境的变量始终在作用域链的第0位
  • 第一次调用fn1
    (因为fn2()还没有被调用,所以没有fn2的执行环境)
    image
    可以看到fn1活动对象里并没有scope变量,于是沿着作用域链(scope chain)向后寻找,结果在全局变量对象里找到了scope,所以就返回全局变量对象里的scope值。
    fn1执行完毕后,执行fn2按照同样的原理构成fn2的作用域链。
    一般来说,当某个环境中的所有代码执行完毕后,该环境被销毁(弹出环境栈),保存在其中的所有变量和函数也随之销毁(全局执行环境变量直到应用程序退出,如网页关闭才会被销毁)

但像下面这种情况又有所不同:函数执行结束,执行环境被销毁,但是其关联的活动对象并没有随之销毁,而是一直存在于内存中,因为该活动对象被其内部函数的作用域链所引用。

function outer () {
    var scope = "outer";
    function inner () {
        return scope;
    }
    return inner;
}
var fn = outer();
fn();

outer()内部返回了一个inner函数,当调用outer时,inner函数的作用域链就已经被初始化了(复制父函数的作用域链,再在前端插入自己的活动对象),具体如下图:
image
outer执行结束,内部函数开始被调用
image
当outer()函数执行结束,执行环境本应该被销毁,但是其关联的活动对象并没有随之销毁,而是一直存在于内存中,因为该活动对象被其内部函数的作用域链所引用。

像上面这种内部函数的作用域链仍然保持着对父函数活动对象的引用,就是闭包(closure)

闭包

虽然闭包的定义十分抽象,但是理解了作用域链,就不难理解闭包。我们可以这样理解,闭包就是能够读取父函数内部变量的函数,其实质就是函数内部和外部的连接桥梁

  • 作用
    1.读取函数内部的变量
    2.就是让某些变量始终存在在内存中
function outer () {
    var n = 0;
    add = function () {
        n += 1;
        console.log('add', n)
    }
    function inner () {
        console.log(n);
    }
    return inner;
}
var result = outer();
result();// 0
add(); // add 1
result();// 1
// result实际上就是闭包inner函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数outer中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
  • 闭包使用的注意点
    1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

    2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

  • 闭包的应用
    创建10个a标签,点击的时候弹出对应的序号

var i;
for (i = 0; i < 10; i++) {
    (function (i) {
        var a = document.createElement('a');
        a.innerHTML = i + '
'
; a.addEventListener('click', function (e) { e.preventDefault(); alert(i); }); document.body.appendChild(a); })(i); }


JavaScript关于作用域、作用域链和闭包的理解
学习Javascript闭包(Closure)

你可能感兴趣的:(JavaScript基础知识)