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

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

著作权声明

本文译自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),结果如下图显示,很有趣:

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

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

和上个测试一样,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真幸福,有那么多的好活动可以参加,呵呵。

(0)

相关推荐

  • 巧用局部变量提升javascript性能

    javascript中一个标识符所在的位置越深,它的读写速度也越慢.因此,函数中读写局部变量总是最快的,而读写全局变量通常是最慢的.一个好的经验法则是:如果某个跨作用域的值在函数中被引用一次以上,那么就把它存储到局部变量里. 例如: 复制代码 代码如下: <!-- 优化前 --> <script type="text/javascript"> function initUI () { var bd = document.body, links = documen

  • 基于JavaScript 声明全局变量的三种方式详解

    JS中声明全局变量主要分为显式声明或者隐式声明下面分别介绍. 声明方式一: 使用var(关键字)+变量名(标识符)的方式在function外部声明,即为全局变量,否则在function声明的是局部变量.该方式即为显式声明详细如下: <script> var test = 5;//全局变量 function a(){ var a = 3;//局部变量 alert(a); } function b(){ alert(test); } //a();//调用a方法,那么方法里面的内容才会执行 //b(

  • javascript中局部变量和全局变量的区别详解

    javascript有两种变量:局部变量和全局变量.当然,我们这篇文章是帮助大家真正的区别这两种变量. 首先,局部变量是指只能在本变量声明的函数内部调用.全局变量时整个代码中都可以调用的变量.当然,单单从字面上理解肯定是不清楚的,下面我详细的介绍下: 大家都知道,变量是需要用var关键字声明的.但是javascript中也可以隐式的使用变量,就是不用声明,直接使用.而且,千万注意,javascript把隐式声明的变量总是当成全局变量来使用的. 例如: 复制代码 代码如下: function my

  • JavaScript中全局变量、函数内变量以及常量表达式的效率测试

    var r, s = "this is a very very looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong string", x = /^\s+|\s+$/g; document.writeln("字符串测试 "); (function(){ document.write("全局->全局:"); var t = new Date(); for(

  • javascript(jquery)利用函数修改全局变量的代码

    我是将当前页用一个全局变量存储.同时在"最后一页"链接里单击方法调用一个函数自动去修改页数为最后一页,然后再调用载入评论的方法. 可是我发现点击"最后一页"第一次的时候系统没反应,再点击一次就抓去最后一页的资料了! 要说全局变量没改,应该是改了,就仿佛改了页数而载入评论的方法失效了一样. 代码如下: 复制代码 代码如下: var page = 1; //初始化页数为第一页 var str = ""; $(document).ready(funct

  • javascript全局变量封装模块实现代码

    下面的代码是我的测试代码,注释很重要: 复制代码 代码如下: /*global window,jQuery,validate_email,masterUI,$,rest*/ /** Enable ECMAScript "strict" operation for this function. See more: * http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/ * http://stackoverflow.c

  • javascript 用局部变量来代替全局变量第1/2页

    为什么要这么做?有什么根据么?不这么做,对性能到底能带来多大的损失?本文就来探讨这些问题的答案,从根本上了解变量的读写性能都和哪些因素有关. 著作权声明        本文译自 Nicholas C. Zakas 于2009年2月10日在个人网站上发表的<JavaScript Variable Performance>.原文是唯一的正式版,本文是经过原作者(Nicholas C. Zakas)授权的简体中文翻译版(Simplified Chinese Translation).译者(明达)在翻

  • Javascript全局变量var与不var的区别深入解析

    相信你对全局变量一定不陌生,在函数作用域里用a=1这种形式定义的变量会是一个全局变量,在全局作用域里,用下面3种形式都可以创建对全局可见的命名: 复制代码 代码如下: <script>var a = 1;b = 2;window.c = 3;</script> 对于b=2这种方式, 它其实和c是一样的,在执行这个赋值语句的时候,会沿着作用域链寻找名字叫做b的变量,一直找到作用域链的顶端还没有找到,于是给window添加一个属性b然后赋值. var与不var有两个区别: 1 var的

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

    不这么做,对性能到底能带来多大的损失?本文就来探讨这些问题的答案,从根本上了解变量的读写性能都和哪些因素有关. 著作权声明 本文译自Nicholas C. Zakas于2009年2月10日在个人网站上发表的<JavaScript Variable Performance>.原文是唯一的正式版,本文是经过原作者(Nicholas C. Zakas)授权的简体中文翻译版(Simplified Chinese Translation).译者(明达)在翻译的准确性上做了大量的努力,并承诺译文的内容完全

  • 深入浅析JavaScript中with语句的理解

    JavaScript 有个 with 关键字, with 语句的原本用意是为逐级的对象访问提供命名空间式的速写方式. 也就是在指定的代码区域, 直接通过节点名称调用对象. with语句的作用是暂时改变作用域链.减少的重复输入. 其语法结构为: with(object){ //statements } 举一个实际例子吧: with(document.forms[]){ name.value = "lee king"; address.value = "Peking";

  • JavaScript中的垃圾回收与内存泄漏示例详解

    前言 程序的运行需要内存.只要程序提出要求,操作系统或者运行时就必须供给内存.所谓的内存泄漏简单来说是不再用到的内存,没有及时释放.为了更好避免内存泄漏,我们先介绍Javascript垃圾回收机制. 在C与C++等语言中,开发人员可以直接控制内存的申请和回收.但是在Java.C#.JavaScript语言中,变量的内存空间的申请和释放都由程序自己处理,开发人员不需要关心.也就是说Javascript具有自动垃圾回收机制(Garbage Collecation). 一.垃圾回收的必要性 下面这段话

  • 一文搞懂JavaScript中的内存泄露

    目录 什么是内存泄漏 怎么检测内存泄漏 Performance Memory 内存泄漏的场景 垃圾回收算法 引用计数 循环引用 标记清除 闭包是内存泄漏吗 总结 以前我们说的内存泄漏,通常发生在后端,但是不代表前端就不会有内存泄漏.特别是当前端项目变得越来越复杂后,前端也逐渐称为内存泄漏的高发区.本文就带你认识一下Javascript的内存泄漏. 什么是内存泄漏 什么是内存?内存其实就是程序在运行时,系统为其分配的一块存储空间.每一块内存都有对应的生命周期: 内存分配:在声明变量.函数时,系统分

  • Javascript中正则表达式的使用及基本语法

    前面的话 正则表达式在人们的印象中可能是一堆无法理解的字符,但就是这些符号却实现了字符串的高效操作.通常的情况是,问题本身并不复杂,但没有正则表达式就成了大问题.javascript中的正则表达式作为相当重要的知识,本文将介绍正则表达式的基础语法 定义 正则表达式(Regular Expression)是一门简单语言的语法规范,是强大.便捷.高效的文本处理工具,它应用在一些方法中,对字符串中的信息实现查找.替换和提取操作 javascript中的正则表达式用RegExp对象表示,有两种写法:一种

  • Javascript 中的类和闭包

    有人说javascript也是面向对象的,只是它是prototype based,当然这只是概念上的区别,我不想讨论js是不是面向对象的,关键是想说明虽然javascript的类表现得很像其他语言中的类,但是内部的实现机理确不太一致,如果一味的把javascript中的类类比作其他语言中的类,有时候脑子会犯混. 先看一段简单的代码,一般教材上介绍如何新建一个类的时候都是这样的(当然还有更复杂的方法,不过本质上是一样的): 复制代码 代码如下: function MyClass(x) { this

  • javascript中错误使用var造成undefined

    在javascript中根据变量作用的范围不同分为局部变量和全局变量,直接定义的变量是全局变量,全局变量可以被所有的脚本访问:在函数中定义的变量是局部变量,局部变量只在函数内有效. 如果全局变量和局部变量使用相同的变量名,则局部变量将会覆盖全局变量. 例子代码: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>js中全局变量与局部变量</title&

  • JavaScript中的DSL元编程介绍

    在看JavaScript Template源码的时候,发现有一个很有意思的用法用来生成函数,想到这不就是元编程么? JavaScript 元编程 复制代码 代码如下: 元编程(Metaprogramming)是指某类计算机程序的编写,这类计算机程序编写或者操纵其他程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作. JavaScript eval 复制代码 代码如下: eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码. 可以有下面的用法: 复

  • javascript中闭包概念与用法深入理解

    本文实例分析了javascript中闭包概念与用法.分享给大家供大家参考,具体如下: 1.问题的引出,什么时候会遇到闭包? 首先因为JS是没有块状作用域的,但是有函数作用域即函数作为了局部变量之间的界限,不同函数内的局部变量具有独立性, 因为JS没有块状作用域,笔者初学JS时,在事件的监听时,因为不理解JS中局部变量的作用域,犯过不少错误! (1)JS中的变量作用域 for(var i=0;i<9;i++) { } alert(i) //输出9 我们发现,虽然变量i是块状区域for()内的一个局

随机推荐