A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

【济南校区】前端就业班笔记服务器与Ajax:day04

代码的预解析
预解析:提前的翻译解释, 在运行代码之前的一个解释.
作用:
编译型语言: C, C++, C#, Java需要一个 "翻译" 程序, 将源代码翻译成计算机可以读懂的二进制数据( 指令 ).然后存储成可执行文件. 提前翻译好, 运行时直接执行得结果。
解释型( 脚本型 ): JavaScript, SQL, ...代码在执行的时候, 有一个翻译程序, 读一句代码执行一句代码. 再读一句代码, 再执行一句代码.一句一句的翻译执行. 每次运行都需要翻译一次. 代码在执行之前, 需要快速的 "预览" 一遍. 那么可以尽可能提高执行效率。

在 js 中预解析的特点
代码是如何执行的: 读取 js 文本, 预解析, 一句一句地执行
js 在预解析的过程中完成了声明部分的标记与变量作用域的设定

js 中的声明
简单的说就是让 js 执行引擎知道有什么东西( 标识符 )

[JavaScript] 纯文本查看 复制代码
console.log( num );        // error: num is not defined
num(); // error: is not function

即代码在执行之前的预解析, 首先让 js 的执行引擎知道在当前运行环境中有什么东西( 名字, 标识符 )是可以被使用的. 它是变量, 还是函数等?
在 js 中有哪些声明:
1) 标识符的声明( 变量的声明 )
2) 函数的声明

变量的声明:
语法:                 var 变量名;
目的: 告诉解释器, 有一个名字是一个变量, 在当前环境中可以被使用.
语句: 就是可以执行的东西.
var a = 123; 是一个语句
在使用 var 声明变量, 同时完成赋值的时候. 实际上, 预解析将其做了一定处理:
凡是读取到 var 的时候, 就检查 var 紧跟的名字是否已经标记了
如果没有标记, 就表明这个名字是一个标识符, 需要被标记
如果已经被标记了, 那么 这个 var 被忽略
结论:
[JavaScript] 纯文本查看 复制代码
                        var a;
                        var a = 10;
                        等价
                        var a;
                        a = 10;

                        var a = 123;
                        var a = 456;
                        var a = 789;
                        等价于
                        var a = 123;
                        a = 456;
                        a = 789;

如果在代码中有多个 var 后面紧跟的名字是一样的. 那么只有第一个 var 起作用,后面的所有 var 都会被自动的忽略.
变量名提升
[JavaScript] 纯文本查看 复制代码
        // 注意: '字符串' in 对象
        //                该字符串描述的名字, 是否在对象中存在一个属性, 与之同名 
        //                 var o = { num: 123 }
        //                'num' in o                => true
        //                'age' in o                => false
        if ( 'a' in window ) {
                var a = 123;
        }

        console.log( a );

函数的声明
函数的各种定义形式
[JavaScript] 纯文本查看 复制代码
声明式:        

        function func () {
                console.log( '使用声明式定义' );
        }

表达式式( 匿名函数, 字面量函数, lambda 函数 ):

        var func = function () {
                console.log( '使用表达式式定义' );
        };

特点:
1 函数的声明是独立于语句. 不需要加分号结束. 也不能嵌入到代码表达式中.
2 表达式式, 本质上是使用函数表达式( 字面量 )给变量赋值. 因此它是语句.
表达式:将运算符与操作数连接起来的式子. 就是一个有结果的代码单元( 不包括语句 )
        var a;                // 声明, 不是语句, 也没有结果
        123                        // 字面量, 有值, 是表达式. 是常量表达式
        a = 123                // 赋值, 有值, 就是被赋值的那个值. 是赋值表达式.
       function () {}
如果将变量的声明与函数的声明放在一起有些需要注意的情况
1)函数的声明实际上包含两部分
   1 告诉解释器 xxx 名字已经可以使用( 函数名, 标识符 )
   2 告诉解释, 这个名字代表着一个函数( 变量里存储着函数的引用 )
2) 当函数声明与变量声明冲突的时候. 只看谁先有数据.
一个在新版本的浏览器中的特性

[JavaScript] 纯文本查看 复制代码
        if ( true ) {

                function foo() {
                        console.log( true );
                }

        } else {

                function foo() {
                        console.log( false );
                }
        }
        foo();//在早期的浏览器中( 2015 年 ) 所有的浏览器( 除了火狐 )都是将其解释为声明 : false

词法作用域
作用域: 就是变量可以使用到不能使用的范围
块级作用域:
块: 代码块, 即 {  }
变量的使用从定义开始, 到其所在的块级作用域结束
[JavaScript] 纯文本查看 复制代码
                        // js 伪代码
                        {
                                console.log( num );                // error: num 未定义
                                var num = 123;
                                {
                                        console.log( num ); // => 123
                                }
                                console.log( num );         // => 123
                        }
                        console.log( num );  // error: num 未定义

