一般来说我们知道js是一种弱类型语言。 要声明变量,只需要 var 保留字。 如果在函数中不使用 var 声明变量,该变量将被提升为全局变量并脱离函数作用域,如下所示:
function f(){ b = 123; } f(); console.log(b);//123
此时,相比之前使用var声明的a变量,b变量被提升为全局变量,仍然可以在函数作用域外访问。
由于在函数作用域中不使用 var 声明变量会将该变量提升为全局变量,那么如果在全局作用域中不使用 var 会发生什么情况呢?
//全局下不使用var声明,该变量依然是全局变量 c = "hello scope"; console.log(c);//hello scope console.log(window.c);//hello scope //查看c变量的属性 console.log(Object.getOwnPropertyDescriptor(window, 'c'));//Object {value: "hello scope", writable: true, enumerable: true, configurable: true} ,此时c变量可赋值,可列举,可配置 //试着删除c变量 delete c;//true 表示c变量被成功删除 console.log(c);//c is not defined console.log(window.c);//undefined //使用var声明后再删除d变量 var d = 1; console.log(Object.getOwnPropertyDescriptor(window, 'd'));//Object {value: 1, writable: true, enumerable: true, configurable: false} ,此时d变量可赋值,可列举,但不可配置 delete d;//false 表示d变量删除失败 console.log(d);//1 console.log(window.d);//1
总结起来,有以下规则:
JS 中的作用域链
函数对象与其他对象一样,具有可以通过代码访问的属性和一组只能由引擎访问的内部属性。 内部属性之一是 [[Scope]],由 ECMA-262 标准第三版定义。 此内部属性包含创建函数的范围内的对象集合。 这个集合称为函数的作用域链,它决定了函数可以访问哪些数据。
我们先看一个栗子:
var e = "hello"; function f(){ e = "scope chain"; var g = = "good"; }
上述作用域链图如下所示:
当函数执行时,函数f内部会生成一个作用域链。 引擎的内部对象会被放入。外部e变量位于作用域链的第二级,index=1,内部g变量位于作用域链的顶层。 index=0,因此访问 g 变量总是比访问 e 变量快。
关闭
说到范围,我们就不得不谈一下闭包。 那么,什么是闭包呢?
“官方”的解释是:闭包是一个表达式(通常是一个函数),它有很多变量以及这些变量绑定到的环境,因此这些变量也是表达式的一部分。
这是什么意思? 简而言之:
在 ES6 之前,我们通常实现使用闭包的模块。 闭包依赖的结构有一个显着的特点,那就是:函数在词法作用域之外执行。 如下,f2是闭包的关键,它的词法作用Scope是函数f的内部私有作用域,它在f的作用域之外执行。
var h = 1; function f(){ var i = 2; return function f2(){ var j = 3 + i + h; console.log(j); } } var ff = f(); ff();//6
由于f2在定义时位于f内部,因此f2可以访问f的内部私有作用域。 这样,通过返回f2,就保证了i变量也可以在f函数之外被访问到。
当f2执行时,变量j位于作用域链的位置,变量i和变量h分别位于作用域链的位置。 因此,j的赋值过程实际上就是沿着作用域链的第二层和第三层找到i和h。 然后将h的值与3相加,最后赋值给j。
浏览器沿着作用域链搜索变量总是需要 CPU 时间。 作用域链的外层(或者离f2越远的变量),浏览器搜索的时间越长,因为需要遍历作用域链的次数越多。 。 所以全局变量()总是需要最多的访问时间。
封闭内的微观世界
如果我们想更深入的理解闭包以及函数f和嵌套函数f2的关系,还需要引入其他几个概念:函数执行环境()、活动对象(调用)、作用域(作用域)、作用域链(作用域)链)。 以函数a从定义到执行的过程为例来说明这些概念。
至此,整个函数f从定义到执行的步骤就完成了。 此时,f将函数f2的引用返回给ff,函数f2的作用域链中包含了函数f的活动对象的引用,这意味着f2可以访问f中定义的所有变量和函数。 函数f2被ff引用,而函数f2又依赖于函数f,所以函数f返回后不会被GC回收。
执行函数f2时,与上述步骤相同。 因此,f2执行时的作用域链包含3个对象:f2的活动对象、f的活动对象、对象,如下图所示:
如图所示,在函数f2中访问变量时,查找顺序为:
综上所述,这一段提到了两个重要的词:函数定义和执行。 文章提到,函数的作用域是在定义函数时确定的,而不是在执行函数时确定的(参见步骤 1 和 3)。 用一段代码来说明这个问题:
function f(x) { var g = function () { return x; } return g; } var h = f(1); alert(h());
在此代码中,变量 h 指向 f 中的匿名函数(由 g 返回)。
如果第一个假设为真,则输出值为; 如果第二个假设成立,则输出值为 1。
运行结果证明第二个假设是正确的,说明函数的作用域确实是在函数定义的时候就确定了。
闭包可能导致IE浏览器内存泄漏
我们先看一个栗子:
function f(){ var div = document.createElement("div"); div.onclick = function(){ return false; } }
上面div的点击事件是一个闭包。 由于这个闭包的存在,f函数内部的div变量对DOM元素的引用将一直存在。
在早期的IE浏览器中(IE9之前),js对象和DOM对象使用不同的垃圾收集方法。 DOM 对象使用计数垃圾收集机制。 只要匿名函数(比如事件)存在,DOM对象的引用至少为1,所以它所占用的内存永远不会被破坏。
有趣的是,不同的IE版本会导致不同的现象:
总结一下,闭包的优点是:共享函数作用域,可以方便地开放一些接口或者变量供外部使用;
注意:由于闭包可能会导致函数中的变量长期保存在内存中,从而消耗大量内存并影响页面性能,因此不能滥用,并且可能会导致 IE 浏览时出现内存泄漏。 解决办法是先退出函数,删除所有未使用的局部变量。
for循环问题分析
我们先来看看一开始的for循环问题。 添加匿名函数后,for循环内的变量将位于匿名函数的局部范围内。 此时访问属性或者访问i属性只需要在匿名函数的作用域内进行查找即可。 是的,因此查询效率大大提高(测试数据发现提高了200倍以上)。
使用匿名函数后,不仅作用域查询速度更快,而且作用域内的变量也与外界隔离,避免了像i这样的变量影响后续代码。 可谓一石二鸟。
步入范围陷阱
现在让我们来探讨一个经典的范围陷阱。
var div = document.getElementsByTagName("div"); for(var i=0,len=div.length;i上面代码的本意是在每次点击div时打印div的索引,但实际上打印的是len的值。 我们来分析一下原因。
单击 div 时,将执行 .log(i) 语句。 显然 i 变量不在单击事件的本地范围内。 浏览器会沿着作用域链寻找 i 变量,在哪里,也就是 for 循环开始的地方,这里定义了一个 i 变量,而 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变量,最终会在闭包中找到n变量。 n 变量,并打印出 div 的索引。
这个范围
前面我们学习了作用域链、闭包等基础知识。 现在我们来谈谈这个神秘的范围。
熟悉OOP的开发人员都知道,这是对对象实例的引用,并且始终指向对象实例。 然而在js的世界里,this随着它的执行环境的变化而变化,它始终指向它所在方法的对象。 如下,
function f(){ alert(this); } var o = {}; o.func = f; f();//[object Window] o.func();//[object Object] console.log(f===window.f);//true当f单独执行时,其内部this指向对象,但是当f成为o对象的属性func时,this指向o对象,而f === .f,所以它们实际上都指向this所在的方法位于。 目的。
下面我们来应用一下
Array.prototype.slice.call([1,2,3],1);//[2,3],正确用法 Array.prototype.slice([1,2,3],1);//[], 错误用法,此时slice内部this仍然指向Array.prototype var slice = Array.prototype.slice; slice([1,2,3],1);//Uncaught TypeError: Array.prototype.slice called on null or undefined //此时slice内部this指向的是window对象,离开了原来的Array.prototype对象作用域,故报错~~综上所述,使用时只需要注意一件事:
this 始终指向它所在方法的对象。
有声明
说到作用域链,就不得不说到with语句。 with 语句可用于临时更改作用域,并将语句中的对象添加到作用域的顶部。
语法:with (){}
例如:
var k = {name:"daicy"}; with(k){ console.log(name);//daicy } console.log(name);//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对象的方法。 数组l继承了这个方法,所以可以正确执行。
注意:with语句很容易引起歧义。 由于需要强制改变作用域链,因此会带来更多的CPU消耗。 建议谨慎使用with语句。
我将在这里分享闭包的用处。 希望以上内容能够对大家有所帮助,可以学到更多的知识。 如果您觉得文章不错,可以分享出去,让更多的人看到。