之前在网上搜寻了关于 js 的 变量提升 的文章,但真正能让我深入理解的很少。在翻阅了他们的文章之后,我决定自己写一篇,那么为了深入地理解和加深记忆,我会结合着实际的代码去讲解,也希望能够对看到这篇文章的人有所启发。
💭本文首发掘金: 浅谈 JS 中的变量提升 Hoisting
概念
变量提升(Hoisting)被认为是, JavaScript 中执行上下文 (特别是创建和执行阶段)工作方式的一种认识。 ----- 摘自 【MDN-变量提升】
我们可以从比较权威的 MDN 官方文档中的这句话分析出以下结果
JavaScript是含有执行上下文(作用域) 的- 变量的声明到执行是有一个过程的
案例分析
下面将以三个不同的典范和一个综合为例,分析变量提升对结果导向的一些影响
变量的提升
if ( !('a' in window) ) {
var a = 10;
}
console.log(a); // undefined
分析为什么输出的是 a 是 undefined
-
首先我们知道
js中没有块级作用域 -
所以,在
if语句中「定义」的变量a会被js引擎解析到当前的上下文(在这里表示全局环境window)中,即会被预先「提升」到if语句外部 -
上述代码等效于:
var a; if ( !('a' in window) ) { a = 10; } console.log(a); // undefined -
变量
a被创建了,没有初始化和赋值,所以其值为默认的undefined,又由于它属于window对象上的一个属性,故为true,取反后false,条件语句内部没有执行,所以最终打印结果为undefined
结论: 「变量声明可被提升到作用域的最顶层」
变量和函数的提升
var a = 10;
test();
function test() {
console.log('01----', a); // undefined
var a = 20;
console.log('02----', a); // 20
var a = 30;
}
console.log('03----', a); // 10
跟上一案例类似,都有一个变量提升的过程,我们来分析下
-
首先在全局环境中「定义」了一个
a变量,为其赋值为 10 -
当前环境含有一个
test函数,所以它会被js引擎解析(提升)到当前作用域中,而且是全局作用域的最顶层 -
函数
test内部定义了两个局部变量a,所以同样会被解析到当前作用域中,这里是在test函数作用域的最顶层,注意第二个a的声明被忽略了 -
上述代码等效于:
var a = 10; test(); function test() { var a; console.log('01----', a); // undefined a = 20; console.log('02----', a); // 20 a = 30; } console.log('03----', a); // 10
结论: 「变量声明和函数声明均可被提升到作用域的最顶层,且同一变量实际上只会被声明一次」
从以上两个例子可以看出,「函数声明」和「变量声明」都会发生「作用域的提升」,那么这两个谁的声明提升层级更高一些呢,换句话说谁先被 js 引擎预解析呢?我们看下一个案例
提升层级对比
console.log(A) // function A(){}
function A(){}
var A = 'aaa'
console.log(A) // aaa
/* 颠倒声明的位置结果也一样 */
console.log(B) // function B(){}
var B = 'bbb'
function B(){}
console.log(B) // bbb
从打印的结果可以得知 「函数声明」层级高于「变量声明」
-
我们可以将上述代码理解为:
function A(){} // A 和 B 两个函数被提升到作用域最顶层 function B(){} console.log(A) // function A(){} var A; var B; A = 'aaa' console.log(A) // aaa /* 颠倒声明的位置结果也一样 */ console.log(B) // function B(){} B = 'bbb' console.log(B) // bbb -
函数
A和B被提升到了最顶层,接下来是执行打印代码,执行完之后,「变量声明」开始提升,由于仅仅是声明而未设置初值,所以此时的A和B依然为函数,直到重新被赋值后类型才发生改变
结论:
- 「函数的声明提升层级高于变量声明」
- 「函数声明的层级提升和它所处的声明位置无关」
OK,了解了这些,我们来看一道比较综合的题目
综合
function Foo() {
getName = function(){
console.log('1');
};
return this;
}
Foo.getName = function() {
console.log('2');
};
Foo.prototype.getName = function(){
console.log('3');
};
var getName = function() { // 函数表达式
console.log('4');
};
function getName(){ // 函数声明
console.log('5');
}
/* ------------打印,答案在后面------------ */
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
我们来具体分析下此题的变量提升
-
首先在全局环境中「定义」了一个变量:
getName(此时为函数表达式的形式) ,两个声明式的函数:getName和Foo -
声明式的函数
getName,会被js引擎解析(提升)到当前作用域的最顶层,此时它与Foo函数并列 -
变量
getName开始提升,随后将一个匿名函数function(){console.log('4')}赋给它,故之后调用getName函数时都会打印出来 4,除非该函数再次被更改 -
我们可以将上述部分代码理解为:
function Foo() { getName = function(){ console.log('1'); }; return this; } function getName(){ // 函数声明 console.log('5'); } var getName; getName = function() { // 函数表达式 console.log('4'); }; Foo.getName = function() { console.log('2'); }; Foo.prototype.getName = function(){ console.log('3'); }; -
来看看实际打印的结果跟你想的是否正确
/* ------------打印------------ */ Foo.getName(); // 2, getName(); // 4 Foo().getName(); // 1 getName(); // 1 new Foo.getName(); // 2 new Foo().getName(); // 3 new new Foo().getName(); // 3 -
我们来具体分析下
Foo.getName()很明显,这里调用的是函数Foo的getName私有方法,打印「2」getName(),在全局环境中调用,因函数和变量表达式的提升,打印「4」Foo().getName(),首先在全局中直接调用了Foo函数,函数内部对全局的getName函数进行了重新赋值,故之后调用getName函数时都会打印出来 1,函数返回了this,其指向window对象,再调用window下的getName函数,打印「1」getName()直接调,打印「1」new Foo.getName(),这是一个实例化操作,但是执行的优先级为Foo.getName(),其次才是new运算,打印「2」new Foo().getName(),依然是一个实例化,只不过执行的优先级变成了先new运算,其次再是调用getName函数(原型中的),打印「3」new new Foo().getName(),两次实例化,较上面的操作复杂一些,可以理解为:new ( new Foo().getName )(),打印「3」
总结
重复声明的变量会被忽略
变量和函数表达式的提升是声明提升,而函数则是连带函数体一起提升
函数声明提升层级高于变量提升
以上就是我对变量提升的一些思考,如有不足,欢迎评论区指正 ~