javasrcipt的作用域和闭包(二)续篇之:函数内部提升机制与Variable Object
一个先有鸡还是先有蛋的问题,先看一段代码:
a = 2;
var a;
console.log(a);
通常我们都说JavaScript代码是由上到下一行一行执行,但实际这段代码输出的结果是2。但这段代码并不能为我们要讨论的问题提供完整的参考意义,所以再看一下代码:
console.log(a)
var a = 2;
这段代码的测试结果输出了undefined。
这两段代码打破了我们常说的JavaScript代码从上往下执行的说法,那到底是变量声明在前还是赋值在前呢?
函数预编译:
还记得我在上篇博客中分析词法作用域,如有代码是var a = 2;引擎会把这段代码分成两个声明来进行编译,第一次编译的是var a,第二次编译的是a = 2,在前面的所有内容我并没有对编译逻辑做任何解释,通常情况下我们也都认为这两个编译环节是一个先后相邻的编译逻辑过程,但是真实情况显然不是,如果是这样的话,上面两段代码的执行结果就会有很大的不同了,这是显而易见。
这就是JavaScript特有的一种编译机制,函数预编译的提升机制。
那这个机制到底在代码执行的时候干了什么呢?可以通过对上面两个示例采用内部机制的方式做一个人为的显示修改,来模仿内部提升机制处理程序。
第一段代码可以如下形式处理:
var a;
a = 2;
console.log(a);
第二段代码可以如下形式处理:
var a;
console.log(a);
a = 2;
由修改的代码可以看到一个规律,就是变量声明操作会被提升到程序的最上方。是的,这就是JavaScript的内部编译的提升机制。
而且这个机制不止适应于变量声明提升,还适应与函数声明提升。请看以下代码:
function foo(){
fn(a);//输出undefined
var a = 2;
fn(a);//输出2
function fn(sum){
console.log(sum);
}
}
foo();
这段代码不仅说明了函数声明也适应与提升机制,而且还说明了变量声明适应于提升机制,但是赋值还是会留在原地。
但是这并没有结束,请看以下代码:
function foo(){
var a = 2;
function a(){
console.log("aaa");
}
console.log(a)//2
a();//TypeError: a is not a function
}
foo();
别急,我再把这段代码稍微修改以下,你会发现惊喜的。
function foo(){
a();//aaa
var a = 2;
a();//TypeError: a is not a function
function a(){
console.log("aaa");
}
}
foo();
是不是感觉这两段代码瞬间摧毁了我们之前通过提升机制理解的内部机制,之前的提升机制好像在正常的情况下(命名不冲突的情况下)能为我们解决函数的内部执行问题,但是当遇到变量声明与函数声明冲突时就会让这个机制变得脆弱不堪,可能有的人会说,我们可开始规范点就好了,不要把命名混淆着用就是了。但是,在实际的开发中,我们有时候会需要利用同一个声明同时做变量和函数的载体,而且遇到问题就解决问题是我们开发人员的价值所在,所以我们有必要理解这种情况下,函数内部的编译到底发生了什么?
Variable Object:
很明显变量提升机制已经不足以解释我们的疑惑,通常我们都知道数据信息在编译的时候就是向内存写入数据,再在调用参数和方法时从内存中读取出来,也就是说变量和函数的声明都是在内存中开辟一个内存空间,然后在赋值的时候根据引擎提供的物理地址,将值保存到对应的内存空间里。然后在程序需要引用变量的值或者执行某个函数时再去到对应的内存地址取出这些数据,提供给引擎来执行。那么这里就会有一套管理这种数据读写的机制,这种机制成为变量对象(Variable Object)。
变量对象是一种特殊的对象,这个对象用来保存和管理函数的内部变量和函数,以及与内外嵌套作用域的关系。我们暂且不管它为什么特殊,先通过对象这个特性来理解函数的内部数据的读写机制。用下面这段代码来理解变量对象:
function foo(){
a();//aaa
var a = 2;
function a(){
console.log("aaa");
}
var b;
function b(){
console.log("bbb");
}
b();//bbb
b = 4;
console.log(a);//
console.log(b);//
}
foo();
在这里,我想重申一次,函数内部的提升机制任然存在,前面的代码出现的混乱情况只是内存管理机制所导致的,我们可以先模仿变量对象机制来处理变量声明。
var VO = {
a:undefined,
b:undefined
}
当变量提升后,上面的示例代码就会进行下一步操作,函数声明提升,变量对象的内部就会发生如下变化:
var VO = {
a:function(){...},
b:function(){...}
}
函数声明的内部提升与函数体被保存到内存可以看做是同步进行的,当函数名与之前提升的变量名相同时,变量会被覆盖成函数。然后就到了代码执行阶段。
函数执行的第一条代码就是a()执行,这时候执行的是被提升的a函数,所以打印出字符串aaa。
然后紧接着执行第二行代码var a = 2,而实际上因为变量名提升的机制,这行代码在引擎看来只是a = 2这样的声明存在了。所以VO会发生如下变化:
var VO = {
a:2,
b:function(){...}
}
因为提升机制,后面的a函数声明和b变量声明再到b函数声明都会跳过,因为这三行代码在之前的提升机制中被提升到了函数的最上方。所以会直接执行b()函数,打印出字符串bbb,然后紧接着又执行b = 4 赋值操作,变量对象内部的b属性的值会被修改成4,所以后面打印a,b的结果分别是2,4。
到这里应该就会很清楚之前代码的报错原因了,但是上面的例子中还缺了一点东西,就是如果函数带有参数怎么办?
其实有了上面的数据读写机制的流程解释就很好理解了,当函数带有参数时,因为js的参数没有严格的限制,形参和实参会有很大的区别,但是js的编译器的容错性很强,参数不对称并不会发生错误。而是会被统一看成是变量,如果有实际传入参数就相当于声明变量并赋值的操作。而这个赋值会在函数提升前操作,所以如果出现同名的函数声明也会被覆盖。
最后函数的内部提升机制和变量对象的读写机制做一个浏览总结,可以总体上分为四个步骤:
1.当函数执行时(准确说是执行的前一刻),内部会创建一个变量对象;
2.然后将形参和变量声明提升,作为VO的属性名,并赋值undefined
3.将实参的参数赋给对应的形参(实际上赋给变量对象对应变量的属性)
4.在函数体里面找到函数声明提升,然后将函数体作为值赋给在变量对象内对应的属性。
接下来就是函数真正执行的时刻了,再执行的时候就是对VO进行修改和查询操作了。
javasrcipt的作用域和闭包(二)续篇之:函数内部提升机制与Variable Object的更多相关文章
- javasrcipt的作用域和闭包(二)
这篇博客主要对词法作用域与欺骗词法作用域.函数作用域与块级作用域.函数内部的变量提成原理进行详细的分析,在这篇博客之前,关于作用域.编译原理.浏览器引擎的原理及关系在javaScript的作用域和闭包 ...
- 你不知道的JS之作用域和闭包(三)函数 vs. 块级作用域
原文:你不知道的js系列 在第(二)节中提到的,标识符在作用域中声明,这些作用域就像是一个容器,一个嵌套一个,这个嵌套关系是在代码编写时定义的. 那么到底是什么产生了一个新的作用域,只有函数能做到 ...
- javascript的作用域和闭包(三)闭包与模块
一些很重要的说明:前面三篇博客详细的介绍了,引擎与编译器和作用域的关系,重点需要理解的是编译器中的分词与词法分析,JavaScript的特有的“赋值操作的左右侧”引用操作:编译阶段的词法作用域的工作原 ...
- JS三座大山再学习(二、作用域和闭包)
原文地址 作用域 JS中有两种作用域:全局作用域|局部作用域 栗子1 console.log(name); //undefined var name = '波妞'; var like = '宗介' c ...
- python-函数(命名空间、作用域、闭包)
一.命名空间 全局命名空间 局部命名空间 内置命名空间 *内置命名空间中存放了python解释器为我们提供的名字:input,print,str,list,tuple...它们都是我们熟悉的,拿过来就 ...
- JS三座大山再学习 ---- 作用域和闭包
本文已发布在西瓜君的个人博客,原文传送门 作用域 JS中有两种作用域:全局作用域|局部作用域 栗子1 console.log(name); //undefined var name = '波妞'; v ...
- Python进阶-II 参数陷阱、命名空间、嵌套、作用域、闭包
一.参数陷阱 在使用默认参数时,可能碰见下列情况 def show_args_trap(i, li = []): li.append(100) li[i] = 101 print(li) show_a ...
- 我不知道的js(一)作用域与闭包
作用域与闭包 作用域 什么是作用域 作用域就是一套规则,它负责解决(1)将变量存在哪儿?(2)如何找到变量?的问题 作用域工作的前提 谁赋予了作用域的权利?--js引擎 传统编译语言编译的过程 分词/ ...
- JavaScript之作用域与闭包总结
博主最开始接触程序是C语言,C++,后来是java,现在是php,无论哪一种语言与javascript在机制上都还是有比较大的区别. 下面总结一下用面向对象的思想写javascript需要区分的要点: ...
随机推荐
- 对strom的理解
1.什么是strom: storm是一个分布式实时计算系统,用户只需要提供自己的插件(例如一个jar包,其中编写用户自己的逻辑代码),然后将它部署到storm服务器上,storm的master服务器就 ...
- 我的SSH框架实例(附源码)
整理一下从前写的SSH框架的例子,供新人学习,使用到了注解的方式. 源码和数据库文件在百度网盘:http://pan.baidu.com/s/1hsH3Hh6 提取码:br27 对新同学的建议:最好的 ...
- 【刷题】BZOJ 2759 一个动态树好题
Description 有N个未知数x[1..n]和N个等式组成的同余方程组: x[i]=k[i]*x[p[i]]+b[i] mod 10007 其中,k[i],b[i],x[i]∈[0,10007) ...
- 本文之后都以Vol1来指代
本文参考文档是<64-ia-32-architectures-software-developer-vol-1-manual>(本文之后都以Vol1来指代),介绍了x86架构的基础.这些基 ...
- linux中shell脚本引用另一shell脚本
调用有三种方法: 1.fork:不同的shell,调用后返回父shell,子shell从父shell中继承变量,但子shell的变量不会带回父shell,直接用path/to/file.sh调用: 2 ...
- canvas路径剪切和判断是否在路径内
1.剪切路径 clip() var ctx=mycanvas.getContext('2d'); ctx.beginPath(); // 建一个矩形路径 ctx.moveTo(20,10) ctx.l ...
- iptables防火墙详解(二)
-- 基于状态的iptables 如果按照tcp/ip来划分连接状态,有11种之多(课后可以自己去读一下相关知识) 但iptables里只有4种状态:ESTABLISHED.NEW.RELATED及I ...
- MYSQL主从复制制作配置方案
1. 主从复制机器配置 操作系统:centos7 x64 基于vagrant下的virtual box的虚拟机两台 master ip:192.168.21.11, slave ip 192.168. ...
- Servlet -- 重定向
重定向的两种方式: 1: protected void doGet(HttpServletRequest request, HttpServletResponse response) throws S ...
- Dubbo x Cloud Native 服务架构长文总结(很全)
Dubbo x Cloud Native 服务架构长文总结(很全) mercyblitz SpringForAll社区 3天前 分享简介 Cloud Native 应用架构随着云技术的发展受到业界特别 ...