• js-作用域-变量申明提升


    作用域

    域,表示的是一个范围,作用域,就是作用范围。

    作用域说明的是一个变量可以在什么地方被使用,什么地方不能被使用。

    块级作用域

    JavaScript中没有块级作用域

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

    上面这段代码在JavaScript中是不会报错的,但是在其他的编程语言中(C#、C、JAVA)会报错。

    这是因为,在JavaScript中没有块级作用域,使用{}标记出来的代码块中声明的变量num,是可以被{}外面访问到的。

    但是在其他的编程语言中,有块级作用域,那么{}中声明的变量num,是不能在代码块外部访问的,所以报错。

    词法作用域

    什么是词法作用域?

    词法( 代码 )作用域, 就是代码在编写过程中体现出来的作用范围. 代码一旦写好, 不用执行, 作用范围就已经确定好了. 这个就是所谓词法作用域.

    在 js 中词法作用域规则:

    • 函数允许访问函数外的数据.

    • 整个代码结构中只有函数可以限定作用域.

    • 作用域规则首先使用提升规则分析

    • 如果当前作用规则中有名字了, 就不考虑外面的名字

     

    JavaScript 预解析

    JavaScript引擎在对JavaScript代码进行解释执行之前,会对JavaScript代码进行预解析,在预解析阶段,会将以关键字varfunction开头的语句块提前进行处理。

    关键问题是怎么处理呢?

    当变量和函数的声明处在作用域比较靠后的位置的时候,变量和函数的声明会被提升到作用域的开头。

    重新来看上面的那段代码

    func();
    function func(){
         alert("Funciton has been called");
    }
    

    由于JavaScript的预解析机制,上面的代码就等效于:

    function func(){
        alert("Funciton has been called");
    }
    func();
    

    看完函数声明的提升,再来看一个变量声明提升的例子:

    alert(a);
    var a = 1;
    

    由于JavaScript的预解析机制,上面这段代码,alert出来的值是undefined,如果没有预解析,代码应该会直接报错a is not defined,而不是输出值。

    Wait a minute, 不是说要提前的吗?那不是应该alert出来1,为什么是undefined?

    那么在这里有必要说一下声明定义初始化的区别。其实这几个概念是C系语言的人应该都比较了解的。

    行为说明
    声明 告诉编译器/解析器有这个变量存在,这个行为是不分配内存空间的,在JavaScript中,声明一个变量的操作为:var a;
    定义 为变量分配内存空间,在C语言中,一般声明就包含了定义,比如:int a;,但是在JavaScript中,var a;这种形式就只是声明了。
    初始化 在定义变量之后,系统为变量分配的空间内存储的值是不确定的,所以需要对这个空间进行初始化,以确保程序的安全性和确定性
    赋值 赋值就是变量在分配空间之后的某个时间里,对变量的值进行的刷新操作(修改存储空间内的数据)

    所以我们说的提升,是声明的提升。

    那么再回过头看,上面的代码就等效于:

    var a; //这里是声明
    alert(a);//变量声明之后并未有初始化和赋值操作,所以这里是 undefined
    a = 1;
    

    复杂点的情况分析

    通过上一小节的内容,我们对变量、函数声明提升已经有了一个最基本的理解。那么接下来,我们就来分析一些略复杂的情况。

    函数同名

    观察下面这段代码:

    func1();
    function func1(){
         console.log('This is func1');
    }
    
    func1();
    function func1(){
         console.log('This is last func1');
    }
    

    输出结果为:

    This is last func1
    This is last func1
    

    原因分析:由于预解析机制,func1的声明会被提升,提升之后的代码为:

    function func1(){
         console.log('This is func1');
    }
    
    function func1(){
         console.log('This is last func1');
    }
    
    func1();
    func1();
    

    同名的函数,后面的会覆盖前面的,所以两次输出结果都是This is last func1

    变量和函数同名

    alert(foo);
    function foo(){}
    var foo = 2;
    

    当出现变量声明和函数同名的时候,只会对函数声明进行提升,变量会被忽略。所以上面的代码的输出结果为

    function foo(){}
    

    我们还是来吧预解析之后的代码展现出来:

    function foo(){};
    alert(foo);
    foo = 2;
    

    再来看一种

    var num = 1;
    function num () {
         alert( num );
    }
    num();
    

    代码执行结果为:

    Uncaught TypeError: num is not a function
    

    直接上预解析后的代码:

    function num(){
         alert(num);
    }
    
    num = 1;
    num();
    

    预解析是分作用域的

    声明提升并不是将所有的声明都提升到window对象下面,提升原则是提升到变量运行的环境(作用域)中去。

    function showMsg()
    {
        var msg = 'This is message';
    }
    alert(msg); // msg未定义
    

    还是直接把预解析之后的代码写出来:

    function showMsg()
    {
        var msg;
        msg = 'This is message';
    }
    alert(msg); // msg未定义
    

    预解析是分段的

    分段,其实就分script标签的

    <script>
    func(); // 输出 AA2;
    function func(){
        console.log('AA1');
    }
    
    function func(){
        console.log('AA2');
    }
    </script>
    
    <script>
    function func(){
        console.log('AA3');
    }
    </script>
    

    在上面代码中,第一个script标签中的两个func进行了提升,第二个func覆盖了第一个func,但是第二个script标签中的func并没有覆盖上面的第二个func。所以说预解析是分段的。

    tip:但是要注意,分段只是单纯的针对函数,变量并不会分段预解析。

    函数表达式并不会被提升

    func();
    var func = function(){
        alert("我被提升了");
    };
    

    这里会直接报错,func is not a function,原因就是函数表达式,并不会被提升。只是简单地当做变量声明进行了处理,如下:

    var func;
    func();
    func = function(){
        alert("我被提升了");
    }
    

    条件式函数声明

    console.log(typeof func);
    if(true){
        function(){
            return 1;
        }
    }
    console.log(typeof func);
    

    上面这段代码,就是所谓的条件式函数声明,这段代码在Gecko引擎中打印"undefined""function";而在其他浏览器中则打印"function""function"

    原因在于Gecko加入了ECMAScript以外的一个feature:条件式函数声明。

    Conditionally created functions Functions can be conditionally declared, that is, a function declaration can be nested within an if statement.

    Note: Although this kind of function looks like a function declaration, it is actually an expression (or statement), since it is nested within another statement. See differences between function declarations and function expressions.

    Note中的文字说明,条件式函数声明的处理和函数表达式的处理方式一样,所以条件式函数声明没有声明提升的特性。

    作用域链

    什么是作用域链

    只有函数可以制造作用域结构, 那么只要是代码,就至少有一个作用域, 即全局作用域。

    凡是代码中有函数,那么这个函数就构成另一个作用域。如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域。

    将这样的所有的作用域列出来,可以有一个结构: 函数内指向函数外的链式结构。就称作作用域链。

    例如:

    function f1() {
        function f2() {
        }
    }
    
    var num = 456;
    function f3() {
        function f4() {    
        }
    }
    

    绘制作用域链的步骤:

    1. 看整个全局是一条链, 即顶级链, 记为 0 级链

    2. 看全局作用域中, 有什么成员声明, 就以方格的形式绘制到 0 级练上

    3. 再找函数, 只有函数可以限制作用域, 因此从函数中引入新链, 标记为 1 级链

    4. 然后在每一个 1 级链中再次往复刚才的行为

    变量的访问规则

    • 首先看变量在第几条链上, 在该链上看是否有变量的定义与赋值, 如果有直接使用

    • 如果没有到上一级链上找( n - 1 级链 ), 如果有直接用, 停止继续查找.

    • 如果还没有再次往上刚找... 直到全局链( 0 级 ), 还没有就是 is not defined

    • 注意,同级的链不可混合查找

      •   变量的搜索原则:
        1.在使用变量的时候
        * 首先在所在的作用域中查找
        * 如果找到了 就直接使用
        * 如果没有找到 就去上级作用域中查找
        2.重复以上步骤
        * 如果直到0级作用域链也就是全局作用域还没有找到,报错

    练习:绘制作用域链

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

    如何分析代码

    1. 在分析代码的时候切记从代码的运行进度上来分析, 如果代码给变量赋值了, 一定要标记到图中
    2. 如果代码比较复杂, 可以在图中描述代码的内容, 有事甚至需要将原型图与作用域图合并分析

    练习

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

    补充

    声明变量使用`var`, 如果不使用`var`声明的变量就是全局变量( 禁用 )

    因为在任何代码结构中都可以使用该语法. 那么再代码维护的时候会有问题. 所以除非特殊原因不要这么用.

    下面的代码的错误

    function foo () {
        var i1 = 1 // 局部
        i2 = 2, // 全局
        i3 = 3; // 全局
    }
    

    此时注意

    var arr = [];
    for ( var i = 0; i < 10; i++ ) {
        arr.push( i );
    }
    
    for ( var i = 0; i < 10; i++ ) {
        console.log( arr[ i ] );
    }
    
    // 一般都是将变量的声明全部放到开始的位置, 避免出现因为提升而造成的错误
    var arr = [],
    i = 0;
    
    for ( ; i < 10; i++ ) {
        arr.push( i );
    }
    
    for ( i = 0; i < 10; i++ ) {
        console.log( arr[ i ] );
    }



    补充:
     
  • 相关阅读:
    supper 关键字
    self 关键字
    Setter/Getter方法
    0013.HBase进阶
    0012.HBase基础
    0011.MapReduce编程案例2
    0010.MapReduce编程案例1
    0009.Mapreduce的高级功能
    0008.MapReduce基础
    0007.HDFS上传与下载的原理
  • 原文地址:https://www.cnblogs.com/gshao/p/9561029.html
Copyright © 2020-2023  润新知