深入理解JavaScript高级之词法作用域和作用域链


Posted in Javascript onDecember 10, 2013

主要内容:
1、分析JavaScript的词法作用域的含义

2、解析变量的作用域链

3、变量名提升时什么

最近在传智播客讲解JavaScript的课程,有不少朋友觉得JavaScript是如此的简单,但是又如此的不知如何使用,因此我准备了一些内容给大家分享一下.
这个系列主要讲解JavaScript的高级部分的内容,包括作用域链、闭包、函数调用模式、原型以及面向对象的一些东西. 在这里不包含JavaScript的基本语法,如果需要了解基础的同学可以到http://net.itcast.cn里面去下载免费的视频进行学习. 好了,废话不多说,直接进入我们的正题.

一、关于块级作用域
说到JavaScript的变量作用域,与咱们平时使用的类C语言不同.
例如C#中下面代码:

static void Main(string[] args)
{
 if(true)
 {
  int num = 10;
 }
 System.Console.WriteLine(num);
}

这段代码如果进行编译,是无法通过的,因为"当前上下文中不存在名称num". 因为这里
变量的作用域是由花括号限定的,称为块级作用域.

在块级作用域下,所有的变量都在定义的花括号内,从定义开始到花括号结束这个
范围内可以使用. 出了这个范围就无法访问. 也就是说代码

if(true)
{
 int num = 10;
 System.Console.WriteLine(num);
}

这里可以访问,因为变量的定义与使用在同一个花括号内.

但是在JavaScript中就不一样,JavaScript中没有块级作用域的概念.

二、JavaScript中的作用域
在JavaScript中,下面代码:

if(true) {
 var num = 10;
}
alert(num);

运行的结果是弹窗10. 那么在JavaScript中变量的作用范围是怎么限定的呢?

2.1 函数限定变量作用域
在JavaScript中,只有函数可以限定一个变量的作用范围. 什么意思呢?
就是说,在JavaScript中,在函数里面定义的变量,可以在函数里面被访问,但是在函数外
无法访问. 看如下代码:

var func = function() {
 var num = 10;
};
try {
 alert(num);
} catch ( e ) {
 alert( e );
}

这段代码运行时,会抛出一个异常,变量num没有定义. 也就是说,定义在函数中的变量无法
在函数外使用,当然在函数内可以随意的使用, 即使在赋值之前. 看下面代码:

var func = function() {
 alert(num);
 var num = 10;
 alert(num);
};
try {
 func();
} catch ( e ) {
 alert( e );
}

这段代码运行后,不会抛出错误,弹窗两次,分别是 undefined 和 10(至于为什么,下文解释).

从这里可以看得出,变量只有在函数中可以被访问. 同理在该函数中的函数也可以访问.

2.2 子域访问父域
前面说了,函数可以限定变量的作用域,那么在函数中的函数就成为该作用域的子域. 在子域
中的代码可以访问到父域中的变量. 看下面代码:

var func = function() {
 var num = 10;
 var sub_func = function() {
  alert(num);
 };
 sub_func();
};
func();

这段代码执行得到的结果就是 10. 可以看到上文所说的变量访问情况. 但是在子域中访问父域的
代码也是有条件的. 如下面代码:

var func = function() {
 var num = 10;
 var sub_func = function() {
  var num = 20;
  alert(num);
 };
 sub_func();
};
func();

这段代码比前面就多了一个"var num = 20;",这句代码在子域中,那么子域访问父域的情况就发
生了变化,这段代码打印的结果是 20. 即此时子域访问的num是子域中的变量,而不是父域中的.

由此可见访问有一定规则可言. 在JavaScript中使用变量,JavaScript解释器首先在当前作
用域中搜索是否有该变量的定义,如果有,就是用这个变量;如果没有就到父域中寻找该变量.
以此类推,直到最顶级作用域,仍然没有找到就抛出异常"变量未定义". 看下面代码:

(function() {
 var num = 10;
 (function() {
  var num = 20;
  (function(){
   alert(num);
  })()
 })();
})();

这段代码执行后打印出20. 如果将"var num = 20;"去掉,那么打印的就是10. 同样,如果再去掉
"var num = 10",那么就会出现未定义的错误.

三、作用域链
有了JavaScript的作用域的划分,那么可以将JavaScript的访问作用域连成一个链式树状结构.
JavaScript的作用域链一旦能清晰的了解,那么对于JavaScript的变量与闭包就是非常清晰的了.
下面采用绘图的办法,绘制作用域链.