代表语言: C, C++, C#, Java, ...
js 是词法作用域
词法: 就是定义, 书写代码的规则.
所以 所谓的 词法作用域, 就是 在书写代码的时候, 根据书写代码的结构就可以确定数据的访问范围的作用域.
js 不受 块的影响, 即使在块中定义声明变量, 在块的外面依旧可以使用
[JavaScript] 纯文本查看 复制代码
                        console.log( num );  // => undefined
                        {
                                var num = 123;
                        }
                        console.log( num );  // => 123

所谓的 js 的词法作用域, 就是根据预解析规则定义变量的使用范围, 全部代码中
只有函数可以限定范围. 其他均不能限定访问范围. 在内部是一个独立的作用范围结构.
结论:词法作用域就是描述变量的访问范围:
        1 在代码中只有函数可以限定作用范围. 允许函数访问外部的变量. 反之不允许.
        2 在函数内优先访问内部声明的变量, 如果没有才会访问外部的.
        3 所有变量的访问规则, 按照预解析规则来访问

案例
[JavaScript] 纯文本查看 复制代码
        var num = 123;
        function f1 () {
                console.log( num );
        }

        function f2 () {
                console.log( num );
                var num = 456;
                f1();
                console.log( num );
        }
        f2();


1. 读取代码预解析. 得到 num, f1, f2
2. 逐步的执行代码
    1) 赋值 num = 123;   注意 f1 和 f2 由于是函数, 所以也有数据.
    2) 调用 f2进入到函数体内. 相当于做一次预解析. 得到 num. 注意, 此时有内外两个 num执行每一句代码打印 num. 因为函数内部有声明 num. 所以此时访问的是函数内部的 num. 未赋值, 得到 undefined
        -> 赋值 num = 456
        -> 调用 f1(). 调用函数的规则也是一样. 首先看当前环境中是否还有函数的声明. 如果有直接使用. 如果没有, 则在函数外面找, 看时候有函数. 此时在函数 f2 中没有 f1 的声明. 故访问的就是外面的 f1 函数
        -> 跳入 f1 函数中. 又要解析一次. 没有得到任何声明.
        > 执行打印 num. 当前环境没有声明 num. 故在外面找. 外面的是 123. 所以打印 123. 函数调用结束, 回到 f2 中.
        > 继续执行 f2, 打印 num. 在 f2 的环境中找 num. 打印 456.
任务:
[JavaScript] 纯文本查看 复制代码
                var num = 123;
                function f1 () {
                        console.log( num );
                }
                function f2 () {
                        console.log( num );   // => 123 , 456, 456
                        num = 456;        
                        f1();
                        console.log( num );
                }
                f2();

案例
[JavaScript] 纯文本查看 复制代码
        (function ( a ) {
                console.log( a );
                var a = 10;
                console.log( a );
        })( 100 );

拆解
        ( 函数 ) ( 100 )
        第一个圆括号就是将函数变成表达式
        后面一个圆括号就是调用该函数
[JavaScript] 纯文本查看 复制代码
        var func = function ( a ) {
                console.log( a );
                var a = 10;
                console.log( a );
        }
        func( 100 );

注意: 函数定义参数, 实际上就是在函数最开始的时候, 有一个变量的声明function ( a ) { ... }其含义就是, 在已进入函数体, 在所有操作开始之前( 预解析之前 )就有了该变量的声明.由于已经有了 a 参数的声明. 所以在代码中 var a = 10 是重复声明. 其声明无效.所以上面的代码, 等价于
[JavaScript] 纯文本查看 复制代码
        var func = function ( a ) {
                console.log( a );                        // => 100
                a = 10;
                console.log( a );                        // => 10
        }
        func( 100 );
        // 变式
        (function ( a ) {
                console.log( a );
                var a = 10;
                console.log( a );
                function a () {
                        console.log( a );
                }
                a();
        })( 100 );

1> 直接调用
2> 进入到函数中, 已有声明 a 并且其值为 100
3> 在函数内部预解析. 得到 一个结论. 函数声明是两个步骤.
     1) 让当前环境中, 有变量名 a 可以使用. 但是不需要. 因为已经有 a 的声明了
     2) 让 a 指向函数. 相当于
               var a;
               function a () {}
                  ...
4> 开始逐步执行每一句代码
       1) 打印 a. 所以打印函数体
       2) 赋值 a = 10
       3) 打印 a, 打印出 10
       4) 如果让 a 调用, 那么报错 error: a is not function

