一个先有鸡还是先有蛋的问题,先看一段代码:

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的更多相关文章

  1. javasrcipt的作用域和闭包(二)

    这篇博客主要对词法作用域与欺骗词法作用域.函数作用域与块级作用域.函数内部的变量提成原理进行详细的分析,在这篇博客之前,关于作用域.编译原理.浏览器引擎的原理及关系在javaScript的作用域和闭包 ...

  2. 你不知道的JS之作用域和闭包(三)函数 vs. 块级作用域

      原文:你不知道的js系列 在第(二)节中提到的,标识符在作用域中声明,这些作用域就像是一个容器,一个嵌套一个,这个嵌套关系是在代码编写时定义的. 那么到底是什么产生了一个新的作用域,只有函数能做到 ...

  3. javascript的作用域和闭包(三)闭包与模块

    一些很重要的说明:前面三篇博客详细的介绍了,引擎与编译器和作用域的关系,重点需要理解的是编译器中的分词与词法分析,JavaScript的特有的“赋值操作的左右侧”引用操作:编译阶段的词法作用域的工作原 ...

  4. JS三座大山再学习(二、作用域和闭包)

    原文地址 作用域 JS中有两种作用域:全局作用域|局部作用域 栗子1 console.log(name); //undefined var name = '波妞'; var like = '宗介' c ...

  5. python-函数(命名空间、作用域、闭包)

    一.命名空间 全局命名空间 局部命名空间 内置命名空间 *内置命名空间中存放了python解释器为我们提供的名字:input,print,str,list,tuple...它们都是我们熟悉的,拿过来就 ...

  6. JS三座大山再学习 ---- 作用域和闭包

    本文已发布在西瓜君的个人博客,原文传送门 作用域 JS中有两种作用域:全局作用域|局部作用域 栗子1 console.log(name); //undefined var name = '波妞'; v ...

  7. Python进阶-II 参数陷阱、命名空间、嵌套、作用域、闭包

    一.参数陷阱 在使用默认参数时,可能碰见下列情况 def show_args_trap(i, li = []): li.append(100) li[i] = 101 print(li) show_a ...

  8. 我不知道的js(一)作用域与闭包

    作用域与闭包 作用域 什么是作用域 作用域就是一套规则,它负责解决(1)将变量存在哪儿?(2)如何找到变量?的问题 作用域工作的前提 谁赋予了作用域的权利?--js引擎 传统编译语言编译的过程 分词/ ...

  9. JavaScript之作用域与闭包总结

    博主最开始接触程序是C语言,C++,后来是java,现在是php,无论哪一种语言与javascript在机制上都还是有比较大的区别. 下面总结一下用面向对象的思想写javascript需要区分的要点: ...

随机推荐

  1. C Looooops POJ - 2115 (exgcd)

    一个编译器之谜:我们被给了一段C++语言风格的循环 for(int i=A;i!=B;i+=C) 内容; 其中所有数都是k位二进制数,即所有数时膜2^k意义下的.我们的目标时球出 内容 被执行了多少次 ...

  2. ans Single VIP LLB and SLB config

    ans Single VIP LLB and SLB config 配置命令: # 配置设备工作模式和开启的功能 > enable ans mode FR MBF Edge USNIP L3 P ...

  3. [luogu3810][bzoj3262]陌下花开【cdq分治】

    题目描述 有n朵花,每朵花有三个属性:花形(s).颜色(c).气味(m),用三个整数表示.现在要对每朵花评级,一朵花的级别是它拥有的美丽能超过的花的数量.定义一朵花A比另一朵花B要美丽,当且仅Sa&g ...

  4. 一种使用 emwin 绘制图片的方法

    @2018-12-10 [小记] 使用官方 <GUIBuilder.exe> 软件里的 Image 控件,注意格式为 .bmp,这种方式是将图片数据直接转为十六进制数据存储至静态区 具体使 ...

  5. H5左侧滑删除简单实现

    简单的左滑删除 实现功能 在一个列表的一条中,往左滑动时,右边出现删除按钮,点击可删除这一条 实现办法 列表中一项为父div,其中包含内容div和删除按钮span 父div相对定位,设置宽度.内容di ...

  6. luogu5008 逛庭院 (tarjan缩点)

    首先如果这是一个DAG,我按照拓扑序倒着去选,一定能选到所有入度不为0的点 然后考虑有环的情况 我们拎出来一个强连通分量 先假设它缩点以后是没有入度的 那我最后它里面一定至少剩一个不能选 因为就剩一个 ...

  7. SCOI2008着色方案(记忆化搜索)

    有n个木块排成一行,从左到右依次编号为1~n.你有k种颜色的油漆,其中第i 种颜色的油漆足够涂ci 个木块.所有油漆刚好足够涂满所有木块,即 c1+c2+...+ck=n.相邻两个木块涂相同色显得很难 ...

  8. 按奇偶排序数组 II

    题目描述 给定一个非负整数数组 A, A 中一半整数是奇数,一半整数是偶数. 对数组进行排序,以便当 A[i] 为奇数时,i 也是奇数:当 A[i] 为偶数时, i 也是偶数. 你可以返回任何满足上述 ...

  9. hdu 1160 FatMouse's Speed (最长上升子序列+打印路径)

    Problem Description FatMouse believes that the fatter a mouse is, the faster it runs. To disprove th ...

  10. CodeForces - 589D(暴力+模拟)

    题目链接:http://codeforces.com/problemset/problem/589/D 题目大意:给出n个人行走的开始时刻,开始时间和结束时间,求每个人分别能跟多少人相遇打招呼(每两人 ...