3.1 绘制规则:
1) 作用域链就是对象的数组
2) 全部script是0级链,每个对象占一个位置
3) 凡是看到函数延伸一个链出来,一级级展开
4) 访问首先看当前函数,如果没有定义往上一级链检查
5) 如此往复,直到0级链

3.2 举例
看下面代码:

var num = 10;
var func1 = function() {
 var num = 20;
 var func2 = function() {
  var num = 30;
  alert(num);
 };
 func2();
};
var func2 = function() {
 var num = 20;
 var func3 = function() {
  alert(num);
 };
 func3();
};
func1();
func2();

下面分析一下这段代码:
-> 首先整段代码是一个全局作用域,可以标记为0级作用域链,那么久有一个数组
var link_0 = [ num, func1, func2 ];// 这里用伪代码描述
-> 在这里func1和func2都是函数,因此引出两条1级作用域链,分别为
var link_1 = { func1: [ num, func2 ] };// 这里用伪代码描述
var link_1 = { func2: [ num, func3 ] };// 这里用伪代码描述
-> 第一条1级链衍生出2级链
var link_2 = { func2: [ num ] };// 这里用伪代码描述
-> 第二条1级链中没有定义变量,是一个空链,就表示为
var link_2 = { func3: [ ] };
-> 将上面代码整合一下,就可以将作用域链表示为:

// 这里用伪代码描述
var link = [ // 0级链
 num,
 { func1 : [ // 第一条1级链
  num,
  { func2 : [ // 2级链 
   num
  ] }
 ]},
 { func2 : [ // 第二条1级链
  num,
  { func3 : [] }
 ]}
];

-> 用图像表示为

深入理解JavaScript高级之词法作用域和作用域链

图:01_01作用域链.bmp

注意:将链式的图用js代码表现出来,再有高亮显示的时候就非常清晰了.

有了这个作用域链的图,那么就可以非常清晰的了解访问变量是如何进行的:
在需要使用变量时,首先在当前的链上寻找变量,如果找到就直接使用,不会
向上再找;如果没有找到,那么就向上一级作用域链寻找,直到0级作用域链.

如果能非常清晰的确定变量所属的作用域链的级别,那么在分析JavaScript
代码与使用闭包等高级JavaScript特性的时候就会非常容易(至少我是这样哦).

三、变量名提升与函数名提升

有了作用域链与变量的访问规则,那么就有一个非常棘手的问题. 先看下面
的JavaScript代码:

var num = 10;
var func = function() {
 alert(num);
 var num = 20;
 alert(num);
};
func();

执行结果会是什么呢?你可以想一想,我先不揭晓答案.

先来分析一下这段代码.
这段代码中有一条0级作用域链,里面有成员num和func. 在func下是1级作用
域链,里面有成员num. 因此在调用函数func的时候,就会检测到在当前作用域中
变量num是定义过的,所以就会使用这个变量. 但是此时num并没有赋值,因为代
码是从上往下运行的. 因此第一次打印的是 undefined,而第二次打印的便是20.
你答对了么?

像这样将代码定义在后面,而在前面使用的情况在JavaScript中也是常见的
问题. 这时就好像变量在一开始就定义了一样,结果就如同下面代码:

var num = 10;
var func = function() {
 var num; // 感觉就是这里已经定义了,但是没有赋值一样
 alert(num);
 var num = 20;
 alert(num);
};
func();

那么这个现象常常称为变量名提升. 同样也有函数名提升这一说. 如下面代码:

var func = function() {
 alert("调用外面的函数");
};
var foo = function() {
 func(); var func = function() {
  alert("调用内部的函数");
 };
 func();
};

好了,这段代码结果如何?或则应该有什么不一样,我先不说没留着读者思考吧!
下一篇再做解答.

由于有了这些不同,因此在实际开发的时候,推荐将变量都写在开始的地方,
也就是在函数的开头将变量就定义好,类似于C语言的规定一样. 这个在js库中也
是这么完成的,如jQuery等.

四、小结

好了这篇文章主要是说明JavaScript的词法作用域是怎么一回事儿,以及解释
如何分析作用域链,和变量的访问情况,最后留再一个练习收尾吧!!!

看下面代码执行结果是什么:

