JavaScript 提升
[JS#5 WIL 🤔 帖子]
JavaScript 代码提升是指编译器在代码执行之前为变量和函数声明分配内存的过程[ 1 ]。这意味着,无论代码作用域是全局的还是局部的,声明在代码执行之前都会被移动到其作用域的顶部。
目录
📌 JavaScript 提升
从概念上讲,
提升是编译器拆分变量声明和初始化,并仅将声明移动到代码的顶部。
因此,变量甚至可以在定义之前就出现在代码中。但是,变量初始化只会在该行代码执行时发生。
下面的代码片段展示了提升的实际效果。第一个代码片段展示了代码的预期写法:先声明函数,然后再使用/调用它。
function printIamHoisted(str) {
console.log(str);
}
printIamHoisted("Am I hoisted??")
在下面的代码片段中,首先调用方法,然后编写函数声明。然而,它们都有相同的输出——它们打印字符串Am I hoisted?
printIamHoisted("Am I hoisted?")
function printIamHoisted(str) {
console.log(str);
}
因此,即使在函数编写之前调用该函数,代码仍然有效。这是由 JavaScript 中的上下文执行机制决定的。
📌 JavaScript 上下文执行
当 JavaScript 引擎执行代码时,它会创建执行上下文。每个上下文都包含两个阶段:创建和执行。
📌 创建阶段
当脚本执行时,JS 引擎会创建一个全局执行上下文 (Global Execution Context)。在这个阶段,它执行以下任务:
window
在 Web 浏览器中创建全局对象- 创建
this
属于全局对象的对象绑定 - 设置用于存储变量和函数引用的内存
- 将声明以
undefined
初始值存储在全局执行上下文中的内存中。
回顾一下这个片段,
printIamHoisted("I am hoisted!!!")
function printIamHoisted(str) {
console.log(str);
}
📌执行阶段
在这个阶段,JS 引擎会逐行执行代码。但是由于代码提升,函数的声明与行序无关,因此调用声明之前的方法不会出现问题。
对于每个函数调用,JS 引擎都会创建一个新的函数执行上下文。此上下文类似于全局执行上下文,但它不是创建全局对象,而是创建arguments
包含传递给函数的所有参数的引用的对象。此阶段的上下文看起来有点像:
📌 仅提升声明(函数和变量)
JS 只会提升声明,而不会提升初始化。如果一个变量被使用,但只在声明之后才初始化,那么它被使用时的值将是初始化时的默认值。
📌 默认值:undefined
和ReferenceError
对于用关键字声明的变量var
,默认值为undefined
。
console.log(hoistedVar); // Returns 'undefined' from hoisted var declaration (not 6)
var hoistedVar; // Declaration
hoistedVar = 78; // Initialization
hoistedVar
在变量初始化之前记录该变量将打印undefined
。但是,如果删除了变量的声明,即
console.log(hoistedVar); // Throw ReferenceError Exception
hoistedVar = 78; // Initialization
ReferenceError
由于没有发生提升,因此会引发异常。
📌let
提升:暂时死区(TDZ)
使用let
和声明的变量const
也会被提升。但是,与使用 声明的变量不同var
,它们不会被初始化为默认值undefined
。在执行初始化代码行之前,任何访问它们的代码都会引发异常。这些变量被称为从代码块开始到初始化完成处于“暂时死区”(TDZ)中。访问未初始化的变量let
会导致ReferenceError
。
{ // TDZ starts at beginning of scope
console.log(varVariable); // undefined
console.log(letVariable); // ReferenceError
var varVariable = 1;
let letVariable = 2; // End of TDZ (for letVariable)
}
使用“时间”一词是因为该区域依赖于执行顺序(指时间 -时间),而不是代码的编写顺序(位置)。然而,下面的代码片段可以正常工作,因为即使在声明之前sampleFunc
使用了,该函数也是在 TDZ 之外调用的。letVariable
{
// TDZ starts at beginning of scope
const sampleFunc = () => console.log(letVariable); // OK
// Within the TDZ letVariable access throws `ReferenceError`
let letVariable = 97; // End of TDZ (for letVariable)
sampleFunc(); // Called outside TDZ!
}
变量提升
请记住,所有函数和变量声明都会提升到作用域的顶层。声明会在执行任何代码之前进行处理。因此,未声明的变量在代码赋值执行之前不存在。对未声明变量的赋值会在执行赋值操作时隐式地将其创建为全局变量。这意味着任何未声明(但已赋值)的变量都是全局变量。
function demo() {
globalVar = 34;
var functionScopedVar = 78;
}
demo();
console.log(globalVar); // Output: 34
console.log(functionScopedVar) // throws a ReferenceError
这就是为什么声明变量总是好的,无论它们是函数范围还是全局范围。
📌 ES5 严格模式
严格模式于 EcmaScript 5 中引入,它是一种选择使用受限制的 JS 变体的方式。严格模式对常规 JS 语义进行了一些更改
- 通过抛出错误来消除静默错误
- 禁止可能在未来版本的 ES 中定义的语法
- 修复导致 JS 引擎执行优化的错误
关于提升,使用严格模式将不允许在声明之前使用变量。
📌 函数提升
JS 函数可以是声明或表达式。
函数声明会被完全提升到顶部。因此,函数在声明之前就可以被调用。
amIHoisted(); // Output: "Yes I am."
function amIHoisted() {
console.log("Yes I am.")
}
函数表达式不会被提升。这是由于 JS 函数和变量的优先级顺序决定的。下面的代码片段会抛出一个异常,TypeError
因为被提升的变量amIHoisted
被视为变量,而不是函数。
amIHoisted(); //Output: "TypeError: expression is not a function
var amIHoisted = function() {
console.log("No I am not.");
}
上述代码的执行结果看起来是这样的
var amIHoisted; // undefined
amIHoisted();
/*Function is invoked, but from the interpreter's perspective it is not a function.
Thus would throw a type error
*/
amIHoisted = function() {
console.log("No I am not.");
}
/*The variable is assigned as a function late. It was already invoked before the assignment.*/
📌 提升优先级
- 变量赋值优先于函数声明。 的类型
amIABoolean
将是 aboolean
,因为该变量被赋值为true
。
var amIABoolean = true;
function amIABoolean() {
console.log("No.")
}
console.log(typeof amIABoolean); // Output: boolean
- 函数声明优先于变量声明。从下面的代码片段中可以看出, 的类型
amIAFunction
应该是 ,function
因为在第一行,变量只是被声明了,而没有被赋值。由于函数声明优先,因此它被解析为 类型function
。
var amIAFunction;
function amIAFunction() {
console.log("Yes.")
}
console.log(typeof amIAFunction); // Output: function
结论
JS 中的提升是指编译器将变量的声明和初始化拆分,并仅将声明部分移至代码顶部。因此,即使函数和变量在编写之前就被调用/使用,代码仍然有效。这是由 JavaScript 中的上下文执行机制决定的。
请注意,只有声明会被提升,初始化不会被提升。对于使用var
关键字声明的变量,默认值为undefined
。对于let
变量,在执行初始化代码行之前,任何访问它们的代码都会引发异常。这些变量被称为从代码块开始到初始化完成处于“暂时死区”(TDZ)中。访问未初始化的let
变量会导致异常ReferenceError
。
这就是 JS 提升的基础知识,这是我的第五篇 WIL(我学到了什么)开发帖子😄。
一如既往,为终身学习干杯🍷!