宝哥软件园

深入理解Javascript中的范围链和闭包

编辑:宝哥软件园 来源:互联网 时间:2021-09-05

首先,让我们回顾一下前面介绍数组遍历的文章:

请先阅读上一篇文章中提到的for循环代码:

var数组=[];array.length=10000000//(千万)为(var i=0,length=array.lengthilengthI){ array[I]=' hi ';}var t1=新日期();for(var i=0,length=array.lengthilengthi ){}var t2=新日期();console . log(T2-t1);//以下是连续5次的运行时间。//168 158 170 159 165=820(ms)我们来看看下面的代码。测试环境是Chrome 52.0.2743.116 (64位):

var t1=新日期();(function(){//闭包for(var i=0,length=array.lengthilengthI){//array . push(I);}})();var t2=新日期();console . log(T2-t1);//以下是连续5次的运行时间://8 6 8 7 6=35(ms)。计算: 820/35=23的效率,大约高出20倍。事实上,当Firefox和Safari在底层进行了优化后,性能仍然高出4~6倍。为什么呢?

我们注意到这两个代码之间最大的区别是第二个代码用匿名函数包装了for循环。我们稍后再谈,请耐心阅读。

行动范围

范围意味着变量在声明它们的函数体和嵌套在这个函数体中的任何函数体中定义。

js中只有函数作用域

众所周知,JS中没有块作用域,只有函数作用域。以下是:

for(var I=0;i10I){;} console . log(I);//10 function f(){ var a=123;} f();console . log(a);//a没有定义,所以js中只有一个局部作用域,即函数作用域。

用var声明变量

一般来说,作为弱类型语言,js只需要var保留字来声明变量。如果不使用var来声明函数中的变量,该变量将被提升为全局变量,然后脱离函数的范围,如下图:所示

函数f(){ b=123;} f();console . log(b);//123此时,与var声明的A变量相比,B变量被提升为全局变量,仍然可以在函数范围之外访问。

由于变量在函数的作用域中声明时没有var,所以它们将被提升为全局变量,那么如果var没有在全局上下文中使用会发生什么呢?

var声明没有全局使用,这个变量仍然是全局变量c=' hello scopeconsole . log(c);//hello scope console . log(window . c);//hello scope//检查c变量的属性console . log(object . getowntpropertysdescriptor(window,' c '));//对象{value:' hello scope ',writeable: true,enumerable: true,可配置: true},此时c变量可以赋值、枚举和配置。//尝试删除c变量删除c;//true表示已成功删除c变量console . log(c);//c未定义console . log(window . c);//undefined//使用var声明,然后删除d变量var d=1;console . log(object . getowntpropertysdescriptor(窗口,“d”));//对象{value: 1,writeable: true,enumerable: true,可配置: false},此时可以赋值和枚举d变量,但不能配置delete d;//false表示d变量删除失败console . log(d);//1 console . log(window . d);//1综上所述,有以下规则:

变量在没有var保留字的情况下被声明,并且它被提升为全局变量,不管它在什么范围内。如果不使用var声明,可以通过删除保留字配置删除变量,删除后变量不可访问;如果使用var声明,则变量不可配置,即不能被删除保留字删除;只要是全局变量,就可以直接访问或者使用“窗口”访问。变量名”,不管变量是否由var声明;JS中的范围链

像其他对象一样,函数对象具有可以通过代码访问的属性和一系列只能由JavaScript引擎访问的内部属性。内部属性之一是[[范围]],由第三版ECMA-262标准定义。该内部属性包含创建函数的范围内的一组对象。这个集合被称为函数的作用域链,它决定了函数可以访问哪些数据。

我们先来看一个栗子:

var e=' hello函数f(){ e='作用域链';var g==' good}上述范围链的图形显示在:下方

函数执行时,函数f内部会生成一个活动对象和作用域链,JavaScript引擎的内部对象会放入活动对象中,外部E变量在作用域链的第二层,索引=1,而内部G变量在scope chain的顶层,索引=0,所以访问G变量总是比访问E变量快。

关闭

当我们谈论范围时,我们不得不说结束。那么,什么是封闭?

官方的解释是闭包是一个有很多变量的表达式(通常是一个函数),环境绑定了这些变量,所以这些变量也是表达式的一部分。

这是什么意思?简单来说就是:

当函数执行时,它返回内部私有函数,或者以其他方式将其留在外部(例如,通过将其内部私有函数的引用分配给外部变量),从而防止函数的内部范围被执行引擎回收。在函数外部,通过访问暴露的内部私有函数,它具有访问函数内部私有范围的效果,即在关闭之前。es6,通常,我们实现的模块使用闭包。闭包所依赖的结构有一个明显的特点,那就是:函数是在词法范围之外执行的。如下,f2是闭包的关键,它的词法范围是函数F的内部私有范围,在F的范围之外执行.

var h=1;函数f(){ var I=2;返回函数F2(){ var j=3i h;console . log(j);} } var ff=f();ff();//6由于f2定义时在F内部,F的内部私有作用域可以在f2内部访问,这样通过返回f2就可以在F函数外部访问I变量。

执行f2时,变量J在作用域链的index0位置,变量I和变量H分别在作用域链的index 1和index 2位置。因此,J的赋值过程是沿着范围链的第二层和第三层依次求I和H的值,然后用3求和,最后赋给J .

浏览器沿着作用域链搜索变量总是要花费CPU时间。范围链的外层(或者离f2越远的变量),浏览器搜索的时间就越长,因为范围链需要遍历的次数更多。因此,全局变量(窗口)总是需要最多的访问时间。

封闭的微观世界

如果我们想对闭包以及函数f和嵌套函数f2之间的关系有更深的理解,我们需要引入其他几个概念:函数执行上下文、调用对象、作用域和作用域链。以函数A从定义到执行的过程为例来说明这些概念。

定义函数F时,js解释器会将函数A的作用域链设置为定义F时A所在的“环境”,如果F是全局函数,则作用域链中只有窗口对象。执行函数f时,f会进入相应的执行上下文。在创建执行上下文的过程中,首先会给f增加一个作用域属性,也就是a的作用域,第一步的值是作用域链,也就是a的作用域链,作用域=f,然后执行环境会创建一个调用对象。call对象也是一个带有属性的对象,但是它没有原型,不能通过JavaScript代码直接访问。创建活动对象后,将活动对象添加到f的范围链的顶部。此时,a的范围链包含两个对象, f的活动对象和窗口对象。下一步是向活动对象添加一个arguments属性。它存储调用函数F时传递的参数,最后将函数F的所有形式参数和函数f2的内部引用也加入到F的活动对象中,这一步完成了函数f2的定义,所以和第三步一样,将函数f2的作用域链设置为f2定义的环境,也就是F的作用域,整个函数F从定义到执行都完成了。此时f将函数f2的引用返回给ff,函数f2的作用域链包含函数f的活动对象的引用,这意味着f2可以访问f中定义的所有变量和函数,函数f2被ff引用,函数f2依赖于函数f,所以函数f返回后不会被GC回收。

当函数f2被执行时,它将与上述步骤相同。因此,f2的范围链包括三个对象: f2的活动对象、F的活动对象和窗口对象,如下图所示:

如图所示,当访问函数f2中的变量时,搜索顺序是:

先搜索自己的活动对象,如果存在,返回,如果不存在,继续搜索函数f的活动对象,依次查找,直到找到为止。如果原型对象存在于函数f2中,则在搜索自己的活动对象后,首先找到自己的原型对象。继续搜索。这是Javascript中的变量搜索机制。如果找不到整个范围链,将返回undefined。总结。这一段提到了两个重要的词:函数定义和执行。本文中提到的函数的范围是在定义函数时确定的,而不是在执行函数时确定的(参见步骤1和3)。用一段代码解释这个问题。

函数f(x){ var g=function(){ return x;}返回g;} var h=f(1);警报(h());这段代码中的变量h指向f中的匿名函数(由g返回)。

假设函数H的作用域是在执行alert(h())时确定的,那么H的作用域链就是: h的活动对象——alert-window对象的活动对象。假设函数H的作用域是在定义的时候确定的,也就是说H所指向的匿名函数在定义的时候已经确定了它的作用域。执行时,H的作用域链是: h的活动对象——F窗口对象的活动对象。如果第二个假设成立,输出值为1。

运行结果证明第二个假设是正确的,这表明函数的范围是在定义函数时确定的。

关闭可能导致IE浏览器内存泄漏

先看一个栗子:

函数f(){ var div=document . create element(' div ');div . onclick=function(){ return false;}}上面div的click事件是一个闭包。由于这种闭包的存在,F函数内部的div变量将总是引用DOM元素。

在早期的IE浏览器中(IE9之前),js对象和DOM对象使用了不同的垃圾收集方式,DOM对象使用了计数垃圾收集机制。只要匿名函数(比如onclick事件)存在,DOM对象的引用至少是1,所以它占用的内存永远不会被破坏。

有趣的是,不同版本的IE会导致不同的现象:

如果是IE 6,内存泄漏,直到IE进程关闭;如果是IE 7,内存会一直漏到离开当前页面;如果是IE 8,GC回收器回收他们的内存,不管目前是不是兼容模式。综上所述,闭包:的优点是共享函数范围,方便打开一些接口或变量对外使用;

注:由于闭包可能会导致函数中的变量长时间存储在内存中,消耗大量内存,影响页面性能,因此不能滥用,在IE浏览中可能会导致内存泄漏。解决方法是在退出函数之前删除所有未使用的局部变量。

for循环问题分析

让我们看看循环问题的开头。添加匿名函数后,for循环中的变量位于匿名函数的局部范围内。此时,访问length属性或访问I属性只需要在匿名函数的范围内查找,查询效率大大提高(测试数据显示提高了200倍以上)。

使用匿名函数后,不仅范围内的查询更快,而且范围内的变量与外界隔离,避免了I、长度等变量对后续代码的影响。

踩在视野的坑上

让我们踩上一个经典范围的坑。

var div=document . getelementsbytagname(' div ');for(var i=0,len=div.length伊琳;i ){ div[i]。onclick=function(){ console . log(I);}}上面代码的初衷是每次点击都打印div的索引,但实际上打印的是len的值。我们来分析一下原因。

单击div,将执行console.log(i)语句。显然,I变量不在click事件的本地范围内,浏览器将沿着范围链搜索I变量。这里在index1定义了一个I变量,for循环从这里开始,js没有块范围。所以I变量在for循环块执行后不会被破坏,I的最后一次自加使得i=len,所以浏览器停在范围链index=1的索引处,返回I的值,也就是len的值。

为了解决这个问题,我们会根据症结对症下药,把点击事件的局部范围从范围上改变,如下:

var div=document . getelementsbytagname(' div ');for(var i=0,len=div.length伊琳;i ){(函数(n){ div[n])。onclick=function(){ console . log(n);} })(I);}因为click事件由闭包包装,而闭包是自动执行的,所以每次闭包中n个变量的值都不一样。当你点击div时,浏览器会沿着作用域链寻找n个变量,最后在闭包中找到n个变量,打印出div的索引。