if( ! "a" in window ) {
 var a = "定义变量";
}
alert(a);
Javascript 相关文章推荐
javascript学习笔记(十二) RegExp类型介绍
Jun 20 Javascript
jquery 元素控制(追加元素/追加内容)介绍及应用
Apr 21 Javascript
JS的get和set使用示例
Feb 20 Javascript
js实现鼠标触发图片抖动效果的方法
Feb 27 Javascript
jquery文字填写自动高度的实现方法
Nov 07 Javascript
使用Ajax生成的Excel文件并下载的实例
Nov 21 Javascript
shiro授权的实现原理
Sep 21 Javascript
详解Vue Elememt-UI构建管理后台
Feb 27 Javascript
js中的 || 与 && 运算符详解
May 24 Javascript
vue-video-player 通过自定义按钮组件实现全屏切换效果【推荐】
Aug 29 Javascript
让webpack+vue-cil项目不再自动打开浏览器的方法
Sep 27 Javascript
JavaScript 中的无穷数(Infinity)详解
Feb 13 Javascript
javascript四舍五入函数代码分享(保留后几位)
Dec 10 #Javascript
javascript如何创建表格(javascript绘制表格的二种方法)
Dec 10 #Javascript
javascript修改表格背景色实例代码分享
Dec 10 #Javascript
jquery全选checkBox功能实现代码(取消全选功能)
Dec 10 #Javascript
javascript中文本框中输入法切换的问题
Dec 10 #Javascript
javascript 数字格式化输出的实现代码
Dec 10 #Javascript
多个jquery.datatable共存,checkbox全选异常的快速解决方法
Dec 10 #Javascript
You might like
php excel reader读取excel内容存入数据库实现代码
2012/12/06 PHP
PHP内置过滤器FILTER使用实例
2014/06/25 PHP
php更新mysql后获取改变行数的方法
2014/12/25 PHP
正确的PHP匹配UTF-8中文的正则表达式
2015/05/13 PHP
PHP实现微信JS-SDK接口选择相册及拍照并上传的方法
2016/12/05 PHP
php file_get_contents取文件中数组元素的方法
2017/04/01 PHP
自写的利用PDO对mysql数据库增删改查操作类
2018/02/19 PHP
浅谈PHP无限极分类原理
2019/03/14 PHP
Yii框架分页技术实例分析
2019/08/30 PHP
javascript实现仿银行密码输入框效果的代码
2007/12/13 Javascript
浏览器无法运行JAVA脚本的解决方法
2008/01/09 Javascript
javascript replace()正则替换实现代码
2010/02/26 Javascript
也说JavaScript中String类的replace函数
2011/09/22 Javascript
浅谈Javascript鼠标和滚轮事件
2012/06/27 Javascript
js实现键盘操作实现div的移动或改变的原理及代码
2014/06/23 Javascript
AngularJS 遇到的小坑与技巧小结
2016/06/07 Javascript
BootStrap Table 分页后重新搜索问题的解决办法
2016/08/08 Javascript
简单实现JavaScript弹幕效果
2020/08/27 Javascript
深入解析nodejs HTTP服务
2017/07/25 NodeJs
详解vue中使用protobuf踩坑记
2019/05/07 Javascript
微信小程序实现上传多个文件 超过10个
2020/03/30 Javascript
element 中 el-menu 组件的无限极循环思路代码详解
2020/04/26 Javascript
JavaScript交换变量的常用方法小结【4种方法】
2020/05/07 Javascript
快速了解Vue父子组件传值以及父调子方法、子调父方法
2020/07/15 Javascript
[04:05]TI9战队采访 - Natus Vincere
2019/08/22 DOTA
Python基于sklearn库的分类算法简单应用示例
2018/07/09 Python
解决python web项目意外关闭,但占用端口的问题
2019/12/17 Python
Python打印不合法的文件名
2020/07/31 Python
法国大使拉杆箱官网:DELSEY Paris
2018/03/20 全球购物
2014年小学植树节活动方案
2014/03/02 职场文书
领导班子个人查摆问题对照检查材料
2014/10/02 职场文书
市委常委会班子党的群众路线教育实践活动整改方案
2014/10/25 职场文书
2015年小学一年级班主任工作总结
2015/05/21 职场文书
2019幼儿园感恩节活动策划书
2019/11/28 职场文书
话题作文之生命的旋律
2019/12/17 职场文书
MySQL表的增删改查基础教程
2021/04/07 MySQL