深入理解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 相关文章推荐
jQuery学习笔记[1] jQuery中的DOM操作
Dec 03 Javascript
JavaScript中使用typeof运算符需要注意的几个坑
Nov 08 Javascript
浅谈JavaScript中null和undefined
Jul 09 Javascript
jQuery zclip插件实现跨浏览器复制功能
Nov 02 Javascript
JS绘制微信小程序画布时钟
Dec 24 Javascript
JS中用try catch对代码运行的性能影响分析
Dec 26 Javascript
JavaScript制作简易计算器(不用eval)
Feb 05 Javascript
微信小程序 侧滑删除(左滑删除)
May 23 Javascript
Angular2 自定义validators的实现方法
Jul 05 Javascript
node中使用es5/6以及支持性与性能对比
Aug 11 Javascript
js实现弹窗猜数字游戏
Nov 26 Javascript
基于JavaScript实现年月日三级联动
Jun 22 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
PHP安全防范技巧分享
2011/11/03 PHP
PHP静态调用非静态方法的应用分析
2013/05/02 PHP
php支付宝接口用法分析
2015/01/04 PHP
学习php设计模式 php实现策略模式(strategy)
2015/12/07 PHP
PHP使用数组依次替换字符串中匹配项
2016/01/08 PHP
PHP函数rtrim()使用中的怪异现象分析
2017/02/24 PHP
ThinkPHP 3.2.3实现页面静态化功能的方法详解
2017/08/03 PHP
Yii框架常见缓存应用实例小结
2019/09/09 PHP
JavaScript获取GridView中用户点击控件的行号,列号
2009/04/14 Javascript
js 创建快捷方式的代码(fso)
2010/11/19 Javascript
教您去掉ie网页加载进度条的方法
2010/12/09 Javascript
基于JQuery的多标签实现代码
2012/09/19 Javascript
js监听鼠标事件控制textarea输入字符串的个数
2014/09/29 Javascript
js生成随机数的过程解析
2015/11/24 Javascript
js随机生成26个大小写字母
2016/02/12 Javascript
JS实现兼容火狐及IE iframe onload属性的遮罩层隐藏及显示效果
2016/08/23 Javascript
jQuery滚动条美化插件nicescroll简单用法示例
2018/04/18 jQuery
JavaScript设计模式之构造器模式(生成器模式)定义与用法实例分析
2018/07/26 Javascript
layer.confirm取消按钮绑定事件的方法
2018/08/17 Javascript
jQuery实现动态加载(按需加载)javascript文件的方法分析
2019/05/31 jQuery
IE11下处理Promise及Vue的单项数据流问题
2019/07/24 Javascript
[01:08]2014DOTA2展望TI 剑指西雅图LGD战队专访
2014/06/30 DOTA
[05:24]TI9采访——教练
2019/08/24 DOTA
Python getopt模块处理命令行选项实例
2014/05/13 Python
Python单元测试框架unittest使用方法讲解
2015/04/13 Python
python的else子句使用指南
2016/02/27 Python
python判断数字是否是超级素数幂
2018/09/27 Python
Python爬虫设置代理IP(图文)
2018/12/23 Python
Python中利用LSTM模型进行时间序列预测分析的实现
2019/07/26 Python
使用Python实现牛顿法求极值
2020/02/10 Python
Python3自定义json逐层解析器代码
2020/05/11 Python
Canvas与图片压缩的示例代码
2017/11/28 HTML / CSS
Nº21官方在线商店:numeroventuno.com
2019/09/26 全球购物
党员创先争优公开承诺书
2014/03/28 职场文书
公司任命书模板
2014/06/06 职场文书
小学三好学生事迹材料
2014/08/15 职场文书