JAvascript执行机制进阶,深入底层理解执行上下文,调用栈,以及词法环境

324 阅读8分钟

变量提升到底是什么?它是好是坏呢?

在JavaScript中,变量提升是一个有趣且有时令人困惑的概念。它源于JavaScript的编译过程,在代码执行前的短暂编译阶段,JavaScript引擎会将变量声明和函数声明提升到它们所在作用域的顶部。这意味着,无论你在代码的何处声明变量或函数,它们都会被视为在作用域的最开始处声明。

那么变量提升到底是好是坏呢?

来看这段代码就知道了

console.log(a,fun);
console.log(b);
var a=1;
function fun(){
}
let b=2;

亲自试试-> 可以看出a的打印结果为:undefined

其实可以看出在a变量还没声明之前就打印其值是有问题的应该直接报错的但由于变量提升的存在,程序并没有报错而是打印undefined。 也就是说变量提升是一个坏问题而在最新的es6中引入了块级作用域来解决这个坏问题。 在 ES6(ECMAScript 2015)中,letconst 引入了对变量声明的新的语法和行为,替代了旧有的 var 声明方式。它们解决了 var 在作用域、提升、以及变量可变性方面的一些问题。下面是对 letconst 的详细解释:

1. letconst 的基本区别

  • let:声明一个可以被重新赋值的变量。
  • const:声明一个常量,声明后不能重新赋值

块级作用域

  • let 声明的变量只在其所在的代码块内有效(即包括花括号 {} 中的部分),而 var 声明的变量是函数作用域的,可以在函数的任何地方访问。

    if (true) {
        let a = 10;
        console.log(a);  // 10
    }
    console.log(a);  // ReferenceError: a is not defined
    
  • 不提升(No Hoisting)let 声明的变量不会被提升到代码块顶部。虽然 let 变量在物理上也会被解析器提前,但在访问之前会处于一个“暂时性死区”(TDZ,Temporal Dead Zone),访问时会抛出错误。

console.log(a);  // ReferenceError: Cannot access 'a' before initialization
let a = 10;

不允许重复声明:在同一个作用域内,不能重复声明同一个变量。

let x = 10;
let x = 20;  // SyntaxError: Identifier 'x' has already been declared

const 的特点

let 一样,const 也具有块级作用域

if (true) {
const pi = 3.14;
console.log(pi); // 3.14
}
console.log(pi); // ReferenceError: pi is not defined
  • 常量const 声明的变量不能被重新赋值。一旦定义,它的值就不能再修改。

    const x = 10;
    x = 20;  // TypeError: Assignment to constant variable.
    
  • 必须初始化const 声明时必须立即进行初始化,否则会抛出错误。

    const y;  // SyntaxError: Missing initializer in const declaration
    
  • 与对象/数组的不可变性const 确保的是变量绑定的不可变性,而不是对象或数组内容的不可变性。对于对象或数组的值仍然可以修改(例如修改对象的属性或数组的元素)。

    const obj = { a: 1 };
    obj.a = 2;  // 允许,obj 仍然指向相同的对象
    console.log(obj.a);  // 2
    obj = {};  // TypeError: Assignment to constant variable.
    
    const arr = [1, 2, 3];
    arr.push(4);  // 允许
    console.log(arr);  // [1, 2, 3, 4]
    arr = [5, 6];  // TypeError: Assignment to constant variable.
    

执行上下文与调用栈

在 JavaScript 中,执行上下文有三种主要类型:

  1. 全局执行上下文:这是代码执行的默认上下文。整个 JavaScript 程序从全局上下文开始运行。全局上下文只有一个,并且它会在页面加载时被创建。
  2. 函数执行上下文:每当一个函数被调用时,都会为该函数创建一个新的执行上下文。这个上下文包含了该函数的局部变量、函数参数以及其他的执行环境信息。
  3. eval 执行上下文:当代码通过 eval 函数执行时,也会创建一个执行上下文,但这种情况较少使用。

在代码的执行前,也就是短暂的编译阶段,执行上下文将经历创建阶段。在创建阶段会发生三件事:

  1. this值的绑定。
  2. 创建词法环境。
  3. 创建变量环境。

屏幕截图 2024-11-28 171639.png

词法环境

词法环境表示一个变量、函数以及它们的作用域的环境。在每个执行上下文中,词法环境用于管理和存储变量以及函数声明的绑定。

