变量提升

一、什么是变量提升

JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分函数的声明部分提升到代码开头的行为,叫做变量提升

变量被提升后,会给变量设置默认值 undefined

变量的声明部分提升:

console.log(a) // undefined

var a = 1
复制代码

函数的声明部分提升:

console.log(add(1, 2)) // 3

function add(x, y) {
  return x + y
}
复制代码

二、变量提升的原理

变量提升 = 物理移动?

不,变量和函数声明在代码中的位置是不会改变的,它们在编译阶段被 JavaScript 引擎放入内存中

一段代码,经过编译后,会生成两部分内容:

  • 执行上下文:JavaScript 执行一段代码时的运行环境
    • 变量环境
    • 词法环境
  • 可执行代码

先编译,再执行:

  • 编译阶段:变量和函数的声明会被存放到变量环境中,变量的默认值会被设置为 undefined
  • 执行阶段:JavaScript 引擎会从变量环境中去查找自定义的变量和函数
console.log(a)

console.log(add(1, 2))

var a = 1

function add(x, y) {
  return x + y
}
复制代码

编译后:

// 执行上下文的变量环境

a = undefined

add = address1 -> function (x, y) { return x + y }
复制代码
// 可执行代码

console.log(a) // undefined

console.log(add(1, 2)) // 3

a = 1
复制代码

三、变量提升的缺陷

导致很多代码与直觉不符

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

function fn() {
  var a = 1

  if (true) {
    var a = 2
    console.log(a)
  }

  console.log(a)
}

fn()
复制代码

编译后:

// fn 的执行上下文的变量环境

a = undefined
复制代码
// fn 的可执行代码

a = 1

if (true) {
  a = 2
  console.log(a) // 2
}

console.log(a) // 2
复制代码

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

function fn() {
  for (var i = 0; i < 5; i++) {
    console.log(i)
  }

  console.log(i)
}

fn()
复制代码

编译后:

// fn 的执行上下文的变量环境

i = undefined
复制代码
// fn 的可执行代码

for (i = 0; i < 5; i++) {
  console.log(i) // 0 1 2 3 4
}

console.log(i) // 5
复制代码

四、ES6 如何解决变量提升带来的问题

作用域:变量和函数的可访问范围

ES6 之前:

  • 全局作用域
  • 函数作用域

ES6 之后:

  • 块级作用域 {}

ES6 通过使用 let 或者 const 关键字来实现块级作用域

function fn() {
  var a = 1
  let b = 2

  {
    let b = 3
    var c = 4
    let d = 5

    console.log(a)
    console.log(b)
  }

  console.log(b)
  console.log(c)
  console.log(d)
}

fn()
复制代码

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

函数内部:

  • 通过 var 声明的变量,在编译阶段全都被存放到变量环境
  • 通过 let 声明的变量,在编译阶段全都被存放到词法环境
  • 作用域块内部,通过 let 声明的变量并没有被存放到词法环境
// fn 的执行上下文

// 变量环境
a = undefined
c = undefined

// 词法环境
b = undefined
复制代码

第二步:继续执行代码

当进入作用域块时,通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域的变量并不影响作用域块外面的变量

词法环境内部,维护了一个小型的栈结构

  • 栈底是函数最外层的变量
  • 进入一个作用域块后,就会把该作用域块内部的变量压到栈顶
  • 当作用域执行完成之后,该作用域的信息就会从栈顶弹出

执行 ing…

// fn 的可执行代码

a = 1
b = 2
复制代码
// fn 的执行上下文

// 变量环境
a = 1
c = undefined

// 词法环境
b = 2
复制代码

执行 ing…

// fn 的可执行代码

{
  // 进入作用域块
}
复制代码
// fn 的执行上下文

// 变量环境
a = 1
c = undefined

// 词法环境
---
b = undefined
d = undefined
---
b = 2
复制代码

执行 ing…

// fn 的可执行代码

{
  b = 3
  c = 4
  d = 5
}
复制代码
// fn 的执行上下文

// 变量环境
a = 1
c = 4

// 词法环境
---
b = 3
d = 5
---
b = 2
复制代码

执行 ing…

// fn 的可执行代码

{
  // 沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中找到了,就直接返回给 JavaScript 引擎,如果没有找到,那么继续在变量环境中查找
  console.log(a) // 1
  console.log(b) // 3
}

console.log(b) // 2
console.log(c) // 4
console.log(d) // Uncaught ReferenceError: d is not defined
复制代码

五、总结

  • 由于 JavaScript 的变量提升存在着变量覆盖、变量污染等设计缺陷,所以 ES6 引入了块级作用域关键字 letconst 来解决这些问题
  • 变量提升是通过变量环境来实现的,块级作用域是通过词法环境的栈结构来实现的
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享