首先我们回顾下之前一篇关于介绍数组遍历的文章:
请先看上一篇中提到的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)</div>
我们再看下面一段代码, 测试环境为 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)</div>
计算一下: 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</div>
因此 js 中只有一种局部作用域, 即函数作用域.
使用 var 声明变量
通常我们知道, js 作为一种弱类型语言, 声明一个变量只需要var保留字, 如果在函数中不使用 var 声明变量, 该变量将提升为全局变量, 进而脱离函数作用域, 如下:
function f(){ b = 123; } f(); console.log(b);//123</div>
此时相对于前面使用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</div>
综上, 有如下规律:
- 不使用var保留字声明变量, 变量提升为全局变量, 而不论变量处于哪种作用域;
- 如果不使用var声明, 该变量便可配置, 即可被 delete 保留字删除, 删除后该变量便不可访问; 如果使用var声明, 该变量便不可配置, 即不能被 delete 保留字删除;
- 只要是全局变量都可以直接访问, 也可使用 “window.变量名” 来访问, 不管该变量是不是通过var来声明的;
JS中的作用域链
函数对象和其它对象一样,拥有可以通过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性。其中一个内部属性是[[Scope]],由ECMA-262标准第三版定义,该内部属性包含了函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链,它决定了哪些数据能被函数访问。
我们先看一个栗子:
var e = "hello"; function f(){ e = "scope chain"; var g = = "good"; }</div>
以上作用域链的图如下所示:
函数执行时, 在函数 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</div>
由于定义时 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 的时候, js解释器会将函数a的作用域链(scope chain)设置为定义 f 时 a 所在的”环境”, 如果 f 是一个全局函数,则scope chain中只有window对象。
- 当执行函数 f 的时候, f 会进入相应的执行环境(excution context).
- 在创建执行环境的过程中, 首先会为 f 添加一个scope属性, 即a的作用域, 其值就为第1步中的scope chain. 即a.scope=f 的作用域链.
- 然后执行环境会创建一个活动对象(call object). 活动对象也是一个拥有属性的对象, 但它不具有原型而且不能通过JavaScript代码直接访问. 创建完活动对象后, 把活动对象添加到 f 的作用域链的最顶端. 此时a的作用域链包含了两个对象: f 的活动对象和window对象.
- 下一步是在活动对象上添加一个arguments属性, 它保存着调用函数 f 时所传递的参数.
- 最后把所有函数 f 的形参和内部的函数 f2 的引用也添加到 f 的活动对象上. 在这一步中, 完成了函数 f2 的定义, 因此如同第3步, 函数 f2 的作用域链被设置为 f2 所被定义的环境, 即 f 的作用域.
到此, 整个函数 f 从定义到执行的步骤就完成了. 此时 f 返回函数 f2 的引用给 ff, 又函数 f2 的作用域链包含了对函数 f 的活动对象的引用, 也就是说 f2 可以访问到 f 中定义的所有变量和函数. 函数 f2 被 ff 引用, 函数 f2又依赖函数 f , 因此函数 f 在返回后不会被GC回收.
当函数 f2 执行的时候亦会像以上步骤一样. 因此, 执行时 f2 的作用域链包含了3个对象: f2 的活动对象、f 的活动对象和window对象, 如下图所示: