您的位置:首页 > Web前端 > JavaScript

4 js面向对象基础 - 预解析,词法作用域,作用域链

2016-11-01 20:31 399 查看

代码的预解析

预解析 分为 预 和 解析,指 提前的翻译解释, 在运行代码之前的一个解释.

为什么需要它?可以尽可能提高执行效率。

编译型语言: 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>
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  javascript 面向对象