代码的预解析
预解析 分为 预 和 解析,指 提前的翻译解释, 在运行代码之前的一个解释.
为什么需要它?可以尽可能提高执行效率。
编译型语言: C, C++, C#, Java
就是需要一个 “翻译” 程序, 将源代码翻译成计算机可以读懂的二进制数据( 指令 ).然后存储成可执行文件.
-> 提前翻译好, 运行时直接执行得结果
解释型( 脚本型 ): JavaScript, SQL, …
代码在执行的时候, 有一个翻译程序, 读一句代码执行一句代码. 再读一句代码,再执行一句代码.一句一句的翻译执行. 每次运行都需要翻译一次,效率低下
-> 代码在执行之前, 需要快速的 “预览” 一遍. 检查一些核心问题是否存在,然后在真正执行的时候,就不需要再去检查这些问题了,那么可以尽可能提高执行效率.
在 js 中预解析的特点
-> 代码是如何执行的: 读取 js 文本, 预解析, 一句一句地执行
-> js 在预解析的过程中完成了这两件事情
- 声明部分的标记
- 变量作用域的设定
什么是 js 中的声明
-> 简单的说就是让 js 执行引擎 知道有什么东西( 标识符 )
console.log( num ); // error: num is not defined
num(); // error: is not function
这里报错,就是因为解析引擎不知道有他们,引擎不认识这个变量,不知道这个函数,所以报错。
即代码在执行之前的预解析, 首先让 js 的执行引擎知道在当前运行环境中,有什么东西( 名字, 标识符 )是可以被使用的. 它是变量, 还是函数等?
在 js 中有哪些声明
(1) 标示符的声明(变量的声明)
(2) 函数的声明
变量的声明
语法: var 变量名
目的:告诉解释器,有一个名字是一个变量,在当前环境可以被使用
语句:就是可以执行的东西 var a = 123; 就是一个语句
在使用 var 声明变量, 同时完成赋值的时候. 实际上, 预解析将其做了一定处理:
凡是读取到 var 的时候, 就检查 var 紧跟的名字是否已经标记了
-> 1. 如果没有标记, 就表明这个名字是一个标识符, 需要被标记
-> 2. 如果已经被标记了, 那么 这个 var 被忽略
结论:如果在代码中有多个 var 后面紧跟的名字是一样的. 那么只有第一个 var 起作用.后面的所有 var 都会被自动的忽略
var a;
var a = 10;
等价
var a; // var 被忽略
a = 10;
var a = 123; // 声明同时被赋值
var a = 456; // var 被忽略
var a = 789; // var 被忽略
等价于
var a = 123;
a = 456;
a = 789;
变量名提升
读取所有的代码( 字符串 ). 包含每一个字节, 每一个数据. 但是 “只留意” var
判断 var 后面紧跟的名字是否被标记. 如果没有, 则标记上.
表示在当前环境中已经有该变量了. 如果已标记, 则忽略.
读取完毕后, 代码再从头开始, 从上往下, 从左至右一句一句的执行代码.
执行 ‘a’ in window. 很显然当前环境中已有变量 a, 这位结果为真.
…
// 注意: '字符串' in 对象
// 该字符串描述的名字, 是否在对象中存在一个属性, 与之同名
// var o = { num: 123 }
// 'num' in o => true
// 'age' in o => false
if ( 'a' in window ) {
var a = 123;
}
console.log( a ); // 123
变量名提升小例子
console.log( num ); // undefined, 变量名提升了,解析器认识这个变量,只是没赋值,不会报错
var num = 123; // 赋值
console.log( num ); // 123
函数的声明
函数的各种定义形式
声明式:
function func() { console.log( '111' ); }
表达式式( 匿名函数, 字面量函数, lambda 函数 ):
var func = function () { console.log( '使用表达式式定义' ); };
new 大写 Function 等等…
特点:
函数的声明是独立于语句. 不需要加分号结束. 也不能嵌入到代码表达式中
表达式式, 本质上是使用函数表达式( 字面量 )给变量赋值. 因此它是语句
- 表达式: 将运算符与操作数连接起来的式子.
就是一个 有结果的代码单元( 不包括语句 )
用操作符连接的一个式子 1+2 , 3-4, a=b, a instancof b 等…
var a; // 声明, 不是语句, 也没有结果
1234 // 字面量, 有值, 是表达式. 是常量表达式
a = 1234 // 赋值, 有值, 就是被赋值的那个值. 是赋值表达式.
function () {}
各种函数定义形式的异同
声明式: ( 重点是语法 )
函数声明是独立于代码执行的. 代码在执行的时候, 声明部分已在预解析阶段处理完毕
因此在代码调试阶段, 无法给函数声明添加断点. 而且由于预解析在执行之前完成,
所以可以先调用, 后声明函数. 有时在开发的时候, 将函数全部声明在后面, 前面为了保证代码的紧凑, 而直接调用.
func();
function func () {
console.log( '声明了一个函数' );
}
函数表达式
使用这个方式定义函数, 实际上是利用函数是 js 中的一个数据类型的特点
利用赋值, 使用变量存储函数的引用. 此时没有函数的声明. 但是有变量的声明
1> 读取代码, 发现 var func, 存储 func 这个名字.
2> 开始执行代码, 第一句是赋值语句, 将函数赋值给 func
3> 开始调用
如果将调用放到 赋值之前, 就会报错: error
// 函数表达式 // func(); 无法调用 会报错: error var func = function () { console.log( '使用函数表达式创建了函数' ); }; func();
函数表达式的名字问题
函数.name 可以用来获取函数的名字
var f1 = function f2() {};
console.log( f1.name ); // f1 ?????
function f2() {}
console.log(f2.name); // f2
我们的函数表达式也是可以带有函数名
当函数声明语法嵌入表达式环境中, 会自动进行转换, 将转换成函数表达式.
1> 引用函数的规则还是使用变量赋值, 所以外部可以使用该名字调用函数.
2> 函数表达式带有名, 该名字只允许在函数内部使用. 属于局部作用域. ( IE8 除外 )
3> 带有名字的函数表达式, 函数的 name 属性即为该名字
//var 函数名1 = function 函数名2 () { ... }
var f1 =
function f2 () {
console.log( '带有名字的 函数表达式' );
console.log( f2 ); // 函数表达式带有名, 该名字只允许在函数内部使用. 属于局部作用域.
};
f1(); // 带有名字的 函数表达式
console.log( f1.name ); // f2
// f2(); '报错'
如果将变量的声明与函数的声明放在一起有些需要注意的情况
函数的声明实际上包含两部分
a. 告诉解释器 xxx 名字已经可以使用( 函数名, 标识符 )
b. 告诉解释器, 这个名字代表着一个函数( 变量里存储着函数的引用 )
function func() { } // 声明了函数 // in 运算符 console.log('func' in window); // true 在当前执行环境中已经存在了 func 标识符 // func 是一个指向函数的 "变量" console.log(typeof func); // => function func = 123; console.log(typeof func); // => number func = [ 1, 2, 3, 4 ]; console.log( 'func = ' + func ); // func = 1,2,3,4 console.log( typeof func ); // object // 获得对象的类型 console.log( Object.prototype.toString.call( func ) ); // [object Array]
当函数声明与变量声明冲突的时候. 只看谁先有数据.
函数的声明与变量的声明意义多一层. 声明变量, 是告诉解释器当前环境可以使用该名字了
而声明函数, 是告诉解释器, 除了可以使用该名字, 该名字还表示一个函数体.
案例 1:
var num; function num () { console.log( 'Hello js' ); } console.log( num ); // 函数体
1> 先 var num; 后 function num …
首先告知解释器有 名字 num 了
后面是函数声明. 由于已经有 num 名字可以使用了, 所以就不再告诉解释器可以使用 num
而是直接将 num 与函数结合在一起,所以直接是函数体
案例 2:
function num () { console.log( 'Hello js' ); } var num; console.log( num ); // 也是函数体
2> 先 function num … 后 var num;
一开始已经有 num 了, 而且是函数. 所以后面的 var num; 属于重复声明, 所以还是函数体
案例 3:
var num = 123; function num () { console.log( 'Hello js' ); } console.log( num ); // 123
一个浏览器的新特性
if ( true ) {
// 以声明的形式来解释
function foo() {
console.log( true );
}
} else {
function foo() {
console.log( false );
}
}
foo(); // 运行结果 true
在早期的浏览器中( 2015 年 ) 所有的浏览器( 除了火狐 )都是将其解释为声明 : false
但是现在的运行结果, 得到: true. 表示 if 起到了作用
if ( true ) {
// 以声明的形式来解释
function foo1() {
console.log( true );
}
} else {
function foo2() { // 有做函数名提升,作用域中有 foo2,但是 b 不是一个函数
console.log( false );
}
}
foo1(); // true
foo2(); // error: foo2 is not function. 已定义, 但是函数为被指向
// 好比: var foo1 = function foo1 () { ... }
虽然这两个函数不是声明, 但是也不能解释成函数表达式. 如果是函数表达式 foo1 与 foo2 只能在函数内部使用.
所以函数声明不要放在代码块,就算要在代码块中放函数,就放表达式,提高代码准确性
词法作用域
作用域: 就是变量可以使用到不能使用的范围
块级作用域:
块: 代码块, 即 { }
变量的使用从定义开始, 到其所在的块级作用域结束
// 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 不受 块的影响, 即使在块中定义声明变量, 在块的外面依旧可以使用
console.log( num ); // => undefined { var num = 123; } console.log( num ); // => 123
所谓的 js 的词法作用域, 就是根据预解析规则定义变量的使用范围, 全部代码中只有函数可以限定范围. 其他均不能限定访问范围. 在内部是一个独立的作用范围结构.
结论: 词法作用域就是描述变量的访问范围。
在代码中只有函数可以限定作用范围. 允许函数访问外部的变量. 反之不允许
在函数内优先访问内部声明的变量,如果没有才会访问外部的
所有变量的访问规则,按照预解析规则来访问
// 在没有函数的情况下,所有的变量访问规则依据预解析规则 // 只有函数可以限定作用域其他的不行 function foo() { var num = 123; // 限定了作用域范围 } foo(); console.log( num ); // Uncaught ReferenceError: num is not defined(…)
- 在函数内部也有与解析的过程
function foo() { // 在函数内部也有与解析的过程 console.log( num ); // 发现有 undefined { var num = 123; } console.log( num ); // 123 } foo();
- 在函数内部允许再定义函数,同时两个层次的函数都是作用域的独立体
function foo() { func(); function func() { console.log( num ); // 发现有 undefined { var num = 123; } console.log( num ); // 123 } } foo(); //
1. 预解析,找 var 和 function 发现 foo, 然后没了,开始从上往下执行,执行 foo(),进入foo 2. 进入了foo函数,又开始了新一阶段的预解析,找 var 和 function,发现了 func,然后没了, 开始从上往下执行,执行 func(), 进入 func 3. 进入了func函数,又是一个独立的作用域,又开始新一阶段预解析,找 var 和 function,发现了 num,然后没了,开始从上往下执行,输出 num,他认识num,所以是 undefined, 赋值 num = 123, 最后 打印num ,值是123。 执行结束。
- 允许在函数内, 访问函数外的变量. 前提是函数内没有该变量的声明( *** )
var num = 123; function foo () { console.log( num ); // 输出 123 } foo();
function foo () { console.log( num ); // 输出 undefiend } foo(); var num = 123; // 1> 读取代码, 发现有声明 foo 与 num // 2> 执行代码: // 2.1 调用 // 访问变量. 在外面找. 是可以找得到的. 但是没有被赋值 // 2.2 赋值
var num = 123; function foo () { console.log( num ); // undefiend var num = 456; console.log( num ); // 456 } foo(); // 1> 预解析. 得到 foo 和 num // 2> 执行代码: 先赋值, 在调用 // 3> 进入 foo 内部执行, 再次预解析. 得到 num // 4> 执行 foo 的代码. 首先 打印 num, 没有被赋值, 因此是 undefined // 5> 再给 foo 中的 num 赋值, 再打印 num, 所以得到 456
特点:先在自己作用域范围内找,没有再往上找,优先访问当前作用域的数据
var num = 123; function foo () { console.log( num ); // => 123 自己里面没有,往上一层作用域找 num = 456; // 为外面的 num 赋值 console.log( num ); // => 456 } foo(); console.log( num ); // => 456
var num = 123; function f1 () { console.log( num ); // 123 } function f2 () { console.log( num ); // undefiend 在自己里面找,找到了 num,优先使用自己的,自己的num 没值,所以 undefiend var num = 456; // 给自己的 num 赋值 f1(); // 自己的作用域没有 f1 往外找 f1 console.log( num ); // 456 } 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.
作用域案例
(function ( a ) {
console.log( a );
var a = 10;
console.log( a );
})( 100 );
拆解 ( 函数 ) ( 100 )
第一个圆括号就是将函数变成表达式
后面一个圆括号就是调用该函数
– 折解后 –
var func = function ( a ) { console.log( a ); // 打印 100 var a = 10; // 重复声明,声明无效 console.log( a ); // 打印 10 } func( 100 );
注意: 函数定义参数, 实际上就是在函数最开始的时候, 有一个变量的声明
function ( a ) { … }
其含义就是, 在已进入函数体, 在所有操作开始之前( 预解析之前 )就有了该变量的声明.
变式
(function ( a ) { console.log( a ); var a = 10; console.log( a ); function a () { console.log( a ); } a(); })( 100 );
解析:
直接调用
进入函数中,已有声明,且值为 100
在函数内部预解析,函数声明有两个部分
(1)让当前环境作用中,有变量 a 可以使用,但不需要,因为已有 a 的声明
(2)让 a 指向函数。相当于
```javascript var a; function a () {} ... ```
开始逐步执行每一句代码
1) 打印 a. 所以打印函数体
2) 赋值 a = 10
3) 打印 a, 打印出 10
4) 如果让 a 调用, 那么报错 error: a is not function
作用域链规则
什么是作用域链 ? 链指的就是访问规则
function foo() {
console.log( num );
} // 当前作用域没有 num 就往上一层找
function func () {
function foo() {
console.log( num );
}
foo();
} // 当前作用域上一层也没有 num 就往上上一层找
function F () {
function func () {
function foo() {
console.log( num );
}
foo();
}
func();
} // 当前作用域上一层的上一层还没有 num 就再往上找,直到全局...
... ...
由于这种一环套一环的访问规则,这样的作用域构成一个链式结构. 所以直接称其为作用域链.
作用域链是用来做变量查找的. 因此变量可以存储什么东西. 链中就应该有什么东西. 换句话说就是, 链里面存储的是各种对象. 可以将其想象成对象的序列( 数组 )
绘制作用域链的规则
- 将所有的 script 标签作为一条链结构,标记为 0 级别的链
- 将全局范围内, 所有的声明变量名和声明函数名按照代码的顺序标注在 0 级链中.
- 由于每一个函数都可以构成一个新的作用域链. 所以每一个 0 级链上的函数都延展出 1 级链.
- 分别在每一个函数中进行上述操作. 将函数中的每一个名字标注在 1 级链中.
- 每一条 1 级链中如果有函数, 可以再次的延展出 2 级链. 以此类推.
var num;
function foo() {
console.log( num );
var num = 123;
console.log( num );
function num() {}
}
var arr = [];
function func() {}
绘制作用域链图
分析代码的执行
当作用域链绘制完成后. 代码的的分析也需要一步一步的完成.
- 根据代码的执行顺序( 从上往下, 从左至右 )在图中标记每一步的变量数据的变化
- 如果需要访问某个变量. 直接在当前 n 级链上查找变量. 查找无序
- 如果找到变量, 直接使用. 如果没有找到变量在 上一级, n - 1 级中查找.
- 一直找下去, 知直到 0 级链. 如果 0 级链还没有就报错. xxx is not defined.
作用域绘图 分析 1
var num;
function foo () {
console.log( num ); // 函数体
var num = 123;
console.log( num ); // 123
function num () {}
}
var arr = [];
function func () {}
foo();
作用域绘图 分析 2
var num = 123;
function f1 () {
console.log( num ); // 456
}
function f2 () {
console.log( num ); // => 123
num = 456;
f1();
console.log( num ); // 456
}
f2();
经典面试题
// console.log( i ); undefined
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 );
};
}
// 可以打印
// for ( var i = 0; i < arr.length; i++ ) {
// arr[ i ].sayHello();
// }
// 报错 Uncaught TypeError: Cannot read property 'name' of undefined(…)
for ( var j = 0; j < arr.length; j++ ) {
arr[ j ].sayHello();
}
为什么 i 可以? j 就报错呢?
解析:
用 j 做 for 循环时
- 在整个过程中, i 是全局变量
- 执行 for 循环
- i = 0, arr[ 0 ] 绑定的是 arr[ i ] 并未执行
- i = 1, arr[ 1 ] 绑定的也是 arr[ i ] 并未执行
- i…
- i++ => i === 4 不再小于 4( arr.length ) 跳出循环, 注意此时 i = 4
- 接下来调用 arr[ j ].sayHello(); 执行 sayHello(), 请注意,此时 sayHello 自己函数作用域中没有 i,往全局去找,全局中 i 值为 4,所以此时打印 arr[ 4 ].name -> 报错,所以不行了
而用 i 做 for 循环时,实际是每次循环都将全局的 i 进行重新赋值了
面试题出处
改所有的 目录提供点击事件, 在点击后 弹出目录中的文本内容
<body>
<ul>
<li>目录 1 </li>
<li>目录 2 </li>
<li>目录 3 </li>
<li>目录 4 </li>
<li>目录 5 </li>
</ul>
</body>
<script>
// 改所有的 目录提供点击事件, 在点击后 弹出目录中的文本内容
var list = document.getElementsByTagName( 'li' );
var i;
for ( i = 0; i < list.length; i++ ){
list[ i ].onclick = function () {
console.log( i ); // 页面中 点击时,i 已经是 5 了
// 打印出当前 li 中的 文本
alert( list[ i ].innerHTML ); // 点击时永远是 list[ 5 ].innerHTML,所以报错
// 正确写法 - > 打印当前 li 文本
// alert( this.innerHTML );
};
}
</script>