剖析javascript闭包原理--结合java版js引擎讲解

1.前言

javascript的闭包是后端java程序员比较头痛不好理解的概念,所以本文结合js引擎从原理上剖析闭包的运行原理,让大家能对闭包有一个深入的理解.

2. 为什么基于java版Rhino引擎剖析

  1. java是最好的语言
  2. Rhino引擎是著名的javascript引擎 spiderMonkey 的java版
Rhino是jdk 1.6自带的js引擎,出自mozilla,其实现原理与firefox的js引擎高度相似
    项目介绍: https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino 
    源码地址: https://github.com/mozilla/rhino
最新版本的Nashorn为了执行性能,将js转换成为jvm字节码,
不利于我们剖析javascript的真正运行原理

3. 例子

3.1 典型闭包例子

var fun = (function(){
    var count = 101;
    function addCount(){
        count+=1;
        return count;
    }   
    return addCount; 
  })();
var result = fun();

一些java后端的程序员对类似上面的代码结构会有点不安.因为这里面函数又套一层函数,而且最终返回一个函数,而且还有(…)();这样的结构.我们可以把上面的例子改为如下,或者大家能还好理解.

function testFun(){
    var count = 101;
    function addCount(){
        count+=1;
        return count;
    }   
    return addCount; 
  } 
var result = testFun()();

修改为上面例子后,相对清淅不少,执行testFun()后返回的是addCount函数,再加个()就是执行addCount()函数.
如果有读者不习惯()(),那上面的代码最终可以改写为最终版例子

3.2 最终版例子

function testFun(){
    var count = 101;
    function addCount(){
        count+=1;
        return count;
    }   
    return addCount; 
  } 
var funMethod = testFun();
var result = funMethod();

上面最终例子相对于java程序员来讲,舒服多了。
但一些程序员可能还有疑惑,按习惯性思维testFun()函数返回结束后,返回结果后,其局部变量count应该要回收才是,addCount函数为什么还能使用它.如果在上面代码后面再执行:

var f1 = testFun();
var f2 = testFun();
console.log("闭包调用结果:"+testFun()());
console.log("f1第1次调用结果:"+f1());
console.log("f1第2次调用结果:"+f1());
console.log("f2第1次调用结果:"+f2());
console.log("f2第2次调用结果:"+f2());

结果为:

闭包调用结果:102
f5.html:18 f1第1次调用结果:102
f5.html:19 f1第2次调用结果:103
f5.html:20 f2第1次调用结果:102
f5.html:21 f2第2次调用结果:103

你们能正确且肯定地猜出结果吗,如果可以,本文你可以不用看下去了,如果猜不对的话,可以花几分钟继续阅读本文.

4 运行原理详解

4.1 作用域对象scope

javascript与c,c++,java等语言不一样,c,c++,java这些语言的方法调用的栈是存在于进程预先分配的线程空间中.随着方法不断调用栈越来越深(不过其栈顶sp值越来越小,因为在进程空间中各线程的栈空间是在分配在高地址位置,而堆空间是分配在低地址空间),而随着方法的退出其栈越来越浅.
但 javascript 是解释性语言,其栈的结构与c,c++,java很不一样.
javascript每个变量都必须保存在指定的scope中,除了全局作用域外,每个函数执行时,都会创建栈及作用域对象scope,函数执行多次,会创建多个scope
本最终版例子的作用域如下图所示
在这里插入图片描述

4.2 var funMethod = testFun()到底做了什么事情

从表面上看,这行代码首先是执行了testFun()方法,并最终返回addCount函数.
如下图所示:
在这里插入图片描述

4.2.1 testFun函数的执行

  1. testFun函数执行时,默认会创建其函数对应的scope对象
本函数中的局部变量及函数中定义的函数都会记录在这个scope中
  1. 返回addCount函数对象
返回的addCount函数对象的parentScope会指向
testFun函数所创建的scope对象

4.2.2 addCount函数对象赋值给funMethod

最终addCount函数会赋值给funMethod变量
而testFun函数的调用栈会销毁.
但是testFun函数执行过程中创建的作用域scope因为被addCount函数的parentScope所引用,所以逃过被销毁的命运

4.3 var result = funMethod()

这里表面执行funMethod,实际是执行funMethod所指向的addCount函数对象本身.且addCount在执行时,没有创建新作用域,而是使用像父scope即 testFun的作用域,所以能正常地使用count变量

5 代码执行过程分析

5.1 javascript执行流程

5.1.1 分词/词法分析(Lexing/Tokenizing)

这个过程会将字符组成的字符串分解成为Token.

5.1.2解析/语法分析(Parsing)

将词法单元法转换成为抽象语法树(Abstract Syntax Tree AST)

5.1.3 生成ByteCode代码

将ast语法树生成IR码,并最终生成byteCode码

5.1.4 以解释方式执行byteCode

以解释方式执行byteCode码

5.2 实例分析

以 最终版例子 演示闭包运行原理

5.2.3 主代码块的byteCode

