logo头像

孙宇

javascript 面向对象:4

本文于974天之前发表,文中内容可能已经过时。

  1. 复习
    加入我希望所有的函数都具有 inherit 功能

    function Person() {}
    
    var Student = Person.inherit( 成员 );
    // 生成函数 Student, 但是 Student 里面的成员( this.XXX )
    // 由参数来提供, 而 Student 的对象, 继承自 Person 的对象
    

    如果希望所有的函数都有 inherit 方法, 那么可以给 Function.prototype 添加该方法

  1. 代码的预解析
    -> 预解析

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

    -> 为什么需要它

    -> 编译型语言: C, C++, C#, Java
        就是需要一个 "翻译" 程序, 将源代码翻译成计算机可以读懂的二进制数据( 指令 ).
        然后存储成可执行文件. 
    
        提前翻译好, 运行时直接执行得结果
    
    -> 解释型( 脚本型 ): JavaScript, SQL, ...
        代码在执行的时候, 有一个翻译程序, 读一句代码执行一句代码. 再读一句代码, 
        再执行一句代码.
    
        一句一句的翻译执行. 每次运行都需要翻译一次.
    
    -> 代码在执行之前, 需要快速的 "预览" 一遍. 那么可以尽可能提高执行效率.
    
  2. 在 js 中预解析的特点
    -> 代码是如何执行的: 读取 js 文本, 预解析, 一句一句地执行
    -> js 在预解析的过程中完成了声明部分的标记与变量作用域的设定

  3. 什么是 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 紧跟的名字是否已经标记了
    -> 如果没有标记, 就表明这个名字是一个标识符, 需要被标记
    -> 如果已经被标记了, 那么 这个 var 被忽略
    
    结论:
        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 都会被自动的忽略.
    

    -> 变量名提升

    -> 函数的声明

    -> 函数的各种定义形式
        -> 声明式:    
            function func () {
                console.log( '使用声明式定义' );
            }
        -> 表达式式( 匿名函数, 字面量函数, lambda 函数 ):
            var func = function () {
                console.log( '使用表达式式定义' );
            };
        -> ... 
    -> 特点:
        1> 函数的声明是独立于语句. 不需要加分号结束. 也不能嵌入到代码表达式中.
        2> 表达式式, 本质上是使用函数表达式( 字面量 )给变量赋值. 因此它是语句.
    
    -> 表达式:
        -> 将运算符与操作数连接起来的式子.
        -> 就是一个有结果的代码单元( 不包括语句 )
            var a;        // 声明, 不是语句, 也没有结果
            123            // 字面量, 有值, 是表达式. 是常量表达式
            a = 123        // 赋值, 有值, 就是被赋值的那个值. 是赋值表达式.
    
            function () {}
    
  4. 如果将变量的声明与函数的声明放在一起有些需要注意的情况
    1) 函数的声明实际上包含两部分

    1> 告诉解释器 xxx 名字已经可以使用( 函数名, 标识符 )
    2> 告诉解释, 这个名字代表着一个函数( 变量里存储着函数的引用 )
    

    2) 当函数声明与变量声明冲突的时候. 只看谁先有数据.

  5. 一个在新版本的浏览器中的特性
    if ( true ) {

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

    } else {

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

    }
    foo();

  6. 词法作用域
    -> 作用域: 就是变量可以使用到不能使用的范围
    -> 块级作用域:

    -> 块: 代码块, 即 {  }
    -> 变量的使用从定义开始, 到其所在的块级作用域结束
        // 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 的词法作用域, 就是根据预解析规则定义变量的使用范围, 全部代码中
        只有函数可以限定范围. 其他均不能限定访问范围. 在内部是一个独立的作用范围结构.
    
    -> 结论:
        词法作用域就是描述变量的访问范围:
        1> 在代码中只有函数可以限定作用范围. 允许函数访问外部的变量. 反之不允许.
        2> 在函数内优先访问内部声明的变量, 如果没有才会访问外部的.
        3> 所有变量的访问规则, 按照预解析规则来访问
    
  7. 案例
    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.
    
任务:
    var num = 123;
    function f1 () {
        console.log( num );
    }
    function f2 () {
        console.log( num );   // => 123 , 456, 456
        num = 456;            
        f1();
        console.log( num );
    }
    f2();
  1. 案例
    (function ( a ) {

    console.log( a );
    var a = 10;
    console.log( a );
    

    })( 100 );

    拆解
    ( 函数 ) ( 100 )
    第一个圆括号就是将函数变成表达式
    后面一个圆括号就是调用该函数

    var func = function ( a ) {

    console.log( a );
    var a = 10;
    console.log( a );
    

    }
    func( 100 );

    注意: 函数定义参数, 实际上就是在函数最开始的时候, 有一个变量的声明
    function ( a ) { … }
    其含义就是, 在已进入函数体, 在所有操作开始之前( 预解析之前 )就有了该变量的声明.

    由于已经有了 a 参数的声明. 所以在代码中 var a = 10 是重复声明. 其声明无效.
    所以上面的代码, 等价于
    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
    
  2. 作用域链规则
    -> 什么是作用域链

    链指的就是访问规则
    function foo() {
        console.log( num );
    }
    
    --------------------
    function func () {
        function foo() {
            console.log( num );
        }
        foo();
    }
    
    --------------------
    function F () {
        function func () {
            function foo() {
                console.log( num );
            }
            foo();
        }
        func();
    }
    ... ...
    
    由于这种一环套一环的访问规则, 这样的作用域构成一个链式结构. 所以直接称其为作用域链.
    

    -> 作用域链是用来做变量查找的. 因此变量可以存储什么东西. 链中就应该有什么东西.

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

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

  5. 经典面试题

  6. 经典面试题的变式( 练习 )
    写一段代码, 生成 5 个 a 标签. 要求在 a 标签中分别显示出 1, 2, 3, 4, 5
    即最后得到 12345

  7. 闭包的概念
    -> 字面意义

    闭: 关闭, 封闭
    包: 包裹, 打包
    闭包的含义就是一个被包裹的隔离的空间.
    

    -> 在 js 中, 什么是闭包

    在 js 中函数是一个具有变量作用域隔离特性的一个内存结构, 即为一个闭包.
    
    function foo () {
    
    }
    
  8. 学习闭包, 在 js 中到底要解决什么问题
    在 js 中闭包要解决的问题就是间接的访问到这个被隔离的数据.

    function foo () {

    var num = 123;
    

    }

    // 在外界访问到 num 中的数据. 怎么做?

  1. 函数, 在 js 中与普通的对象具有一样的意义
    -> 函数可以像变量一样使用

    -> 赋值
    -> 传递
    
  2. 闭包的间接访问
    -> 使用 return 数据不能直接访问原来的数据, 那么可以考虑利用函数的返回访问原始数据

    function foo () {

    var num = 123;   // 原始数据
    
    function func () {
        return num; // 它就是原始数据 num
    }
    
    return func;
    

    }

19,当函数声明语法嵌入表达式环境中,会自动进行转换,将转换成函数表达式。
1)引用函数的规则还是使用变量赋值,所以外部可以使用该名字调用函数。
2)函数表达式带有名字,该名字只允许在函数内部使用。属于局部作用域。
3)带有名字的函数表达式,函数的name属性即为该名字。