前言

JavaScript是解释型语言是毋庸置疑的,但它是不是仅在运行时自上往下一句一句地解析的呢?

事实上或某种现象证明并不是这样的,通过《JavaScript权威指南》及网上相关资料了解到,JavaScript有“预解析”行为。理解这一特性是很重要的,不然在实际开发中你可能会遇到很多无从解析的问题,甚至导致程序bug的存在。为了解析这一现象,也作为自己的一次学习总结,本文逐步引导你来认识JavaScript“预解析”,如果我的见解有误,还望指正。

在ES6之前,变量使用var声明,会存在变量的预解析(函数也有预解析),我相信很多同学在刚开始学JavaScript的时候被预解析搞得团团转,虽然在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代码执行的环境,称之为全局作用域。

基本数据类型按照值来操作,引用数据类型按照地址来操作。

根据以上原则,以上的代码在内存中的模型为:


内存模型.png

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

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

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

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


第一行代码输出结果.png

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

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


第二行代码输出结果.png

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

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

二、预解析

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

2.1. 声明和定义

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

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

2.2. var声明的变量和function声明的函数在预解析的区别

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

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

程序最开始的时候,只对window下的变量和函数进行预解析,只有函数执行的时候才会对函数中的变量很函数进行预解析。

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。


内存模型.png

三、 作用域链

先理解以下三个概念:

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

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

  1. 先给形参num1、num2赋值,分别为100、200;
  2. func中的代码进行预解析;
  3. 执行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;
 

输出结果.png

num2 = 12; 相当于给window增加了一个num2的属性名,属性值是12;

var num1 = 12; 相当于给全局作用域增加了一个全局变量num1,但是不仅如此,它也相当于给window增加了一个属性名num,属性值是12;

问题:在私有作用域中出现一个变量,不是私有的,则往上级作用域进行查找,上级没有则继续向上查找,一直找到window为止,如果window也没有呢?

获取值:console.log(total); --> 报错 Uncaught ReferenceError: total is not defined

设置值:total= 100; --> 相当于给window增加了一个属性名total,属性值是100

function fn() {
// console.log(total); // Uncaught ReferenceError: total is not defined
total = 100;
}
fn();
console.log(total);

注意:JS中,如果在不进行任何特殊处理的情况下,上面的代码报错,下面的代码都不再执行了

五、 预解析中的一些变态机制

5.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。

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

fn(); // -> undefined(); // Uncaught TypeError: fn is not a function
var fn = function () {
console.log('ok');
} fn(); -> 'ok'
function fn() {
console.log('ok');
}
fn(); -> 'ok'

建议:声明变量的时候尽量使用var fn = ...的方式。

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

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

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

补充:其他定义自执行函数的方式

~ function (num) {}(100)
+ function (num) {}(100)
- function (num) {}(100)
! function (num) {}(100)

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

function fn() {
console.log(num); // -> undefined
return function () { };
var num = 100;
}
fn();

函数体中return下面的代码,虽然不再执行了,但是需要进行预解析,return中的代码,都是我们的返回值,所以不进行预解析。

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

var fn = 13;
function fn() {
console.log('ok');
}
fn(); // Uncaught TypeError: fn is not a function

经典题目

fn(); // -> 2
function fn() {console.log(1);}
fn(); // -> 2
var fn = 10; // -> fn = 10
fn(); // -> 10() Uncaught TypeError: fn is not a function
function fn() {console.log(2);}
fn();

总结

以上就是关于JavaScript中预解析的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。

