对于过程式编程来说,代码执行的时间与数据标识的空间是密不可分的。我们只有把指令执行的具体时刻与标识映射的具体地址结合起来,才能确定程序在执行瞬间的上下文状态。于是,代码时刻与数据标识的结构,就形成了作用域的概念。在一个作用域中的上下文状态,对于另一个作用域来说是不适用的。

当定义了一个个标识符,写下一条条语句时,我们只得到了程序的一个静态映像。还必须把这些元素放到动态的作用域环境去思考,才能理解整个程序的运行世界。下面我们来讨论一下JavaScript的作用域。

任何程序都会在一个原始的环境中开始运行,这个原始的环境就被称为全局环境。全局环境中包含了一些预定义的元素,这些元素对于我们的程序来说是自然存在的。它们本来就在那儿了,我们拿来就可使用。

在JavaScript里的全局环境就是一个对象,这个对象是JavaScript运行环境的根。对于浏览器中的JavaScript来说,这个根对象就是window。对于全局的JavaScript语句来说,window对象就相当于当前的作用域。

  1. var myName = "Kevin"; //定义了window作用域下的一个变量myName
  2.  
  3. myName = "Kevin"; //定义了window对象的一个属性myName

在这里,window作用域的一个变量myName和window对象的一个属性myName几乎是等价的。对于全局的JavaScript语句来说,加不加var都无所谓,两种写法非常相似。

但是,对于一个函数体内的语句,有没有var就要小心了。

  1. <script type="text/javascript">
  2. myName = "Kevin";
  3. var yourName = "Rose";
  4.  
  5. alert(myName + " like " + yourName); //显示Kevin like Rose
  6.  
  7. changeName();
  8.  
  9. function changeName(){
  10. alert("yourName is " + yourName); //显示yourName is undefined
  11. alert("myName is " + myName); //显示myName is Kevin
  12.  
  13. var yourName = "Marry";
  14. myName = "Steven";
  15.  
  16. alert(myName + " like " + yourName); //显示Steven like Marry
  17. }
  18.  
  19. alert(myName + " like " + yourName); //显示Steven like Rose
  20. </script>

这里我们看到,显然用var修饰的yourName标识符在函数内外是两个东西。外面的Rose不会因为changeName函数内改成了Mary而改变,回到外面还是Rose,然而,myName没有用var修饰,就是一个东西了,函数内的修改也就在函数外表现出来了。

还有,我们要注意的是changeName函数中的一句话,我们本是希望输出最外面的yourName的值,但此时的输出却表明yourName的值是undefined。而第二句中输出的myName却又是我们所期望的值。这是为什么呢?

这是因为使用var定义的是作用域上的一个变量,没有var的标识符却可能是全局跟对象的一个属性。当代码运行在全局作用域时,作用域就是跟对象window,所以,有没有var都无所谓。

但是,当代码运行进入一个函数时,JavaScript会创建一个新的作用域,来作为当前作用域的子作用域。然后,将当前全局作用域切换为这个新建的子作用域,开始执行函数逻辑。JavaScript执行引擎会把此函数内的逻辑代码,当一个代码段单元来分析和执行。

在第一步的预编译分析中,JavaScript执行引擎将所有定义式函数直接创建为作用域上的函数变量,并将其值初始化为定义的函数代码逻辑,也就是为其建立了可调用的函数变量。而对于所有var定义的变量,也会在第一步的预编译中创建起来,并将初始值设为undefined。

随后,JavaScript开始解释执行代码。当遇到对函数名或变量名的使用时,JavaScript执行引擎会首先在当前作用域查找函数或变量,如果没有找到,就到上层作用域查找,依次类推。因此,前面的语句引用后面语句定义的var变量时,该变量其实已经存在,只是初始值为undefined而已。

因此,用var定义的变量只对本作用域有效,尽管此时上层作用域有同名的东西,都与本作用域的var变量无关。退出本作用域之后,此var变量消失,回到原来的作用域该有啥就还有啥。

其实,函数在每次调用时都会产生一个子作用域,退出函数时,这个子作用域就会消失,下次调用相同函数时又是另一个子作用域了。在运行的函数内再调用另外的函数是,又产生了另一个作用域,这就会随着函数调用的深入自然形成一种叫作用域链的东西。甚至在递归调用的情况下,作用域链会变得很长很长。

