深入理解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与DropDownList 区别分析
Jan 01 Javascript
实现web打印的各种方法介绍及实现代码
Jan 09 Javascript
javascript改变position值实现菜单滚动至顶部后固定
Jan 18 Javascript
js截取中英文字符串、标点符号无乱码示例解读
Apr 17 Javascript
window.print打印指定div指定网页指定区域的方法
Aug 04 Javascript
JS组件系列之Bootstrap table表格组件神器【终结篇】
May 10 Javascript
jQuery设置和获取select、checkbox、radio的选中值方法
Jan 01 Javascript
jquery实现自定义图片裁剪功能【推荐】
Mar 08 Javascript
AngularJS解决ng-if中的ng-model值无效的问题
Jun 21 Javascript
基于Vue实现图书管理功能
Oct 17 Javascript
详解create-react-app 自定义 eslint 配置
Jun 07 Javascript
vuex 实现getter值赋值给vue组件里的data示例
Nov 05 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
虹吸式咖啡壶操作
2021/03/03 冲泡冲煮
浅析ThinkPHP中的pathinfo模式和URL重写
2014/01/06 PHP
php身份证号码检查类实例
2015/06/18 PHP
php编程实现简单的网页版计算器功能示例
2017/04/26 PHP
使用PHPExcel实现数据批量导出为excel表格的方法(必看)
2017/06/09 PHP
PHP空值检测函数与方法汇总
2017/11/19 PHP
jquery常用技巧及常用方法列表集合
2011/04/06 Javascript
图片无缝滚动代码(向左/向下/向上)
2013/04/10 Javascript
js冒泡法和数组转换成字符串示例代码
2013/08/14 Javascript
Ajax请求在数据量大的时候出现超时的解决方法
2014/02/27 Javascript
老生常谈Javascript中的原型和this指针
2016/10/09 Javascript
bootstrap日历插件datetimepicker使用方法
2016/12/14 Javascript
JS区分Object与Aarry的六种方法总结
2017/02/27 Javascript
使用原生js+canvas实现模拟心电图的实例
2017/09/20 Javascript
Koa代理Http请求的示例代码
2018/10/10 Javascript
JavaScript JMap类定义与使用方法示例
2019/01/22 Javascript
vue使用Proxy实现双向绑定的方法示例
2019/03/20 Javascript
layui数据表格 table.render 报错的解决方法
2019/09/29 Javascript
vue组件入门知识全梳理
2020/09/21 Javascript
Python实现全局变量的两个解决方法
2014/07/03 Python
简单了解python的break、continue、pass
2019/07/08 Python
python 实现矩阵填充0的例子
2019/11/29 Python
Python 按比例获取样本数据或执行任务的实现代码
2020/12/03 Python
html5 svg 中元素点击事件添加方法
2013/01/16 HTML / CSS
html5+css3实现一款注册表单实例
2013/04/17 HTML / CSS
linux面试题参考答案(11)
2016/11/26 面试题
实习教师个人的自我评价
2013/11/08 职场文书
车间班组长的职责
2013/12/13 职场文书
留学推荐信写作指南
2014/01/25 职场文书
招商专员岗位职责
2014/02/08 职场文书
指导教师评语
2014/04/26 职场文书
室内设计专业毕业生求职信
2014/05/02 职场文书
代领报检证委托书范本
2014/10/11 职场文书
golang中的并发和并行
2021/05/08 Golang
Vue+Flask实现图片传输功能
2022/04/01 Vue.js
《巫师》是美食游戏?CDPR10月将推出《巫师》官方食谱
2022/04/03 其他游戏