浅析JavaScript中的作用域链,闭包以及私有变量

388 阅读5分钟

充实的暑假怎么少得了看书呢,特此留下学习足迹

必要的知识概念

我们都知道JavaScript中有一个重要概念——执行环境,什么是执行环境呢?

执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。

啥意思? 简单的说就是你所定义的变量和函数都会被框在一个环境中,这个环境会限制你做一些非法操作。就像你在A函数中定义了一个变量a,你不能在B函数中使用变量a吧

    function A() {
        var a = 1;
    }
    function B() {
        console.log(a);
    }
    B(); 

image.png

这里变量a定义在了函数A的执行环境中,在函数B的执行环境中当然就不能访问a咯,值得注意的是当这个执行环境中的所有代码执行完毕后,该环境会自动销毁,保存在其中的所有变量和函数定义也随之销毁

让我们来看看JavaScript具体做了啥

我们知道每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境

我们接着上个例子讲,有没有什么办法让函数B访问到函数A中的变量呢,看一下这个例子

    A();
    var a = 2;
    function A() {
        var a = 1;
        B(); 
        function B() {
            console.log(a);
        }
    }

image.png

这时就有人说了,就这就这?

哈哈哈,先别急,这里我想讲的是作用域链

作用域链

还记得我介绍执行环境的时候说每个执行环境都有一个与之关联的变量对象吗,如果这个环境是函数的话,则将活动对象作为这个变量对象

作用域链本质上就是一个指向变量对象的指针列表,这个例子中执行打印变量a这一语句时,标识符解析会沿着作用域链一级一级的搜索,当他发现函数B的执行环境的活动对象中没有变量a,就去找上一级函数A的执行环境的活动对象,bingo!找到了。这也是为什么控制台打印1而不是2

话不多说,直接上图

image.png

这个例子中不同背景颜色的矩形代表一个执行环境,值得一提的是,在Web浏览器中,全局执行环境被认为是window对象,因为所有全局变量和函数都是作为window对象的属性和方法创建的。

有细心的小伙伴发现我刚刚那个例子中是先调用函数A再定义的吗?

这就是函数声明提升

定义函数的方式有两种:一种是函数声明,另一种就是函数表达式

第一种是这样的:

function fn (arg0,arg1){
    //函数体
}

第二种是这样的:

var fn = function(arg0,arg1){
    //函数体
}

关于函数声明,它的一个重要特征就是函数声明提升,意思是在执行代码之前会先读取函数声明。这就意味着我们可以像上个例子一个把函数声明放在调用它的语句后面。

有兴趣的小伙伴可以用函数表达式的方式写写看,不出以为应该会报错undifined,这里就不过多演示了。

关于闭包

闭包是指有权访问另一个函数作用域中的变量的函数

闭包其实很简单,我们上一个例子中事实上就已经创建了闭包,也就是函数B访问函数A中的变量a。

思考下面这段代码

    function createComparisonFunction(propertyName){
        return function (object1,object2) {
            var value1 = object1[propertyName];
            var value2 = object2[propertyName];
            if(value1 < value2){
                return -1;
            } else if(value1 > value2){
                return 1;
            } else {
                return 0;
            }
        }
    }

    //创建函数
    var compareNames = createComparisonFunction("number");

    //调用函数
    var result = compareNames({ number: "15" }, { number: "12" });
    console.log(result);
    
    //解除对匿名函数的引用(以便释放内存)
    compareNames = null;
    
    

compareNames这个函数是能够访问createComparisonFunction中定义的所有变量,尽管这个例子只访问了propertyName,有想过为什么吗? 原因是创建函数这一行代码执行完后,就有这样的作用域链 createComparisonFunction的活动对象->window全局对象,并且有趣的是createComparisonFunction这个函数返回后,其执行环境就会销毁,但其活动对象依然会留在内存中,因为匿名函数的作用域链仍然再引用这个活动对象。至于为什么活动对象依然会留在内存中,这就和JavaScript的垃圾收集机制有关了,这里不展开细讲。

这时候就有小伙伴问了为什么没有这个匿名函数的活动对象呢,我想是因为这个匿名函数还未调用的原因,所以没有压入环境栈,没有生成执行环境

思考下面这个例子,给出你的答案

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

有人和我想的一样是 0 吗

那恭喜你 上当了 hhh        答案是10

我们先来看这个函数createFunctions返回了一个数组,数组里的每一项都是一个匿名函数,返回了createFunctions中定义的变量i。其实原理很简单,createFunctions()[0]这个匿名函数保存着createFunctions这个函数的活动对象,所以数组里的每一项引用的都是同一个变量i

闭包的缺点

  • 内存占用

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,所以在开发中能不使用闭包尽量不要使用闭包

  • 内存泄漏

所谓内存泄漏指的是,由于由于疏忽或者错误未能释放那些不再使用的内存,造成内存浪费

闭包会维持包含它的函数的局部变量,使其得不到释放,造成内存泄漏

function fn() {
        var element = document.getElementById("xxx");
        element.onclick = function () {
            console.log(element);
            //函数体内引用fn中的局部变量
        }
    }

要解决也很简单:element = null 接触对DOM对象的引用,确保正常回收其占用的内存

私有变量

任何在函数中定义的变量,都可以认为是私有变量,并且我们可以通过闭包访问私有变量。

我们把有权访问私有变量和私有函数的公有方法称为特权方法

function fn() {
        //私有变量
        var str = 'hello world';

        //特权方法
        this.publicMethod = function () {
            return str
        }
    }
    var func = new fn();
    console.log(func.publicMethod());

用途

防止过多的全局变量和函数造成的命名冲突

最后

如有疑问和不足之处希望大家能在评论区指出。