研磨JavaScript系列(四):代码的时空
对于过程式编程来说,代码执行的时间与数据标识的空间是密不可分的。我们只有把指令执行的具体时刻与标识映射的具体地址结合起来,才能确定程序在执行瞬间的上下文状态。于是,代码时刻与数据标识的结构,就形成了作用域的概念。在一个作用域中的上下文状态,对于另一个作用域来说是不适用的。
当定义了一个个标识符,写下一条条语句时,我们只得到了程序的一个静态映像。还必须把这些元素放到动态的作用域环境去思考,才能理解整个程序的运行世界。下面我们来讨论一下JavaScript的作用域。
任何程序都会在一个原始的环境中开始运行,这个原始的环境就被称为全局环境。全局环境中包含了一些预定义的元素,这些元素对于我们的程序来说是自然存在的。它们本来就在那儿了,我们拿来就可使用。
在JavaScript里的全局环境就是一个对象,这个对象是JavaScript运行环境的根。对于浏览器中的JavaScript来说,这个根对象就是window。对于全局的JavaScript语句来说,window对象就相当于当前的作用域。
var myName = "Kevin"; //定义了window作用域下的一个变量myName myName = "Kevin"; //定义了window对象的一个属性myName
在这里,window作用域的一个变量myName和window对象的一个属性myName几乎是等价的。对于全局的JavaScript语句来说,加不加var都无所谓,两种写法非常相似。
但是,对于一个函数体内的语句,有没有var就要小心了。
<script type="text/javascript">
myName = "Kevin";
var yourName = "Rose"; alert(myName + " like " + yourName); //显示Kevin like Rose changeName(); function changeName(){
alert("yourName is " + yourName); //显示yourName is undefined
alert("myName is " + myName); //显示myName is Kevin var yourName = "Marry";
myName = "Steven"; alert(myName + " like " + yourName); //显示Steven like Marry
} alert(myName + " like " + yourName); //显示Steven like Rose
</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隐含对象。
<script type="text/javascript">
function func(){
alert(func.toString()); //函数体内可以使用自身的标识符
} func();
</script>
函数自身有一个caller属性,其标示调用当前函数的上层函数。这就是提供了一个可以追溯函数调用来源的线索,特别对于调试JavaScript来说是不可多得宝贝。
<script type="text/javascript">
function whoCallerMe(){
alert("My Caller is " + whoCallerMe.caller); //输出自己的caller
} function callerA(){
whoCallerMe();
} function callerB(){
whoCallerMe();
} alert(whoCallerMe.caller); //输出null whoCallerMe(); //输出My Caller is null callerA(); //输出My Caller is function callerA(){whoCallerMe();} callerB(); //输出My Caller is function callerB(){whoCallerMe();}
</script>
函数的caller属性是null,表示函数没有被调用或者是被全局代码调用。函数的caller属性值实际上是动态变化的。函数的初始caller值都是null,当调一个函数时,如果代码已经运行在某个函数体内,JavaScript执行引擎将被函数的caller属性设置为当前函数。在退出被调用函数作用域时,被调函数的caller属性会恢复为null值。
this关键字表示函数正在服务的这个对象。
arguments对象,从字面上看是表示调用当前函数的参数们。除了我们可以直接使用函数参数定义列表中的那些标示符莱访问之外,还可以用arguments对象按数组方式来访问参数。
有一个质的注意的情况是,用eval()函数动态执行的代码并不创建新的作用域,其代码就是在当前作用域中执行的。eval()中的动态代码可以访问当前作用域的this,arguments等对象,因此可以使用eval()实现一些高级的多态和动态扩展方面的应用。
文章声明:本文部分内容参考自《悟透JavaScript》,这是一本学习JavaScript非常好的书。
研磨JavaScript系列(四):代码的时空的更多相关文章
- 研磨JavaScript系列
JavaScript是一种基于对象和事件驱动并具有相对安全性的客户端脚本语言.同时也是一种广泛用于客户端Web开发的脚本语言,常用来给HTML网页添加动态功能,比如响应用户的各种操作.它最初由网景公司 ...
- 研磨JavaScript系列(一):回归简单
想要理解JavaScript,你得首先放下对象和类的概念,回到数据和代码的本原.编程世界只有数据和代码两种基本元素,而这两种元素又有着纠缠不清的关系.JavaScript就是把数据和代码都简化到最原始 ...
- 研磨JavaScript系列(五):奇妙的对象
在JavaScript中,只有object和function两种东西有对象化的能力.我们先来说说函数的对象化能力. 任何一个函数都可以为其动态地添加或去除属性,这些属性可以是简单类型,可以是对象,也可 ...
- 研磨JavaScript系列(三):函数的魔力
JavaScript的代码中就只有function一种形式,function就是函数的类型.在其他的编程语言中可能还存在Procedure或者是method等代码概念,在JavaScript中只有fu ...
- 研磨JavaScript系列(二):没有类
object就是对象的类型.在JavaScript中不管多么复杂的数据和代码.都可以组织成object形式的对象. 但JavaScript没有"类"概念. 看下面这段JavaScr ...
- 深入理解JavaScript系列(1):编写高质量JavaScript代码的基本要点
深入理解JavaScript系列(1):编写高质量JavaScript代码的基本要点 2011-12-28 23:00 by 汤姆大叔, 139489 阅读, 119 评论, 收藏, 编辑 才华横溢的 ...
- JavaScript 系列博客(四)
JavaScript 系列博客之(四) 前言 本篇介绍 JavaScript 中的对象.在第一篇博客中已经说到 JavaScript 是一种''对象模型''语言.所以可以这样说,对象是 JavaScr ...
- 深入理解JavaScript系列(46):代码复用模式(推荐篇)
介绍 本文介绍的四种代码复用模式都是最佳实践,推荐大家在编程的过程中使用. 模式1:原型继承 原型继承是让父对象作为子对象的原型,从而达到继承的目的: function object(o) { fun ...
- 深入理解JavaScript系列(45):代码复用模式(避免篇)
介绍 任何编程都提出代码复用,否则话每次开发一个新程序或者写一个新功能都要全新编写的话,那就歇菜了,但是代码复用也是有好要坏,接下来的两篇文章我们将针对代码复用来进行讨论,第一篇文避免篇,指的是要尽量 ...
随机推荐
- python整数转ASCII码
# *-* coding:utf-8 *-* import binascii data = [1441465642, 251096121, -870437532, -944322827, 647240 ...
- BZOJ5089: 最大连续子段和
维护一个序列支持以下操作:区间加,区间求最大子段和.n<=50000,m<=50000. 我TM再也不写分块了... 先分块,对于块整体加的操作,假设块里面有若干二元组(x,y),表示一个 ...
- java数组知识总结(一)//按类
在线api 目录: 零/数组(基本元素) 1. 声明一个数组 2. 创建一个数组 3. 数组名.length 4. 数组的引用 一/java.lang.reflect.Array / ...
- Linux查看设备信息命令
系统 #查看内核/操作系统/CPU信息 uname -a #查看操作系统版本 head -n 1 /etc/issue #查看CPU信息 cat /proc/cpuinfo #查看计算机名 hostn ...
- Ubuntu中LightDM是什么(转)
LightDM(Light Display Manager)是一个全新的轻量级Linux桌面显示管理器,而传统的Ubuntu是使用GNOME桌面标准的GDM. LightDM是一个跨桌面显示管理器,其 ...
- 21行python代码实现拼写检查器
引入 大家在使用谷歌或者百度搜索时,输入搜索内容时,谷歌总是能提供很好的拼写检查,比方你输入 speling,谷歌会立即返回 spelling. 前几天,看到http://norvig.com/spe ...
- HDOJ--1285--确定比赛名次
确定比赛名次 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) Total Sub ...
- 行香子·过尽千山
<行香子·过尽千山> 文/天地尘埃2020 过尽千山.水瘦山寒. 思来路.地咽天玄. 千金散尽,欲补穹天. 但孟春暖,仲春炫.暮春喧. 三皇五帝,魏武挥鞭. 朴无欲.衡玉玑璿. 金生丽水, ...
- Android实现一个自己定义相机的界面
我们先实现拍照button的圆形效果哈.Android开发中,当然能够找美工人员设计图片,然后直接拿进来.只是我们能够自己写代码实现这个效果哈.最经常使用的的是用layout-list实现图片的叠加, ...
- 关于Android中的四大组件(Service的开启与关闭)
前言 服务(Service)是Android系统中的四大组件之中的一个.服务主要用于两个目的:后台执行和跨进程訪问. 通过启动 一个服务.能够在不显示界面的前提下在后台执行指定的任务,这样能够不影响用 ...