JavaScript预编译、作用域


( 部分蓝色字体可点击跳转,查看对应叙述)

预编译

预编译指的是在函数执行之前系统会浏览一遍代码,对一些低级语法错误会报错,并且在这个过程中会产生程序执行的环境(比如变量声明提升,函数整体提升等)
预编译的效果:

预编译会将函数声明整体提升到函数调用之前。即使函数调用在前,声明在后代码也可正常执行。

预编译会将变量声明提升到使用之前,但变量赋值仍然会在使用之后,比如下面代码执行结果,第一次输出为undefined因为预编译只提升了变量的声明(此时存值为undefined),并没有赋值。

<script>
        console.log(a);
        var a = 12;
        console.log(a);
    </script>
预编译前奏:(发生在函数执行前)
  • 任何变量未经声明就赋值,则此变量为全局对象(window)所有。
  • 一切声明的全局变量全是window的属性。
    总结:window就是全局的域
  • var a = 123;相当于window { a:123; },我们在访问a时会到window中寻找有没有对应的属性。
    【例】此处b便是全局对象的属性

    <script>
            function test()
                {
                    var a = b = 123;
                }
                test();
        </script>
    

    再来反过来看预编译四部曲(只是一个执行顺序,重在最终的结果)

    1. 创建AO Activation Object(不重要)(执行期上下文,理解为作用域)

    	AO {
    	            
    	        }
    

    2. 找形参和变量声明,将变量和形参名作为AO属性名,值为undefined(注意此处虽然有a函数和a变量,但只出现一次)

    	AO {
    	            a : undefined;
    	            b : undefined;
    	        }
    

    3. 将实参值和形参统一

    	AO {
    	            a : 2;
    	            b : undefined;
    	        }
    

    4. 在函数体里面找到函数声明,值赋予函数体

    	AO {
    	            a : function a(){};//由2变为函数体
    	            b : undefined;
    	            d : function d(){}
    	        }
    

    结合以上可以判断以下代码输出

    <script>
            function fn(a){
               console.log(a);
                var a = 123;
                console.log(a);
                function a(){}
                console.log(a);
                var b = function(){} //这是函数表达式,不是声明
                console.log(b);
                function d(){}
            }
            fn(2);
        </script> 
    

    解析
    在预编译完成之后才会执行代码,首先到 AO {}中找到 a的值,为 ƒ a(){},然后到 var a = 123;,注意这里不会再声明 a,因为预编译已经使得 变量声明提升了,所以只会对 a赋值 123,所以第二项输出123,接下来 function a(){}忽略,因为预编译已经 该函数声明整体提升了,故输出仍然时123,最后输出 b变量,值为其函数体
    【练习】以下代码会怎么输出?

    <script>
            function fn(a , b){
                console.log("a="+a);
                console.log("c="+c);
                c = 0;
                console.log("c="+c);
                var c;
                a = 3;
                console.log("b="+b);
                b = 2;
                console.log("a="+a);
                console.log("b="+b);
                function b(){}
                function d(){}
                console.log("b="+b);
            }
            fn(2);
        </script> 
    
    同时需注意此处
    说明了预编译不仅发生在函数内部,同时也放生在全局,但不一样的地方在于生成的不是AO对象,而是 GO
    (Global Object, 实际上就是window,并且GO一定在AO之前生成)
    对象,同时没有预编译的 第三步,因为它没有实参传值,回到 此处,它实际的存储是这样的
    	AO {
                a:undefined
            }
            
            GO {
                b:123
            }
    

    可用如下代码验证

    function test()
                {
                    console.log("1a="+a);
                    console.log("2window.a="+window.a);
                    
                    console.log("3window.b="+window.b);
                    var a = b = 123;
                    console.log("4a="+a);
                    console.log("5window.a="+window.a);
                    console.log("b="+b);
                    console.log("6window.b="+window.b);
                }
                test();
                console.log("7window.a="+window.a);
                console.log("8b="+b);
                console.log("9window.b="+window.b);
    

    【例1】以下两份代码如何输出?

    console.log("1="+test);
    
            function test(test){
    
                console.log(test);
                var test = 345;
                console.log(test);
                function test(){}
            }
            test(1);
            var test = 123;
    

    可将第二份代码tr()中的变量声明去掉,看看又是如何输出,会让你对GO和AO的关系理解更深

    var a = 123;
            
            function tr(){
                console.log(a);
                var a = 23;
                console.log(a);
                }
                tr();
    

    【例2】以下代码先输出undefined,在输出200,300。

    		ad = 100;
            function fb(){
                console.log(ad);
                ad = 200;
                console.log(ad);
                var ad = 300;
                console.log(ad);
            }
            fb();
            var ad;
    

    作用域

    作用域是啥?

    作用域是根据名称查找变量的一套规则。负责收集并维护有所有声明的标识符(变量)组成的一系列查询,并实施一套严格规则,确定当前执行的代码对这些标识符的访问权限。

    在预编译的基础上

    • [[scope]]:每个javascript函数都是一个对象,对象中有些属性我们可以访问,但有些不可以,这些属性仅供javascript引擎存取,[[scope]]就是其中一个。[[scope]]指的就是我们所说的作用域,其中存储了执行期上下文的集合。
    • 作用域链:[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。
    • 运行期上下文:当函数执行时,会创建一个称为执行期上下文的内部对象。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,执行期上下文被销毁。
    • 查找变量:从作用域链的顶端依次向下查找
    结合以下代码理解
    function a() {
            function b() {
                var b = 234;
            }
            var a = 123;
            b();
        }
        var glob = 100;
        a();
    

    在这里插入图片描述

    function a(){
            function b(){
                function c(){
                }
                c();
            }
            b();
        }
        a();
    

    以下为预编译顺序

    a定义:a.[[scope]]——> 0 : GO
    a执行:a.[[scope]]——> 0 : a.AO
                                 ——> 1 : GO

    在a的基础上

    b定义:b.[[scope]]——> 0 : a.AO
                                 ——> 1 : GO
    b执行:b.[[scope]]——> 0 : b.AO
                                 ——> 1 : a.AO
                                 ——> 2 : GO

    在a、b的基础上

    c定义:a.[[scope]]——> 0 : b.AO
                                 ——> 1 : a.AO
                                 ——> 2 : GO
    c执行:a.[[scope]]——> 0 : c.AO
                                 ——> 1 : b.AO
                                 ——> 2 : a.AO
                                 ——> 3 : GO

    值得强调的是

    • 查找变量时,会在当前函数的作用域链中从顶端开始查找,从a的AO到a的GO
    • 在a执行时会定义b,b在执行时会在a的作用域链的基础上在创建自己的执行器上下文,这里b所用的只是a创建时的作用域链的引用,所以说他们是同一个东西,在一个地方改变,另一个地方也会改变
    • 每次调用函数都会创建一个独一无二的执行期上下文,执行完毕就会销毁执行期上下文

    附:关于作用域的补充

    取自《你不知道的JavaScript》上卷

    LHS查询:变量出现在赋值操作符左侧,试图找到变量的容器本身,从而对其进行赋值
                   ——>赋值操作的目标是谁?

    RHS查询:变量出现在赋值操作符右侧(理解为非左侧),试图取得变量的源值
                   ——>谁是赋值操作的源头(要把谁赋给变量)?

    【例】此处都有什么查询?

    function foo (a) {
                console.log(a);
            }
            foo(2);
    
    foo引用,找到函数体RHS
    找到a的源值,传给console.log()RHS
    对a进行查询,查看它传参操作的目标(foo的形参a)LHS
    对console查询RHS
    对console对象进行查询,检查是否有log方法RHS
    foo(2)函数传参,即为赋值LHS
    假设log()函数的原生实现中可接受参数,在将2赋值给第一个参数(实参列表某一位)之前,需进行引用查询LHS

    【例】如下代码有几处查询?

    function foo(a) {
                var b = a;
                return a + b;
            }
            var c = foo(2);
    
    共有七处3LHS、4RHS
    把返回的结果赋给cLHS
    foo(2)传参(隐式赋值)LHS
    b=a;LHS
    查询foo函数RHS
    =a;需要找到a的值RHS
    a+ 需要找到a的值RHS
    +b;需要找到b的值RHS
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值