JS的变量提升

163 阅读7分钟

image.png

一、变量提升?

什么是变量提升?

console.log(a)
var a = 1

你认为这段话会打印什么出来 ? log函数会打印出1的结果。这就是变量提升。它使其想当于

var a 
console.loga)
a = 1

存在有varletconst隐式声明这四者都可以声明变量

隐式声明:

a = 1
  1. 不存在变量提升
  2. 声明的变量会被当成全局变量成为 window 的属性 var 声明的变量在函数作用域中不会被当作全局变量
  3. 可以被 delete 删去(var 声明的变量删不掉)
  4. use strict严格模式下不允许这样。

letconstvar 这些声明都会将变量的声明进行提升。

  • var 命令在变量的定义被执行之前就初始化变量,并拥有一个默认的 undefined 值。
  • letconst 命令会形成暂时性死区,在变量的定义被执行之前都不会初始化变量,避免在声明语句之前的不正确调用。如果定义时没有给定值的话,let 声明的变量会赋值为 undefined,而 const 声明的变量会报错。

摘自JavsScript 变量提升和函数提升

注意一下 const 它是一开始就要进行声明和赋值初始化的 其他的特性和 let 差不多。

const foo;
foo = 1 // 报错
console.log(foo);// 报错
//如果在一开始就没有对其进行初始化(赋值)的操作那么就会导致无法在对其使用和初始化。

const 、let 和 var 的主要区别就是前者绑定块作用域后者绑定函数作用域。

Var 的变量提升和 let 的”变量“提升

包括 var, letconst 在内的一切声明都会被“提升”。访问在此之前没申明变量的角度上来说只有 var 才会进行变量提升。let const 会爆出一个错误。而这个错误产生的原因是在于 TDZ (暂时性死区)也是“变量”提升的证明(与其说变量提升不如说是被绑定在此作用域中在未被赋值之前都不能进行访问)。

{
    console.log(a) // 报错
    const a = 1;
}
  1. var 命令在变量的定义被执行之前就初始化变量,并拥有一个默认的 undefined 值。
  2. letconst 命令会形成暂时性死区,在变量的定义被执行之前都不会初始化变量,避免在声明语句之前的不正确调用。如果定义时没有给定值的话,let 声明的变量会赋值为 undefined,而 const 声明的变量会报错。

二、函数名提升

(一)函数名也会出现函数提升

func()
function func () {
    console.log('1')
}

(二)在变量和函数都有的情况下函数的声明比变量的声明会优先

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

(三)变量在函数内的提升也需要考虑函数参数的影响。

function func (v) {
    console.log(v); // 2
    var v = 1
}
func(2)

变形一下可以这样看

function func (v) {
    // 相当于
    var v = 2 
    console.log(v); // 2
    var v = 1
}
func(2)
// ---------仔细看一下------------
function func (v) {
    var v  // 相当于
    var v 
    v = 2
    console.log(v); // 2
    v = 1
}
func(2)

这里是通过var声明的变量通过它声明的变量只提升声明不提升赋值 声明和赋值是两回事。

变量提升最重要的一点是要区分 声明赋值

var a = 1;
var a // 声明
a = 1 // 赋值

参考:我知道你懂 hoisting,可是你了解到多深?

如果根据 执行上下文的内容来解释就是 在执行上下文阶段 会经过一个创造的步骤 会创建变量对象 但在变量对象中会优先处理 、函数的参数、 其次 函数的声明、 最后 变量的声明 ,如果变量的声明和函数的参数或者是函数的声明发生冲突那么这个变量不会去干扰它们。

详细的解释函数的参数影响到变量声明的原因

(一)首先在执行上下文中有三个步骤分别是构建、执行、回收这个三个阶段 先看创建阶段如果感兴趣可以看到文末最后。

创建阶段

  • 创建变量对象
  • 创建作用域链
  • 确定this

如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

由于变量对象是存储上下文中所有定义的变量和函数声明,问题就出现在在了创建变量对象的身上。

引用一下mqyqingfeng对变量对象的总结

变量对象初始化的顺序:

  1. 函数的所有形参 (如果是函数上下文)

    • 由名称和对应值组成的一个变量对象的属性被创建
    • 没有实参,属性值设为 undefined
  2. 函数声明

    • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性
  3. 变量声明

    • 由名称和对应值(undefined)组成一个变量对象的属性被创建;
    • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

问题的关键:变量对象会优先处理函数声明 然后再保存变量声明 如果变量声明和函数声明一致则变量声明不会去影响函数声明。

(二)从另一个的角度来看 js 代码的执行过程分为三个阶段 1.生成抽象语法树 2.生成字节码 3.生成机器码

在第一个阶段又分为 词法分析和语法分析。词法分析阶段 JS会检测当前作用域中所有的变量和函数声明,然后将其添加到词法环境中 

注意:这里先不考虑 ES6 环境 这里引用他对变量提升的理解-文章也写的非常不粗先看我的文章在看他的好嘛?

在词法分析阶段,对于变量声明和函数声明,词法环境的处理是不一样的:

  • 对于变量声明如 var a = 3,会为变量分配内存并初始化为 undefined,赋值语句在生成机器码阶段真正执行代码的时候才进行。
  • 对于函数声明如 function sayHello() { console.log('Hello there!') },会在内存里创建函数对象,并且直接初始化为该函数对象。

因此,对于变量声明,在真正执行到赋值语句之前,我们就已经可以使用此变量,但是初值为 undefined;而对于函数声明,在执行到函数声明之前,函数对象就已经存在在内存当中,并可以直接调用了。

变量提升和函数提升都是将声明提升到当前作用域的顶端

(三)在ES6中执行上下文又引入变量环境和词法环境的概念

摘自JavaScript进阶-执行上下文

执行上下文的生命周期:

  1. 创建阶段

    • 确定this
    • 创建词法环境组件
    • 创建变量环境组件
  2. 执行阶段

  3. 销毁阶段

重点看一下这两个词法环境变量环境 由这两个共同组成了执行上下文。

词法环境有两部份组成:

  1. 环境记录
  2. 对外部环境的引用

变量环境其实也是一个词法环境, 因此它具有上面定义的词法环境的所有属性。

词法环境存储变量绑定(let 和 const)以及函数的声明。但变量环境仅仅用于存储 var 声明。

函数被直接存储到内存中,var 声明的变量名会被存储到变量环境中初始化为 undefiend 而通过let const 进行声明的会存储到词法环境中如果在赋值之前访问就会抛出错误。

推荐文章

  1. 关于具体的执行上下文还要具体的看我的下一个文章 深刻理解执行上下文
  2. 为什么会有变量提升和函数提升以及抽象泄露的概念虽然没什么用但很有意思

参考链接

我知道你懂 hoisting,可是你了解到多深?

[JavsScript 变量提升和函数提升-涉及到了V8的字节码和js的工作流程可以抽空看看]

JavaScript进阶-执行上下文

作者:小橙子🍊