foo();
function foo(){
console.log(a) // undefined
};
var a = 1;
从这个例子来看,至少有两个奇异的地方:
- 我们在定义函数
foo
之前就执行了它,却没有报错。 - 我们在定义变量
a
之前就访问了a
,却返回了undefined
也没有报错,说明在执行foo
的时候a
就已经存在了,只是没有被赋值。
这就涉及到 JavaScript 的提升(Hoisting)。
提升只是个描述
虽然上述代码可以被看作:
var a;
function foo(){
console.log(a)
};
foo();
a = 1;
但这是对 JavaScript 引擎处理的一种具象描述,仿佛变量和函数声明都被提升到了当前代码块的顶部。但是事实上代码并没有发生变化,只是引擎在幕后做了一些别的准备工作。
在代码执行前的编译阶段,JavaScript 引擎会扫描我们的代码并收集所有的函数声明及变量声明,然后将他们添加到一个被称为词法作用域(Lexical Environment)下的环境记录器的数据结构中。
词法作用域由两部分构成:
- 环境记录器(Environment Record)
- 一个对外部词法环境的引用(如不存在则引用为空,possibly null reference to an outer Lexical Environment)
环境记录器所做的是所谓:标识符 - 变量的映射(identifier - variable mapping),它的数据结构可以理解为某种形式的一组键值对。但需要注意的是,此处“标识符”(identifier)就是变量名或函数名,“变量”指的是基本数据(primitive value)或者对引用类型(reference,函数、类等)的引用。概念上可以这样描述:
// lexicalEnvDemo
lexicalEnvironment = {
outerLexicalEnvironment: null,
environmentRecord: {
a: <value>,
funcA: <reference to funcA>,
objB: <reference to objB>,
}
}
词法作用域在执行前创建且生命贯穿整个执行阶段,这也就解释了为什么看上在 foo
函数声明前就能使用它,和在变量 a
被定义前就能访问它。这就是整个”提升“的过程。在整个执行过程中,代码所需要的变量和函数都在词法作用域当中搜索,自然而然会有两个结果:
- 找到了
identifier
,返回值或引用,如果没有赋值就返回undefined
- 找不到
identifier
, 报错并打印Uncaught ReferenceError: <identified> is not defined
提升的过程
当 JavaScript 引擎在编译阶段找到一个声明,它就会将这个声明添加到环境记录器。这个过程分为 2 步,只涉及创建和初始化:
- 创建:将
identifier
添加到环境记录器中,开辟一个identifier: <uninitialized>
位置 - 初始化:
- 函数:
identifier: <reference to aFunction>
- 变量:
identifier: undefined
(毕竟引擎在此时无从知晓我们的代码到底会给a
一个什么类型)
- 函数:
从一开始的例子中我们也可以隐约发现,引擎对变量 a
和函数 foo
的处理不太一样。变量 a
在被提升后,值却没有被提升,而是给了个 undefined
。函数 foo
却全面被提升了,以至于 foo()
可以正确执行。
最后,当引擎在执行阶段遭遇对 identifier
的赋值语句的时候,它会最终将值或引用添加到环境记录器,identifier: <reference to someObject>
或 a: <someValue>
网上的许多教程认为提升变量声明的时候还有第 3 个赋值的步骤,把 identifier: undefined
变成 identifier: <value>
。对此,我不太认同,原因正是因为开头的代码:
foo();
function foo(){
console.log(a) // undefined
};
var a = 1;
foo()
的执行说明已经到了执行阶段,a
依然是 undefined
。所以我认为赋值在提升过程当中是不存在的。我可能大错特错,望指正。
提升函数声明(function
)
function
语句的函数声明的提升可以看作有最高的优先级,且不会被同名的变量声明干扰。这就解释了 foo
函数在顶部可以正常执行的原因。
提升变量声明(var
)
var a = 1;
会被引擎分拆处理
var a; // 归编译期处理
a = 1; // 归执行期处理
函数表达式与类表达式
var a = function (){};
var b = new Object();
引擎对函数表达式和类表达式的处理可以看作是变量。
/* 归编译期处理 */
var a;
var b;
/* 归执行期处理 */
a = function (){};
b = new Object();
提升与赋值是发生在两个不同时期的事情,不相互影响。
重名处理
- 如果出现同名的函数,后到的会覆盖之前的。
foo(); // 2
function foo(){
console.log(1)
};
function foo(){
console.log(2)
};
- 如果出现同名的变量,它们不会互相影响,以为它们的初始化结果都是
undefined
。 - 如果出现同名的函数和变量,由于函数有提升的最高优先级,
identifier
所指向的一定是函数,undefined
不会覆盖对函数的引用。
存在变量提升优先级吗?
网上有着五花八门对提升优先级的说法。比方说,Mabishi Wakio 这篇教程让我收益良多,但我认为其在优先级(Order of precedence)这部分的讲述不够严谨。文章认为提升声明是有优先级的,变量赋值 > 函数声明,而函数声明 > 变量声明。理由如下两段代码所示:
var double = 22;
function double(num) {
return (num * 2);
}
console.log(typeof double); // Output: number
变量赋值 > 函数声明,所以 double
是 number
类型。
var double;
function double(num) {
return (num * 2);
}
console.log(typeof double); // Output: function
而函数声明 > 变量声明,所以 double
是 function
类型。
但这样的优先级无法解释下面的输出
double(4);
var double = 22;
function double(num) {
console.log(num * 2, typeof double); // 8, "function"
}
显然函数声明优先于变量赋值了。所以我认为没有这么复杂,应该只有一种优先级,即 funciton
语句的声明优先于 var
语句的声明被环境记录器所记录。同名的 double
变量声明不会用 undefined
覆盖同名的 double
对 function double(){}
的引用。对 double
的赋值的确会覆盖函数引用,那已经是执行阶段的事情了,与提升阶段无关了。
// 提升了先占位
function double(num) {
return (num * 2);
}
// 提升而未覆盖
var double;
double = 22;
console.log(typeof double); // Output: number
这样的解释我觉得更简洁一些。Again, 我也可能大错特错,望指正。
let
存在变量提升
var a = 1;
function foo(){
// beginning of TDZ
console.log(a) // Uncaught ReferenceError: Cannot access 'a' before initialization
/* ... */
// end of TDZ
let a;
}
foo();
这里例子说明被 let
声明的变量在其作用域中一定是被提升创建的但不提升初始化和赋值。要不然 foo
里面的 a
应该是全局的 1
。只不过从代码块开始到 a
被 let
声明之前该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
仅限于提升方面的区别,let
可以近似看作严格模式下的 var
,不声明就不能使用。 const
则更严格,const
声明的变量必须直接初始化。
const a; // Uncaught SyntaxError: Missing initializer in const declaration
总结
在 ES6 推出后,let
和 const
的应用基本上可以终结提升与否的迷思,它们不再像 var
这般暧昧。相当于说别折腾了,没声明就是用不了。
参考资料
- Hoisting
- JavaScript Hoisting
- Demystifying JavaScript Variable Scope and Hoisting
- Annotated ECMAScript 5.1
- javascript的执行环境、词法环境、变量环境
- Lexical environment and function scope
- Hoisting in Modern JavaScript — let, const, and var
- es6中let存在变量声明提升
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!