块级作用域: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);
相关推荐
老K(郭云开)14 分钟前
最新版Chrome浏览器加载ActiveX控件之SolidWorks 3D控件
前端·javascript·chrome·安全·3d·firefox
w2sfot18 分钟前
如何修复三方库bug:marked.js 15.0.6 bug修复经过
开发语言·javascript·bug
新中地GIS开发老师27 分钟前
80个Three.js 3D模型资源
javascript·数码相机·3d·arcgis·three.js·gis开发·地信
xuxuejian982428 分钟前
后台管理系统-axios网络请求的封装
前端·javascript·学习
明月看潮生33 分钟前
青少年编程与数学 02-006 前端开发框架VUE 14课题、生命周期
前端·javascript·vue.js·青少年编程·编程与数学
摇光9337 分钟前
js策略模式
开发语言·javascript·策略模式
AI大模型learner38 分钟前
Vue 环境配置与项目创建指南
前端·javascript·vue.js
傻小胖40 分钟前
React PureComponent使用场景
前端·javascript·react.js
明月看潮生40 分钟前
青少年编程与数学 02-006 前端开发框架VUE 13课题、事件处理
前端·javascript·vue.js·青少年编程·编程与数学
viqecel1 小时前
页面滚动下拉时,元素变为fixed浮动,上拉到顶部时恢复原状,js代码以视频示例
前端·javascript·ajax·滚动下拉浮动