转自:http://www.cnblogs.com/mrsunny/archive/2011/11/03/2233978.html

这可能是每一个jser都曾经为之头疼的却又非常经典的问题,关系到内存,关系到闭包,关系到javascript运行机制。关系到功能,关系到性能。

文章内容主要参考自《High Performance JavaScript》,这本书对javascript性能方面确实讲的比较深入,大家有空都可以尝试着阅读一下,下载地址:中英电子版

复习,笔记,更深入的理解。

欢迎拍砖指正。

作用域:

下面我们先搞明白这样几个概念:

  • 函数对象的[[scope]]属性、ScopeChain(作用域链)
  • Execution Context(运行期上下文)、Activation Object(激活对象)

[[scope]]属性:

javascript中每个函数都是一个函数对象(函数实例),既然是对象,就有相关的属性和方法。[[scope]]就是每个函数对象都具有的一个仅供javascript引擎内部使用的属性,该属性是一个集合(类似于链表结构),集合中保存了该函数在被创建时的作用域中的所有对象,而这个作用域集合形成的链表则被称为ScopeChain(作用域链)。

该作用域链中保存的作用域对象,就是该函数可以访问的所有数据。例如(例子引用自《High Performance JavaScript高性能javascript》):

function add(num1, num2){
    var sum = num1 + num2;
    return sum;
}
图 1

当add函数被创建时,函数所在的全局作用域的全局对象被放置到add函数的作用域链([[scope]]属性)中。我们可以从图1中看到作用域链的第一个对象保存的是全局对象,全局对象中保存了诸如this,window,document以及全局对象中的add函数,也就是他自己。这也就是我们可以在全局作用域下的函数中访问window(this),访问全局变量,访问函数自身的原因。当然还有函数作用域不是全局的情况,等会儿我们再讨论。

Execution Context(运行期上下文)、Activation Object(激活对象):

(前天看了老罗的演讲,老罗说过年的时候给全公司的人每人发一台电冰箱,要给校舍的所有的厕所门上都安上新锁,保证童鞋们能有个真正隐私的地方。)

var total = add(5, 10);

当开始执行此函数时,就会创建一个Execution Context的内部对象,该对象定义了函数运行时的作用域环境(注意这里要和函数创建时的作用域链对象[[scope]]区分,这是两个不同的作用域链对象,这样分开我猜测一是为了保护[[scope]],二是为了方便根据不同的运行时环境控制作用域链。函数每执行一次,都会创建单独的Execution Context,也就相当于每次执行函数前,都把函数的作用域链复制了一份到当前的Execution Context中)。Execution Context对象有自己的作用域链,在Execution Context创建时初始化,会将函数创建时的作用域链对象[[scope]]中的全部内容按照在[[scope]]作用域链中的顺序复制到Execution Context的作用域链中。

此时,在Execution Context的作用域链的顶部会插入一个新的对象,叫做Activation Object(激活对象),这个激活对象又是干嘛的呢?这个激活对象保存了函数中的所有形参,实参,局部变量,this指针等函数执行时函数内部的数据情况,这个Activation Object是一个可变对象,里面的数据随着函数执行时的数据的变化而变化,当函数执行结束之后,就会销毁Execution Context,也就会销毁Execution Context的作用域链,当然也就会销毁Activation Object(但如果存在闭包,Activation Object就会以另外一种方式存在,这也是闭包产生的真正原因,具体的我们稍后讨论。)。具体情况如图所示:

图 2

我们从左往右看,第一部分是函数执行时创建的Execution Context,它有自己的作用域链,第二部分是作用域链中的对象,索引为1的对象是从[[scope]]作用域链中复制过来的,索引为0的对象是在函数执行时创建的,第三部分是作用域链中的对象的内容Activation Object和Global Object。

函数在运行过程中,没遇到一个变量,都会去Execution Context的作用域链中从上到下依次搜索,如果在第一个作用域链(假如是Activation Object)中找到了,那么就返回这个变量,如果没有找到,那么继续向下查找,直到找到为止,这也就是为什么函数可以访问全局变量,当局部变量和全局变量同名时,会使用局部变量而不使用全局变量,以及javascript中各种看似怪异的、有趣的作用域问题的答案(你可以用这种方法来解释你以前碰到的所有作用域问题,当然,如果还是有疑问的话,非常希望你能贴出代码,我们一起讨论。)

一般情况下,一个函数的作用域链是不会在函数运行时被改变的,但有些运算符会临时改变作用域链,with和try catch的catch子句。看下面的例子:

function initUI(){
    with (document){     //avoid!
        var bd = body,
            links = getElementsByTagName("a"),
            i= 0,
            len = links.length;
        while(i < len){
            update(links[i++]);
        }
        getElementById("go-btn").onclick = function(){
            start();
        };
        bd.className = "active";
    }//eOf with
}

