`
jamm19860411
  • 浏览: 86550 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

作用域链深层理解

阅读更多

可能这些日子技术又有点提高了,所以看得远了,觉得不懂得东西又多了,真是一山还有一山高,技术无止境,以前作用域总是马马虎虎,上网查了下,觉得这篇不错,转下来以后当字典,忘了,模糊了就拿来瞧瞧


关于js的作用域链,早有耳闻,也曾看过几篇介绍性的博文,但一直都理解的模棱两可。近日又精心翻看了一下《悟透Javascript》这本书,觉得写得太深刻,在“代码的时空”一节里有一段介绍作用域链的地方寥寥数语,回味无穷(其实还是理解的模棱两可^_^)。现在整理下自己的读书笔记,顺便借鉴网上资源,写下来。
一、从一个简单的问题说起
下面的js代码在页面中运行显示什么结果:

var arg = 1;
function fucTest(arg) {
    alert(arg);
    var arg = 2;
    //alert(arg);
}
fucTest(10);

 您的答案是什么?没错,就是弹出10。我的理解是这样的,funTest函数有一个形参arg,funTest函数传入实参10,alert方法把10弹出就是了,囧。
好,问题又来了:

var arg = 1;
function funcTest() {
    alert(arg);
    var arg = 2;
}
arg = 10;
funcTest();

 答案是什么?如果是5年前的我,肯定不会再往下想了,还是10!这么简单的问题还用想什么呀?我的理解是这样的:funTest函数是一个无参数的函数, 函数内部通过alert方法,调用外部(全局)的变量arg,在函数执行前,arg赋值为10,弹出arg值后改变arg值为2,所以弹出值为10。
真的是10吗?是还是不是?
测试的结果:弹出“undefined”,瀑布汗.
二、理解作用域链,从javascript运行机制说起
1、js的运行顺序
如果一个文档流中包含多个script代码段(用script标签分隔的js代码或引入的js文件),它们的运行顺序是:
步骤1. 读入第一个代码段(js执行引擎并非一行一行地执行程序,而是一段一段地分析执行的)
步骤2. 做语法分析 ,有错则报语法错误(比如括号不匹配等),并跳转到步骤5
步骤3. 对var变量和function定义做“预解析 ”(永远不会报错的,因为只解析正确的声明)
步骤4. 执行代码段,有错则报错 (比如变量未定义)
步骤5. 如果还有下一个代码段,则读入下一个代码段,重复步骤2
步骤6. 结束
上面的分析已经足够清楚,步骤二、三和步骤四里的红色字体可能是我们新手理解上的一个盲点,尤其是步骤三的“预解析”,如果不清楚什么叫预解析,总觉得不踏实。而步骤四的“有错则报错”也是经常碰到的。举例来说:

function funcTest() {
    alert(arg);
    var arg = 2;
}
funcTest();

 上面这段代码执行时,弹出“undefined”,也就是说arg没有定义,js的变量不是不用定义也可以吗?
2、语法分析和“预解析”
(1)、从解释型语言的编译过程 说起
众所周知,javascript是解释型语言,它不同于c#和java等编译型语言。对于传统编译型语言来说,编译步骤分为:词法分析、语法分析、语义检查、代码优化和字节生成;但对于解释型语言来说,通过词法分析语法分析 得到语法树后,就可以开始解释执行了。
a、词法分析
简单地说,词法分析是将字符流(char stream)转换为记号流(token stream)。
但是这个转换过程并不是可以用一句话就可以概括的那么简单,我们可以试着用伪代码理解一段简单的程序:

代码var result=x-y;的转换大致可以表示如下:

NAME "result"
EQUALS
NAME "x"
MINUS
NAME "y"
SEMICOLON

b、语法分析
简单地说,语法分析就是为了构造合法的语法分析树,而语法分析树可以直观地表示出推导的过程。
那么什么是语法分析树?简单地说,就是程序推导过程的描述。但是到底什么是语法树,请参考专业文章,本篇略过。
c、其他
通过语法分析,构造出语法分析树后,接下来还可能需要进一步的语义检查。对于传统强类型语言来说,语义检查的主要部分是类型检查,比如函数的实参和形参类型是否匹配等等。
结论:通过上面的分析可以看出,对于javascript引擎来说,肯定有词法分析和语法分析,之后可能还有语义检查、代码优化等步骤,等这些编译步骤完成之后(任何语言都有编译过程,只是解释型语言没有编译成二进制代码 ),才会开始执行代码。
(2)、执行过程
a、javascript的作用域机制
通过编译,javascript代码已经翻译成了语法树,然后会立刻按照语法树执行。
进一步的执行过程,需要理解javascript的作用域机制:词法作用域 (lexcical scope)。通俗地讲,就是javascript变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,编译器通过静态分析就能确定 ,因此词法作用域也叫做静态作用域(static scope)。但需要注意,with和eval 的语义无法仅通过静态技术实现,所以只能说javascript的作用域机制非常接近词法作用域(lexical scope).
javascript引擎在执行每个函数实例时,都会创建一个执行环境 (execution context)。执行环境中包含一个调用对象(call object), 调用对象是一个scriptObject 结构(scriptObject 是 与函数相关的一套静态系统,与函数实例的生命周期保持一致),用来保存内部变量表varDecls、内嵌函数表funDecls、父级引用列表 upvalue等语法分析结构(注意varDecls和funDecls等信息是在语法分析阶段就已经得到,并保存在语法树中。函数实例执行时,会将这些 信息从语法树复制到scriptObject上)。
b、javascript作用域机制的实现方法
词法作用域(lexical scope)是javascript的作用域机制,还需要理解它的实现方法,就是作用域链 (scope chain)。作用域链是一个name lookup机制,首先在当前执行环境的scriptObject中寻找,没找到,则顺着upvalue到父scriptObject中寻找,一直lookup到全局调用对象(global object)。
现在回过头来分析第二个问题:

var arg = 1;
function funcTest() {
    alert(arg);
    var arg = 2;
}
arg = 10;
funcTest();

 在执行funcTest函数时,也即进入了funcTest对应的作用域,js引擎在执行时,当遇到对变量名或者函数名的使用时,会首先在当前作用域(也 即funcTest对应的作用域)查找变量或者函数(显然,arg变量在funcTest对应的作用域里被定义为var arg=2 所以alert方法的参数采用的是当前作用域的arg,但是因为arg被定义在alert方法后,所以arg变量默认值为undefined)。当然,如 果没有找到就到上层作用域查找,依此类推(作用域范围可以持续到javascript运行环境的根:window对象)。
最后,让你看的更清楚,上面的代码其实可以等价于:

var arg = 1;
function funcTest() {
    var arg; //默认值undefined
    alert(arg);
    arg = 2;
}
arg = 10;
funcTest();

 c、闭包 (closure)
当一个函数实例执行时,会创建或关联到一个闭包 。 (关于闭包,打算另写一篇学习笔记)
scriptObject用来静态 保存与函数相关的变量表,闭包则在执行期动态 保存这些变量表及其运行值;
闭包的生命周期有可能比函数实例长。函数实例在活动引用为空后会自动销毁;
闭包则要等要数据引用为空后,由javascript引擎回收(有些情况下不会自动回收,就导致了内存泄漏)。
ps:关于“执行过程”这一段比较拗口,名词很多,不过别被它们吓住,一旦理解了执行环境(execution context)、调用对象(call object)、词法作用域(lexical scope)、作用域链(scope chain)、闭包(closure)等这些概念,javascript的很多现象都能迎刃而解。
三、结语
通过第二段的分析,对照第一段笔者曾经做出的判断(你是不是也觉得笔者曾经的分析和结论很幼稚(哪怕有时结果碰巧也对!)?!不是一般的肤浅啊,^_^),你会发现原来javascript还有这么多“玄机”,而要真正理解精通又谈何容易?先“悟透”再说吧。

 

(^_^)原文地址:http://www.cnblogs.com/wjfluisfigo/archive/2009/05/17/1457674.html

 

分享到:
评论

相关推荐

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

    局部变量的性能优势在于它们位于作用域链的前端,即激活对象中,这意味着查找它们的速度非常快,因为无需遍历整个作用域链。相反,全局变量通常需要更长的时间来查找,因为它们位于作用域链的末尾。这种性能差异在...

    C&C++深层探索

    3. **命名空间**:C++引入了命名空间,避免了全局作用域中的名字冲突问题,使得大型项目更易于管理。 4. **模板**:C++的模板机制允许编写泛型代码,即可以在多种数据类型上工作的函数或类。这大大提高了代码的复用...

    上海面试题.docx

    在JavaScript中,有函数作用域和块级作用域,但没有类作用域。`let`和`const`的引入引入了块级作用域,而`var`声明的变量具有函数作用域。 理解这些核心概念和技术是成为合格前端开发者的基础,尤其是在面试中,...

    深层探索C和C++总结_超级有用.doc

    外部变量存储在数据段,如果在函数内部使用static修饰,其作用域局限于该文件,避免了不同文件间的命名冲突。extern关键字用于声明全局变量,但不分配空间,真正的分配发生在首次定义时。如果使用static修饰外部变量...

    javascript深层探讨

    闭包是一种特殊的作用域,可以访问并保留外部作用域的变量,即使在其外部作用域已经销毁的情况下。 1. 闭包的产生:函数内部定义的函数可以访问外部函数的变量。 2. 闭包的应用:模块化、数据封装、异步操作等。 ...

    JavaScript语言精粹_修订版

    3. **作用域与闭包**:JavaScript有全局作用域和局部作用域,变量的生命周期和可见性由它们在代码中的位置决定。闭包是一种特殊的作用域机制,它允许函数访问并操作外部作用域的变量,即使在其外部函数已经执行完毕...

    Javascript中55个经典技巧

    9. **作用域链**:理解查找变量时如何沿着作用域链向上查找,以及闭包的概念。 10. **事件委托**:通过在父元素上设置事件监听器,处理子元素的事件,提高性能。 11. **防抖与节流**:防抖(debounce)用于限制函数...

    JavaScript权威指南(第5版)中文版(上)

    对象和原型链是理解JavaScript面向对象编程的关键,而闭包则揭示了JavaScript中作用域和内存管理的深层机制。 在实际Web开发中,JavaScript往往与DOM(文档对象模型)和BOM紧密联系。DOM允许JavaScript操作HTML和...

    Angular1.x安装与配置1

    这一设计理念体现在其数据绑定、指令和作用域等特性中,使得UI的构建更加直观和高效。框架的主要特点包括: 1. **数据绑定**:模型和视图之间的数据同步是自动的,无需手动操作,提高了开发效率。 2. **Scope**:...

    深层思想

    1. 高级JavaScript语法:JavaScript提供了许多高级特性,如闭包、原型链、作用域、异步编程(Promise和async/await)、模块系统(ES6模块和CommonJS),这些都是实现深层思想的基础。理解和熟练运用这些特性可以编写...

    深度探索C++对象模型.rar

    了解作用域规则、链接性和extern关键字,有助于避免命名冲突和理解代码的组织结构。 8. **异常处理**:C++的异常处理机制提供了在程序运行时处理错误的方式。理解try、catch、throw和noexcept关键字,以及异常的...

    常用面试题及答案.pdf

    - 在非严格模式下的全局作用域中,`this` 指向全局对象(在浏览器环境中通常是 `window`)。 - 当函数作为某个对象的方法被调用时,`this` 指向该对象。 - 当函数使用 `new` 关键字调用时,`this` 指向新创建的对象...

    Expert C Programming Deep C Secrets

    作者提醒读者注意宏定义的副作用,如它们如何影响作用域和类型,并提供了最佳实践来避免这些问题。 书中还深入讨论了C语言中的内存管理,包括动态内存分配、堆栈和堆的区别,以及如何有效地使用malloc和free函数。...

    you-dont-know-js:你不知道JavaScript博文演示代码

    函数表达式(匿名函数和命名函数表达式)、函数声明、函数作用域、闭包(理解这一点对于优化代码和实现模块化至关重要)以及箭头函数(ES6引入的新特性)都可能是讨论的重点。 在面向对象编程方面,JavaScript的...

    编译原理课程设计

    语义分析(Semantic Analysis)紧随其后,这一步确保代码在语义上是正确的,例如类型检查、作用域解析和常量折叠等。在这个阶段,编译器还会生成中间代码,如三地址码,这是一种简化了的虚拟机器指令集,用于代码...

    js常见经典面试题汇总

    - **const**:块级作用域,声明时必须初始化,值不可变,但对引用类型,其地址不可变而内容可变。 #### 5. 普通函数和箭头函数的区别 - **写法**:箭头函数语法更简洁。 - **this指向**:普通函数的`this`指向调用...

    收藏AngularJS中最重要的核心功能

    **控制器**是用于扩展作用域并处理业务逻辑的JavaScript函数,它们与特定的作用域绑定。控制器可以访问并修改作用域中的数据,进而影响视图的展示。 **服务**是AngularJS中可复用的单例对象,它们提供了各种功能,...

Global site tag (gtag.js) - Google Analytics