JavaScript变量提升

一、变量提升(Hoisting)

以下这段代码可以工作


speak()

var test

console.log(test)

function speak() {
    console.log(test)
}

因为变量提升。

变量声明和赋值


var name = "Mark";

这句可以看成是下面两句:


var name; // 声明部分

name = "Mark"; // 赋值部分

函数声明和赋值


function foo() {
 console.log('foo')
}

var bar =  function() {
    console.log('bar')
}

第一个函数 foo 是一个完整的函数声明,也就是说没有涉及到赋值操作。

第二个函数,先是声明变量bar,在把匿名函数赋值给了bar


var bar;

bar = function() {
    console.log('bar')
}

所谓的变量提升,是指在JavaScript代码执行过程中,JavaScript引擎把变量的声明部分和 函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认 值就是我们熟悉的 undefined 。

模拟变量提升:

书写代码


test()
console.log(name)

var name = "mark"

function test() {
    console.log("test run")
}

模拟变量提升,结果如下


// 变量提升部分 ----start

// 把变量 name 提升到开头,同时赋值为 undefined
var name = undefined

// 把函数 test 提到开头
function test() {
    console.log("test run")
}

// 变量提升部分 ----end

// 代码执行部分
test()
console.log(name)

// 去掉声明部分的赋值语句
name = "mark"

JavaScript代码的执行流程

“变量提升”意味着变量和函数的声明会在物理层面移动到代码的前面,正如模拟的那样。但是,这样子理解并不准确。

实际上变量和函数声明在代码里的位置是不会改变的。而且是在编译阶段被JS引擎放入内存中。

一段JS代码在执行之前需要被JS引起编译,编译完成之后才会进入执行阶段:

大致流程

一段JS代码 ---> 编译阶段 ---> 执行阶段

1. 编译阶段

编译阶段和变量提升是什么关系呢?

分析上面的模拟代码:

第一部分:变量提升部分的代码

var name = undefined

function test() {
    console.log("test run")
}

第二部分:执行部分的代码

test()
console.log(name)
name = "mark"

JS的执行流程详细化:

输入一段JS代码(原始代码)
|
|
// 编译阶段
执行上下文 { 变量环境 (代码提升部分) 、词法环境 、可执行代码(代码提升后剩余部分)}
|
|
//执行代码
输出结果


执行上下文是JS执行一段代码时候的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数执行时候用到的诸如 this、变量、对象以及函数等。

简单说执行上下文中存在一个“变量环境对象(Variable Environment)”用来保存变量提升的内容。

VariableEnvironment:
    name ->  undefined,
    test -> function : { console.log(name)}

过程分析

1. 编译阶段


test()
console.log(name)
var name = "mark"
function test() {
    console.log("test run")
}

第1行、第2行,这两行代码不是声明操作,JS引擎不做任何处理

第3行由于是声明语句 var 于是JS引擎将在“变量环境对象”中新建一个名为name的属性,并且使用undefined初始化

第4行JS引擎发现一个通过function定义的函数,所以将函数储存到了 堆(heap)中,并且在“变量环境对象”中创建了一个 test属性,并且指向堆中这个函数的地址。

这样就生成了“变量环境对象”。接下来JS引擎会把声明之外的语句编译为字节码,接下来就是执行阶段。

2. 执行阶段

JS引擎开始执行“可执行代码”,按照顺序一行一行执行。

当执行到 test 函数,JS引擎便在“变量环境对象”中查找该函数,由于保存了函数的地址,所以JS开始执行这个函数,并且输出执行结果。

接下来打印 name,JS引擎继续在 “变量环境对象”中查找,并且输出其值为 undefined。

接下来是一个赋值语句,环境变量中值被修改如下:

VariableEnvironment:
    name ->  mark,
    test -> function : { console.log(name)}

这就是一段代码的编译和执行流程。实际的编译和执行阶段都非常复杂,包括词法分析、语法解析、代码优化、代码生成等。

代码中出现相同的变量或者函数怎么办?

当变量和函数名称相同时,函数声明始终优先于变量声明。这是因为函数声明被提升到作用域顶部时,它们会覆盖任何同名的变量声明。这种情况下,函数可以在代码中的任何地方调用,而变量仅在声明之后才能被访问。

  1. 函数位置先于变量声明
console.log(a); // undefined

function a() {
  console.log("function a");
}
var a = "hello";
console.log(a); // function a

函数声明在 变量前面,输出的是:

 [Function: a]
 hello
  1. 变量位置先于函数声明
console.log(a); // [Function: a]

function a() {
  console.log("function a");
}
var a = "hello";
console.log(a); // hello

思考

追加 2023年03月17日

为什么 变量提升,函数的优先级要比变量高?

在JavaScript中,变量提升是编译阶段发生的行为,而函数的优先级则是在代码执行阶段发生的。当JavaScript解释器第一次遍历代码时,它会扫描代码标识符,将变量和函数声明提升到顶部并将它们存储在内存中。这种行为导致在执行代码时,函数可以在它们实际被声明之前被调用,而变量却不能。这也是为什么JavaScript中的函数可以被认为是“第一等公民”(first-class citizen)的原因之一。

另一个原因是函数的声明方式不同于变量的声明方式。函数可以使用函数声明或函数表达式来声明,而变量只能使用var或let等关键字声明。在使用函数声明方式时,函数实际上是在编译阶段创建的一个对象,并在解析代码时将其名称提升到顶部,因此它们的优先级比变量高。而函数表达式则类似于变量声明,因此它们与变量的优先级类似。

综上所述,JavaScript中的变量提升和函数优先级是由JavaScript解释器的处理方式和不同的声明方式所导致的。熟悉这些特性可以帮助开发人员更好地理解和编写JavaScript代码。

来自 chatGPT 的回复。

这里面有一点我个人是比较倾向于的:

1) 函数是一等公民。

所有功能围绕函数。

参考

Mark24

Everything can Mix.