JavaScript预解析

简介
js作为一门解释型脚本语言,特点是边解析边执行。而预解析则是,提前解析的意思,也就是所说的变量提升。在ES6之前,变量使用var声明,会存在变量和函数的预解析,虽然在ES6的时候引入let和const,但是现阶段ES6并没有完全普及,而且很多比较老的代码都还是按照ES5的标准甚至是ES3的标准来书写的。

变量和函数在内存中的展示

JavaScript中的变量类型和其他语言一样,有基本数据类型和引用数据类型。基本数据类型包括:undefined、null、boolean、String、Number;引用数据类型主要是对象(包括{}、[]、/^$/、Date、Function等)。

var num = 24;
var obj = {name:'iceman' , age:24};
function func() {
    console.log('hello world');
}
  • 当浏览器加载html页面的时候,首先会提供一个供全局JavaScript代码执行的环境,称之为全局作用域。
  • 基本数据类型按照值来操作,引用数据类型按照地址来操作。

以上的代码在内存中的模型为:


image.png

基本类型是直接存储在栈内存中,而对象是存储在堆内存中,变量只是持有该对象的地址。所以obj持有一个对象的地址oxff44,函数func持有一个地址oxff66。

在以上的代码的基础上再执行:

console.log(func);
console.log(func());

第一行输出的是整个函数的定义部分(函数本身):


image.png

上面已经说明了,func存储的是一个地址,该地址指向一块堆内存,该堆内存就保留了函数的定义。

第二行输出的是func函数的返回结果:


image.png

由于func函数没有返回值,所以输出undefined。

注意:函数的返回结果,return后面写的是什么,返回值就是什么,如果没有return,默认返回值是undefined。

作用域

在解释JavaScript的预解析之前先来了解一下JavaScript的作用域,在ES6之间JavaScript的作用域主要分为:全局和函数级作用域,在ES6中,引入了块级作用域。因为自己是学Java的,所以这一块很好理解。

全局作用域
1.显示的声明:带有关键字 var 的声明;

var testValue = 123;

    var testFunc = function () { console.log('just test') };

    /**---------全局变量会挂载到 window 对象上------------**/

    console.log(window.testFunc)        // ƒ () { console.log('just test') }

    console.log(window.testValue)       // 123

2.隐式声明:不带有声明关键字的变量,JS 会默认帮你声明一个全局变量

 function foo(value) {

      result = value + 1;    // 没有用 var 修饰

      return result;
    };

    foo(123);               // 124
    
    console.log(window.result);         // 124 <=  挂在了 window全局对象上 

函数级作用域
函数作用域内,对外是封闭的,从外层的作用域无法直接访问函数内部的作用域

function bar() {
  var testValue = 'inner';
}

console.log(testValue);     // 报错:ReferenceError: testValue is not defined

外层的作用域无法直接访问函数内部的作用域,但是可以通过 return 访问函数内部变量:

function bar(value) {
  var testValue = 'inner';
  
  return testValue + value;
}

console.log(bar('fun'));        // "innerfun"

也可以通过 闭包 访问函数内部变量:

function bar(value) {
  var testValue = 'inner';
  
  var rusult = testValue + value;
  
  function innser() {
     return rusult;
  };
  
  return innser();
}

console.log(bar('fun'));        // "innerfun"

块级作用域:
在 ES6 之前,是没有块级作用域的概念的。如果你有 C++ 或者 Java 经验,想必你对块级作用域并不陌生;

for(var i = 0; i < 5; i++) {
  // ...
}

console.log(i)              // 5

很明显,用 var 关键字声明的变量,在 for 循环之后仍然被保存这个作用域里;

这可以说明:for() { }仍然在,全局作用域里,并没有产生像函数作用域一样的封闭效果;

如果想要实现 块级作用域 那么我们需要用 let 关键字声明!

for(let i = 0; i < 5; i++) {
  // ...
}

console.log(i)              // 报错:ReferenceError: i is not defined

同样能形成块级作用域的还有 const 关键字:

if (true) {
  const a = 'inner';
}

console.log(a);             // 报错:ReferenceError: a is not defined

let 和 const 关键字,创建块级作用域的条件是必须有一个 { } 包裹:

预解析

有了以上的内存模型的理解之后,就能更好的了解预解析的机制了。所谓的预解析就是:在当前作用域中,JavaScript代码执行之前,浏览器首先会默认的把所有带var和function声明的变量进行提前的声明或者定义。

