时间:2020-10-26 23:23:14 | 栏目:JavaScript代码 | 点击:次
首先我们回顾下之前一篇关于介绍数组遍历的文章:
请先看上一篇中提到的for循环代码:
var array = []; array.length = 10000000;//(一千万) for(var i=0,length=array.length;i<length;i++){ array[i] = 'hi'; } var t1 = +new Date(); for(var i=0,length=array.length;i<length;i++){ } var t2 = +new Date(); console.log(t2-t1); //以下是连续5次的运行时间 //168+158+170+159+165 = 820(ms)
我们再看下面一段代码, 测试环境为 chrome 52.0.2743.116 (64-bit):
var t1 = +new Date(); (function(){//闭包 for(var i=0,length=array.length;i<length;i++){ //array.push(i); } })(); var t2 = +new Date(); console.log(t2-t1); //以下是连续5次的运行时间: //8+6+8+7+6 = 35(ms)
计算一下: 820/35 = 23 效率提升大致20倍. 实际上, 在 Firefox 及 Safari 对 for有做底层优化的情况下, 仍然有4~6倍的性能提升. 这是为什么呢?
我们注意到两段代码最大的区别就是, 第二段代码使用了匿名函数包裹for循环. 我们将在后面讲到, 请耐心阅读.
作用域
所谓作用域, 指的是, 变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的.
js中只有函数作用域
众所周知, JS中并没有块作用域, 只有函数作用域. 如下:
for(var i=0;i<10;i++){ ; } console.log(i);//10 function f(){ var a = 123; } f(); console.log(a);//a is not defined
因此 js 中只有一种局部作用域, 即函数作用域.
使用 var 声明变量
通常我们知道, 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中的作用域链
函数对象和其它对象一样,拥有可以通过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性。其中一个内部属性是[[Scope]],由ECMA-262标准第三版定义,该内部属性包含了函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链,它决定了哪些数据能被函数访问。
我们先看一个栗子:
var e = "hello"; function f(){ e = "scope chain"; var g = = "good"; }
以上作用域链的图如下所示:
函数执行时, 在函数 f 内部会生成一个 active object 和 scope chain. JavaScript引擎内部对象会放入 active object中, 外部的 e 变量处于scope chain的第二层, index=1, 而内部的g变量处于scope chain的顶层, index=0, 因此访问g变量总比访问e变量来的快些.
闭包
聊到作用域, 就不得不说闭包, 那么, 什么是闭包?
“官方”的解释是:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
这是什么意思呢, 简单来说就是:
ES6之前, 通常我们实现的模块就是利用了闭包. 闭包依赖的结构有个鲜明的特点, 即: 一个函数在词法作用域之外执行. 如下, f2是闭包的关键, 它的词法作用域便是函数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 就能保证在 f 函数外部也能访问到 i 变量.
当f2执行时, 变量 j 处于scope chain的 index0的位置上, 变量 i 和变量 h 分别处于 scope chain 的 index1 index2 的位置上. 因此 j 的赋值过程其实就是沿着 scope chain 第二层 第三层 依次找到 i 和 h 的值, 然后将它们和3一起求和, 最终赋值给 j .
浏览器沿着 scope chain 寻找变量总是需要耗费CPU时间, 越是 scope chain 的 外层(或者离f2越远的变量), 浏览器查找起来越是需要时间, 因为 scope chain 需要历经更多次遍历. 因此全局变量(window)总是需要最多的访问时间.
闭包内的微观世界
如果要更加深入的了解闭包以及函数 f 和嵌套函数 f2 的关系,我们需要引入另外几个概念:函数的执行环境(excution context)、活动对象(call object)、作用域(scope)、作用域链(scope chain)。以函数a从定义到执行的过程为例阐述这几个概念。
到此, 整个函数 f 从定义到执行的步骤就完成了. 此时 f 返回函数 f2 的引用给 ff, 又函数 f2 的作用域链包含了对函数 f 的活动对象的引用, 也就是说 f2 可以访问到 f 中定义的所有变量和函数. 函数 f2 被 ff 引用, 函数 f2又依赖函数 f , 因此函数 f 在返回后不会被GC回收.
当函数 f2 执行的时候亦会像以上步骤一样. 因此, 执行时 f2 的作用域链包含了3个对象: f2 的活动对象、f 的活动对象和window对象, 如下图所示:
如图所示, 当在函数 f2 中访问一个变量的时候, 搜索顺序是:
小结, 本段中提到了两个重要的词语: 函数的定义与执行. 文中提到函数的作用域是在定义函数时候就已经确定, 而不是在执行的时候确定(参看步骤1和3).用一段代码来说明这个问题:
function f(x) { var g = function () { return x; } return g; } var h = f(1); alert(h());
这段代码中变量h指向了f中的那个匿名函数(由g返回).
alert(h())
确定的, 那么此时h的作用域链是: h的活动对象->alert的活动对象->window对象.如果第一种假设成立, 那输出值就是undefined; 如果第二种假设成立, 输出值则为1。
运行结果证明了第2个假设是正确的,说明函数的作用域确实是在定义这个函数的时候就已经确定了.
闭包有可能导致IE浏览器内存泄漏
先看一个栗子:
function f(){ var div = document.createElement("div"); div.onclick = function(){ return false; } }
上述div的click事件就是一个闭包, 由于该闭包的存在使得 f 函数内部的 div 变量对DOM元素的引用将一直存在.
而早期IE浏览器( IE9之前 ) js 对象和 DOM 对象使用不同的垃圾收集方法, DOM对象使用计数垃圾回收机制, 只要匿名函数( 比如说onclick事件 )存在, DOM对象的引用便至少为1,因此它所占用的内存就永远不会被销毁.
有趣的是,不同的IE版本将导致不同的现象:
总结一下, 闭包的优点: 共享函数作用域, 便于开放一些接口或变量供外部使用;
注意事项: 由于闭包可能会使得函数中变量被长期保存在内存中, 从而大量消耗内存, 影响页面性能, 因此不能滥用, 并且在IE浏览中可能导致内存泄露. 解决方法是,在退出函数之前,将不使用的局部变量全部删除.
for循环问题分析
我们再来看看开篇的for循环问题, 增加匿名函数后, for循环内部的变量便处于匿名函数的局部作用域下, 此时访问 length 属性, 或者访问 i 属性, 都只需要在匿名函数作用域内查找即可, 因此查询效率大大提升(测试数据发现提升有两百多倍).
使用匿名函数后, 不止是作用域查询更快, 作用域内的变量还与外部隔离, 避免了像 i , length 这样的变量对后续代码产生影响. 可谓一举两得.
踩个作用域的坑
下面我们来踩一个作用域经典的坑.
var div = document.getElementsByTagName("div"); for(var i=0,len=div.length;i<len;i++){ div[i].onclick = function(){ console.log(i); } }
上述代码的本意是每次点击div, 打印div的索引, 实际上打印的却是 len 的值. 我们来分析下原因.
点击div时, 将会执行 console.log(i)
语句, 显然 i 变量不在 click 事件的局部作用域内, 浏览器将沿着 scope chain 寻找 i 变量, 在 index1
的地方, 即 for循环开始的地方, 此处定义了一个 i 变量, 又 js 没有块作用域, 故 i 变量并不会在 for循环块执行完成后被销毁,又 i的最后一次自加使得 i = len
, 于是浏览器在scope chain index=1
索引的地方停下来了, 返回了i的值, 即len的值.
为了解决这个问题, 我们将根据症结, 对症下药, 从作用域入手, 改变click事件的局部作用域, 如下:
var div = document.getElementsByTagName("div"); for(var i=0,len=div.length;i<len;i++){ (function(n){ div[n].onclick = function(){ console.log(n); } })(i); }
由于 click 事件被闭包包裹, 并且闭包自执行, 因此闭包内 n 变量的值每次都不一样, 点击div时, 浏览器将沿着 scope chain 寻找 n 变量, 最终会找到闭包内的 n 变量, 并且打印出div 的索引.
this作用域
前面我们学习了作用域链, 闭包等基础知识, 下面我们来聊聊神秘莫测的this作用域.
熟悉OOP的开发人员都知道, this是对象实例的引用, 始终指向对象实例. 然而 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指向window对象, 但是当f成为o对象的属性func时, this指向的是o对象, 又f === window.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的使用只需要注意一点:
this 总是指向它所在方法的对象.
with语句
聊到作用域链就不得不说with语句了, with语句可以用来临时改变作用域, 将语句中的对象添加到作用域的顶部.
语法: with (expression){statement}
例如:
var k = {name:"daicy"}; with(k){ console.log(name);//daicy } console.log(name);//undefined
with 语句用于对象 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 语句.
总结