深入理解JavaScript中的预解析


Posted in Javascript onJanuary 04, 2017

前言

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代码执行的环境,称之为全局作用域。

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

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

深入理解JavaScript中的预解析
内存模型.png

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

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

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

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

深入理解JavaScript中的预解析
第一行代码输出结果.png

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

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

深入理解JavaScript中的预解析
第二行代码输出结果.png

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

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

二、预解析

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

2.1. 声明和定义

var num = 24;

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

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

深入理解JavaScript中的预解析

输出结果.png

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

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

深入理解JavaScript中的预解析
内存模型.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;

深入理解JavaScript中的预解析

输出结果.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中预解析的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。

Javascript 相关文章推荐
js调试工具 Javascript Debug Toolkit 2.0.0版本发布
Dec 02 Javascript
Extjs Ajax 乱码问题解决方案
Apr 15 Javascript
JavaScript快速检测浏览器对CSS3特性的支持情况
Sep 26 Javascript
window.open 以post方式传递参数示例代码
Feb 27 Javascript
JS点击某个图标或按钮弹出文件选择框的实现代码
Sep 27 Javascript
Vue.js 单页面多路由区域操作的实例详解
Jul 17 Javascript
原生JS获取元素的位置与尺寸实现方法
Oct 18 Javascript
nginx部署访问vue-cli搭建的项目的方法
Feb 12 Javascript
如何在 JavaScript 中更好地利用数组
Sep 27 Javascript
vue如何限制只能输入正负数及小数
Jul 04 Javascript
VueJS 取得 URL 参数值的方法
Jul 19 Javascript
React实现评论的添加和删除
Oct 20 Javascript
jQuery操作json常用方法示例
Jan 04 #Javascript
Bootstrap select下拉联动(jQuery cxselect)
Jan 04 #Javascript
jQuery Validate表单验证插件的基本使用方法及功能拓展
Jan 04 #Javascript
过期软件破解办法实例详解
Jan 04 #Javascript
jQuery.Validate表单验证插件的使用示例详解
Jan 04 #Javascript
javascript实现一个网页加载进度loading
Jan 04 #Javascript
AngularJS使用带属性值的ng-app指令实现自定义模块自动加载的方法
Jan 04 #Javascript
You might like
造就帕卡马拉的帕卡斯是怎么被发现的
2021/03/03 咖啡文化
PHP获取163、gmail、126等邮箱联系人地址【已测试2009.10.10】
2009/10/11 PHP
php实现根据字符串生成对应数组的方法
2014/09/22 PHP
ecshop实现smtp发送邮件
2015/02/03 PHP
php中header跳转使用include包含解决参数丢失问题
2015/05/08 PHP
兼容ie和firefox js关闭代码
2008/12/11 Javascript
JavaScript日历实现代码
2010/09/12 Javascript
使用jQuery获得内容以及内容的属性
2015/02/26 Javascript
js自调用匿名函数的三种写法(推荐)
2016/08/19 Javascript
Node.js与MySQL交互操作及其注意事项
2016/10/05 Javascript
微信小程序实现实时圆形进度条的方法示例
2017/02/24 Javascript
fullPage.js和CSS3实现全屏滚动效果
2017/05/05 Javascript
socket.io与pm2(cluster)集群搭配的解决方案
2017/06/02 Javascript
jQuery实现动态显示select下拉列表数据的方法
2018/02/05 jQuery
JS中判断某个字符串是否包含另一个字符串的五种方法
2018/05/03 Javascript
mui js控制开关状态、修改switch开关的值方法
2019/09/03 Javascript
JS监听组合按键思路及实现过程
2020/04/17 Javascript
[06:37]2014DOTA2国际邀请赛 昔日王者渴望重回巅峰
2014/07/12 DOTA
[09:59]DOTA2-DPC中国联赛2月7日Recap集锦
2021/03/11 DOTA
Python and、or以及and-or语法总结
2015/04/14 Python
Python中的条件判断语句基础学习教程
2016/02/07 Python
python实现用户登录系统
2016/05/21 Python
win7上python2.7连接mysql数据库的方法
2017/01/14 Python
Python进阶学习之特殊方法实例详析
2017/12/01 Python
PyQt5每天必学之创建窗口居中效果
2018/04/19 Python
一行代码让 Python 的运行速度提高100倍
2018/10/08 Python
Python中is和==的区别详解
2018/11/15 Python
500行Python代码打造刷脸考勤系统
2019/06/03 Python
Python爬虫库BeautifulSoup获取对象(标签)名,属性,内容,注释
2020/01/25 Python
python单例模式的应用场景实例讲解
2021/02/24 Python
水果花束:Fruit Bouquets
2017/12/20 全球购物
经济实惠的豪华背包和行李袋:Packs Project
2018/10/17 全球购物
如何防止同一个帐户被多人同时登录
2013/08/01 面试题
行政答辩状范文
2015/05/21 职场文书
LayUI+Shiro实现动态菜单并记住菜单收展的示例
2021/05/06 Javascript
python自动化八大定位元素讲解
2021/07/09 Python