var声明的变量和function声明的函数在预解析的时候有区别,var声明的变量在预解析的时候只是提前的声明,function声明的函数在预解析的时候会提前声明并且会同时定义。也就是说var声明的变量和function声明的函数的区别是在声明的同时有没同时进行定义。

声明和定义

var num = 24;

这行简单的代码其实是两个步骤:声明和定义。

  • 声明:var num; 告诉浏览器在全局作用域中有一个num变量了,如果一个变量只是声明了,但是没有赋值,默认值是undefined。
  • 定义:num = 12; 定义就是给变量进行赋值。

预解析只发生在当前的作用域下

只会对全局变量进行预解析

程序最开始的时候,只对window下的变量和函数进行预解析,全局变量是挂在windows对象下的,所以只会对全局变量进行预解析,只有函数执行的时候才会对函数中的变量和函数进行预解析。

console.log(num);
var num = 24;
console.log(num);

func(100 , 200); 
function func(num1 , num2) {
    var total = num1 + num2;
    console.log(total);
}
结果.png

第一次输出num的时候,由于预解析的原因,只声明了还没有定义,所以会输出undefined;第二次输出num的时候,已经定义了,所以输出24。

由于函数的声明和定义是同时进行的,所以func()虽然是在func函数定义声明处之前调用的,但是依然可以正常的调用,会正常输出300。

作用域链

先理解以下三个概念:

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

在私有作用域中,代码执行的时候,遇到了一个变量,首先需要确定它是否为私有变量,如果是私有变量,那么和外面的任何东西都没有关系,如果不是私有的,则往当前作用域的上级作用域进行查找,如果上级作用域也没有则继续查找,一直查找到window为止,这就是作用域链

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

  1. 如果有形参,先给形参赋值;
  2. 进行私有作用域中的预解析;
  3. 私有作用域中的代码从上到下执行

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

console.log(total); 
var total = 0;
function func(num1, num2) {
    console.log(total); 
    var total = num1 + num2;
    console.log(total);
}
func(100 , 200);
console.log(total); 

以上代码执行的时候,第一次输出total的时候会输出undefined(因为预解析),当执行func(100,200)的时候,会执行函数体里的内容,此时func函数会形成一个新的私有作用域,按照之前描述的步骤:

先给形参num1、num2赋值,分别为100、200;
func中的代码进行预解析;
执行func中的代码

因为在func函数内进行了预解析,所以func函数里面的total变量会被预解析,在函数内第一次输出total的时候,会输出undefined,接着为total赋值了,第二次输出total的时候就输出300。 因为函数体内有var声明的变量total,函数体内的输出total并不是全局作用域中的total。
最后一次输出total的时候,输出0,这里输出的是全局作用域中的total。

console.log(total); 
var total = 0;
function func(num1, num2) {
    console.log(total); 
    total = num1 + num2;
    console.log(total);
}
func(100 , 200);
console.log(total); 

将代码作小小的变形之后,func函数体内的total并没有使用var声明,所以total不是私有的,会到全局作用域中寻找total,也就说说这里出现的所有total其实都是全局作用域下的。

全局作用域下带var和不带var的区别

在全局作用域中声明变量带var可以进行预解析,所以在赋值的前面执行不会报错;声明变量的时候不带var的时候,不能进行预解析,所以在赋值的前面执行会报错。

console.log(num1);
var num1 = 12;

console.log(num2);
num2 = 12;
image.png
  • num2 = 12; 相当于给window增加了一个num2的属性名,属性值是12;
  • var num1 = 12; 相当于给全局作用域增加了一个全局变量num1,但是不仅如此,它也相当于给window增加了一个属性名num,属性值是12;
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 在ES6之前,变量使用var声明,会存在变量的预解析(函数也有预解析),我相信很多同学在刚开始学JavaScrip...
    iceman_dev阅读 8,665评论 5 34
  • 对一段js代码,js引擎并不是读一句执行一句,而是读取一段、解释执行一段。而一段一段执行,js会对读取的这段js代...
    糖小工阅读 695评论 0 0
  • js是运行在浏览器端的,浏览器内部众多功能中有一个小功能叫做js解析器。js在解析器在运行js代码时分为下面两步:...
    帅气的刘小二阅读 228评论 0 0
  • JavaScript是解释型语言是毋庸置疑的,那么它是不是仅在运行时自上往下一句一句地解析的呢? 事实上或某种...
    SandyFrankie阅读 185评论 0 0
  • 方松潭阅读 342评论 0 1