上例中,在onclick事件的事件处理器中引用了外部函数assignEvents的局部变量id,形成了闭包,下面我们看一下它们的作用域图示:

图 4

我们一起来从作用域的角度分析一下闭包的形成过程:

  1. assignEvents函数创建,词法解析后,函数对象assignEvents的[[scope]]属性被初始化,作用域链形成,作用域链中包含了全局对象的所有属性和方法(注意,此时因为assignEvents函数并未执行,所以闭包函数并没有被解析)。
  2. assignEvents函数执行,在开始执行时,创建Execution Context(我们将图4按照从左到右,从上到下的顺序划分为6部分,第一部分就是运行期上下文),在运行期上下文的作用域链中创建Activation Object(第二、三部分),并将Activation Object放置与作用域链顶点,在其中保存了函数执行时所有可访问函数内部的数据。
  3. 当执行到闭包时,javascript引擎发现了闭包函数的存在,按照通常的手法,将闭包函数解析,为闭包函数对象创建[[scope]]属性,初始化作用域链(此时闭包函数对象的作用域链中有两个对象,一个是assignEvents函数执行时的Activation Object,还有一个是全局对象,图4的4、5、6部分。)。我们看到图中闭包函数对象的作用域链和assignEvents函数的执行上下文作用域链相同?为什么相同呢?我们来分析一下,闭包函数是在assignEvents函数执行的过程中被发现并且解析的,而函数执行时的作用域是Activation Object,那么结果就很明显了,闭包函数被解析的时候它的作用域正是assignEvents作用域链中的第一个作用域对象Activation Object,当然,由于作用域链的关系,全局对象作用域也被引入到闭包函数的作用域链中。 那么我们现在考虑另一个问题,闭包作用域链中的Activation Object,是引用了assignEvents函数的Activation Object,还是拷贝了一个副本到闭包的作用域链中了?我们可以做一个小的测试,在有多个闭包同时引用外层函数局部变量(i)的情况下,如果其中一个闭包改变了i的内容,而其他闭包中的i的内容没有发生改变,则说明产生了拷贝,反之,则引用了同一个Activation Object。
  4. function fn(){
      var i = 0;
      (function(){++i;console.log(i)})();
      (function(){++i;console.log(i)})();
    }
    
    
    fn();
    //1
    //2
  1. 我们发现变量i从1变为了2,说明两个闭包引用的是同一个变量i,也就说明他们引用的fn的Activation Object是同一个,其实完全可以换一种非常简单的方式来解释:全局对象肯定是同一个吧?
  2. 下面讨论当闭包函数执行时的情况,因为在词法分析的时候闭包函数就已经在作用域链中保存了对assignEvents函数的Activation Object的引用,所以当assignEvents函数执行完毕之后,闭包函数虽然还没有开始执行,但依然可以访问assignEvents的局部数据(并不是因为闭包函数要访问assignEvents的局部变量id,所以当assignEvents函数执行完毕之后依然保持了对局部变量id的引用。而是不管是否存在变量引用,都会保存对assignEvents的Activation Object作用域对象的引用。因为在词法分析时,闭包函数没有执行,函数内部根本就不知道是否要对assignEvents的局部变量进行访问和操作,所以只能先把assignEvents的Activation Object作用域对象保存起来,当闭包函数执行时,如果需要访问assignEvents的局部变量,那么再去作用域链中搜索)。
  3. 闭包函数执行时创建了自己的Execution Context和Activation Object,在运行期上下文的作用域链中保存了自己的Activation Object,外层函数assignEvents的Execution Context的Activation Object,以及Global Object,如图:

图 5

这也就是闭包为何能“记得”在它周围到底发生了什么,为何闭包能访问外层函数的局部数据,为何闭包能保持这些局部数据而不在外层函数执行完毕销毁时一起销毁等等的原因。

前些天一个前辈(Darrel文叔)告诉我一句话,一针见血:没有内存,就没有闭包。

性能问题:

在作用域链和闭包中的性能问题主要表现在数据读写的速度上。

由于作用域链的原因,我们访问全局作用域的数据(这里为什么不说变量呢?因为不仅包括变量,还有函数,对象等其他内容)时,效率是最低的,而访问局部数据时的效率是最高的。

所以一个非常经典的解决数据访问性能问题的方案出现了:将需要访问的数据尽量的以局部数据的方式缓存起来。这样当标识符解析程序在作用域链中寻找数据时,直接就可以在作用域链的最上层找到想要的数据,效率自然就提升了。

这句话可以解决很多性能问题:设置缓存,将数据保存在局部变量中。

转载请注明出处:

参考:

举例详细说明javascript作用域、闭包原理以及性能问题(转)的更多相关文章

  1. JavaScript作用域闭包简述

    JavaScript作用域闭包简述 作用域 技术一般水平有限,有什么错的地方,望大家指正. 作用域就是变量起作用的范围.作用域包括全局作用域,函数作用域以块级作用域,ES6中的let和const可以形 ...

  2. JavaScript的闭包原理

    什么是js(JavaScript)的闭包原理,有什么作用? 一.定义 官方解释:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. 个人的理解是 ...

  3. Js(javaScript)的闭包原理

    问题?什么是js(javaScript)的闭包原理,有什么作用? 一.定义 官方解释:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分.  小编 ...

  4. JavaScript作用域闭包(你不知道的JavaScript)

    JavaScript闭包.是JS开发project师必须深入了解的知识. 3月份自己曾撰写博客<JavaScript闭包>.博客中仅仅是简单阐述了闭包的工作过程和列举了几个演示样例,并没有 ...

  5. 浅谈JavaScript的闭包原理

    在一般的教程里,都谈到子作用域可以访问到父级作用域,进而访问到父级作用域中的变量,具体是如何实现的,就不得不提及到函数堆栈和执行上下文. 举个例子,一个简单的闭包:   首先,我们可以知道,examp ...

  6. js 闭包原理理解

    问题?什么是js(JavaScript)的闭包原理,有什么作用? 一.定义 官方解释:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. 很显然 ...

  7. 转载:Javascript作用域原理

    首先看一个例子: var name = 'laruence'; function echo() { alert(name); var name = 'eve'; alert(name); alert( ...

  8. 读《你不知道的JavaScript(上卷)》后感-作用域闭包(二)

    github原文 一. 序言 最近我在读一本书:<你不知道的JavaScript>,这书分为上中卷,内容非常丰富,认真细读,能学到非常多JavaScript的知识点,希望广大的前端同胞们, ...

  9. 《前端之路》之四 JavaScript 的闭包、作用域、作用域链

    04:JavaScript 的闭包 一.定义: 常规定义: 闭包的定义: 有权利访问外部函数作用域的函数. 通俗定义: 1.函数内部包含了函数.然后内部函数可以访问外部函数的作用域. 2.内部函数可以 ...

随机推荐

  1. 63. Unique Paths II

    题目: Follow up for "Unique Paths": Now consider if some obstacles are added to the grids. H ...

  2. HTTP Basic Authorization

    在HTTP中,Basic Authorization基本认证是一种用来允许Web浏览器或其他客户端程序在请求时提供用户名和口令形式的身份凭证的一种登录验证方式. 在发送之前是以用户名追加一个冒号然后串 ...

  3. Hibernate学习笔记(1)

    1 使用Hibernate (1)创建User Library,命名为HIBERNATE3,加入需要的jar (2)创建hibernate配置文件hibernate.cfg.xml, 为了便于调试最好 ...

  4. Android zxing连续扫描

    initCamera(); if (mHandler != null) mHandler.restartPreviewAndDecode(); 在扫描完毕后执行这3句即可. 说明: 1.扫描处理方法为 ...

  5. nyoj-291 互素数个数 欧拉函数

    LK的数学题 时间限制:1000 ms  |  内存限制:65535 KB 难度:3   描述 LK最近遇到一个问题,需要你帮她一下.一个整数n,求[1,n)中,和n互素的数的个数.   输入 多组测 ...

  6. GetKeyState和GetAsyncKeyState以及GetKeyboardState函数的用法与区别2-------C#检查键盘大小写锁定状态

    1.命名空间:using System.Runtime.InteropServices;2.导入方法[DllImport("user32.dll", EntryPoint = &q ...

  7. Android远程图片获取和本地缓存

    对于客户端——服务器端应用,从远程获取图片算是经常要用的一个功能,而图片资源往往会消耗比较大的流量,对 应用来说,如果处理不好这个问题,那会让用户很崩溃,不知不觉手机流量就用完了,等用户发现是你的应用 ...

  8. HDU4888 Redraw Beautiful Drawings(2014 Multi-University Training Contest 3)

    Redraw Beautiful Drawings Time Limit: 3000/1500 MS (Java/Others)    Memory Limit: 65536/65536 K (Jav ...

  9. [转]使用微软的官方类库CHSPinYinConv获得汉字拼音

    原文链接:http://outofmemory.cn/code-snippet/4392/ms-CHSPinYinConv-convert-hanzi-to-pinyin 微软为中文,日文以及韩文提供 ...

  10. 06day1

    Rabbit Number 枚举 [问题描述] 设 S(N)表示 N 的各位数字之和,如 S(484)=4+8+4=16,S(22)=2+2=4.如果一个正整数 x满足 S(x*x)=S(x)*S(x ...