这方面的范围

之前,我们学习了范围链和闭包的基础知识。现在,让我们来谈谈这个神秘的范围。

熟悉OOP的开发人员知道这是一个对象实例的引用,并且总是指向它。但是,在js世界中,这是随着其执行环境的变化而变化的,它总是指向其方法的对象。如下所示,

函数f(){ alert(this);} var o={ }o . func=f;f();//[对象窗口]o . func();//[Object Object]console . log(f===window . f);//true当f单独执行时,它的内部this指向window对象,但是当f成为o对象的属性func时,这个指向o对象,而f===window.f,所以它们实际指向这个方法的对象。

让我们在下面应用它

Array.prototype.slice.call([1,2,3],1);//[2,3],array.prototype.slice ([1,2,3],1)的正确用法;//[],用法错误,这个内部切片仍然指向array . prototype var slice=array . prototype . slice;切片([1,2,3],1);//unsighttypeerror : Array.prototype . slice在null或undefined上调用//此时,slice中的这个指向窗口对象,留下了原始array . prototype对象的范围,所以报告了一个错误~ ~综上所述,使用这个的时候只需要注意一点。

这总是指向其方法的对象。

带语句

说到范围链,就不得不说with语句,可以用来临时改变范围,将语句中的对象添加到范围的顶部。

带有(表达式){语句}的语法:

例如:

var k={ name : ' daicy ' };带(k){ console.log(名称);//daicy}console.log(名称);//undefinedwith语句用于对象k,第一层作用域是k对象的内部作用域,可以直接打印name的值,with以外的语句不受此影响。看一个栗子:

var l=[1,2,3];with(l){ console . log(map(function(I){ return I * I;}));//[1,4,9]}在本例中,with语句用于数组,因此在调用map()方法时,解释器将检查该方法是否是局部函数。如果不是,它会检查伪对象L,看它是不是这个对象的方法,map是Array对象的方法,由Array L继承,所以可以正确执行。

注意: with语句容易造成歧义,而且因为需要强行改变作用域链,会带来更多的cpu消耗。建议谨慎使用with语句。

摘要

以上就是本文的全部内容。希望本文的内容能给你的学习或工作带来一些帮助。有问题可以留言交流。谢谢你的支持。

更多资讯
游戏推荐
更多+