在当前执行上下文中(不论是全局,还是函数执行私有的),
JS
代码自上而下执行之前,首先会默认把所有带VAR
和FUNCTION
关键字的进行声明或者定义。
- 带
VAR
的只是提前声明- 带
FUNCTION
的会提前声明+定义
思维导图
一、先简单了解两个语法
拿var n = 100
为例,我们之前讲过的操作步骤应该是:
- 创建
值100
存储到栈内存中(引用数据类型首先创建堆,存完数据后,把堆的地址存放到栈中) - 创建一个变量
var a
; - 让变量和值关联在一起
- 创建
这是我们之前说过的创建变量并赋值的过程;
- 然而在这个过程中,我们所谓的创建变量的专业描述叫做=>
变量声明(declare)
; - 让变量和值关联在一起的专业描述叫做 =>
变量定义(defined)
上面我们所说的是我们常规的创建变量并赋值的操作。
那当我们只声明变量时,也就是
var a
;但是没有给变量赋值(也就是未定义),所以默认值是undefined
未定义 (这也是undefined
的由来)
二、理解变量提升的含义
为了更好的理解变量提升,我们还是得先从浏览器的机制说起:
1、浏览器运行机制
1、为了能够让代码执行,浏览器首先会形成一个
执行环境栈ECStack(Execution Context Stack)
- 栈内存的作用:
- 1.代码执行
- 2.基本数据值存储在栈内存中
- 堆内存作用:(本次我们用不到,只是提到栈内存时,自然提一下堆内存)
- 1.存储引用数据类型的值
这里我们不在过多讲解堆栈内存,前面已经专门讲过。
有了栈内存代码就可以自上而下的执行了,只不过刚开始,是要把全局下的代码先执行;
2、开始执行全局下的代码,就会形成一个
全局执行上下文EC(GLOBAL 简写 为G)
(上下文就是上文下文加一起就是环境的意思);- 相当于在栈内存中形成了一个小空间,用来执行全局下的代码;
- 以后我们的函数执行也会形成一个这样的小空间,用来执行函数中的代码,这个我们一会说;
1、2 两步整体原理:我们的目的是为了执行代码,所以围绕代码执行引发的下列操作:
- ==> 浏览器加载页面,想让代码执行,首先会形成一个
栈内存(执行环境栈 ECStack)
;然后开始让代码准备执行;- ==> 最开始要执行的一定是全局下的代码,
- ==> 此时形成一个
全局代码的执行环境(全局执行上下文 EC(G))
- ==> 形成之后,
把EC(G)压缩到栈内存中去执行(进栈
);每一个函数的执行也是这样操作的…- ==> 有些上下文在
代码执行完成后,会从栈内存中移除去(出栈)
,但是有些情况是不能移出去的(例如:全局上下文就不能移除…);- ==> 在下一次有新的执行上下文进栈的时候,会把
之前没有移除的都放到栈内存的底部,让最新要执行的在顶部执行
3、对应代码会在自己所属的执行上下文中执行,而这个环境中有一个存放变量的地方:
变量对象(VO/AO)
- 全局下存储变量的地方叫做:
全局变量对象VO(G) ->全称Variable Object (Global)
- 全局下存储变量的地方叫做:
- 所谓的变量对象,就是专门为了存储当前环境下要创建的变量的地方
在我们之前的理解,创建完一系列的环境后,就可以执行代码了,然而并没有,在代码执行之前还有一系列的操作要做;例如:词法解析 => 形参赋值 => 变量提升…等好多好多事情;词法解析和形参赋值我们后面会说,现在我们先来理解:变量提升
2、变量提升
变量提升:就是在当前上下文中,JS代码执行之前要做的事情;首先会默认把所有带
VAR
和FUNCTION
关键字的进行声明或者定义。
- 带
VAR
的只是提前声明- 带
FUNCTION
的会提前声明+定义
我们以下题为例
console.log(a);
console.log(func);
var a = 10;
function func(){
console.log(b);
var b=20;
console.log(b);
}
func();
console.log(a);
console.log(func);
复制代码
第一步:找到带
VAR
和先声明,找到带FUNCTION
的声明+定义- 1、
var a
; - 2、
function func(){...}
;(按照函数创建的步骤,即声明又定义)
- 1、
- 1).创建堆内存,把函数体内的代码当作字符串存储进堆内存中;
- 2).把堆内存的十六进制地址存进栈内存;
- 3).创建变量
func
与十六进制地址关联;
第二步:代码开始自上而下执行
- 1、
conlose.log(a)
:=> 此时在当前执行上下文中,已经声明过a
但是并没有赋值,所以打印结果为undefined
; - 2、
console.log(func)
:=> 此时当前上下文中已经声明了func
变量,并且已经赋值,所以打印func
的结果为f func(){...}
- 3、
var a = 10
:=> 之前我们在变量提升阶段,已经完成了var a
操作(浏览器是又懒惰性的,做完了这件事,不会在做第二遍了),所以此时只创建了一个值10
,没有在创建变量a
;把10
与之前在变量提升阶段声明的变量a
关联; - 4、
function func(){...}
:=> 上面我们说了,同一件事情浏览器不会做两便;很明显之前在变量提升阶段已经做过这件事了,所以此步就会跳过;直接进行下一步; - 5、
func()
:=> 让函数执行;
- 1、
- 目的:也是执行之前存储到堆内存中的代码,既然是要执行代码;就要形成一个执行上下文;这个上下文就叫做
私有上下文EC(func随便起的名字)
; - 1).浏览器处理过程:
- 目的:也是执行之前存储到堆内存中的代码,既然是要执行代码;就要形成一个执行上下文;这个上下文就叫做
- 形成后也同样要压缩进入执行环境栈中执行,
- 此时,执行环境栈中已有的全局执行上下文就会向下压缩(给函数的执行上下文腾出执行空间)
- 私有上下文中存储变量的地方叫做:
私有变量对象AO(func1随便起的名字) ->全称Active Object
;
- 2).函数执行过程:
- 第一步:变量提升:找到带
VAR
和先声明,找到带FUNCTION的声明+定义
- 第一步:变量提升:找到带
var b
;- 没有带
function
的
- 第二步:函数执行
console.log(b)
:=> 此时b
以声明为定义,所以是undefined
;var b=20
:=> 同全局一样,跳过var b
,直接创建值,并与之声明的b
关联;console.log(b)
:=> 此时,b
已经被赋值,所以结果为20
;- 函数执行完成
- 3).浏览器处理:
- 函数执行完成,出栈,
- 执行环境栈中空间被释放(以后再讲,这里先这样写),
- 之前被压缩到下面的全局执行上下文又回到原来的位置;
- 继续执行全局下的代码;
- 6、
console.log(a)
:=> 此时a
的值为10
; - 7、
console.log(func)
:=> 此时func
依然指向堆内存所对应的十六进制地址,所以打印结果为f func(){...}
;
- 6、
三、避免变量提升在函数执行时的不严谨性(不是重点简单介绍)
上面我们简单的理解了变量提升的含义;
- 那么我们思考一下这个题,是否会报错呢?
func();
function func(){
console.log('OK');
}
+ 答案是不会报错
复制代码
func();
var func = function (){
console.log('OK');
}
+ 答案是会报错:
+ 变量提升阶段:var func; 只定义不赋值 (默认值是undefined)
+ //=> undefined() =>Uncaught TypeError: func is not a function
复制代码
对比上面两题的区别,我们得出结论:
- 函数表达式方式创建函数,在变量提升阶段,并不会给函数赋值,这样只有代码执行的过程中,函数赋值后才可以执行(不能在赋值之前执行) =>符合严谨的执行逻辑 =>真实项目中推荐的方式
- 为了保证语法的规范性,JS中原本创建的匿名函数,我们最好设置一个名字,但是这个名字在函数外面还是无法使用的,只能在本函数体中使用(一般也不用) 例如:
var func = function anonymous() {
// console.log(anonymous); //=>当前这个函数
};
// console.log(anonymous); //=>Uncaught ReferenceError: anonymous is not defined
func();
//================================================
var func = function func() {
console.log(func); //=>函数名func
};
console.log(func); //=>变量func,存储的值是函数
func();
复制代码
四、变量提升在条件判断下的处理
截止目前(注意是目前),我们的上下文只有两种:
- => 全局上下文
- => 函数执行产生的私有上下文
1、全局上下文中带VAR
的
/*
* 全局上下文中的变量提升:
* [VO(G) 全局变量对象]
* var n; 不论IF条件是否成立都进行(只要不在函数里面,带VAR的都要变量提升)
* 无论是IF还是FOR中的VAR都进行提升;
*/
console.log(n); //=>undefined
if (1 > 1) { //=>条件不成立
var n = 100;
}
console.log(n); //=>undefined
复制代码
2、全局上下文中带FUNCTION
的
-1)[IE 10及以前以及谷歌等浏览器低版本状态下]:function func(){ ... }
声明+定义都处理了
-2)[最新版本的浏览器中,机制变了]:function func
; 用判断或者循环等包裹起来的函数,在变量提升阶段,不论条件是否成立,只会先声明
/*
* 全局上下文中的变量提升:
* [VO(G) 全局变量对象]
* [IE 10及以前以及谷歌等浏览器低版本状态下]
* function func(){ ... } 声明+定义都处理了
*
* [最新版本的浏览器中,机制变了]
* function func; 用判断或者循环等包裹起来的函数,在变量提升阶段,不论条件是否成立,此处只会先声明
*/
console.log(func); //=>undefined
if (1 === 1) {
// 此时条件成立,进来的第一件事情还是先把函数定义了(迎合ES6中的块作用域) => func=function(){ .... }
console.log(func); //=>函数
function func() {
console.log('OK');
}
console.log(func); //=>函数
}
console.log(func); //=>函数
复制代码
1、在当前执行上下文中,不管条件是否成立,变量提升是有效的
2、[IE 10及以前以及谷歌等浏览器低版本状态下]:
function func(){ ... }
声明+定义都处理了3、[最新版本的浏览器中,机制变了]:
function func
; 用判断或者循环等包裹起来的函数,在变量提升阶段,不论条件是否成立,只会先声明 重点在写一遍强调:
五、变量提升的两道经典面试题
1、写出结果
// 浏览器有一个特征:做过的事情不会重新再做第二遍(例如:不会重复声明)
/*
* 全局上下文中的变量提升
* fn = function(){ 1 } 声明+定义
* = function(){ 2 }
* var fn; 声明这一步不处理了(已经声明过了)
* = function(){ 4 }
* = function(){ 5 }
* 结果:声明一个全局变量fn,赋的值是 function(){ 5 }
*/
fn(); //=>5
function fn(){ console.log(1); } //=>跳过(变量提升的时候搞过了)
fn(); //=>5
function fn(){ console.log(2); } //=>跳过
fn(); //=>5
var fn = function(){ console.log(3); } //=>var fn; 这一步跳过,但是赋值这个操作在变量提升阶段没有搞过,需要执行一次 => fn = function(){ 3 }
fn(); //=>3
function fn(){ console.log(4); } //=>跳过
fn(); //=>3
function fn(){ console.log(5); } //=>跳过
fn(); //=>3
复制代码
2、分别写出在低版本浏览器和高版本浏览器下的输出结果
低版本
/*
* 低版本浏览器(包含IE10及以内)
* 全局上下文中变量提升
* 没有
*/
f=function (){return true;};//给GO中设置一个属性 f = function () {return true;}
g=function (){return false;};//给GO中设置一个属性 g = function () {return false;}
(function () {
/*
* 自执行函数执行,形成一个私有的执行上下文
* [变量提升]
* function g(){return true;}
*/
// 条件解析:
// g() => 私有的G执行 TRUE
// []==![] => []==false => 0==0 => TRUE
if (g() && [] == ![]) { //=>条件成立
f = function () {return false;} //f不是自己私有的,则向上查找,属于全局对象中的f,此处是把全局对象中的 f = function () {return false;}
function g() {return true;} //跳过(变量提升处理过了)
}
})();
console.log(f()); //=>FALSE
console.log(g()); //=>FALSE 这个G找全局的(函数里面的G是自己私有的)
复制代码
高版本
/*
* 高版本浏览器
* 全局上下文中变量提升:没有
*/
f=function (){return true;};//给GO中设置一个属性 f = function () {return true;}
g=function (){return false;};//给GO中设置一个属性 g = function () {return false;}
(function () {
/*
* 自执行函数执行,形成一个私有的执行上下文
* [变量提升]
* function g; 高版本浏览器中,在判断和循环中的函数,变量提升阶段只声明不定义
*/
// 条件解析:
// g() => undefined() => Uncaught TypeError: g is not a function 下面操作都不在执行了
if (g() && [] == ![]) {
f = function () {return false;}
function g() {return true;}
}
})();
console.log(f());
console.log(g());