灵魂拷问,你真的懂 JavaScript 中的变量提升吗?

997 阅读5分钟

引言

对于变量提升这个问题,我想从事前端的同学都或多或少认为我懂这个。曾经,我也是这样认为的,我懂变量提升,并且可以从变量在 Chrome 中的内存分配讲起,以及中间发生了什么。

但是,在一次面试中,我遇到了几个一起面前端的同学(当然技术水平参差不齐,并不是很高),在和他们聊这次笔试中的变量提升的问题时,发现大家都支支吾吾的,很多讲的都是值的覆盖

当时的面试题是这样的:

function fn(a) {
    console.log(a)
    var a = 2
    function a() {}
    console.log(a)
}
fn(1)

这个题目,最终会输出function a(){}2。那么,为什么是这个答案,这个过程发生了什么很重要。所以,今天我们就来彻底刨析一下变量提升的过程。

一、变量在内存中的分配

在分析整个过程前,我们先来回顾一下 JavaScript 中变量在内存中的分配。

大家都知道的是,对于原始类型会存储在栈空间中,对于引用类型会将引用存储在栈空间中,将数据存储在堆空间中

其实这个过程还牵扯到函数上下文的创建,而每一个函数上下文中又会创建一个变量环境、词法环境。有兴趣的同学可以去看李斌老师的浏览器工作原理与实践

所以,我们来看一个简单的栗子,分析一下它在内存中的分配: 栗子:

var a = 1
var b = 2
var student = {name: 'wjc', age: 22}

它内存中的分配:

二、运行前的简单编译

众所周知,JavaScript 是一门动态类型的语言,即它是在运行时确定变量的类型,不同于静态类型语言的先编译再运行的过程。但是,事实是 V8 引擎在解析运行 JavaScript 之前是会进行一次简单的编译,也就是我们通常所说的初始化过程。

这个初始化过程,会做这几件事:

  • 区分执行代码和变量声明代码
  • 变量声明代码划分为赋值代码和初始化代码
  • 初始化代码有两种情况,一是对变量(原生类型、对象类型)初始化为 undefined;二是对函数的初始化,即直接指向函数在堆空间中的内存

那么,我们就来看一个简单的栗子:

console.log(a)
sayHi()
var a = 2
function sayHi() {
    console.log('Hi')
}

那么按照我们上面所说,这段代码的赋值只有 var a = 2,函数声明只有进行编译阶段的代码会是这样的:

// 编译代码
var a = undefined
var sayHi = function () {
    console.log('Hi')
}

此时,它在内存中的分布:

然后,在执行阶段的代码会是这样:

// 执行代码
console.log(a)
sayHi()
a = 2

所以,也就是当我们真正执行的时候会走执行代码,所以很显然会输出:

undefined
Hi

而当走完所有执行代码后,此时内存是这样的:

我想通过这个栗子,大家应该大致搞懂变量提升的过程。但是,仍然存在一个较为特殊的情况,就是当函数形参存在时的变量提升,也就是我们文章开头提及的面试题。

三、函数形参的编译执行

首先,我们需要对函数调用做一个简单的理解,在我们平常调用函数的时候,真正会经历两个步骤

  • 如果此时存在形参,则进行函数形参的编译和执行过程
  • 然后进入函数体,进行函数体内部的编译和执行

可以看到这里我们提到了当函数存在形参时,会先进行函数形参的编译和执行过程。

这里我们就来分析文章开头这个栗子:

function fn(a) {
    console.log(a)
    var a = 2
    function a() {}
    console.log(a)
}
fn(1)

首先,此时是存在函数形参的,那么函数形参的编译和执行会是这样:

var a = undefined
a = 1

然后,才会进行函数体的编译和执行:

// 编译
a = function a() {} // 重点!!!
// 执行
console.log(a)
a = 2
console.log(a)

可以看到的是,如果函数体内的变量名和形参的变量名重复时,则不会进行普通变量的编译赋值 undefined 的过程。但是,如果存在该变量是函数时,那么则会进行函数变量的编译赋值,即直接指向函数在堆空间中的地址。

所以,我们这个栗子在编译后,可以看作是这样的:

function fn() {
    var a = undefined
    a = 1
    a = function a() {}
    console.log(a)
    a = 2
    console.log(2)
}

很显然,它会输出会输出function a(){}2

结语

不知大家在深度理解过变量提升过程后,是否有和我一样的感受就是学习编程的本质是追溯本源。现今,虽然我们可以用 ES6letconst 来声明变量来避免 var 的种种缺陷。但是,如果因为这样而不去思考 var 为什么会存在这些缺陷。我想这是非常遗憾的。

点赞

通过阅读本篇文章,如果有收获的话,可以点个赞,这将会成为我持续分享的动力,感谢~

我是五柳,喜欢创新、捣鼓源码,专注于源码(Vue 3、Vite)、前端工程化、跨端等技术学习和分享,欢迎关注我的微信公众号:Code center