资讯专栏INFORMATION COLUMN

谈js中的作用域链和闭包

LucasTwilight / 1340人阅读

摘要:所以,当在函数中使用全局变量的时候,所产生的代价是最大的,因为全局对象一直处于作用域链的最末位置,读取局部变量是最快的。

什么是作用域

在编程语言中,作用域控制着变量与参数的可见性及生命周期,它能减少名称冲突,而且提供了自动内存管理(javascript 语言精粹)

静态作用域

再者,js不像其他的编程语言一样,拥有着块级作用域,就像下面一段代码。

function afunction(){
    var a = "sf";
    console.log(b);
    console.log(c);
    var b = function(){
        console.log("这是b中的内容");
    }
    function c(){
        console.log("这是c中的内容");
    }
    (function d(){
        console.log("这是d中的内容");
    })()
}

实用var声明的变量和函数声明将会进行声明提前afunction函数的执行环境中,故上述代码相当于以下的代码,在一个变量声明提前的时候,其值为undefined,而函数声明则是将函数体作为值。

function afunction(){
    var a;
    var b;
    function c(){
        console.log("这是c中的内容");
    }
    a = "sf";
    console.log(b);
    console.log(c);
    b = function(){
        console.log("这是b中的内容");
    }
    (function d(){
        console.log("这是d中的内容");
    })()
}
全局作用域与局部作用域

将上述的代码稍作改动如下

var outer = "outer";
function afunction(){
    function c(){
        console.log("这是c中的内容");
    }
    a = "sf";
    console.log(outer);
}

我们在afunction函数的外部定义了outer变量,假设这段代码运行在浏览器上,那么变量提前的过程中outer变量被声明在了window作用域上,也就是浏览器中的全局作用域上,而函数中的变量则在函数运行时被声明在了afunction作用域上,这个就是局部作用域,在这个局部作用域中,outer变量被访问到了,这种跨作用域的读取变量的形式就是根据作用域链来实现的。

什么是作用域链

在js中,函数也是对象,函数与其他的对象一样,拥有可以访问的属性,[[Scope]]就是其中的一个属性,它指明了哪些东西可以被函数访问。
考虑下面的函数

function add(a,b){
    var sum = a + b;
    return sum;
}

当函数add创建时候,add的[[Scope]]属性会指向作用域链对象,该对象的初始位置指向全局对象,如下图所示。

var t = add(1,2);

上述语句执行了add函数,对于函数的每一次执行,浏览器会创建一个执行环境的内部对象,一个执行环境定义了一个函数执行时的环境。函数的每次执行时对应的执行环境都说唯一的。每一个执行环境都有自己的作用域链,此对象的局部变量,thisarguments等组成活动对象,插入在作用域链对象的最前端,也就是图中所示的0号位置,当运行结束后,执行环境和活动对象都将销毁。
函数的执行过程中,每遇到一个变量,都会从作用域链的顶部,也就是0号位置查找该变量,如果查找成功则返回,查找失败则按照作用域链查找下一个位置的对象,该例子中也就是1号位置的全局对象。

作用域链带来的性能问题

如上面所讨论的那样,每一次遇到读取变量的时候,都意味着一次搜索作用域链的过程,如果搜索的作用域链的层次越多的话,将严重影响性能。
所以,当在函数中使用全局变量的时候,所产生的代价是最大的,因为全局对象一直处于作用域链的最末位置,读取局部变量是最快的。
所以,一个提高效率的规则是尽可能的使用局部变量。如下面的代码所示。

function demo(){
    var d = document,
        bd = d.body,
        div = d.getElementsByTagName("div");
    d.getElementById("id1").innerHTML = "aaa";
    //(许多使用document,body和div的操作)
}

上面的代码首先将全局的document对象保存在了局部变量d中,这样当下次频繁的使用document对象时,仅仅需要从局部变量中即可获得。

动态作用域

js中实用的是静态作用域,作用域链一般不可改变,但是withtry-catch可以改变作用域链,发生在函数的执行时候

with语句
function withTest(){
    var foo = "sf";
    var obj = {foo:"abc"};
    with(obj){
        function f(){
            alert(foo);
        }
        (function(){
            alert(foo);
        })();
        f();
    }
}
withTest();

在函数声明的时候,作用域链没有考虑with的情况,当函数执行的时候,动态生成with的对象,推入在作用域链的首位,这就意味着函数的局部变量存在作用域链的第二个位置,访问的代价提高了,虽然访问with对象的代价降低了,完全可以将with对象保存在局部变量中,故with语句不推荐使用。

try-catch语句
try{
    anErrorFunction();
}catch(e){
    errorHandler(e);
}

由于catch语句中只有一条语句,将error传递给errorHandler函数,所以运行时作用域链的改变不会影响性能。

什么是闭包

闭包是允许函数访问局部作用域之外的数据。即使外部函数已经退出,外部函数的变量仍可以被内部函数访问到。
因此闭包的实现需要三个条件:

内部函数实用了外部函数的变量

外部函数已经退出

内部函数可以访问

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

