这里有新鲜出炉的 Javascript 教程,程序狗速度看过来!
Javascript 是一种由 Netscape 的 LiveScript 发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如 Perl,遗留的速度问题,为客户提供更流畅的浏览效果。
这篇文章主要介绍了 JavaScript 高级之词法作用域和作用域链,有需要的朋友可以参考一下
主要内容: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 : [] }
- ]}
- ];
-> 用图像表示为
图: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);
来源: http://www.phperz.com/article/17/0413/277946.html