logo MasterYi
湘ICP备19018432号

    如何理解JS变量函数提升和JS上下文问题

    更新时间: 2022-08-16 17:29:08
    点击量: 96
    标签: 前端js

    简介:众所周知JS是单线程、解释性语言(由解释器解释一句执行一句) 讲到js变量提升&函数提升就不得不提到js的编译运行机制来解答这个问题

    文章均为个人原创, 搬运请附上原文地址感谢, 原文来自MasterYi博客

    前言

    • 推荐先看 JS作用域 :文案功底有限怕结合解释引起理解的混乱, 上下文和作用域是息息相关的, 作用域决定了当前执行上下文的取值范围
    • 众所周知JS是单线程、解释性语言(由解释器解释一句执行一句)
    • 讲到js变量提升&函数提升就不得不提到js的编译运行机制来解答这个问题

    js的编译运行机制

    1. 进行语法分析,检查整体代码有无语法上的错误
    2. 预编译过程, 这个过程便会去创建我们AO、GO对象 (此过程便会产生所谓的变量提升、函数提升问题)
    3. 最后解释性执行, 读一句执行一句,也就是我们熟知的从上至下,从左至右

    预编译过程

    • 此过程属于执行前置过程, 预编译又会分为全局 & 函数体局部, 也就是我们熟知的 GO全局上下文 、AO 执行上下文
    • 任何变量,如果未声明就赋值,那该变量为全局变量,即暗示全局变量(imply global)。并且所有的全局变量都是window的属性

    GO对象 (全局上下文)

    • 创建global object
    • 全局上下文也 等同于 window 对象
    • 将var 变量声明的变量名当做GO对象的属性名,值为undefined (变量提升)
    • 将声明函数的函数名当做GO对象的属性名,值为函数体 (函数提升)
    • 页面关闭后销毁

    AO对象 (执行上下文)

    • 函数体执行时预编译创建AO对象, 每次创建对应执行的AO对象都会是独一无二的。
    • 将var 变量声明的变量名当做AO对象的属性名,值为undefined (变量提升)
    • 将声明函数的函数名当做AO对象的属性名,值为函数体 (函数提升)
    • 在执行读取变量的顺序,从作用域的顶端依次往下找 (栈结构)
      1、 优先读取当前AO(栈顶) ->
      2、 当前AO有则返回 无则继续往下读 ->
      3、 以此往复读取AO, 直到读取栈底的GO ->
      4、 读取GO, 有则返回, 无则抛异常, 下方会有示例二会简单解释此问题
    • 函数执行结束后销毁

    两者区别

    • AO 和 GO 在预编译以及执行过程是类似的
    • AO 是在函数体执行时创建再进行预编译,解释性执行
    • AO 执行中的var 赋值不会被加入window对象中

    示例一

    • 变量提升示例
    console.log(demo); // undefined 此处打印undefined 预编译过程中加入GO 赋值 undefined
    var demo = 'demo'; // 解释执行时赋值 demo
    console.log(demo, window.demo); // 打印 demo, demo . var声明的变量会加入window

    示例二

    • 变量提升示例
    var a = 1;
    function test(){
        console.log(a); // 打印 undefined 是因为在test执行时创建了AO, AO在预编译时加上了a属性赋值undefined
        // 在执行读取变量的顺序 读AO -> AO有则返回无则 -> 读取GO, 有则返回, 无则抛异常
        // 所以在这里打印的a的过程: 访问AO -> 读到了 a -> 返回a 所以打印了 undefined , 因为在当前AO中找到了就不去再往执行栈中寻找这个值
        var a = 2;
        console.log(a); // 打印2 解释执行时赋值了2所以打印2
    }
    test();

    示例三

    • es6新增的块级作用域对执行过程的影响
    • 变量提升示例
    var a = 1;
    if(true){
        console.log(a); // 执行1 
        var a = 2;
        console.log(a); // 执行2  var没块级作用域所以很正常
    }
    
    let b = 1;
    if(true){
        /**
         * let和const不会有变量提升 抛出致命错误Uncaught ReferenceError: Cannot access 'b' before initialization , b没被声明就进行了操作,
         * 大家会感到奇怪的一个点, 外部明明声明了let b, 我这里使用应该打印1才对, 因为js执行时认定的你的块级作用域里面存在b的声明, 但是你在声明执行前进行了使用所以会报错
         * 两种情况就不会报错, 不在let b = 2;前进行引用  或者 不进行let b = 2; 的声明
         */
        console.log(b);
        
        let b = 2; // 假设此处不声明都会正常打印1, 此处声明的同时上方引用就会引发异常
        console.log(b); // 上方使用去掉这里会正常打印2 
    }

    示例四

    • 函数提升示例
    testA(); // 打印A 函数提升
    console.log(testB) // 打印undefined testB使用的函数表达式声明, 变量虽然提升了, 但赋值得等执行阶段
    
    function testA(){
        console.log(`A`)
    }
    
    var testB = function (){
        console.log(`B`)
    }
    testB(); // 打印B  赋值为了 function 所以可以执行, 表达式在赋值之前不可作为函数执行 ( 未赋值前都是undefined ) 会抛出致命错误

    示例五

    • 函数变量提升的优先级示例
    • 详细执行过程
    test(); // B  执行test 不会报错因为函数在预编译过程便会加入GO对象, 函数名当做GO对象的属性名,值为函数体
    
    function test(){
        console.log(`A`)
    }
    test(); // B  
    
    function test(){
        console.log(`B`)
    }
    
    var test = function (){
        console.log(`C`)
    }
    test(); // C 此处执行打印C 因为 test 进行赋值, 并覆盖了之前的 B
    
    var test = function (){
        console.log(`D`)
    }
    test();  // D 此处执行打印D 因为 test 进行赋值, 并覆盖了之前的 C
    
    /**
     * 第五个例子的执行过程  (之所以写这种看着很憨的重复代码是为了大家更好的理解 js的真实执行过程, 避免遇到疑难杂症时摸不透这玩意咋变成这样了)
     * 1. 预编译阶段:将testC 加入 GO  赋值undefined
     * 2. 预编译阶段:将testD 加入 GO  赋值undefined  覆盖testC
     * 3. 预编译阶段:将testA 加入 GO  赋值函数体 覆盖 testD
     * 4. 预编译阶段:将testB 加入 GO  赋值函数体 覆盖 testA
     * 5. 执行第一个 test (testB) 打印B
     * 6. 执行第二个 test (testB) 打印B, 此处可能大家会有一个疑点,解释性语言解释到到了testA 为什么执行还是B, 因为全局函数的赋值在预编辑阶段就结束了, 后续再出现也不会覆盖
     * 7. 执行 test = functionC 的赋值并覆盖GO对象中原来的 testB
     * 8. 执行第三个 test (testC) 打印C
     * 9. 执行 test = functionD 的赋值并覆盖GO对象中原来的 testC
     * 10. 执行第四个 test (testC) 打印D
     */

    JS执行栈管理

    • 程序执行进入一个执行环境时,它的AO就会被创建,并被推入执行栈中(入栈);程序执行完成时,它的执行上下文就会被销毁,并从栈顶被推出(出栈),控制权交由下一个执行上下文。
    • 全局上下文只有一个处于栈底,页面关闭时出栈
    • 函数执行上下文可存在多个,但也会存在堆栈溢出的问题 (常见于死循环的递归)

    结尾

    • 经历上方的解释大家应该能更好的看待所谓的变量提升,函数提升问题了
    • 执行栈的话上方只是简单的解释, 会配合所谓的异步问题 单独写上一篇