js 预解析的更多相关文章

  1. js预解析相关知识总结以及一些好玩的面试题

    js预解析的题像在做智力题一样有意思~ 预解析 预解析:在解释这行代码之前发生的事情——变量的声明提前了,函数的声明提前 console.log(num) ——未定义Num,结果是报错 var num ...

  2. js预解析及特效

    预解析: // 作用域: // 域:空间.范围.区域…… // 作用:读.写 script 全局变量.全局函数 自上而下 函数 由里到外 {} 浏览器: “JS解析器” 1)“找一些东西” :var ...

  3. JS预解析

    1.在逐行读js代码前,解析器会先提取所有声明的var变量和函数 js解析器会先把脚本里所有var变量声明读一遍,但是它只读变量名字,不读变量值,一开始它会赋给所有读到的var变量一个[未定义]的值. ...

  4. js预解析问题总结

    //示例 1 alert(a) // undefind. alert(fn) // function 整个函数块. var a = 1; function fn(){ return falss; }; ...

  5. [妙味JS基础]第六课:作用域、JS预解析机制

    知识点总结 浏览器的解析方法 script 全局变量,全局函数 自上而下 函数 由里到外 "JS的解析器": 1)“找一些东西”:var function 参数 var a=未定义 ...

  6. javascript . 03 函数定义、函数参数(形参、实参)、函数的返回值、冒泡函数、函数的加载、局部变量与全局变量、隐式全局变量、JS预解析、是否是质数、斐波那契数列

    1.1 知识点 函数:就是可以重复执行的代码块 2.  组成:参数,功能,返回值 为什么要用函数,因为一部分代码使用次数会很多,所以封装起来, 需要的时候调用 函数不调用,自己不会执行 同名函数会覆盖 ...

  7. JS预解析机制

    JS的预解析过程: 1,预解析 2,再逐行解读代码, 实例: ---------------------------- <script>        var name="xm& ...

  8. js---07 js预解析,作用域---闭包

    js解析器首先不会逐行读代码,这是第二部了. 首先 根据var找到变量,根据function找函数,找到变量var a = 1,js解析器只会读取等号前面的var a,并把a设置值未定义,并不会读取等 ...

  9. js 预解析以及变量的提升

    js在执行之前会进行预解析. 什么叫预解析? 预:提前 解析:编译 预解析通俗的说:js在执行代码之前会读取js代码,会将变量声明提前. 变量声明包含什么?1.var 声明 2.函数的显示声明. 提前 ...

随机推荐

  1. [TJOI2015]组合数学

    题目描述 为了提高智商,ZJY开始学习组合数学.某一天她解决了这样一个问题:给一个网格图,其中某些格子有财宝.每次从左上角出发,只能往右或下走.问至少要走几次才可能把财宝全捡完. 但是她还不知足,想到 ...

  2. mysql 5.7快速部署

    目录 一:官网下载mysql二级制包.... 1 二:mysql二级制包解压.... 1 三:设置mysql库文件路径与授权... 1 四. 创建配置文件... 2 五:数据库初始化... 5 六: ...

  3. Servlet容器初始化IOC容器

    <!-- ServletContext参数,配置Ioc容器的xml文件名 --> <context-param> <param-name>contextConfig ...

  4. MyEclipse安装aptana插件, 8.5之前版本 和 之后版本, MyEclipse10安装aptana

    MyEclipse8.5安装aptana插件说明: 1.在myeclipse的安装目录下,有个common文件夹,建一个myplugIns,此目录下建aptana_update_024747目录 2. ...

  5. TCP_AIO_Server_ZC_02

    ZC: 这个例子是,1个skt 投递 N个未决的接收操作 (记得 以前查过 说 线程数是 CUP数量的2倍 比较合适) 1. // 当需要 投递多个接收操作的时候,可以将接收缓冲封装成类,然后再投递多 ...

  6. Android 中Json解析的几种框架(Gson、Jackson、FastJson、LoganSquare)使用与对比

    介绍 移动互联网产品与服务器端通信的数据格式,如果没有特殊的需求的话,一般选择使用JSON格式,Android系统也原生的提供了JSON解析的API,但是它的速度很慢,而且没有提供简介方便的接口来提高 ...

  7. vector push_back数量大的时候变慢

    才用15000个数据  push_back耗时就好几秒, 解决方法是 先resize 15000, 然后再 for (int i = 0; i < 15000; i++) { Data data ...

  8. javascript异步编程的几种方法

    目前工作中用的比较多的异步模式编程有如下几种方法 一 回调函数 这是异步编程最基本的方法,假设有两个函数f1和f2,后者等待前者的执行结果 f1(); f2(); 如果f1是一个很耗时的任务,可以考虑 ...

  9. 51nod 1639 递推

    http://www.51nod.com/onlineJudge/questionCode.html#!problemId=1639 1639 绑鞋带 基准时间限制:1 秒 空间限制:131072 K ...

  10. zzuli 2179 最短路

    2179: 紧急营救 Time Limit: 1 Sec  Memory Limit: 128 MBSubmit: 89  Solved: 9 SubmitStatusWeb Board Descri ...