JavaScript 虽然没有类, 但变量和一些函数却同样拥有局部作用域的制约, 下面结合代码图文讲解 JavaScript 中的作用域与作用域链:
Javascript 是一种由 Netscape 的 LiveScript 发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如 Perl,遗留的速度问题,为客户提供更流畅的浏览效果。
先上三段说明作用域的代码
- //==========例1==========
- var scope='global';
- function fn(){
- alert(scope);
- var scope='local';
- alert(scope);
- }
- fn(); //输出结果?
- alert(scope);//输出结果?
- //===========例2==========
- var scope='global';
- function fn(){
- alert(scope);
- scope='local';
- alert(scope);
- }
- fn(); //输出结果?
- alert(scope);//输出结果?
- //===========例3=========
- var scope='global';
- function fn(scope){
- alert(scope);
- scope='local';
- alert(scope);
- }
- fn(); //输出结果?
- alert(scope);//输出结果?
这三段代码只有小许差异,但结果缺截然不同,例 1 分别输出 [undefined , local , global],例 2 分别输出 [global , local , local],例 3 结果输出 [undefined , local , global],如果不能答对说明你对 javascript 的作用域特性还未理解透彻。
什么是作用域?
也许有人会问:变量 a 的作用域是什么?一会儿又问:函数 a 的作用域是什么?变量和函数的作用域分别是啥玩意?
我们先来看看 "作用域" 是什么意思,"作用域" 拆开来就是 "作用" 和 "域"
作用域就是变量和函数的可访问范围,或者说变量或函数起作用的区域。
1.javascript 函数的作用域:
函数内的区域,就是这个函数的作用域,变量和函数在这个区域都可以访问操作。最外层函数外的区域叫全局作用域,函数内的区域叫局部作用域。
2.javascript 变量的作用域:
在源代码中变量所在的区域,就是这个变量的作用域,变量在这个区域内可以被访问操作。在全局作用域上定义的变量叫全局变量,在函数内定义的变量叫局部变量。
简单地理解,JS 源代码被函数 { } 划分成一块一块的区域,这些区域换个身份就是某函数或某变量的作用域,变量的作用域和函数的作用域在源代码中有可能指的是同一块区域。
作用域链
作用域链(Scope Chain)是 javascript 内部中一种变量、函数查找机制,它决定了变量和函数的作用范围,即作用域,理解作用域链的作用原理,上一篇文章的三个例子也就能理解了,从而知其然也知其所以然。
作用域链是 ECMAScript-262 说明文档中的概念,javascript 引擎是按 ECMAScript-262 说明文档去实现的,了解 javascript 引擎的工作原理有利于我们理解 javascript 的特性,但绝大多数 js 程序员不会去了解非常底层的技术,所以阅读 ECMAScript-262 说明文档,我们可以有一个直观的方式去模拟 javascript 引擎的工作原理。
本文将通过 1999 年的 ECMAScript-262-3th 第三版来说明作用域链的形成原理,将会介绍执行环境,变量对象和活动对象,arguments 对象,作用域链等几个概念。2009 年发布了 ECMAScript-262-5th 第五版,不同的是取消了变量对象和活动对象等概念,引入了词法环境(Lexical Environments)、环境记录(EnviromentRecord)等新的概念,所以两个版本的概念不要混淆了。
1. 执行环境(Execution Contexts)
执行环境 (Execution Contexts) 也被翻译为执行上下文,当解析器进入 ECMAScript 的可执行代码,解析器就进入一个执行环境,活动的执行环境组成一个逻辑上的栈,在这个逻辑栈顶部的执行环境是当前运行的执行环境。
注:ECMAScript 中有三种可执行代码,Global、Function 和 Eval,全局环境即是 Global 可执行代码,函数即是 Function 可执行代码。逻辑栈是一种特殊的数据存储格式,特点是'先进后出,后进先出',添加数据会先压入逻辑栈顶部,删除数据必须先从顶部开始删除。
变量对象 (Variable Object)、活动对象(Activation Object) 和 Arguments 对象(Arguments Object)
每个执行环境都有一个与之关联的变量对象,当解析器进入执行环境时,就会创建一个变量对象,变量对象保存着在当前执行环境中声明的变量和函数的引用。
变量对象是一个抽象的概念,在不同的执行环境中,变量对象有不同的身份,在解析器进入任何执行环境之前,就已经创建了一个 Global 对象,当解析器进入全局执行环境时,Global 对象就充当变量对象,当解析器进入一个函数时,就会创建一个活动对象充当变量对象。
2. 解析器处理代码时的两个阶段
我们都知道 javascript 解析器是一段一段解析处理代码的,为毛?这就要涉及解析器处理代码时的两个阶段,解析代码和执行代码。
当解析器进入执行环境时,变量对象就会添加执行环境中声明的变量和函数作为它的属性,这就意味着变量和函数在声明之前已经可用,变量值为 undefined,这就是变量和函数声明提升 (Hoisting) 的原因,与此同时作用域链和 this 确定,此过程为解析阶段,俗称预解析。接着解析器开始执行代码,为变量添加相应值的引用,得到执行结果,此过程为执行阶段。
举两个好吃的栗子:
- var a=123;
- var b="abc";
- function c(){
- alert('11');
- }
上述全局环境中的代码解析执行后,会将 Global 对象作为变量对象,保存以下数据。
- function testFn(a){
- var b="123";
- function c(){
- alert("abc");
- }
- }
- testFn(10);
当解析器进入函数执行环境时,则会创建一个活动对象作为变量对象,活动对象还会创建一个 Arguments 对象,arguments 对象是一个参数集合,用来保存参数,这就是我们写函数时可以使用 arguments[0] 等来使用参数的原因。
3. 作用域链 (Scope Chain)
每个执行环境都有一个与之关联的作用域链,当解析器进入执行环境时被定义,作用域链是一个对象列表,用来检索各个变量对象中的变量和函数,这样可以保证执行环境有权访问哪些变量和函数,举个栗子。
- var a='123';
- function testFn(b){
- var c='abc';
- function testFn2(){
- var d='efg';
- alert(a);
- }
- testFn2();
- }
- testFn(10);
testFn2 内未声明变量 a,为什么 testFn2 能调用全局变量 a?整个过程是怎么发生的呢?请看下图。
当解析器进入全局执行环境时,调用变量和函数时只在 Global 对象中查找。
当解析器进入 testFn 函数执行环境时,函数内部属性 [[scope]] 中首先填入 Global 对象,然后将 testFn 活动对象添加到 Global 对象之前,形成一个作用域链。
当解析器进入 testFn2 函数执行环境时,函数内部属性 [[scope]] 首先填入父级的作用域链,然后再将当前的 testFn2 活动对象添加到作用域链的前端,形成一个新的作用域链。
testFn2 调用变量 a 时,首先在当前的 testFn2 活动对象中查找,如果没有找到就顺着作用域链向上,在 testFn 活动对象中查找变量 a,如果没有找到再顺着作用域链向上查找,直到在最后 Global 对象中找到为止,否则报错。所以函数内部可以调用外部环境的变量,外部环境不能调用函数内部的变量,这就是作用域特性的原理。
来源: http://www.phperz.com/article/17/0328/264439.html