超越基础知识:起吊
如今大多数 JavaScript 开发者都听说过“函数提升”(hoisting)这个术语,但通常仍然不清楚其背后的实际运作机制。良好的编程规范和“严格模式”有助于防止许多由函数提升引起的错误和 bug,但了解这些规范的制定原因始终是有益的。
本文涵盖以下内容:
- 什么是提升以及它如何影响你的代码
- 对提升操作理解不当如何导致未定义值或错误
- 函数声明与表达式
- 变量提升与 Let/Const 的区别
什么是起吊?
变量提升通常被定义为将变量/函数声明“提升”到其词法作用域的顶部。这容易让人误以为这些声明被实际移动到了代码的顶部,但实际上,变量/函数声明在编译阶段被加载到内存中,但仍然保留在它们在代码中的原始定义位置。
函数声明
定义函数最常见的两种方法是通过声明和表达式。
通常情况下,我们会在使用函数之前先定义它:
function printName(name) {
console.log(name)
}
printName('Matt'); // logs 'Matt'
但是,如果我们使用函数声明,该函数将在编译期间被放入内存,并且在执行期间甚至到达该代码行之前,就已经在该词法作用域中可用了:
printName('Matt'); // same scope as function definition, still logs 'Matt'
function printName(name) {
console.log(name)
}
虽然该函数已加载到内存中,但它仅在该函数的作用域内有效:
printName('Matt'); // global scope, throws error
// 'Uncaught ReferenceError: printName is not defined'
(function() { // creates a function scope
printName('Matt'); // logs 'Matt'
function printName(name) { // only available in this function scope
console.log(name)
}
})()
只有函数声明会被提升,尝试在函数表达式执行之前调用它会导致错误:
test('Matt'); // Uncaught TypeError: test is not a function
var test = function(name) {
console.log(name);
}
等等……为什么会抛出类型错误?既然函数在定义之前就被调用了,难道不应该抛出引用错误吗?
这个谜团的答案将在下一节中揭晓。
变量声明:var 与 let/const
使用“Var”进行声明
使用“var”声明变量与声明函数类似,它们也会被提升。
那么,为什么上面章节中的函数调用导致了 TypeError 而不是 ReferenceError 呢?
这是因为函数表达式本质上是在同一步骤中完成变量声明和函数赋值。变量会被提升,并undefined在执行过程中一直保持为变量,直到到达该代码行为止。
// 1. variable declaration & assignment in same line
var printName = function() { ... } // declare printName and assign to a function
// 2. Same as above but in two separate steps
var printName; // declare printName
// printName gets hoisted and set as undefined until it reaches the line below
printName = function() { ... } // assign to a variable
现在让我们回顾一下之前的例子:
// var test was hoisted and is undefined
test('Matt'); // throws a TypeError since we're trying to invoke 'undefined'
var test = function(name) { // now test is assigned to a function
console.log(name);
}
test('Matt') // logs 'Matt'
为了确保你理解了变量声明,以下代码会输出什么?
console.log(name);
var name;
name = 'Matt';
如果你猜对了undefined,那就给自己点个赞吧!但是如果我们先赋值再声明变量会发生什么呢?它的作用域还是会变成全局作用域?
name = 'Matt'; // global scope or function scope?
console.log(name); // logs 'Matt'
var name;
如果一个变量在同一作用域内声明,它将被赋值给该变量;否则,它将沿着作用域链向上移动,直到到达全局作用域并在那里被赋值。
(function() {
name = 'Matt'; // since no 'name' var was declared in this function scope it will move up the scope chain
})()
console.log(name); // logs 'Matt'
(function() { // name was declared and got hoisted
name = 'Matt'; // assign 'Matt' to the name variable in this function scope
var name;
})()
console.log(name); // name was scoped to the function above and is not available outside that scope
// Uncaught ReferenceError: name is not defined
使用“Let/Const”进行声明
let/const 和 var 的一个主要区别在于,前者是块级作用域,而后者是函数级作用域。但它们在变量提升方面有何不同呢?
首先,由于该变量const不能重新赋值,因此在声明时必须始终对其进行赋值:
const name = 'Matt'; // Only way to define a constant variable
const anotherName; // throws a error, SyntaxError: Missing initializer in const declaration
anotherName = 'Matt';
等等,但我们之前说过,变量声明会被提升并设置为“undefined”,直到执行到赋值语句时才会被赋值,那么这岂不是意味着我们重新赋值了一个常量变量?
不,因为 let/const 变量不会被提升。
// Const have to be declared before they can be used
console.log(name); // ReferenceError: Cannot access 'name' before initialization
const name = 'Matt';
// Let also has to be declared before it can be used
console.log(name); // ReferenceError: Cannot access 'name' before initialization
let name = 'Matt';
回到我们之前使用“var”的例子,如果我们把它替换成“let”,它将不再被提升,并且会抛出一个错误。
(function() { // name was not hoisted
name = 'Matt'; // ReferenceError: Cannot access 'name' before initialization
let name;
})()
console.log(name);
概括
如果对提升机制理解不足,可能会导致许多错误或意外行为。以下两种方法可以避免提升机制可能造成的损害:
- 务必在每个作用域的顶部声明变量。
- 使用 Let/Const 而不是 Var 可以帮助防止因变量提升而导致的错误。
- 使用严格模式可以防止使用未声明的变量。
我们学到了什么
- 提升操作并不会实际移动任何代码,它只是将函数/变量声明放入内存中。
- 函数声明会被提升,并在执行期间在与函数声明相同的作用域内可用。
- 使用“var”定义的变量会被提升。
- 使用“let/const”定义的变量不会被提升。
更多资源
https://www.w3schools.com/js/js_hoisting.asp
https://developer.mozilla.org/en-US/docs/Glossary/Hoisting