作用域链规则
什么是作用域链:链指的就是访问规则
[JavaScript] 纯文本查看 复制代码
                function foo() {
                        console.log( num );
                }
                --------------------
                function func () {
                        function foo() {
                                console.log( num );
                        }
                        foo();
                }
                --------------------
                function F () {
                        function func () {
                                function foo() {
                                        console.log( num );
                                }
                                foo();
                        }
                        func();
                }
                ... ...

由于这种一环套一环的访问规则, 这样的作用域构成一个链式结构. 所以直接称其为作用域链.
作用域链是用来做变量查找的. 因此变量可以存储什么东西. 链中就应该有什么东西. 换句话说就是, 链里面存储的是各种对象. 可以将其想象成对象的序列( 数组 )

绘制作用域链的规则
1> 将所有的 script 标签作为一条链结构. 标记为 0 级别的链.
2> 将全局范围内, 所有的声明变量名和声明函数名按照代码的顺序标注在 0 级链中.
3> 由于每一个函数都可以构成一个新的作用域链. 所以每一个 0 级链上的函数都延展出 1 级链.
4> 分别在每一个函数中进行上述操作. 将函数中的每一个名字标注在 1 级链中.
5> 每一条 1 级链中如果有函数, 可以再次的延展出 2 级链. 以此类推.

分析代码的执行
当作用域链绘制完成后. 代码的的分析也需要一步一步的完成.
1> 根据代码的执行顺序( 从上往下, 从左至右 )在图中标记每一步的变量数据的变化
2> 如果需要访问某个变量. 直接在当前 n 级链上查找变量. 查找无序.
3> 如果找到变量, 直接使用. 如果没有找到变量在 上一级, n - 1 级中查找.
4> 一直找下去, 知直到 0 级链. 如果 0 级链还没有就报错. xxx is not defined.

经典面试题
[JavaScript] 纯文本查看 复制代码
// console.log( [][ 1 ] );

// console.log( i );
	var  arr = [ { name: '张三1' }, 
				 { name: '张三2' }, 
				 { name: '张三3' }, 
				 { name: '张三4' } ];

	// 利用循环, 给他添加方法, 在方法中打印 name
	for ( var i = 0; i < arr.length; i++) {
		// arr[ i ] 绑定方法
		arr[ i ].sayHello = function () {
			// 打印名字
			console.log( 'name = ' + arr[ i ].name );
		};
	}
	// 批注
	// i == 0  =>  arr[ 0 ].sayHello = function () ... => 在 function 中使用的是 arr[ i ]
	// i == 1	   arr[ 1 ].sayHello = function () ... => 在 function 中使用的是 arr[ i ]
	// ...
	// i == 3      arr[ 3 ]... ...
	// i++ => i === 4 不再小于 4( arr.length ) 跳出循环
	// 依次其中的数据
	// for ( var j = 0; j < arr.length; j++ ) {
	// 	arr[ j ].sayHello();
	// }
	// arr[ 0 ].sayHello();
	// arr[ 1 ].sayHello();
	// ...
	// arr[ 3 ].sayHello()
	for ( var i = 0; i < arr.length; i++ ) {
		arr[ i ].sayHello();
	}
// console.log( i );

闭包的概念
字面意义: 闭: 关闭, 封闭,包: 包裹, 打包闭包的含义就是一个被包裹的隔离的空间.
在 js 中, 什么是闭包
在 js 中函数是一个具有变量作用域隔离特性的一个内存结构, 即为一个闭包.
[JavaScript] 纯文本查看 复制代码
   function foo () {                      
 
}


学习闭包, 在 js 中要解决什么问题
在 js 中闭包要解决的问题就是间接的访问到这个被隔离的数据.
[JavaScript] 纯文本查看 复制代码
	function foo () {
		var num = 123;
		return num;
	}
	*/
	// 在外界访问到 num 中的数据. 怎么做?
	// 通过 return 返回 num 的数据. 就间接访问到 num 了

函数, 在 js 中与普通的对象具有一样的意义函数可以像变量一样使用赋值、传递。

闭包的间接访问
使用 return 数据不能直接访问原来的数据, 那么可以考虑利用函数的返回访问原始数据
[JavaScript] 纯文本查看 复制代码
	function foo () {
		var num = Math.random();   // 原始数据
		
		function func () {
			return num; // 它就是原始数据 num
		}
		
		return func;
	}
	var f = foo(); 	// foo 只调用一次, 就可以创建一个原始数据, 但是返回的函数可以重复调用
					// 每调用一次就是在获得闭包中的数据的值
	var v1 = f();
	console.log( v1 );
	var v2 = f();
	console.log( v2 );



如果你想了解更多黑马课程请点击这里,如果你想加入黑马这个大家庭学习先进技术,广交天下好友!
黑马程序员济南中心联系电话:0531-55696830

0 个回复

您需要登录后才可以回帖 登录 | 加入黑马