深入理解JavaScript作用域链与闭包

QQ群招募中646258285(招募中,没几个人说话),
需要交流的朋友可以直接加我微信( DntBeliv )或QQ( 1121864253 )


作用域链

和函数的原型一样,当创建一个函数之后,也会为函数对象添加一个属性[[Scopes]]记录他的作用域。该属性时一个栈,应该不可以修改,但是我们可以在调试模式中看到。

[[Scopes]]属性记录了当前作用域与活动对象,在函数运行时会向栈内压入一个元素,标识当前的作用域,等函数运行结束后,会将栈顶的元素弹出,销毁掉当前的作用域。

此外,当我们讨论闭包的时候,[[Scopes]]属性有一个非常重要的特点,那就是他会存储调用函数的作用域。举个例子,A函数内创建了函数B,那么B的[[Scopes]]就会有A函数作用域。此外,所有函数都会包括global作用域。我们也可以猜到,表示函数运行时作用域的是栈顶元素,而栈顶的属性会屏蔽掉非栈顶相同属性。

我们看个例子,因为无法看到函数运行时的情况,所有这里看不到函数运行时的作用域。这里主要看一下单个函数和函数内创建函数的不同。

function createComparisonFuntion(propertyName) {
    return function (obj1, obj2) {
        var value1 = obj1[propertyName];
        var value2 = obj2[propertyName];

        if (value1 < value2) {
            return -1;
        } else if (value1 > value2) {
            return 1;
        } else {
            return 0;
        }
    }
}
var compareNames = createComparisonFuntion("name");
var result = compareNames({
    name: "Zachary",
    age: 28
}, {
    name: "Nicholas",
    age: 29
});
compareNames = null;

这里,compareNames 就是createComparisonFuntion函数内创建的函数,compareNames 的作用域应该包括了reateComparisonFuntion作用域,我们来看一下是不是这样。

在这里插入图片描述

可以看到compareNames包括了许多属性,其中有我们之前提到的arguments、caller等,这里主要关注[[Scopes]],可以看到里面有两个元素,第一个元素(也即是栈顶元素)就是它的调用函数createComparisonFuntion,第二个元素是全局作用域。因为函数还没有被调用,所以函数运行时的作用域还没有被创建。最后我们再来看一下createComparisonFuntion的情况。

在这里插入图片描述

最后给一个完整的作用域链的图:

在这里插入图片描述

闭包

闭包是指有权访问另一个函数作用域中变量的函数。我们利用[[Scopes]]就可以实现闭包。

还是上面那个例子,为什么createComparisonFuntion已经销毁了我们还可以访问到"name"这个参数?这就是因为compareNames的[[Scopes]]还引用着这个参数,因为该部分内存还在使用中,因此不会被垃圾回收机制回收。看下面的证明:

在这里插入图片描述

现在来看几个例子:

function createFunction() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = function () {
            return i;
        };
    }
    return result;
}

这段代码希望创建10个不同i值的函数,但实际上所有的i返回都是10。为什么会这样呢?因为返回的10个函数访问的i实际上都是通过闭包访问的createFunction作用域中的活动对象i,只有这一个变量,当createFunction()调用结束后,i就等于10。

var name = "The window";
var obj = {
    name: "my object",
    getName: function () {
        var that = this;
        return function () {
            return that.name;
        };
    }
};
//实际上写成console.log(obj.getName()());就行了,为了便于理解,写成这个
console.log((obj.getName())());//"my object"

在使用vue的回调时经常用到that = this,一直不知道为啥,现在清楚了吧,如果直接返回this.name,那么这个this指的是当前作用域,那么它的值取决于这个(被返回的)函数的调用对象。而如果是that,则就是通过闭包访问的前一个函数的作用域。

再来看一个内存泄露的问题:

//element较大,会占用很大内存,不建议这样写
function assignHandler(){
    var element = document.getElementById("someEle");
    element.onclick = function(){
        console.log(element.id);
    };
}
//建议这样写
function assignHandler(){
    var element = document.getElementById("someEle");
    var id = element.id;
    element.onclick = function(){
        console.log(id);
    };
    element = null;
}

模仿块级作用域与私有变量

JavaScript中没有块级作用域,看下面这个例子:

for (var i = 0; i < 10; i++) {}
var i;
console.log(i);//10

但是函数类似于块级作用域,我们可以通过闭包模仿块级作用域:

//函数声明后面不能跟圆括号
(function() {for (var i = 0; i < 10; i++) {}})();
console.log(i);//报错:ReferenceError: i is not defined

进一步,我们可以创建具有私有变量的对象:

//函数表达式后面可以跟圆括号
var dingleton = function () {
    //私有变量和私有函数
    var privateVar = 10;
    function privateFun(){
        return false;
    }

    //创建对象
    var obj = new Object();

    //添加特权/公有属性和方法
    obj.publicProperty = true;
    obj.publicMethod = function(){
        privateVar++;
        return privateFun();
    }
    //返回这个对象
    return obj;
}();
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章