1.前言
javascript的闭包是后端java程序员比较头痛不好理解的概念,所以本文结合js引擎从原理上剖析闭包的运行原理,让大家能对闭包有一个深入的理解.
2. 为什么基于java版Rhino引擎剖析
- java是最好的语言
- 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函数的执行
- testFun函数执行时,默认会创建其函数对应的scope对象
本函数中的局部变量及函数中定义的函数都会记录在这个scope中
- 返回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初始化栈
- 在这代码段中,scope 使用的是全局的scope
通过ScriptRuntime.initScript(…)将result,funMethod等局部变量存放在当前作用域scope(全局)中 - 根据代码片段的对象 itsNestedFunctions属性检查本代码段是否有含function,如果有调用initFunction()方法初始化function,并将本function对象存在当前的scope中.本例子中,最外层的代码片段含有testFun函数
initFunction方法初始化函数时,会设置本函数的父scope,
将上层的prototype赋给本函数的prototype
- 分配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函数栈
- 创建testFun函数的scope (其类名为NativeCall),并在这个scope中设置其父scope,添加默认变量:arguments,以前count.
- 检查到本函数中有定义新函数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函数栈
- 这里的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];
作者: 吴炼钿