在JavaScript中,为什么要尽可能使用局部变量?


Posted in Javascript onApril 06, 2009

不这么做,对性能到底能带来多大的损失?本文就来探讨这些问题的答案,从根本上了解变量的读写性能都和哪些因素有关。

著作权声明

本文译自Nicholas C. Zakas于2009年2月10日在个人网站上发表的《JavaScript Variable Performance》。原文是唯一的正式版,本文是经过原作者(Nicholas C. Zakas)授权的简体中文翻译版(Simplified Chinese Translation)。译者(明达)在翻译的准确性上做了大量的努力,并承诺译文的内容完全忠于原文,但可能还是包含疏漏和不妥之处,欢迎大家指正。译注的内容是非正式的,仅代表译者个人观点。

以下是对原文的翻译:

在如何提高JavaScript性能这个问题上,大家最常听到的建议应该就是尽量使用局部变量(local variables)来代替全局变量(global variables)。在我从事Web开发工作的九年时间里,这条建议始终萦绕在我的耳边,并且从来没有质疑过,而这条建议的基础,则来自于 JavaScript处理作用域(scoping)和标识符解析(identifier resolution)的方法。

首先我们要明确,函数在JavaScript中具体表现为对象,创建一个函数的过程,其实也就是创建一个对象的过程。每个函数对象都有一个叫做 [[Scope]]的内部属性,这个内部属性包含创建函数时的作用域信息。实际上,[[Scope]]属性对应的是一个对象(Variable Objects)列表,列表中的对象是可以从函数内部访问的。比如说我们建立一个全局函数A,那么A的[[Scope]]内部属性中只包含一个全局对象(Global Object),而如果我们在A中创建一个新的函数B,那么B的[[Scope]]属性中就包含两个对象,函数A的Activation Object对象在前面,全局对象(Global Object)排在后面。

当一个函数被执行的时候,会自动创建一个可以执行的对象(Execution Object),并同时绑定一个作用域链(Scope Chain)。作用域链会通过下面两个步骤来建立,用于进行标识符解析。

1. 首先将函数对象[[Scope]]内部属性中的对象,按顺序复制到作用域链中。
2. 其次,在函数执行时,会创建一个新的Activation Object对象,这个对象中包含了this、参数(arguments)、局部变量(包括命名的参数)的定义,这个Activation Object对象会被置于作用域链的最前面。

在执行JavaScript代码的过程中,当遇到一个标识符,就会根据标识符的名称,在执行上下文(Execution Context)的作用域链中进行搜索。从作用域链的第一个对象(该函数的Activation Object对象)开始,如果没有找到,就搜索作用域链中的下一个对象,如此往复,直到找到了标识符的定义。如果在搜索完作用域中的最后一个对象,也就是全局对象(Global Object)以后也没有找到,则会抛出一个错误,提示用户该变量未定义(undefined)。这是在ECMA-262标准中描述的函数执行模型和标识符解析(Identifier Resolution)的过程,事实证明,大部分的JavaScript引擎确实也是这样实现的。需要注意的是,ECMA-262并没有强制要求采用这种结构,只是对这部分功能加以描述而已。

了解标识符解析(Identifier Resolution)的过程以后,我们就能明白为什么局部变量的解析速度要比其他作用域的变量快,主要是由于搜索过程被大幅缩短了。但是,具体会快多少呢?为了回答这个问题,我模拟了一系列的测试,来测试不同作用域深度中变量的性能。

第一个测试是向一个变量中写入一个最简单的值(这里使用字面量的数值1),结果如下图显示,很有趣:

在JavaScript中,为什么要尽可能使用局部变量?