JavaScript查找标示符时,除非指明特定对象,否则会沿着作用域链寻找符合这一标识符的变量或属性,甚至会自动创建该标识符。正是由于JavaScript的这种灵活性,我们就可能在不经意间覆盖了原来的变量或属性,或者无意中创建了大量无用的全局属性,甚至在毫不知情的情况下影响了其他的代码逻辑,从而造成许多很难排除的Bug。

因此,当我们在编写和调试JavaScript代码时,我们的思绪应该尽量放到具体的作用域时空上去思考。这样,我们就能明白为什么标识符尽量不要与已有的东西重名?为什么变量一定要加var?为什么访问属性一定要指明对象?理解和注意这些问题,也就能轻易地排除某些奇怪的bug。

虽然,我们的代码没有办法访问到JavaScript内部的作用域对象,但JavaScript还是给我们提供了与作用域相关的某些可用元素,我们还是能知道当前作用域上下文中德某些信息的。JavaScript提供的调用上下文信息由几个,一个是函数本身,一个是函数的caller属性,另外还有this关键字和arguments隐含对象。

  1. <script type="text/javascript">
  2. function func(){
  3. alert(func.toString()); //函数体内可以使用自身的标识符
  4. }
  5.  
  6. func();
  7. </script>

函数自身有一个caller属性,其标示调用当前函数的上层函数。这就是提供了一个可以追溯函数调用来源的线索,特别对于调试JavaScript来说是不可多得宝贝。

  1. <script type="text/javascript">
  2. function whoCallerMe(){
  3. alert("My Caller is " + whoCallerMe.caller); //输出自己的caller
  4. }
  5.  
  6. function callerA(){
  7. whoCallerMe();
  8. }
  9.  
  10. function callerB(){
  11. whoCallerMe();
  12. }
  13.  
  14. alert(whoCallerMe.caller); //输出null
  15.  
  16. whoCallerMe(); //输出My Caller is null
  17.  
  18. callerA(); //输出My Caller is function callerA(){whoCallerMe();}
  19.  
  20. callerB(); //输出My Caller is function callerB(){whoCallerMe();}
  21. </script>

函数的caller属性是null,表示函数没有被调用或者是被全局代码调用。函数的caller属性值实际上是动态变化的。函数的初始caller值都是null,当调一个函数时,如果代码已经运行在某个函数体内,JavaScript执行引擎将被函数的caller属性设置为当前函数。在退出被调用函数作用域时,被调函数的caller属性会恢复为null值。

this关键字表示函数正在服务的这个对象。

arguments对象,从字面上看是表示调用当前函数的参数们。除了我们可以直接使用函数参数定义列表中的那些标示符莱访问之外,还可以用arguments对象按数组方式来访问参数。

有一个质的注意的情况是,用eval()函数动态执行的代码并不创建新的作用域,其代码就是在当前作用域中执行的。eval()中的动态代码可以访问当前作用域的this,arguments等对象,因此可以使用eval()实现一些高级的多态和动态扩展方面的应用。

文章声明:本文部分内容参考自《悟透JavaScript》,这是一本学习JavaScript非常好的书。

