JavaScript原生系列-预解析、解析器、变量提升、函数提升、作用域、作用域链

转载请注明预见才能遇见的博客: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.什么是提升、变量提升、函数提升、为什么会有提升

1.什么是提升(Hosting)?

引擎会在解释JavaScript代码之前首先对其进行编译,编译过程中的一部分工作就是找到所有的声明,并用合适的作用域将他们关联起来,这也正是词法作用域的核心内容。

简单说就是在js代码执行前引擎会先进行预编译,预编译期间会将变量声明与函数声明提升至其对应作用域的最顶端。

2.变量提升

变量提升即将变量声明提升到它所在作用域的最开始的部分。

L(a,person);//undefined undefined
var a = 1,person={name:'pca',age:22};
L(a,person);//1 {name:'pca',age:22}

3.函数提升

函数提升即将函数声明提升到它所在作用域的最开始的部分。

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);

 4.为什么会有提升

 下面是Dmitry Soshnikov早些年的twitter,他也对这个问题十分感兴趣, Jeremy Ashkenas想让Brendan Eich聊聊这个话题:

Brendan Eich给出了答案:

大致的意思就是:由于第一代JS虚拟机中的抽象纰漏导致的,编译器将变量放到了栈槽内并编入索引,然后在(当前作用域的)入口处将变量名绑定到了栈槽内的变量。(注:这里提到的抽象是计算机术语,是对内部发生的更加复杂的事情的一种简化。)

然后,Dmitry Soshnikov又提到了函数提升,他提到了相互递归(就是A函数内会调用到B函数,而B函数也会调用到A函数):

Brendan Eich又给出答案:

Brendan Eich很确定的说,函数提升就是为了解决相互递归的问题,大体上可以解决像ML语言这样自下而上的顺序问题。

最后,Brendan Eich还对变量提升和函数提升做了总结:大概是说,变量提升是人为实现的问题,而函数提升在当初设计时是有目的的。

5.函数提升和变量提升的具体情况

1)函数表达式没有函数提升(提升的是变量),函数声明的函数才有函数提升

// a2(); //a2 is not a function
a2_();
var a2 = function(){L("a2")};
function a2_(){L("a2_")};

2)函数和变量同名、变量同名、函数同名

当变量和函数同名时,函数提升比变量提升优先级高。但是和函数名相同变量的赋值还是会覆盖函数的。

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

3)函数内部同样适用于js预解析

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.深入解析过程

1)首先是找到script标签按照script块依次解析

首先是找到<script>标签按照<script>块依次解析,JS预解析不会跨<script>块去进行预解析。

<script>
    // alert(msg);//msg is not defined
    // test();//test is not defined
</script>
<script>
    var msg ="test";
    function test(){
        alert("this is function");
    }
</script>

如果把两个script块调换一下位置,结果就不一样了。

<script>
    var msg ="test";
    function test(){
        alert("this is function");
    }
</script>
<script>
    alert(msg);//test
    test();//this is function
</script>

按照script块顺序进行预解析,当第一个script块预解析完,会解析到var msg 和function test,当再第二个script块中调用alert(msg);和test();时上面的js程序已经执行完毕了,自然会弹出 test 和this is function。

2)解析执行环境

function test(){
    var msg ='This is test';
}
alert(msg); // 报错msg未定义 (作用域不同,解析执行环境不同)

3)对标识符( var function)进行解析

 

如果一个文档流中包含多个script代码段(用script标签分隔的js代码或引入的js文件),它们的运行顺序是:

步骤1. 读入第一个代码段(js执行引擎并非一行一行地执行程序,而是一段一段地分析执行的)

步骤2. 做语法分析,有错则报语法错误(比如括号不匹配等),并跳转到步骤5

步骤3. 对var变量和function定义做“预解析”(永远不会报错的,因为只解析正确的声明)

步骤4. 执行代码段,有错则报错(比如变量未定义)

步骤5. 如果还有下一个代码段,则读入下一个代码段,重复步骤2

步骤6. 结束

 

7.案例

1.一个经典的案例 

我们知道函数一执行完是会被垃圾回收机制销毁的。
但了解闭包的朋友会相信,内存暂用这一说法的
其实 函数 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,要看它在哪调用的。

2.代码块中提升的案例(ES5没有块级作用域,所以此处是在全局作用域中)

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出现后,增加了块级作用域

1)作用域:全局作用域和局部作用域

函数里面的作用域成为局部作用域,window所在的作用域称为全局作用域;在全局作用域下声明的变量是全局变量;在“局部作用域中声明的变量”和“函数的形参”都是局部变量;

var a = 0;//全局作用域
func();
function func(){
  var b = 1;//局部作用域(函数作用域)
  L(a); //0 函数作用域中访问全局变量
  L(b); //1
}
L(b); //报错 全局作用域中访问func函数作用域中的局部变量

L(a);先到func中找a,找不到然后到上一层作用域中去找a,形成了作用域链。

2)作用域链

在局部作用域中,代码执行的时候,遇到了一个变量,首先需要确定它是否为局部变量,如果是局部变量,那么和外面的任何东西都没有关系,如果不是局部的,则往当前作用域的上级作用域进行查找,如果上级作用域也没有则继续查找,一直查找到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

3)函数的执行

当函数执行的时候,首先会形成一个新的局部作用域,然后按照如下的步骤执行:

第一步:如果有形参,先给形参赋值;

第二部:进行局部作用域中的预解析;

第三部:局部作用域中的代码从上到下执行

函数形成一个新的私有的作用域,保护了里面的私有变量不受外界的干扰(外面修改不了私有的,私有的也修改不了外面的),这也就是闭包的概念。

4)在ES5中没有块级作用域,ES6出现后,增加了块级作用域

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. 预解析中的一些变态机制

1)不管条件是否成立,都要把带var的进行提前的声明

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。

2)只预解析“=”左边的,右边的是指针,不参与预解析

//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'

3)自执行函数:定义和执行一起完成

(function (num) {
    console.log(num);
})(100);

自执行函数定义的那个function在全局作用域下不进行预解析,当代码执行到这个位置的时候,定义和执行一起完成了。

4)return下的代码依然会进行预解析

//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中的代码,都是我们的返回值,所以不进行预解析。

5)名字已经声明过了,不需要重新的声明,但是需要重新的赋值

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章