从结果中不难看出,当标识符解析的过程需要进行深度搜索时,会伴随性能损失,而且性能损失的程度会随着标识符深度的增加而递增。意料之中的是,Internet Explorer表现的是最差的(但公平的说,IE 8还是有一些改善的)。值得注意的是,这里有一些例外情况,Google Chrome和最新的WebKit午夜版在访问变量的时间保持得很稳定,不会随着作用域深度的递增而增长。当然,这应该归功于它们所使用的下一代 JavaScript引擎,V8和SquirrelFish。这些引擎在执行代码时进行了优化,而且很明显,这些优化使访问变量的速度比以往更快。 Opera表现的也很不错,比IE、Firefox和当前版本的Safari要快的多,但比基于V8和Squirrelfish的浏览器要慢。 Firefox 3.1 Beta 2的表现有点出人意料,对于局部变量执行的效率非常高,但随着作用域层数的增加,效率便大打折扣。需要注意的是,我这里使用的都是默认设置,也就是说 Firefox是没有开启Trace功能的。

上面的结果是通过对变量执行写操作而得出的,其实我很好奇,读取变量时的情况会不会有什么不同,于是接着做了下面的测试。结果发现,读的速度要比写的速度快一些,但是性能变化的趋势是一致的。

在JavaScript中,为什么要尽可能使用局部变量?

和上个测试一样,Internet Explorer和Firefox还是最慢的,Opera表现了非常抢眼的性能,而同样的,Chrome和最新版本的Webkit午夜版显示了和作用域深度无关的性能趋势,同样需要注意的是,Firefox 3.1 Beta 2的变量访问时间还是会伴随着深度出现一个奇怪的跳跃。

在测试的过程中,我发现一个有趣的现象,就是Chrome在访问全局变量的时候会有额外的性能损失。访问全局变量的时间和作用域层数没有关系,但是会比访问同样层数的局部变量的时间多出50%。

这两个测试可以给我们带来什么启示呢?首先是验证了那个古老的观点,就是要尽可能的使用局部变量。在所有的浏览器下,访问局部变量都比访问跨作用域的变量要快,当然也包括全局变量。下面这几点应该是通过这个测试得出的经验吧:

* 仔细检查函数中所有使用的变量,如果有一个变量不是当前作用域定义的,而且使用了不止一次,那么我们就应该把这个变量保存在局部变量中,而使用这个局部变量来进行读写操作。这样可以帮助我们将作用域外的变量的搜索深度减少到1.这对全局变量尤为重要,因为全局变量总是被放到作用域链的最后位置来搜索。
* 避免使用with语句。因为它会修改执行上下文(Execution Context)的作用域链,在最前面添加一个对象(Variable Object)。这就意味着在执行with的过程中,实际上的局部变量都被移到作用域链上的第二个位置,这会带来性能上的损失。
* 如果你确定一段代码肯定会抛出异常,那么就要避免使用try-catch,因为catch分支在作用域链上的处理方法和with是一样的。但try分支的代码是没有性能损失的,所以还是建议用try-catch来捕获那些不可预知的错误。

如果你想围绕这个话题展开更多的讨论,我在上个月的Mountain View JavaScript Meetup中曾经发表了一个小演讲。可以在SlideShare上下载幻灯片,或者观看聚会的完整视频,我的演讲大概从11分钟左右时开始。

译者笔记

大家如果在阅读本文的过程中,有什么疑惑,建议延伸阅读以下两篇文章:
* Richie写的《JavaScript对象模型-执行模型
* 《ECMA-262第三版》,主要看看第十章,就是执行上下文(Execution Context)那张,本文提到的名词在那里都有详细的解释。

在最后的时候,Nicholas提到一个Mountain View JavaScript Meetup,Meetup那个网站其实就是一个各种现实世界活动的组织网站,需要翻墙才能访问,住在California真幸福,有那么多的好活动可以参加,呵呵。