研磨JavaScript系列(四):代码的时空的更多相关文章

  1. 研磨JavaScript系列

    JavaScript是一种基于对象和事件驱动并具有相对安全性的客户端脚本语言.同时也是一种广泛用于客户端Web开发的脚本语言,常用来给HTML网页添加动态功能,比如响应用户的各种操作.它最初由网景公司 ...

  2. 研磨JavaScript系列(一):回归简单

    想要理解JavaScript,你得首先放下对象和类的概念,回到数据和代码的本原.编程世界只有数据和代码两种基本元素,而这两种元素又有着纠缠不清的关系.JavaScript就是把数据和代码都简化到最原始 ...

  3. 研磨JavaScript系列(五):奇妙的对象

    在JavaScript中,只有object和function两种东西有对象化的能力.我们先来说说函数的对象化能力. 任何一个函数都可以为其动态地添加或去除属性,这些属性可以是简单类型,可以是对象,也可 ...

  4. 研磨JavaScript系列(三):函数的魔力

    JavaScript的代码中就只有function一种形式,function就是函数的类型.在其他的编程语言中可能还存在Procedure或者是method等代码概念,在JavaScript中只有fu ...

  5. 研磨JavaScript系列(二):没有类

    object就是对象的类型.在JavaScript中不管多么复杂的数据和代码.都可以组织成object形式的对象. 但JavaScript没有"类"概念. 看下面这段JavaScr ...

  6. 深入理解JavaScript系列(1):编写高质量JavaScript代码的基本要点

    深入理解JavaScript系列(1):编写高质量JavaScript代码的基本要点 2011-12-28 23:00 by 汤姆大叔, 139489 阅读, 119 评论, 收藏, 编辑 才华横溢的 ...

  7. JavaScript 系列博客(四)

    JavaScript 系列博客之(四) 前言 本篇介绍 JavaScript 中的对象.在第一篇博客中已经说到 JavaScript 是一种''对象模型''语言.所以可以这样说,对象是 JavaScr ...

  8. 深入理解JavaScript系列(46):代码复用模式(推荐篇)

    介绍 本文介绍的四种代码复用模式都是最佳实践,推荐大家在编程的过程中使用. 模式1:原型继承 原型继承是让父对象作为子对象的原型,从而达到继承的目的: function object(o) { fun ...

  9. 深入理解JavaScript系列(45):代码复用模式(避免篇)

    介绍 任何编程都提出代码复用,否则话每次开发一个新程序或者写一个新功能都要全新编写的话,那就歇菜了,但是代码复用也是有好要坏,接下来的两篇文章我们将针对代码复用来进行讨论,第一篇文避免篇,指的是要尽量 ...

随机推荐

  1. 【01】CSS3 Gradient 分为 linear-gradient(线性渐变)和 radial-gradient(径 向渐变)(转)

    CSS3 Gradient 分为 linear-gradient(线性渐变)和 radial-gradient(径 向渐变).而我们今天主要是针对线性渐变来剖析其具体的用法.为了更好的应用 CSS3 ...

  2. JavaSE 学习笔记之集合框架(十八)

    集合框架:,用于存储数据的容器. 特点: 1:对象封装数据,对象多了也需要存储.集合用于存储对象. 2:对象的个数确定可以使用数组,但是不确定怎么办?可以用集合.因为集合是可变长度的. 集合和数组的区 ...

  3. noip模拟赛 abcd

    [问题描述]有4个长度为N的数组a,b,c,d.现在需要你选择N个数构成数组e,数组e满足a[i]≤e[i]≤b[i]以及 并且使得 最大.[输入格式]输入文件名为abcd.in.输入文件共 N+1 ...

  4. HBase的集群搭建

    前提:已经安装过jdk,HDFS集群和zookeeper,我的集群规划见HDFS的文章中 1.在1上安装配置hbase 下载:http://mirror.bit.edu.cn/apache/hbase ...

  5. Docker Command

    1. #docker inspect id           这个命令给出和容器相关的所有信息(https://www.imooc.com/video/15730) 2. #docker searc ...

  6. GNS3和Cisco IOU搭建路由交换实验-IOU篇

    http://www.mamicode.com/info-detail-605879.html

  7. C++学习之动态数组类的封装

    动态数组(Dynamic Array)是指动态分配的.可以根据需求动态增长占用内存的数组.为了实现一个动态数组类的封装,我们需要考虑几个问题:new/delete的使用.内存分配策略.类的四大函数(构 ...

  8. Codeforces Round #329 (Div. 2)B. Anton and Lines 贪心

    B. Anton and Lines   The teacher gave Anton a large geometry homework, but he didn't do it (as usual ...

  9. 关于三星手机调用相机返回后activity被回收的问题

    今天遇到个问题很蛋疼啊,别的手机没问题,唯独三星机型的手机跳转到相机之后,回来activity没了.这个或许是三星内部回收机制的关系,因为相机打开之后消耗会比较大, 所以后面的进程都给暂时回收掉了,加 ...

  10. ALSA声卡驱动中的DAPM详解之三:如何定义各种widget

    上一节中,介绍了DAPM框架中几个重要的数据结构:snd_soc_dapm_widget,snd_soc_dapm_path,snd_soc_dapm_route.其中snd_soc_dapm_pat ...