转载请注明预见才能遇见的博客:http://my.csdn.net/
原文地址:https://blog.csdn.net/pcaxb/article/details/102396423
JavaScript原生系列-预解析、解析器、变量提升、函数提升、作用域、作用域链
目录
1.预解析概念
2.预解析过程
3.ES5和ES6提升区别
4.什么是提升、变量提升、函数提升、为什么会有提升
1.什么是提升(Hosting)?
2.变量提升
3.函数提升
4.为什么会有提升
5.函数提升和变量提升的具体情况
1)函数表达式没有函数提升(提升的是变量),函数声明的函数才有函数提升
2)函数和变量同名、变量同名、函数同名
3)函数内部同样适用于js预解析
6.深入解析过程
1)首先是找到script标签按照script块依次解析
2)解析执行环境
3)对标识符( var function)进行解析
7.案例
1.一个经典的案例
2.代码块中提升的案例(ES5没有块级作用域,所以此处是在全局作用域中)
8.作用域和作用域链
1)作用域:全局作用域和局部作用域
2)作用域链
3)函数的执行
4)在ES5中没有块级作用域,ES6出现后,增加了块级作用域
9.全局作用域下带var和不带var的区别
10. 预解析中的一些变态机制
1)不管条件是否成立,都要把带var的进行提前的声明
2)只预解析“=”左边的,右边的是指针,不参与预解析
3)自执行函数:定义和执行一起完成
4)return下的代码依然会进行预解析
5)名字已经声明过了,不需要重新的声明,但是需要重新的赋值
JavaScript是解释型语言是毋庸置疑的,但它不是自上往下一句一句地解析的。JS的解析过程分为两个阶段:预编译期(预处理、预解析、预编译)与执行期。
1.预解析概念
在当前作用域中,JS代码执行之前,浏览器预先会把一些东西(带function和var定义的变量)解析到内存中。
2.预解析过程
1)创建一个当前执行环境下的活动对象(Window)
2)将var声明的变量设置为活动对象的属性(也就是将其添加到活动对象当中),并将其赋值为undefined
3)将function定义的函数也添加到活动对象当中
在浏览器内部,有一块是专门解析JS数据的,我们可以称之为JS解析器。一旦浏览器识别到了SCRIPT标签,JS解析器就开始工作。 JS解析器,分两步执行:
第一步预解析:找一些东西,找程序中var关键字,如果找到了提前给var定义的变量赋值undefined,找程序中的普通函数,如果找到了,函数提升,将整个函数赋值给函数名。
第二步解读代码: 逐行解析代码。按照上下顺序。说明:如果碰到函数定义或者变量声明会忽略。
程序最开始的时候,只对window下的变量和函数进行预解析,只有函数执行的时候才会对函数中的变量和函数进行预解析。
3.ES5和ES6提升区别
这个是var关键字,注意只有var才有变量提升,ES6新增了let和const关键字,使得js也有了“块”级作用域,而且使用let和const声明的变量和函数是不存在提升现象的。let和const也被提升了,只是没有初始化,所以报错,看起来就像是没有被提升一样。
JS中无论哪种形式声明(var, let, const, function, function*, class)都会存在提升现象,不同的是, var,function,function*的声明会在提升时进行初始化赋值为 undefined,因此访问这些变量的时候,不会报 ReferenceError 异常,而使用 let,const,class 声明的变量,被提升后不会被初始化,这些变量所处的状态被称为“temporal dead zone”,此时如果访问这些变量会抛出ReferenceError 异常,看上去就像没被提升一样。
4.什么是提升、变量提升、函数提升、为什么会有提升
引擎会在解释JavaScript代码之前首先对其进行编译,编译过程中的一部分工作就是找到所有的声明,并用合适的作用域将他们关联起来,这也正是词法作用域的核心内容。
简单说就是在js代码执行前引擎会先进行预编译,预编译期间会将变量声明与函数声明提升至其对应作用域的最顶端。
变量提升即将变量声明提升到它所在作用域的最开始的部分。
L(a,person);//undefined undefined
var a = 1,person={name:'pca',age:22};
L(a,person);//1 {name:'pca',age:22}
函数提升即将函数声明提升到它所在作用域的最开始的部分。
L(f1); // ƒ f1() {L("222")}
L(f2); // undefined
function f1() {L("111")} //函数声明
var f2 = function() {} //函数表达式
function f1() {L("222")} // 函数提升,整个代码块提升到文件的最开始
L(f1);//ƒ f1() {L("222")}
L(f2);//ƒ () {}
//分析
function f1() {L("222")}
var f2;
L(f1);
L(f2);
f2 = function() {}
L(f1);
L(f2);
下面是Dmitry Soshnikov早些年的twitter,他也对这个问题十分感兴趣, Jeremy Ashkenas想让Brendan Eich聊聊这个话题:
Brendan Eich给出了答案:
大致的意思就是:由于第一代JS虚拟机中的抽象纰漏导致的,编译器将变量放到了栈槽内并编入索引,然后在(当前作用域的)入口处将变量名绑定到了栈槽内的变量。(注:这里提到的抽象是计算机术语,是对内部发生的更加复杂的事情的一种简化。)
然后,Dmitry Soshnikov又提到了函数提升,他提到了相互递归(就是A函数内会调用到B函数,而B函数也会调用到A函数):
Brendan Eich又给出答案:
Brendan Eich很确定的说,函数提升就是为了解决相互递归的问题,大体上可以解决像ML语言这样自下而上的顺序问题。
最后,Brendan Eich还对变量提升和函数提升做了总结:大概是说,变量提升是人为实现的问题,而函数提升在当初设计时是有目的的。
5.函数提升和变量提升的具体情况
// a2(); //a2 is not a function
a2_();
var a2 = function(){L("a2")};
function a2_(){L("a2_")};
当变量和函数同名时,函数提升比变量提升优先级高。但是和函数名相同变量的赋值还是会覆盖函数的。
2-1)函数和变量同名
L(a);//f a(){L(4)}
var a = 1;
L(a);//1
function a(){L(2)};
L(a);//1
var a = 3;
L(a);//3
function a(){L(4)};
L(a);//3
//分析
function a(){L(4)};
var a;//忽略
L(a);
a = 1;
L(a);
L(a);
a = 3;
L(a);
L(a);
2-2)函数同名
同名函数,后边覆盖前面
function a(){L(1)}
L(a)
function a(){L(2)}
L(a)
a()
//解析
function a(){L(1)}
function a(){L(2)}
L(a)
L(a)
a()
2-3)变量同名
同名变量,声明会被提升,后边会忽略
L(a)
var a = 1
L(a)
var a = 2
L(a)
//分析
var a;
var a; //忽略
L(a) // undfined
a = 1
L(a) //1
a = 2
L(a)//2
function t1(age) {
L(age);
var age = 27;
L(age);
function age() {}
L(age);
}
t1(3);//ƒ age() {} 27 27
//分析
function t1(age) {
function age() {}
var age;//忽略
L(age);
age = 27;
L(age);
L(age);
}
6.深入解析过程
首先是找到
如果把两个script块调换一下位置,结果就不一样了。
按照script块顺序进行预解析,当第一个script块预解析完,会解析到var msg 和function test,当再第二个script块中调用alert(msg);和test();时上面的js程序已经执行完毕了,自然会弹出 test 和this is function。
function test(){
var msg ='This is test';
}
alert(msg); // 报错msg未定义 (作用域不同,解析执行环境不同)
如果一个文档流中包含多个script代码段(用script标签分隔的js代码或引入的js文件),它们的运行顺序是:
步骤1. 读入第一个代码段(js执行引擎并非一行一行地执行程序,而是一段一段地分析执行的)
步骤2. 做语法分析,有错则报语法错误(比如括号不匹配等),并跳转到步骤5
步骤3. 对var变量和function定义做“预解析”(永远不会报错的,因为只解析正确的声明)
步骤4. 执行代码段,有错则报错(比如变量未定义)
步骤5. 如果还有下一个代码段,则读入下一个代码段,重复步骤2
步骤6. 结束
7.案例
我们知道函数一执行完是会被垃圾回收机制销毁的。
但了解闭包的朋友会相信,内存暂用这一说法的
其实 函数 return 直接返回的那个,其实是一个结果或者是值,是不需要预解释的。
说了这么多看代码:
var n = 99;
function outer(){
var n = 0;
return function inner(){
return n++; // 注意不是++n
}
}
var c = outer(); // c=function inner(){ return n++; }
var num1 = c(); // 0,然后再执行n++ 此时n=1;
var num2 = c(); // 1, n++ 2;
var d = outer(); //重新开辟新
var num3 = d(); //0
当我们的一个函数返回一个新的function,我们在外面定义一个变量来接收,这样这个函数
的内存就不能在执行完成后自动销毁,也就是我们所谓的函数内存被占用了。
变量的值要看它在哪定义,this,要看它在哪调用的。
var a = 1;
if(true){
L(a);//1
var a = 2;
L(a);//2
}
L(a);//2
if(false) {//
// var mark1 = 1;//Identifier 'mark1' has already been declared 标识符“mark1”已经声明
function mark1(){
L("exec mark1");//为true时会输出exec mark1
}
// var mark1;//Identifier 'mark1' has already been declared
L(mark1);//为true时直接输出整个函数
}
L(mark1);//为false时输出 undefined ;为true时直接输出整个函数
mark1();//if为true时,就会输出exec mark1,为false是 mark1 is not a function
8.作用域和作用域链
在ES5的时候,只存在两种作用域:函数作用域和全局作用域;ES6出现后,增加了块级作用域。
函数里面的作用域成为局部作用域,window所在的作用域称为全局作用域;在全局作用域下声明的变量是全局变量;在“局部作用域中声明的变量”和“函数的形参”都是局部变量;
var a = 0;//全局作用域
func();
function func(){
var b = 1;//局部作用域(函数作用域)
L(a); //0 函数作用域中访问全局变量
L(b); //1
}
L(b); //报错 全局作用域中访问func函数作用域中的局部变量
L(a);先到func中找a,找不到然后到上一层作用域中去找a,形成了作用域链。
在局部作用域中,代码执行的时候,遇到了一个变量,首先需要确定它是否为局部变量,如果是局部变量,那么和外面的任何东西都没有关系,如果不是局部的,则往当前作用域的上级作用域进行查找,如果上级作用域也没有则继续查找,一直查找到window为止,这就是作用域链。
var i = 1;
function fn1(){
var i = 5;
var j = 20
function fn2()
{
var i = 10;
function fn3()
{
var j = 15;
console.log(i); //10
}
fn3();
console.log(i); //10
console.log(j); //20
}
fn2();
}
fn1();
console.log(i); //1
当函数执行的时候,首先会形成一个新的局部作用域,然后按照如下的步骤执行:
第一步:如果有形参,先给形参赋值;
第二部:进行局部作用域中的预解析;
第三部:局部作用域中的代码从上到下执行
函数形成一个新的私有的作用域,保护了里面的私有变量不受外界的干扰(外面修改不了私有的,私有的也修改不了外面的),这也就是闭包的概念。
var a = 0;
if(a < 10)
{
a++;
var b = a;
}
console.log(b); //1 b是全局变量。处于全局作用域,会成为全局对象window对象的属性
理解:以上代码,虽然b是在if代码块中定义的,但由于ES5只有全局作用域和函数作用域,没有块级作用域,而b变量不是在函数中定义的,所以b只能是全局变量。
let a = 0; //注意:使用'let声明的全局变量不会成为window对象的属性
if(a < 10)
{
a++;
let b = a;
}
console.log(b); //报错 b是if代码块中的变量,只在'if'代码块{}中生效。处于块级作用域。
ES6中{ }会形成一个块级作用域,所以上面代码的b处于if这个块作用域中,不属于全局作用域。
9.全局作用域下带var和不带var的区别
在全局作用域中声明变量带var可以进行预解析,所以在赋值的前面执行不会报错;声明变量的时候不带var的时候,不能进行预解析,所以在赋值的前面执行会报错。
//正确,有变量提升
L(a);
var a = 1;
//错误,没有变量提升
L(b);//b is not defined
b = 2;
b = 2; 相当于给window增加了一个b的属性名,属性值是2;var a = 1; 相当于给全局作用域增加了一个全局变量a,但是不仅如此,它也相当于给window增加了一个属性名a,属性值是1;
function fn() {
// L(total); // Uncaught ReferenceError: total is not defined
total = 100;
}
fn();
L(total);//100
10. 预解析中的一些变态机制
if (!('num' in window)) {
var num = 12;
}
console.log(num); // undefined
JavaScript进行预解析的时候,会忽略所有if条件,因为在ES6之前并没有块级作用域的概念。本例中会先将num预解析,而预解析会将该变量添加到window中,作为window的一个属性。那么 'num' in window 就返回true,取反之后为false,这时代码执行不会进入if块里面,num也就没有被赋值,最后console.log(num)输出为undefined。
//1
fn(); // -> undefined(); // Uncaught TypeError: fn is not a function
var fn = function () {
console.log('ok');
}
//2
fn(); -> 'ok'
function fn() {
console.log('ok');
}
fn(); -> 'ok'
(function (num) {
console.log(num);
})(100);
自执行函数定义的那个function在全局作用域下不进行预解析,当代码执行到这个位置的时候,定义和执行一起完成了。
//return 后面的函数也是会变量提升的
var a = 1;
function foo() {
a = 10;
L(a);//10
return;
function a() {};
}
foo();
L(a);//1
//解析
var a;
a = 1;
function foo() {
function a() {};
a = 10;
L(a);
return;
}
foo();
L(a);
函数体中return后面的代码,虽然不再执行了,但是需要进行预解析,return中的代码,都是我们的返回值,所以不进行预解析。
var fn = 13;
function fn() {
console.log('ok');
}
fn(); // Uncaught TypeError: fn is not a function
//解析
function fn() {
console.log('ok');
}
var fn;//忽略
fn = 13;
fn();
fn(); //2
function fn() {console.log(1);}
fn(); //2
var fn = 10;
fn(); // Uncaught TypeError: fn is not a function
function fn() {console.log(2);}
fn();//不执行
//分析
function fn() {console.log(1);}
function fn() {console.log(2);}
var fn;//忽略
fn();
fn();
fn = 10;
fn();
fn();
参考资料:https://blog.csdn.net/bingo_wangbingxin/article/details/79159015
参考资料:https://www.cnblogs.com/shaohua007/p/7587657.html
JavaScript原生系列-预解析、解析器、变量提升、函数提升、作用域、作用域链
博客地址:https://blog.csdn.net/pcaxb/article/details/102396423