词法环境由两部分组成:

  1. 环境记录(Environment Record) :存储变量和函数的实际值。它包含了当前作用域内所有变量和函数的映射。
  2. 外部词法环境的引用:指向外层执行上下文的词法环境。形成了作用域链,使得当前作用域可以访问外层作用域中的变量。简单来说就是当前词法环境可以访问其父级词法环境(作用域)。

在 JavaScript 中,词法环境的创建和管理与代码的书写位置紧密相关。也就是说,词法环境是在编译阶段就已经确定的,它决定了如何查找变量。每次执行函数时,都会为该函数创建一个新的词法环境,并且新的环境会“继承”外层环境,形成嵌套的作用域链。

词法环境的作用是确保变量和函数可以按预定的作用域规则正确地被访问和执行

变量环境

变量环境(Variable Environment)是 JavaScript 执行上下文中的一个重要概念,它是词法环境的一部分,专门负责存储和管理当前执行上下文中的所有变量和函数声明。

变量环境的构成

变量环境通常包含以下几个内容:

  1. 变量绑定:存储当前作用域内所有的变量、常量及其对应的值。
  2. 函数声明绑定:存储当前作用域内的所有函数声明。

如上所述变量环境也是一个词法环境,在ES6中,词法环境和变量环境的一个不同就是词法环境被用来存储函数声明和letconst的变量声明,而变量环境只用来存储var变量的声明。(就是我们上面说的ES6引入的块级作用域来解决变量提升这个坏问题)。

让我们来看点代码来理解一下吧!

function foo(){
    var a = 1;
    let b = 2;
    {
        let b = 3;
        var c = 4;
        let d = 5;
        console.log(a);
        console.log(b);
    }
    console.log(b);
    console.log(c);
    console.log(d);
    
}
foo();

上图 7e551a66e0b4610cf37e710964c3e602.png

上结果

image.png 怎么样都做对了吗? 嗯...1...3...2? 为什么是2呢? 来,看上面的foo函数的执行上下文中的词法环境有没有很熟悉?它看起来像什么结构?

调用栈 对了就是调用栈,其实在词法环境中会形成一个局部调用栈。

{
        let b = 3;
        var c = 4;
        let d = 5;
        console.log(a);
        console.log(b);
    }

也就是说当这个块级作用域执行结束之后其词法环境中声明的变量会被释放销毁。 所以第11行打印的结果为2,第13行打印显示为引用错误,d是未定义的被销毁了。

10f7c8c0439822760ec16908e76dc3ec.png

再来看一个例子吧

function Bar(){
  console.log(myname);
}
function foo(){
  var myname = "zhangsan";
  Bar();
  console.log(myname);
}
var myname = "lisi";
foo();

这段代码打印的结果是什么呢? image.png

嗯?嗯!为什么Bar()函数打印的结果为lisi?

829317bd0d0d03f3b100434fa07fe66d.png 前面说到过,执行上下文分全局和函数,当Bar()函数执行时它会先查询自己作用域中是否有myname变量,没有的话此时作用域链就工作了它会引用外部的词法作用域来查找变量,也就是bar()函数创建时所处的作用域来查找变量就找到了myname=lisi;

结论

在JavaScript中,变量提升是一个有趣且有时令人困惑的概念。它源于JavaScript的编译过程,在代码执行前的短暂编译阶段,JavaScript引擎会将变量声明和函数声明提升到它们所在作用域的顶部。这意味着,无论你在代码的何处声明变量或函数,它们都会被视为在作用域的最开始处声明。

变量提升会影响代码的执行顺序。例如,如果你在声明变量之前尝试使用它,JavaScript不会抛出错误,而是会使用一个默认值undefined。这可能导致难以调试的错误,因为代码的行为可能与预期不符。

为了解决变量提升带来的问题,ES6引入了letconst关键字。与var不同,letconst声明的变量不会被提升到作用域的顶部。相反,它们被限制在声明它们的块级作用域内,并且在声明之前访问它们会导致错误。这被称为“暂时性死区”(Temporal Dead Zone,TDZ

还有就是词法环境的创建和销毁: 当函数被调用时,会创建一个新的词法环境。这个词法环境包含了函数的参数和局部变量,并且它的外部词法环境引用指向了调用函数时的词法环境。当函数执行完毕后,这个词法环境会被销毁,其中的变量和函数也会随之消失。 最后一个词法作用域(Lexical Scope),也称为静态作用域,是JavaScript中一种定义变量和函数作用域的方式。在词法作用域中,变量和函数的作用域是在代码编写时确定的,而不是在运行时确定的。这意味着,一个函数或变量的作用域取决于它在代码中被定义的位置,而不是它被调用的位置。

微信图片_20241128102721.jpg