JS-预编译的坑

218 阅读4分钟

思考

记一道JS题引发的疑问与学习

一、开始

setTimeout(function a() {
    console.log(a)
    a = 8
    console.log(a)
})

大家看见这题的第一眼,应该都觉得是考JS的变量提升函数提升作用域

a 作为未声明变量直接赋值,在非严格模式下,肯定会挂载到 window 对象,成为全局变量 所以我第一眼结果感觉应该是打印 f a()8

但是,结果并非如此

以下为打印结果

ƒ a() {
    console.log(a)
    a = 8
    console.log(a)
}
ƒ a() {
    console.log(a)
    a = 8
    console.log(a)
}

那么是哪里出了问题?

二、调试

遇事不决,进行调试,加上 debugger 再试一次

image.png

image.png

很明显, a=8 这一步,根本没有执行,也没有报错,即静默失败

MDN:严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常. 例如, NaN 是一个不可写的全局变量. 在正常模式下, 给 NaN 赋值不会产生任何作用; 开发者也不会受到任何错误反馈. 但在严格模式下, 给 NaN 赋值会抛出一个异常. 任何在正常模式下引起静默失败的赋值操作 (给不可写属性赋值, 给只读属性(getter-only)赋值, 给不可扩展对象(non-extensible object)的新属性赋值) 都会抛出异常:

那么,我们打开严格模式,这样我们才能真正看到错误信息 加上 "use strict", 再运行一次,可以看见报错信息了:

image.png

错误信息提示:给常量进行赋值

三、探究

首先我们要知道,JS的变量提升、函数提升,本质上应该归究到JS的预编译 我们需要先理解 JS 的预编译流程

1、JS运行

JS的运行应该分为以下三步

  1. 语法分析
  2. 预编译
  3. 解释执行

需要明白:JS 的声明和执行(赋值)是分开两步操作的,声明属于预编译环节。

而预编译分为全局预编译和函数体预编译

2、全局预编译

  1. 创建全局对象GO (gloabl object: 全局上下文,window对象)
  2. 找到全局里的变量声明,将变量声明为全局对象的属性名,赋值为undefined
  3. 找到全局里的函数声明,将函数名作为全局对象的属性名,赋值为函数体

3、函数中的预编译

  1. 创建AO对象(Active Object:执行上下文)
  2. 找到形参,将形参作为AO属性名;如果有实参,赋值实参;否则,赋值undefined
  3. 找到变量声明,将变量作为AO的属性名,赋值undefined
  4. 找到函数声明,将函数名作为AO的属性名,赋值函数体

4、注意点

  1. 函数的优先级更高

    当变量和函数同名时,只留下函数的值

  2. 函数中的预编译要等到函数执行前的一刻才进行预编译

  3. 函数表达式视为变量级别

  4. 自执行函数不会进行预编译

    其它和普通函数一样,当代码逐行执行到这个位置的时候,定义和执行一起完成

  5. 函数中return后面的代码虽然不会执行,但是会进行预编译

四、理解

那么关于本题、可以得到以下信息:

  1. function a() {..}没有进行预编译提升,GO中并没有看见 a 属性
  2. 调试发现,只有在出现了函数同名变量情况下,AO中才会有 函数属性:

image.png

  1. 这个属性值为函数引用,应该属于常量指针,不可修改

五、扩展

这题我还试了很多其他写法,发现结果会不一样, 比如把 function a() {...}, 提出来进行声明,setTimeout 传入参数 a,这会打印预期的结果:f() 和 8

比如改成:

setTimeout(function() {
    console.log(a)
    function a() {
        console.log(a)
        a = 8
        console.log(a)
    }
    a()
})

六、补充

群里看见的原题,分享一下

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

a = 5;
console.log(a); // 5


(function a() {
    console.log(a); // f a()
    a = 4; // error
    console.log(a); // f a()
})();

function b() {
    console.log(b); // f b()
    b = 6;
    console.log(b); // 6
}

b();

setTimeout(function c() {
    console.log(c); // f c()
    c = 8; // error
    console.log(c); // f c()
})

自执行函数和setTimeout 出现了同样的现象和结果

所以有没有大佬深入讲解一下,我的两个个疑惑:

  1. 为什么在 AO 会自动生成属性,函数名为键、函数体为值 的属性

    而在函数体内不出现 函数名就没有此属性

  2. 这个属性值,是常量指针吗?或者说引用

自己理解:

应该追究到执行上下文:

在 JavaScript 代码执行前,执行上下文将经历创建阶段。在创建阶段会发生三件事:

  • this 值的决定,即我们所熟知的 This 绑定。
  • 创建词法环境组件。
  • 创建变量环境组件。

在 ES6 中,词法 环境和 变量 环境的区别在于前者用于存储**函数声明和变量( let 和 const )绑定,而后者仅用于存储变量( var )**绑定。

题目中当出现了 标识符 a 时,需要建立词法环境或者变量环境,而这只能找到自身的函数定义?