块级作用域:var缺陷以及为什么引入let和const

JS从在变量提升这个特性,从而导致了很多与直觉不符的代码,这是JS的一个重要设计缺陷。

ES6通过引入letconst关键字来避开这种设计缺陷,但是因为JS需要保持向下兼容(毕竟还存在古老版本的浏览器有人使用),所以变量提升整个特性依旧还存在。

一、作用域(scope)

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。可以理解为作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

在es6之前JS只有全局作用域和函数作用域;es6之后引入了块级作用域(因为JS的设计之初,并没有想到JS会这么火,所以很多设计都是按照最简单的方式来设计的,所以有挺多的bug,但是又一直存在)

  • 全局作用域:全局作用域中的变量、函数在代码中的任何地方都可以访问,其生命周期伴随着页面的生命周期
js 复制代码
var globalVar = 'I am a global variable'; 
function exampleFunction( ) { 
    console.log(globalVar); // 可以访问全局变量 
} 
exampleFunction(); // 调用全局函数
  • 函数作用域:在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部能被访问,函数执行结束之后,函数内部定义的变量会被销毁。
js 复制代码
function exampleFunction() {
    var localVar = 'I am a local variable';
  
    function innerFunction() {
      console.log('I am an inner function');
    }
  
    innerFunction(); // 可以在函数内部调用内部函数
  }
  
  exampleFunction();
  
  // 下面的代码将会报错,因为 localVar 和 innerFunction 在函数外部是不可访问的
  console.log(localVar);
  innerFunction();

在 JavaScript 中,内部函数(在外部函数内声明的函数)不会在外部函数执行完毕后被销毁。相反,内部函数会形成一个闭包,它可以访问外部函数的变量和参数,即使外部函数已经执行完毕。

js 复制代码
function outerFunction() {
  var outerVar = 'I am an outer variable';

  function innerFunction() {
    console.log(outerVar); // 内部函数可以访问外部函数的变量
  }

  return innerFunction; // 返回内部函数
}

var closure = outerFunction(); // 调用外部函数,得到内部函数的引用
closure(); // 调用内部函数

innerFunction 是在 outerFunction 内部声明的。当 outerFunction 被调用时,它返回了 innerFunction 的引用,并且将这个引用赋给了 closure。即使 outerFunction 执行完毕,closure 仍然可以访问 outerVar,因为它形成了一个闭包,保留了对外部函数作用域的引用。因此,内部函数的生命周期不仅仅限于外部函数的执行过程,而是由内部函数形成的闭包来决定。只有当没有对内部函数的引用时,它的内存才会被垃圾回收。

  • 块级作用域:块级作用域就是一对花括号包括的一段代码。

引入块级作用域的关键词有 letconst,它们允许变量在块级作用域中声明,而不是在函数作用域或全局作用域中。

使用 letconst 声明的变量在块级作用域内定义,它们在块级作用域外是不可访问的。这与使用 var 声明的变量在函数作用域中的行为不同,var 的作用域是函数作用域,而不是块级作用域。

js 复制代码
function exampleFunction() {
    if (true) {
        let blockScopedVar = 'I am a block-scoped variable';
        const constantVar = 'I am a constant variable';

        console.log(blockScopedVar); // 在块级作用域内可以访问
        console.log(constantVar); // 在块级作用域内可以访问
    }

    // 下面的代码将会报错,因为在块级作用域外无法访问 blockScopedVar 和 constantVar
    // console.log(blockScopedVar);
    // console.log(constantVar);
}

exampleFunction();

二、变量提升所带来的问题

1.变量容易在不被察觉的情况下被覆盖掉

js 复制代码
var x = 10;

function exampleFunction() {
    console.log(x); // 输出 undefined
    var x = 5;
    console.log(x); // 输出 5
}

exampleFunction();

在调用栈中有两个x变量,先使用函数执行上下文中的变量。

2.本应销毁的变量没有被销毁

js 复制代码
function foo() {
    for (var i = 0; i < 5; i++) {
        console.log("inner i ===>", i);
        // inner i ===> 0
        // inner i ===> 1
        // inner i ===> 2
        // inner i ===> 3
        // inner i ===> 4
    }
    console.log("outer i ===>", i); // outer i ===> 5
}

在其他语言中for循环结束之后i变量就已经被销毁,但是在js代码中i没有被销毁,所以最终打印出来是5

3.ES6解决变量提升带来缺陷的方法

为了解决变量提升带来的问题,ES6引入了letconst关键字,从而使JS也有了块级作用域

js 复制代码
let x = 5;
const y = 6;
x = 7;
y = 8; // Uncaught TypeError: Assignment to constant variable.

使用let关键字声明的变量的值可以改变,使用const关键字声明的变量的值是不可以改变的。

  • 引入块级作用域的例子(使用 let 或 const):
js 复制代码
function blockScopeExample() {
    if (true) {
        let blockScopedVar = 'I am a block-scoped variable';
        const blockScopedConst = 'I am a block-scoped constant';

        console.log(blockScopedVar); // 在块级作用域内可以访问
        console.log(blockScopedConst); // 在块级作用域内可以访问
    }

    // 下面的代码将会报错,因为在块级作用域外无法访问 blockScopedVar 和 blockScopedConst
    // console.log(blockScopedVar);
    // console.log(blockScopedConst);
}
  • 没有引入块级作用域的例子(使用 var):
