思考
记一道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 再试一次
很明显, a=8 这一步,根本没有执行,也没有报错,即静默失败
MDN:严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常. 例如,
NaN是一个不可写的全局变量. 在正常模式下, 给NaN赋值不会产生任何作用; 开发者也不会受到任何错误反馈. 但在严格模式下, 给NaN赋值会抛出一个异常. 任何在正常模式下引起静默失败的赋值操作 (给不可写属性赋值, 给只读属性(getter-only)赋值, 给不可扩展对象(non-extensible object)的新属性赋值) 都会抛出异常:
那么,我们打开严格模式,这样我们才能真正看到错误信息
加上 "use strict", 再运行一次,可以看见报错信息了:
错误信息提示:给常量进行赋值
三、探究
首先我们要知道,JS的变量提升、函数提升,本质上应该归究到JS的预编译 我们需要先理解 JS 的预编译流程
1、JS运行
JS的运行应该分为以下三步
- 语法分析
- 预编译
- 解释执行
需要明白:JS 的声明和执行(赋值)是分开两步操作的,声明属于预编译环节。
而预编译分为全局预编译和函数体预编译
2、全局预编译
- 创建全局对象GO (gloabl object: 全局上下文,window对象)
- 找到全局里的变量声明,将变量声明为全局对象的属性名,赋值为undefined
- 找到全局里的函数声明,将函数名作为全局对象的属性名,赋值为函数体
3、函数中的预编译
- 创建AO对象(Active Object:执行上下文)
- 找到形参,将形参作为AO属性名;如果有实参,赋值实参;否则,赋值undefined
- 找到变量声明,将变量作为AO的属性名,赋值undefined
- 找到函数声明,将函数名作为AO的属性名,赋值函数体
4、注意点
-
函数的优先级更高
当变量和函数同名时,只留下函数的值
-
函数中的预编译要等到函数执行前的一刻才进行预编译
-
函数表达式视为变量级别
-
自执行函数不会进行预编译
其它和普通函数一样,当代码逐行执行到这个位置的时候,定义和执行一起完成
-
函数中return后面的代码虽然不会执行,但是会进行预编译
四、理解
那么关于本题、可以得到以下信息:
function a() {..}没有进行预编译提升,GO中并没有看见 a 属性- 调试发现,只有在出现了函数同名变量情况下,AO中才会有 函数属性:
- 这个属性值为函数引用,应该属于常量指针,不可修改
五、扩展
这题我还试了很多其他写法,发现结果会不一样,
比如把 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 出现了同样的现象和结果
所以有没有大佬深入讲解一下,我的两个个疑惑:
-
为什么在 AO 会自动生成属性,函数名为键、函数体为值 的属性
而在函数体内不出现 函数名就没有此属性
-
这个属性值,是常量指针吗?或者说引用
自己理解:
应该追究到执行上下文:
在 JavaScript 代码执行前,执行上下文将经历创建阶段。在创建阶段会发生三件事:
- this 值的决定,即我们所熟知的 This 绑定。
- 创建词法环境组件。
- 创建变量环境组件。
在 ES6 中,词法 环境和 变量 环境的区别在于前者用于存储**函数声明和变量( let 和 const )绑定,而后者仅用于存储变量( var )**绑定。
题目中当出现了 标识符 a 时,需要建立词法环境或者变量环境,而这只能找到自身的函数定义?