块级作用域: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);
相关推荐
vvilkim几秒前
Nuxt.js 全面测试指南:从单元测试到E2E测试
开发语言·javascript·ecmascript
写不出来就跑路19 分钟前
基于 Vue 3 的智能聊天界面实现:从 UI 到流式响应全解析
前端·vue.js·ui
OpenTiny社区22 分钟前
盘点字体性能优化方案
前端·javascript
FogLetter26 分钟前
深入浅出React Hooks:useEffect那些事儿
前端·javascript
Savior`L26 分钟前
CSS知识复习4
前端·css
0wioiw042 分钟前
Flutter基础(前端教程④-组件拼接)
前端·flutter
花生侠1 小时前
记录:前端项目使用pnpm+husky(v9)+commitlint,提交代码格式化校验
前端
猿榜1 小时前
魔改编译-永久解决selenium痕迹(二)
javascript·python
阿幸软件杂货间1 小时前
阿幸课堂随机点名
android·开发语言·javascript