Javascript 相关文章推荐
JavaScript 程序编码规范
Nov 23 Javascript
在jQuery1.5中使用deferred对象 着放大镜看Promise
Mar 12 Javascript
jquery多选项卡效果实例代码(附效果图)
Mar 23 Javascript
javascript实现简单的分页特效
Aug 12 Javascript
基于jQuery实现选项卡效果
Jan 04 Javascript
JavaScript 总结几个提高性能知识点(推荐)
Feb 20 Javascript
在vue-cli脚手架中配置一个vue-router前端路由
Jul 03 Javascript
微信小程序canvas拖拽、截图组件功能
Sep 04 Javascript
Vue 幸运大转盘实现思路详解
May 06 Javascript
使用uni-app开发微信小程序的实现
Dec 13 Javascript
原生JS实现九宫格抽奖
Sep 13 Javascript
JavaScript继承的三种方法实例
May 12 Javascript
JavaScript 在各个浏览器中执行的耐性
Apr 06 #Javascript
javascript 获取图片颜色
Apr 05 #Javascript
Mozilla 表达式 __noSuchMethod__
Apr 05 #Javascript
关于javascript document.createDocumentFragment()
Apr 04 #Javascript
HTML 自动伸缩的表格Table js实现
Apr 01 #Javascript
Javascript 原型和继承(Prototypes and Inheritance)
Apr 01 #Javascript
说说掌握JavaScript语言的思想前提想学习js的朋友可以看看
Apr 01 #Javascript
You might like
神族 Protoss 历史背景
2020/03/14 星际争霸
PHP防注入安全代码
2008/04/09 PHP
WordPress中的shortcode短代码功能使用详解
2016/05/17 PHP
Thinkphp 框架扩展之标签库驱动原理与用法分析
2020/04/23 PHP
初识javascript 文档碎片
2010/07/13 Javascript
嵌入式iframe子页面与父页面js通信的方法
2015/01/20 Javascript
Bootstrap编写导航栏和登陆框
2016/05/30 Javascript
jquery——九宫格大转盘抽奖实例
2017/01/16 Javascript
微信小程序的分类页面制作
2017/06/27 Javascript
用node和express连接mysql实现登录注册的实现代码
2017/07/05 Javascript
AngularJs用户登录问题处理(交互及验证、阻止FQ处理)
2017/10/26 Javascript
JS高阶函数原理与用法实例分析
2019/01/15 Javascript
微信小程序实现富文本图片宽度自适应的方法
2019/01/20 Javascript
ES6基础之字符串和函数的拓展详解
2019/08/22 Javascript
微信小程序以7天为周期连续签到7天功能效果的示例代码
2020/08/20 Javascript
解决vue组件没显示,没起作用,没报错,但该显示的组件没显示问题
2020/09/02 Javascript
python中Pycharm 输出中文或打印中文乱码现象的解决办法
2017/06/16 Python
python程序 线程队列queue使用方法解析
2019/09/23 Python
JupyterNotebook 输出窗口的显示效果调整方法
2020/04/13 Python
美国领先的汽车轮胎和轮毂供应商:TireBuyer
2016/07/21 全球购物
Trina Turk官网:美国时装和泳装品牌
2018/06/10 全球购物
GafasWorld西班牙:购买太阳镜、眼镜和隐形眼镜
2019/09/08 全球购物
英国婴儿产品专家:Samuel Johnston
2020/04/20 全球购物
Java多态性的定义以及类型
2014/09/16 面试题
C语言开发工程师测试题
2016/12/20 面试题
索引覆盖(Index Covering)查询含义
2012/02/18 面试题
销售文员的岗位职责
2013/11/20 职场文书
高一新生军训感言
2014/03/02 职场文书
大学生就业求职信
2014/06/12 职场文书
多媒体编辑专业毕业生求职信
2014/06/13 职场文书
人事经理岗位职责范本
2014/08/04 职场文书
质检员工作总结2015
2015/04/25 职场文书
苦儿流浪记读书笔记
2015/07/01 职场文书
结婚纪念日感言
2015/08/01 职场文书
小学思品教学反思
2016/02/20 职场文书
《极主夫道》真人电影正式预告 定档6月3日上映
2022/04/05 日漫