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

来源:互联网 发布:新西兰移民 知乎 编辑:程序博客网 时间:2024/06/06 13:16

代码的预解析

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

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

  • 编译型语言: 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;

变量名提升

  1. 读取所有的代码( 字符串 ). 包含每一个字节, 每一个数据. 但是 “只留意” var

  2. 判断 var 后面紧跟的名字是否被标记. 如果没有, 则标记上.

    表示在当前环境中已经有该变量了. 如果已标记, 则忽略.

  3. 读取完毕后, 代码再从头开始, 从上往下, 从左至右一句一句的执行代码.

    执行 ‘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. 表达式式, 本质上是使用函数表达式( 字面量 )给变量赋值. 因此它是语句

    • 表达式: 将运算符操作数连接起来的式子.

    就是一个 有结果的代码单元( 不包括语句 )

    用操作符连接的一个式子 1+2 , 3-4, a=b, a instancof b 等…

    var a;      // 声明, 不是语句, 也没有结果    1234            // 字面量, 有值, 是表达式. 是常量表达式    a = 1234        // 赋值, 有值, 就是被赋值的那个值. 是赋值表达式.    function () {}

各种函数定义形式的异同

  1. 声明式: ( 重点是语法 )

    函数声明是独立于代码执行的. 代码在执行的时候, 声明部分已在预解析阶段处理完毕

    因此在代码调试阶段, 无法给函数声明添加断点. 而且由于预解析在执行之前完成,

    所以可以先调用, 后声明函数. 有时在开发的时候, 将函数全部声明在后面, 前面为了保证代码的紧凑, 而直接调用.

    func();    function func () {        console.log( '声明了一个函数' );    }
  1. 函数表达式

    使用这个方式定义函数, 实际上是利用函数是 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(); '报错'

如果将变量的声明与函数的声明放在一起有些需要注意的情况

  1. 函数的声明实际上包含两部分

    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]
  2. 当函数声明与变量声明冲突的时候. 只看谁先有数据.

    函数的声明与变量的声明意义多一层. 声明变量, 是告诉解释器当前环境可以使用该名字了

    而声明函数, 是告诉解释器, 除了可以使用该名字, 该名字还表示一个函数体.

    • 案例 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 只能在函数内部使用.

所以函数声明不要放在代码块,就算要在代码块中放函数,就放表达式,提高代码准确性

词法作用域

作用域: 就是变量可以使用到不能使用的范围

  1. 块级作用域:

    • 块: 代码块, 即 { }

    • 变量的使用从定义开始, 到其所在的块级作用域结束

          // 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, ...
  2. js 是词法作用域

    词法: 就是定义, 书写代码的规则.

    所以 所谓的 词法作用域, 就是 在书写代码的时候, 根据书写代码的结构,就可以确定数据的访问范围的作用域.

    js 不受 块的影响, 即使在块中定义声明变量, 在块的外面依旧可以使用

        console.log( num );  // => undefined    {        var num = 123;    }    console.log( num );  // => 123

    所谓的 js 的词法作用域, 就是根据预解析规则定义变量的使用范围, 全部代码中只有函数可以限定范围. 其他均不能限定访问范围. 在内部是一个独立的作用范围结构.

    结论: 词法作用域就是描述变量的访问范围。

    1. 在代码中只有函数可以限定作用范围. 允许函数访问外部的变量. 反之不允许

    2. 在函数内优先访问内部声明的变量,如果没有才会访问外部的

    3. 所有变量的访问规则,按照预解析规则来访问

        // 在没有函数的情况下,所有的变量访问规则依据预解析规则    // 只有函数可以限定作用域其他的不行    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(),进入foo2. 进入了foo函数,又开始了新一阶段的预解析,找 var 和 function,发现了 func,然后没了, 开始从上往下执行,执行 func(), 进入 func3. 进入了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 );

    解析:

    1. 直接调用

    2. 进入函数中,已有声明,且值为 100

    3. 在函数内部预解析,函数声明有两个部分

    (1)让当前环境作用中,有变量 a 可以使用,但不需要,因为已有 a 的声明

    (2)让 a 指向函数。相当于

    ```javascript    var a;    function a () {}    ...```
    1. 开始逐步执行每一句代码

      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 就再往上找,直到全局...    ... ...

由于这种一环套一环的访问规则,这样的作用域构成一个链式结构. 所以直接称其为作用域链.

作用域链是用来做变量查找的. 因此变量可以存储什么东西. 链中就应该有什么东西. 换句话说就是, 链里面存储的是各种对象. 可以将其想象成对象的序列( 数组 )

绘制作用域链的规则

  1. 将所有的 script 标签作为一条链结构,标记为 0 级别的链
  2. 将全局范围内, 所有的声明变量名和声明函数名按照代码的顺序标注在 0 级链中.
  3. 由于每一个函数都可以构成一个新的作用域链. 所以每一个 0 级链上的函数都延展出 1 级链.
  4. 分别在每一个函数中进行上述操作. 将函数中的每一个名字标注在 1 级链中.
  5. 每一条 1 级链中如果有函数, 可以再次的延展出 2 级链. 以此类推.
    var num;    function foo() {        console.log( num );        var num = 123;        console.log( num );        function num() {}    }    var arr = [];    function func() {}

绘制作用域链图

绘制作用域链的规则

分析代码的执行

当作用域链绘制完成后. 代码的的分析也需要一步一步的完成.

  1. 根据代码的执行顺序( 从上往下, 从左至右 )在图中标记每一步的变量数据的变化
  2. 如果需要访问某个变量. 直接在当前 n 级链上查找变量. 查找无序
  3. 如果找到变量, 直接使用. 如果没有找到变量在 上一级, n - 1 级中查找.
  4. 一直找下去, 知直到 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 循环时

    1. 在整个过程中, i 是全局变量
    2. 执行 for 循环
      • i = 0, arr[ 0 ] 绑定的是 arr[ i ] 并未执行
      • i = 1, arr[ 1 ] 绑定的也是 arr[ i ] 并未执行
      • i…
      • i++ => i === 4 不再小于 4( arr.length ) 跳出循环, 注意此时 i = 4
    3. 接下来调用 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>
1 0
原创粉丝点击