ICode dump, for null, length = 25
MaxStack = 3
 [0] REG_IND_C0
 [1] CLOSURE_EXPR org.mozilla.javascript.InterpreterData@4617c264
 [2] POP_RESULT
 [3] LINE : 1
 [6] REG_STR_C0
 [7] BINDNAME
 [8] REG_STR_C1
 [9] NAME_AND_THIS
 [10] REG_IND_C0
 [11] CALL 0
 [12] REG_STR_C0
 [13] SETNAME
 [14] POP
 [15] REG_STR_C2
 [16] BINDNAME
 [17] REG_STR_C0
 [18] NAME_AND_THIS
 [19] REG_IND_C0
 [20] CALL 0
 [21] REG_STR_C2
 [22] SETNAME
 [23] POP
 [24] RETURN_RESULT

左边中括号中是pc序号.

5.2.3.1创建栈

在执行这段字节码之前需要创建栈对象CallFrame.
通过CallFrame.initializeArgs并初始化这个CallFrame栈.

5.2.3.2初始化栈
  1. 在这代码段中,scope 使用的是全局的scope
    通过ScriptRuntime.initScript(…)将result,funMethod等局部变量存放在当前作用域scope(全局)中
  2. 根据代码片段的对象 itsNestedFunctions属性检查本代码段是否有含function,如果有调用initFunction()方法初始化function,并将本function对象存在当前的scope中.本例子中,最外层的代码片段含有testFun函数
initFunction方法初始化函数时,会设置本函数的父scope,
将上层的prototype赋给本函数的prototype
  1. 分配stack空间
5.2.3.3主要的字节码执行解释

所有的指令都是通过Interpreter.interpretLoop()解释执行的

[1] CLOSURE_EXPR

将testFun函数对象存在stack[3]

[2] POP_RESULT

frame.result = stack[3]//就是把testFun函数对象存在frame.result

[7] BINDNAME

查找到funMethod局部变量所在的scope,将把它放在stack[3]

[9] NAME_AND_THIS

在当前的scope中找到testFun对象,把它放在stack[4]中
并把testFun的像scope(即全局scope)放在stack[5]中

[11] CALL 0

即出stack[4] (即testFun对象)存放在fun对象中
即出stack[5] (即testFun对象)存放在funThisObj对象中
初始化testFun函数执行 新CallFrame
并最终执行testFun函数,进入testFun栈运行
具体见下面的 testFun函数的byteCode 说明

[13] SETNAME

从stack[4]中取出addCount函数对象,将把它存在当前代码块scope的funMethod的局部变量中

[18] NAME_AND_THIS

stack[4]存诉addCount函数对象
stack[5]存放当前scope (即全局的scope)

[20] CALL 0

调用执行addCount函数
fun对象为addCount函数
funThisObj为全局的scope
具体见: 初始化addCount函数栈

[22] SETNAME

将102设置到当前scope的result局部变量中

5.2.4 testFun函数的byteCode

ICode dump, for testFun, length = 14
MaxStack = 2
 [0] LINE : 1
 [3] REG_STR_C0
 [4] BINDNAME
 [5] SHORTNUMBER 101
 [8] REG_STR_C0
 [9] SETNAME
 [10] POP
 [11] REG_STR_C1
 [12] NAME
 [13] RETURN
5.2.4.1 初始化testFun函数栈
  1. 创建testFun函数的scope (其类名为NativeCall),并在这个scope中设置其父scope,添加默认变量:arguments,以前count.
  2. 检查到本函数中有定义新函数addCount,将其初始化,并记录在本scope中
5.2.4.2 testFun字节码执行解释
[4] BINDNAME

检查含有count变量的scope,并将这个scope返回,存放在stack[2]

[5] SHORTNUMBER 101
    stack[3] = DBL_MRK;//标识栈这个位置为double
    sDbl[3] = getShort(iCode, frame.pc);//即101,sDbl是栈中存数值的地方
[9] SETNAME

将stack[3]中的值 存放在testFun函数的scope的count中

NAME

从testFun函数中取出addCount函数,并存放在stack[2]

RETURN

frame.result = stack[2]
将addCount返回

5.2.5 addCount函数的byteCode

ICode dump, for addCount, length = 15
MaxStack = 3
 [0] LINE : 1
 [3] REG_STR_C0
 [4] BINDNAME
 [5] REG_STR_C0
 [6] NAME
 [7] ONE
 [8] ADD
 [9] REG_STR_C0
 [10] SETNAME
 [11] POP
 [12] REG_STR_C0
 [13] NAME
 [14] RETURN
5.2.5.1 初始化addCount函数栈
  1. 这里的scope指向的是testFun函数的scope
5.2.5.2 addCount字节码执行解释
[6] NAME

从当前的scope中查找到count变量值,并存在stack[1]中

[7] ONE
    stack[2] = DBL_MRK;
    sDbl[2] = 1;
[8] ADD

sDbl[1] 的值加为102

[10] SETNAME

将sDbl[1]的值存到scope的count中

[13] NAME

将scope中的count存放在stack[0]

[14] RETURN
    frame.result = stack[0];
    frame.resultDbl = sDbl[0];

作者: 吴炼钿

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值