js 变量提升那些事儿

2,745 阅读4分钟

关于 script 标签的变量提升

<script>
  console.log(num);
</script>
<script>
  var num = 1;
</script>
<script>
  num = 2;
</script>
<script>
  console.log(num);
</script>
<script>
  var num = 3;
</script>
<script>
  console.log(num);
</script>
  • 第一个 console,当前 script 标签中未声明 num 变量就使用了,会报错(变量提升不能跨 script 标签),不会进行输出。就像你引用 jquery,不能在引用它的 script 标签前写 $ 一样
  • 第二个 console,其所在 script 标签的前两个 script 标签中使用 var 声明并初始化其值为 1,由将其赋值为 2,所以打印 2
  • 第三个 console,其所在 script 标签的前一个 script 标签里使用 var 重新声明了 num 变量,并赋值为 3,所以打印 3

普通的变量提升

console.log(str);
var str = "123";
  • 在使用 var 声明的 str 变量之前就访问了 str,str 会被提升,默认值为 undefined,则打印 undefined,不会报错

严格模式下的变量提升 3

"use strict";

console.log(str);
var str = "123";
  • 严格模式下,也允许变量提升,打印 undefined

变量声明前进行赋值

str = "234";

console.log(str);
var str = "123";
  • 只要在当前作用域下,使用 var 声明过一个变量,就可以在该作用域下对该变量进行赋值取值,本题中在 console 前进行赋值操作,因此打印 234

未经声明直接赋值

str = "1";

console.log(str);
console.log(window.str);
  • 未经声明的变量直接赋值,在全局作用域下,会直接挂载到全局对象上

变量声明和函数声明谁先谁后

console.log(typeof a);

function a() {}

var a = "str";
  • 全局预编译
    1. 生成 GO 对象(GLOBAL OBJECT)
    2. 先进行变量声明,且其值为 undefined
    3. 再进行函数声明,值为函数体
    4. 开始执行除 var 和 function 声明外的实际代码
    5. 因此,同名变量和函数,在全局作用域,函数的提升会后于变量声明
    6. 打印 function

函数预编译

function fn(a) {
  console.log(a);
  var a = 123;
  console.log(a);
  function a() {}
  console.log(a);
  var b = function () {};
  console.log(b);
  function d() {}
  console.log(d);
}

fn(1);
  • fn 接收形参 a,fn 调用时传实参为 1
    • 函数预编译(函数执行的前一刻)
    1. 创建 AO 对象(ACTIVATION OBJECT 执行期上下文)
    2. 先找形参和变量声明,将变量名和形参名作为 AO 对象的名,值为 undefined, AO { a: undefined b: undefined }
    3. 将传入实参的值和形参的名相统一, AO { a: 1 b: undefined }
    4. 在函数体中找函数的声明,值赋为函数体, AO { a: function a() {} b: undefined d: function d() {} }
    5. 预编译结束,开始执行上述步骤外的 js 代码
    6. 则按顺序,打印结果为 function a() {}、123、123、function b() {}、function d() {}

function 写在后面就是函数了吗?

var a = 1;

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

a();
  • 报错,不输出,根据 变量提升 6 中讲的原理,先声明 a 变量,然后声明 a 函数,赋值 a 为 1,因此 a 不是函数,不能被调用

let、const 声明不提升

console.log(a);
let a = 1;
  • 报错,不输出,let 声明没有变量提升,访问 a 这个东西时,a 变量还未声明

let、const 声明的特点

{
  let a = 10;
}

console.log(a);
  • 报错,不输出,let 声明的变量只能在当前作用域下访问,除了对象的大括号,其他带大括号的基本都是一个作用域,直接写一个带有 const 声明和 let 声明的大括号,也是作用域

let、const 声明特点 2

function a() {
  console.log(b);
}

const b = 1;

a();
  • const 或 let 声明的变量可以在写在它上面的函数体中访问,不会报错,打印 1

class 声明不能提升

console.log(new A())

class A {}
  • class 声明不会被提升,报错,不输出

let 解决经典问题

for (var i = 0; i < 10; i ++) {
    setTimeout(() => {
        console.log(i)
    }, 1)
}

for (let j = 0; j < 10; j ++) {
    setTimeout(() => {
        console.log(j)
    }, 1)
}
  • 上面的 for 循环打印 10 个 10,下面打印 1 - 10
    1. 上面的 for 循环,i 是用 var 声明在全局作用域中的变量,每次循环都创建了一个 1 毫秒后执行的定时器,最后循环结束,i 变为了 10,然后定时器开始执行,其回调函数中访问的 i 会在当前函数作用域下查找,之后在全局作用域下查找到它为 10,所以 10 次打印就都是 10 了
    2. 下面的 for 循环,由于 j 是使用 let 声明的,每次循环都产生了一个作用域,后面定时器回调执行时,当前函数作用域下查不到,往上层作用域查,发现上层作用域中(也就是每次循环产生的作用域)能查到,就打印了它查到的这个值,所以每个打印的都不一样