js 复制代码
function noBlockScopeExample() {
    if (true) {
        var notBlockScopedVar = 'I am not block-scoped';
        console.log(notBlockScopedVar); // 在块级作用域内也可以访问,这是因为使用 var 声明的变量没有块级作用域
    }

    console.log(notBlockScopedVar); // 在块级作用域外也可以访问
}

在块级作用域中声明的变量不影响块外面的变量。

4.JavaScript如何支持块级作用域

js 复制代码
function foo() {
    var a = 1;
    let b = 2;
    {
        let b = 3;
        var c = 4;
        let d = 5;
        console.log(a); // 1
        console.log(b);   // 3        
    }
    console.log(b); // 2
    console.log(c); // 4
    console.log(d); // VM336:13 Uncaught ReferenceError: d is not defined
}

代码的执行流程

第一步编译并创建执行上下文

可以看出来函数内部通过var声明的变量在编译阶段全都存放到变量环境中。

通过let声明的变量会被存放在词法环境中。

在词法环境内部维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量(通过letconst声明的变量)压到栈顶,当作用域执行完成之后,该作用域的信息会从栈顶弹出。

代码执行过程中查找变量的顺序:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到则直接返回给JS引擎,如果没有找到继续在变量环境中查找。

三、ES6之前的版本如何实现块级作用域

为了兼容ES5以及之前的版本,但是编写代码时使用到ES6的新语法,通过babel将ES6代码转换成ES5的形式。

在ES6之前的JavaScript版本中,由于缺乏 letconst 关键字,没有直接的方法来声明块级作用域的变量。在旧版本中,通常使用函数作用域来模拟块级作用域。通过使用立即执行的匿名函数表达式(Immediately Invoked Function Expression,IIFE),可以创建一个新的函数作用域,从而限制变量的作用范围。

js 复制代码
(function () {
    // 在这里声明的变量仅在该IIFE的作用域内可见
    var blockScopedVar = 'I am a block-scoped variable';

    console.log(blockScopedVar); // 在块级作用域内可以访问
})();

  // 下面的代码将会报错,因为在块级作用域外无法访问 blockScopedVar
  // console.log(blockScopedVar);

Babel 主要通过两个插件来实现:

1. babel-plugin-transform-block-scoping:

这个插件主要用于将 letconst 关键字转换为适用于旧版 JavaScript(ES5)的代码。它将块级作用域转换为函数作用域,使用函数作用域的方式来模拟块级作用域。

原始代码:

js 复制代码
{
    let x = 10;
    console.log(x);
}
// 在这里无法访问 x,因为它在块级作用域内
console.log(typeof x);

经过Babel转换后:

js 复制代码
"use strict";

var _loop = function _loop() {
    var x = 10;
    console.log(x);
};

{
    _loop();
}
// 在这里无法访问 x,因为它在块级作用域内
console.log(typeof x);

在这个例子中,Babel 将原始的块级作用域内的let声明转换为一个函数 _loop,然后在原始的块级作用域位置调用这个函数。这样就实现了通过函数作用域模拟块级作用域的效果。在实际项目中,开发者无需手动进行这种转换,Babel 会根据配置自动处理这些转换。

2. babel-plugin-transform-strict-mode:

在旧版 JavaScript 中,letconst 关键字并不是唯一引入块级作用域的方法。使用严格模式(strict mode) 也可以引入块级作用域。这个插件用于将使用了 letconst 的代码转换为使用严格模式的等效代码。

原始代码:

js 复制代码
{
    let x = 10;
    console.log(x);
}
// 在这里无法访问 x,因为它在块级作用域内
console.log(typeof x);

经过Babel转换后:

js 复制代码
"use strict";

{
    var _x = 10;
    console.log(_x);
}
// 在这里依然无法访问 x,因为它被转换成了 _x,并且在块级作用域外部不可见
console.log(typeof x);
相关推荐
麻花20132 分钟前
WPF学习之路,控件的只读、是否可以、是否可见属性控制
服务器·前端·学习
.5483 分钟前
提取双栏pdf的文字时 输出文件顺序混乱
前端·pdf
jyl_sh11 分钟前
WebKit(适用2024年11月份版本)
前端·浏览器·客户端·webkit
zhanghaisong_20151 小时前
Caused by: org.attoparser.ParseException:
前端·javascript·html·thymeleaf
Eric_见嘉1 小时前
真的能无限试(白)用(嫖)cursor 吗?
前端·visual studio code
DK七七1 小时前
多端校园圈子论坛小程序,多个学校同时代理,校园小程序分展示后台管理源码
开发语言·前端·微信小程序·小程序·php
老赵的博客2 小时前
QSS 设置bug
前端·bug·音视频
Chikaoya2 小时前
项目中用户数据获取遇到bug
前端·typescript·vue·bug
南城夏季2 小时前
蓝领招聘二期笔记
前端·javascript·笔记
Huazie2 小时前
来花个几分钟,轻松掌握 Hexo Diversity 主题配置内容
前端·javascript·hexo