上述代码在执行的时候,b得到的是闭包对象的引用,虽然a执行完毕后,但是a的活动对象由于闭包的存在并没有被销毁,在执行b(1)的时候,仍然访问到了x变量,并将其加1,若在此执行b(1),则x是2,因为闭包的引用b并没有消除。

一个经典的闭包的实例
//ul下面有3个li,实现点击每个li,弹出li的序号
for(var i = 0,len = lis.length;i < len; i++){
    lis[i].onclick = function(i){
        return function(){
            alert(i);
        }
    }(i);
}

在这里,没有把闭包直接给onclick事件,而是先定义了一个自执行函数,该函数中包含着闭包的函数,i的值被保存在自执行的函数中,当闭包函数执行后,会从自执行函数中查找i,达到“保存”变量的目的。

注:匿名函数中的this指向的是window,故在匿名闭包函数使用父函数的this指针时,需要将其存储下来,如 var that = this;

闭包的作用

模块化代码

私有成员

避免全局变量的污染

希望一个变量长期驻扎在内存中

使用闭包所造成的性能问题

如上面的描述,当执行闭包函数后,父函数所保留下来的活动对象并不是在闭包函数的作用域链的首位(首位存放的是闭包的活动对象),当频繁的访问跨作用域的标识符时候,每次都会造成性能的损失,我们仍然可以将常用的跨作用域变量存储在局部变量中,直接访问该局部变量

实用闭包所造成的内存泄露问题(IE9以下)

IE9及以下的版本使用的是引用计数的内存回收机制,当引用计数为0的时候将会回收,但有一种循环引用的情况

window.onload = function(){
    var el = document.getElementById("id");
    el.onclick = function(){
        alert(el.id);
    }
}

这段代码执行时,将匿名函数对象赋值给elonclick属性;然后匿名函数内部又引用了el对象,存在循环引用,所以不能被回收;
(javascript 高级程序设计(第三版))
解决方法:

window.onload = function(){
    var el = document.getElementById("id");
    var id = el.id; //解除了循环引用
    el.onclick = function(){
        alert(id); //并没有出现循环引用
    }
    el = null; // 将闭包引用的外部活动对象清除
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/86108.html

相关文章

  • 【进阶2-1期】深入浅出图解作用链和闭包

    摘要:本期推荐文章从作用域链谈闭包,由于微信不能访问外链,点击阅读原文就可以啦。推荐理由这是一篇译文,深入浅出图解作用域链,一步步深入介绍闭包。作用域链的顶端是全局对象,在全局环境中定义的变量就会绑定到全局对象中。 (关注福利,关注本公众号回复[资料]领取优质前端视频,包括Vue、React、Node源码和实战、面试指导) 本周开始前端进阶的第二期,本周的主题是作用域闭包,今天是第6天。 本...

    levius 评论0 收藏0
  • JS基础知识:变量对象、作用链和闭包

    摘要:前言这段时间一直在消化作用域链和闭包的相关知识。而作用域链则是这套规则这套规则的具体运行。是变量对象的缩写那这样放有什么好处呢我们知道作用域链保证了当前执行环境对符合访问权限的变量和函数的有序访问。 前言:这段时间一直在消化作用域链和闭包的相关知识。之前看《JS高程》和一些技术博客,对于这些概念的论述多多少少不太清楚或者不太完整,包括一些大神的技术文章。这也给我的学习上造成了一些困惑,...

    Keven 评论0 收藏0
  • js知识梳理6:关于函数的要点梳理(2)(作用链和闭包)

    摘要:在此例中,在匿名函数被返回后,它的作用域链初始化为包含函数的活动对象和全局变量对象。函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象,结果就是只是的执行环境的作用域链会被销毁,其活动对象会留在内存中。 写在前面 注:这个系列是本人对js知识的一些梳理,其中不少内容来自书籍:Javascript高级程序设计第三版和JavaScript权威指南第六版,...

    aristark 评论0 收藏0
  • javascript系列--javascript深入浅出图解作用链和闭包

    摘要:变量对象也是有父作用域的。作用域链的顶端是全局对象。当函数被调用的时候,作用域链就会包含多个作用域对象。当函数要访问时,没有找到,于是沿着作用域链向上查找,在的作用域找到了对应的标示符,就会修改的值。 一、概要 对于闭包的定义(红宝书P178):闭包就是指有权访问另外一个函数的作用域中的变量的函数。 关键点: 1、闭包是一个函数 2、能够访问另外一个函数作用域中的变量 二、闭包特性 对...

    Jensen 评论0 收藏0
  • JS 作用域链

    摘要:首先,在创建函数时,作用域链内就会先填入对象,图片只例举了全部变量中的一部分。然后,解释器进入函数的执行环境,同样的,首先填入父级的作用域链,就是的,包括了对象活动对象。之后再把的活动对象填入到作用域链最顶部,这就是的作用域链了。 之前学习JS函数部分时,提到了作用域这一节,但是因为使用材料书不同,今天在读博客的时候发现其实还有一个知识点即作用域链,所以来写一些个人理解和认识加深记忆。...

    darry 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<