深入理解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 相关文章推荐
线路分流自动跳转代码;希望对大家有用!
Dec 02 Javascript
javascript new一个对象的实质
Jan 07 Javascript
使用JavaScript动态设置样式实现代码及演示动画
Jan 25 Javascript
JS 获取select(多选下拉)中所选值的示例代码
Aug 02 Javascript
基于jQuery的图片不完全按比例自动缩小
Jul 11 Javascript
实例详解JSON数据格式及json格式数据域字符串相互转换
Jan 07 Javascript
详解Javascript中DOM的范围
Feb 13 Javascript
bootstrap实现的自适应页面简单应用示例
Mar 09 Javascript
Vue-router 切换组件页面时进入进出动画方法
Sep 01 Javascript
JavaScript创建、读取和删除cookie
Sep 03 Javascript
Node.js API详解之 vm模块用法实例分析
May 27 Javascript
浅析 Vue 3.0 的组装式 API(一)
Aug 31 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对象类型判断
2008/08/27 PHP
php 连接mssql数据库 初学php笔记
2010/03/01 PHP
Zend Studio去除编辑器的语法警告设置方法
2012/10/24 PHP
php unlink()函数使用教程
2018/07/12 PHP
关于this和self的使用说明
2010/08/01 Javascript
js导出格式化的excel 实例方法
2013/07/17 Javascript
javascript实现的DES加密示例
2013/10/30 Javascript
js获取对象为null的解决方法
2013/11/21 Javascript
js/jQuery简单实现选项卡功能
2014/01/02 Javascript
使用变量动态设置js的属性名
2014/10/19 Javascript
Javascript核心读书有感之类型、值和变量
2015/02/11 Javascript
JS获取及设置TextArea或input文本框选择文本位置的方法
2015/03/24 Javascript
jQuery 获取屏幕高度、宽度的简单实现案例
2016/05/17 Javascript
js 自带的sort() 方法全面了解
2016/08/16 Javascript
使用JavaScript开发跨平台的桌面应用详解
2017/07/27 Javascript
JS中的多态实例详解
2017/10/15 Javascript
JS实现碰撞检测的方法分析
2018/01/19 Javascript
JS实现的贪吃蛇游戏案例详解
2019/05/01 Javascript
JavaScript实时更新当前的时间的示例代码
2020/07/15 Javascript
[37:35]DOTA2上海特级锦标赛A组资格赛#1 Secret VS MVP.Phx第二局
2016/02/25 DOTA
Python实现子类调用父类的方法
2014/11/10 Python
Python是编译运行的验证方法
2015/01/30 Python
浅析python3字符串格式化format()函数的简单用法
2018/12/07 Python
python TF-IDF算法实现文本关键词提取
2019/05/29 Python
使用pandas读取文件的实现
2019/07/31 Python
Python求区间正整数内所有素数之和的方法实例
2020/10/13 Python
HTML5中的拖放实现详解
2017/08/23 HTML / CSS
Sport-Thieme荷兰:购买体育用品
2019/08/25 全球购物
新锐科技Java程序员面试题
2016/07/25 面试题
日语专业推荐信
2013/11/12 职场文书
中学生学雷锋活动心得体会
2014/03/10 职场文书
建设投标担保书
2014/05/13 职场文书
单位接收函范文
2015/01/30 职场文书
结婚保证书(卖身契)
2015/02/26 职场文书
Python中文纠错的简单实现
2021/07/07 Python
一次SQL查询优化原理分析(900W+数据从17s到300ms)
2022/06/10 SQL Server