前言:
数据的存储的位置会影响数据检索,读取的速度。书中选择了两个最为常见的的用法深入探究:函数和对象。本文主要记录的是函数相关研究。
结论:
1. 函数尽量使用局部变量,少访问全局变量;2. 不要使用with语句;3. 谨慎使用闭包。
正文:
JavaScript函数中和数据直接联系的就是变量,而变量和作用域息息相关。于是乎如何快速的从作用域中读取变量,便是我们研究的关键。
1. 作用域
首先我们需要正确理解作用域的工作原理。每一个JavaScript函数是Funtion对象的一个实例,作用域Scope就是Funtion对象原型的一个内部属性。而作用域Scope由包含了一个当前函数会用到的各种变量对象的集合,并且按照顺序排列。这个集合被称为作用域链。作用域链是链表结构的。
当创建一个函数时,会在该函数的作用域链中插入一个全局对象,代表所有在全局范围内定义的变量,如图1所示。
图1 作用域链(创建函数)
当执行一个函数时,会为该函数创建一个”执行环境(执行上下文)”的内部对象。每次调用函数时都会创建一个新的执行环境,当函数执行完毕,执行环境就被销毁。
当执行环境被创建时,同时会创建一个其专属的作用域链。先继承创建函数时的作用域链,然后将当前函数中的值,按照它们出现顺序复制到一个新对象中,这个对象称为活动对象。这个活动对象包含了该函数的所有局部变量,命名参数,参数集合以及this,然后这个活动对象推入作用域链的最前端,如图2所示。当函数执行完毕,执行环境被销毁,活动对象也随之销毁。
图2 作用域链(执行函数)
2. 搜索变量
了解了作用域的工作原理后,我们便要开始研究变量。在执行函数过程中,每遇到一个变量,都会按顺序搜索执行环境的作用域链,查找同名的标识符。首先会在活动对象中查找,没找到则继续搜索作用域链的下一个对象,直到找到标识符。最终没找到则视为该标识符未定义。
显而易见,变量存储在作用域链越前端则搜索速度越快。而且需要知道的是,全局变量总是存在于作用域链的最末端。因此,尽量使用局部变量,能有效提高性能。
3. 不要使用with语句的一个原因
with语句用来给对象的所有属性新建一个存储对象,这个对象包含了参数指定的对象的所有属性,然后这个对象会被推入作用域链的首位,如图3所示,随之带来就是访问局部变量的代价变高了,因此这种方式是不可取的。
图3 作用域链(with语句)
4. 慎用闭包
闭包是JavaScript最强大的特性之一,但是使用闭包会影响性能。
function setA () {
return setB(); //这里setB就是一个闭包。
}
setA执行时,会产生图2的作用域链。与此同时,闭包setB被创建,它的初始作用域链的引用和setA的作用域链的引用是完全一致的,如图4所示。闭包被调用时,还会推入自己的活动对象。
图4 作用域链(带有闭包)
但是,由于闭包和setA有相同的作用域对象的引用,当setA销毁时,其作用域链无法销毁,因为闭包还在使用。所以使用闭包时需要更多的内存开销,需要谨慎使用。
拓展:
1. 虽然对象是哈希表结构,但是作用域链却是实实在在的链表结构。
作用域在ES6被称为词法环境,词法环境在ES6 Spec中有这么一段解释:词法环境由一个 环境记录表 和一个指向外部词法环境的引用(可能为null)构成。这表明词法环境(作用域)是一个链表结构,而作用域链初始时便是函数创建时的词法环境。
由三段论即可证明作用域链是链表结构的。作用域链的查询越深,代价越大正是查询链表的特点之一。
2. 全局执行环境是最外围的一个执行环境,即window对象。所整个代码都是在各个执行环境里进行的,执行过程由一个环境栈控制,进入一个函数时,函数的执行环境就会入栈,执行结束则栈会将其执行环境弹出。
3. 作用域链中有多少个对象取决于执行环境的深度,本文举的只是最简单的例子。但是,全局对象在每个作用域链中都处于最末端(参考拓展1)这是不容置喙的。
4. try-catch语句的catch语句也可以改变作用域链,也是在作用域链的最前面加入一个新的对象,其包含的是被抛出的错误对象的声明,图3的with对象换成错误对象即可。
结语:
如有问题之处,还请指正,欢迎大家多多交流!
哦?牛逼牛逼