变量提升:在当前上下文中(全局/私有/块级),JS代码自上而下执行之前,浏览器会提前处理一下事情(可以理解为词法解析之前的一个环节)
- 会把当前上下文中所有带VAR/FUNCTION关键字的进行提前的声明和定义
- var a = 10
- 声明(declare):var a;
- 定义(defined):a = 10;
- 声明是创建变量的过程
- 定义是等号赋值的过程
- 带VAR的只会提前声明
- 带FUNCTION的会提前的声明定义
代码举例:
console.log(a);
var a = 12;
a = 13;
console.log(a)
解析代码:
- 代码执行之前:全局上文中的变量提升
- var a;(只声明,不定义),默认值是:undefined
- 变量提升完成,代码执行
console.log(a); // 此时a是默认值:undefined。所以结果就是:undefined
var a = 12; //
等号赋值分3步:
1. 创建值;2. 创建变量;3. 变量和值关联
但是变量提升阶段已声明,所以不会重复声明,只定义,a = 12
5. a 又被定义为 13
a = 13;
console.log(a); // 13
所以最后结果是:undefined,13
代码举例2:
func();
function func() {
var a = 12;// 函数执行会形成私有上下文,a 是函数的私有变量。
console.log('ok')
}
解析代码:
- 全局上下文中的变量提升
- func = 函数 (全局上下文中,函数会提前声明并提前定义)
- 变量提升完,执行代码
fucn();// ok
- 提示:因为变量提升阶段函数户提前声明定义,浏览器不会重复声明,所以执行完func()这句代码,便不再往下执行
补充:
- 变量提升时 var和function 哪个先声明?自上而下,谁先出现就找谁,只不过不会再重复声明。
- 实际项目中 建议用函数表达式创建,比较严谨
因为这样在变量提升阶段就只会声明func,不会赋值,所以var func = function() {}不过函数表达式还是建议用具名函数, 不要用匿名函数,不大符合规范。func() // func is not a function。 因为用函数表达式,只会声明,不会定义, var func = function() {}var func = function aaa(){ // 把原本作为值得函数表达式匿名函数'具名化',这个名字:aaa 只能在形成的私有上下文中使用,因为会把这个名字作为私有上文中的变量(值就是这个函数),不能在外边使用 console.log('ok'); console.log(aaa) // 当前函数 }; func()
代码举例3:
console.log(a); // ncaught ReferenceError: a is not defined
a = 13;
console.log(a);
解析代码:
- EC(G)变量提升
- 带var和function的才会变量提升,所以此案例中不存在变量提升,所以会报错。报错后,代码便不再往下执行。
- 带var和function的才会变量提升,所以此案例中不存在变量提升,所以会报错。报错后,代码便不再往下执行。
代码举例4:
console.log('ok') // ok
console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
let a = 12;
a = 13;
console.log(a);
解析代码:
- let 不存在变量提升,所以也就不会提前声明。
- 在新版本浏览器当中:走到第一行代码时,浏览器发现没有a,此时还会做一个操作,就是会看下后边代码有没有用let或const声明过a,如果有的话就会报错:不能在声明之前用它
知识点:
- 基于VAR或者FUNCTION 在全局上下文中声明的变量(全局变量)会映射到GO(全局对象window)上一份;基于映射机制,如果一个修改,另外一个也会跟着修改
var a = 12;
console.log(a); // 12 全局变量
console.log(window.a); // 12 映射到GO上得属性a
window.a = 13
console.log(a) // 13 映射机制是一个修改另外一个也会修改
练习题:
console.log(a, func)
if(!('a' in window)) {
var a = 1;
function func() {}
}
console.log(a)
代码解析:
-
EC(G): 全局上下文中的变量提升
-
判断体中,不论条件是否成立,都要进行变量提升(细节点:在判断体中,带FUNCTION的 在新版本浏览器中只会提前声明,不会再提前赋值了)
-
老版本中(新老版本以什么来划分呢?一般以IE10及IE10以内都是老版本):
- var a;
- func = 函数
-
新版本中:
- var a;
- func;
-
-
当然我们能现在还是按新版本来说.
已知不管判断条件是否成立,都要进行变量提升,因为a是全局的变量,前边也提过全局变量对象VO会映射的全局对象GO中,且在浏览器端GO就是指向window的,所以此时相当于给window设置可属性a: window.a,属性func:window.func
if(!('a' in window)) { // 已知存在window.a ,所以'a' in window 条件成立,取反就是条件不成立
var a = 1;
function func() {}
}
console.log(a)
- 等同于以下代码
if(false) {
var a = 1;
function func() {}
}
console.log(a) // undefined
- 条件不成立,所以就没办法执行判断体里边的代码,a也就没有赋值,只声明不定义(赋值)的话默认值就是:undefined
- 最后一行console.log(a) // undefined
和上题有类似地方的经典面试题:
var a = 0;
if (true) {
a = 1;
function a() {};
a = 21;
console.log(a)
}
console.log(a);
重复下现在最新版本浏览器特点:
- 向前兼容ES3/5规范
- 判断体和函数体等不存在块级作上下文,上下文只有私有和全局
- 不论条件是否成立,带function的都要声明+定义
- 向后兼容ES6规范
- 存在块级作用域,大括号中只要出现let/const/function....就会被认为是块级作用域
- 不论条件是否成立,带function的只提前声明,不会提前赋值了
解析:
- 先按老版本来算(IE10及以下)
- EC(G)全局变量提升
- var a;
- 遇到判断体了,不管条件是否成立,都要进行变量提升
- 不管是var a 还是fucntion a 都是声明一个变量。所以var a 会声明一个a,function a 则不在重复声明a,而是直接定义(赋值)a
- var a;
- a = 函数
- 代码执行,第一行代码
var a = 0;// 变量提升阶段 a已经声明过,此时只赋值就好:a = 10- 代码往下执行,判断体条件成立,a 被重新赋值为1:a = 1
- 再往下执行遇到 function a(){},在变量提升阶段已声明+定义a,所以这一行代码不会再执行
- 代码往下执行,a = 21
- 所以输出a结果是21
- 全局代买输出a也是21
- 最后结果是21,21
- EC(G)全局变量提升
- 按新版本来算
- 重复知识点:
- 存在块级作用域,大括号中只要出现let/const/function....就会被认为是块级作用域
- 不论条件是否成立,带function的只提前声明,不会提前赋值了
- EC(G)变量提升:
- var a;
- 不管判断体条件是否成立都要变量提升,但是不定义,所以:function a(已知浏览器不会重复定义,所以此时全局变量对象中只有 a)
- 代码执行
- 遇到 var a = 0; a赋值为0:a = 0
- 代码往下执行遇到判断体,且条件成立并且里边有function,所以会形成一个块级作用域
- 块级作用域是私有上下文,起名为:EC(BLOCK) 2 EC(BLOCK)进栈执行,把EC(G)压缩到底部
- 形成VO(BLOCK)(私有变量对象,也可以用AO((BLOCK),个人习惯问题)
- 初始化作用域链:<EC(BLOCK),EC(G)>
- 初始化this:没有自己的this,用的是上下文中的THIS(和箭头函数类型)
- 初始化ARGUMENT:无
- 形参赋值:无
- 变量提升
- 把大括号看作一个私有上下文,找到里边所有带var和function的
2.在私有上下文中:function要声明加定义(是的,总共要定义两次,1一次是全局的:只定义不声明,2次是块级作用域下 作为私有变量 声明+定义)
- a = 函数(存在AO(BLOCK)中)
- 把大括号看作一个私有上下文,找到里边所有带var和function的
2.在私有上下文中:function要声明加定义(是的,总共要定义两次,1一次是全局的:只定义不声明,2次是块级作用域下 作为私有变量 声明+定义)
- 代码执行
第二行代码: 变量提升阶段已处理过,不会重复操作。 因为要兼容ES3/ES56,function a 在全局下声明过,也在私有下处理过,遇到此行代码 ,私有上下文中不会再处理,但是浏览器会把当前代码之前所有对a的操作映射给全局一份 ,以此兼容ES3,但是它后边的代码和全局没有任何关系了a = 1; // EC(BLOCK)中变量都是私有的:a 是私有的, 赋值为1 function a() {}; a = 21; console.log(a)
第三行:a = 21 ; 此行开始,以后代码和全局都没有关系,私有变量a的值为:21
第四行:输出私有变量a,结果是:21 - 私有上下文EC(BLOCK)执行完毕,出栈,继续执行全局代码
- 全局代码:输出a,结果是:1
- 重复知识点:
练习题:
var a = 1;
var b = 23;
function asd() {
console.log(a) //undefind
var a = 1
console.log(a) // 1
console.log(b) // 23
}
if(true) {
a = 2
}
console.log(func)
if(false) {
function func() {}
}
asd()
console.log(a) //2
console.log(window.a) // 2
代码解析:
从js代码运行机制开始,复习一下
-
代码执行是在ECStack中执行的
-
全局代码执行会形成全局执行上下文EC(G),
-
全局上下文进栈
-
全局代码准备执行
-
全局代码执行前,变量提升,
-
在EC(G)中:变量提升(只发生在当前上下文中)
- var a;
- var b;
- asd = 函数
- 遇到if(true){} 判断体,不管条件是否成立,都会变量提升,判断体内只有一个变量a,a已经被声明,做过的事情不会在做了,所以此步忽略
- 代码再往下,遇到以下代码,不管判断条件是否成立,有var/function,都会进行变量提升注意:新版本浏览器中,function变量提升 只声明不定义
func = undefined;
if(false) { function func() {} }- 全局变量提升完毕,代码正式执行
-
代码自上而下执行
- 创建值1,a与1 关联
- 创建值23,b与23关联
- 函数asd 已在变量提升阶段声明加定义
- if(true),条件成立,a变成2
- console.log(func) ; 只声明,未定义,所以此时结果是:undefined
- asd() 函数执行
- 函数执行会形成自己的私有执行上下文EC(FUN),EC(FUNC)进栈执行,将EC(G)压缩到栈的底部,等待EC(FUNC)执行完毕后,再执行EC(G)
- 在EC(FUNC)中会有一块空间,AO 私有变量对象,这是用来存储私有变量的
- 函数正式执行前还要定义作用域,变量提升....
- 作用域:创建函数/判断体/块级作用域 所在的上下文
- 作用域链(scopeChain):<EC(FUN), EC(G)>。作用域链组成规则:<自己所在的私有上下文,创建函数时的上下文>
- 作用域链作用:在代码私有上下文执行的时候,遇到变量,先在AO中查找此变量是否是自己私有的,如果不是,则根据作用域链找上级上下文中是否存在此变量...知道找到全局
- EC(FUNC)中变量提升
- var a;// a存在私有变量对象AO中,只定义,不声明,值默认为undefined
- 代码执行
- console.log(a) => undefined
- var a = 1; a 赋值为 1
- console.log(a) => 输出1
- console.log(b) => 自己的AO中并没有b,根据作用域链项EC(G)中查找,EC(G)中的 VO中存有变量b,所以输出结果为:23
- 代码执行完毕
- 代码执行完毕,EC(FUNC)并没有被占用,所以出栈,继续执行EC(G)
- 继续执行全局代码:console.log(a) ,a已经在 if(true) 判断体中 赋值为2,所以此时输出结果是:2
- 最后执行 console.log(window.a) 而且接下来是一个修改,另外一个也会跟着修改
- 基于var和function在全局上下文声明的变量(全局变量)会映射到GO(全局对象window)上一份,所以输出结果为2(并且其中一个值修改,另一个也会修改)
变量提升特点总结:
- 变量提升只发生在当前上下文中,把当前上下文中所有带var和function的要提前进行变量声明。带var的提前声明,带function的提前声明加定义
- 在全局上下文当中通过var/function声明过的变量或者函数,也会映射到GO(windown),给GO(windown)增加相应的属性,通过映射机制,其中一个修改,另一个也会修改
- 特殊情况:带条件判断的,不管条件是否成立,判断体中如果有带var/function一定会进行变量提升。在老版本浏览器(IE10及以下)中,function是声明加定义的。 在新版本浏览器中,为了向后兼容ES6,function是只提前声明,不提前定义的。
- 如果在当前上下文中,已经声明过变量,在变量提升阶段,不会再重复声明,但会重新赋值
练习题:
EC(G)变量提升
-
带var/function的
- fn指向一个输出1的函数。 fn => 1
- 遇到第二个function fn,不再声明fn,但是指向新值 =>2
- => 3
- var fn; fn已经声明过
- => 4
- => 5
- 变量提升结束,此时全局上下文中只有一个全局变量fn,值是输出5的函数(此时window.fn => 5)
-
代码执行:
- fn() => 5
- fn() => 5
- fn() => 5
- 代码走到
var fn = function() { console.log(3) }这步时,var fn不用再处理,但是没有在变量提升阶段赋值,所以此时要处理,fn => window.fn => 3
- 接连三个函数调用,所以输出3,3,3
fn(); // 5
function fn() {
console.log(1) // 变量提升阶段以声明+赋值,代码执行阶段不再处理
}
fn() // 5
function fn() {
console.log(2)// 变量提升阶段以声明+赋值,代码执行阶段不再处理
}
fn() // 5
var fn = function() {
console.log(3) // 变量提升阶段没赋值,此时赋值为输出3的函数
}
fn() // 3
function fn() {
console.log(4) // 变量提升阶段以声明+赋值,代码执行阶段不再处理
}
fn()
function fn() {
console.log(5)// 变量提升阶段以声明+赋值,代码执行阶段不再处理
}
fn() // 5
最后结果是:5 5 5 3 3 3
练习题:
var foo = 1
function bar() {
if(!foo){
var foo = 10
}
console.log(foo)
}
bar()
-
执行环境栈供代码执行
-
全局代码执行,形成EC(G),全局上下文
-
EC(G)进栈执行
-
创建VO全局变量对象,存储全局变量
-
VO会映射到GO全局对象
-
变量提升
- var foo
- bar = 函数(创建堆AAAFFF000,bar = 函数会映射到GO)
- 堆内存中储存 代码字符串和键值对(name,prototype,——proto——等)
-
代码执行 foo = 1(结果同时会映射到GO)
-
bar() 函数执行
- 函数执行形成全新的私有上下文EC(BAR)
- EC(BAR)进栈执行,EC(G)被压缩到底部
- 创建AO私有变量对象,用来存储私有变量
- EC(BAR)正式执行前还要做很多事情
- 定于作用域<EC(BAR),EC(G)>
- 初始化this:window
- 初始化ARGUMENT
- 形参赋值....
- 变量提升
- 判断体条件是否成立,var都会进行变脸提升:var foo。 foo会存到AO中
- 代码执行
5.代码执行:
if(!foo){ var foo = 10 }- foo 是个变量,先在自己私有变量对象AO中查找,已知,foo 已给提前声明,未定义,默认值是undfined
- undefined 转布尔值是false,取反就是true
- 条件成立,执行判断体中的代码
- foo = 10 赋值
- 此时私有变量foo的值是10
- 私有上下文中的代码:console.log(foo),所以输出的是四哟变量foo的值,结果是:10
练习题:
var a = 10;
(function(){
console.log(a) // undefined
a = 5
console.log(window.a)
var a = 20
console.log(a)
})()
解析:
- EC(G)变量提升 var a(存到VO(G)中);
- 代码执行
- a = 10
- 自执行函数执行
- 函数作用域链:<EC(自执行),EC(G)>
- EC(自执行)变量提升
- var a(存到AO中);
- 函数内代码执行,console.log(a), AO中存有私有变量a,但没有定义,默认值是undefined,所以输出结果也:undefined
- 私有变量a = 5
- console.log(window.a), AO(G)中的对象会映射到GO(window),所以输出结果是:10
- 私有变量a = 20
- console.log(a) : 20
也就是等价于:
var a = 10;
(function(){
var a;
console.log(a) // undefined
a = 5
console.log(window.a)
a = 20
console.log(a)
})()
练习题:
var name = 'World!';
(function () {
if (typeof name === 'undefined') {
var name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})(); // Goodbye Jack
也就是等价于:
var name = 'World!';
(function () {
var name;
if (typeof name === 'undefined') {
name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
练习题:
f1();
console.log(b); // 9
console.log(c); // 9
console.log(a); a is not defined
function f1(){
var a = b = c = 9;
console.log(a); // 9
console.log(b); // 9
console.log(c); // 9
}
- EC(G)变量提升 f1 = 函数
- 代码执行,f1()
- 函数内代码变量提升
- var a = b = c = 9; 等价于
var a = 9 ; b = 9 ; c = 9; - 带var的提升:var a
- a = 9
- 依次输出a,b,c的结果是:9 9 9
- var a = b = c = 9; 等价于
- f1()执行完,执行全局的代码,依次输出 b,c,a
- b和c 没有带var 的情况下 看作是windown的属性,也就是全局的变量,所以输出b,c 依旧是:9 9
- a并没有定义,所以输出:a is not difined