内存 - 在javascript中,Function对象的本质是什么?
阿神
阿神 2017-04-10 14:26:37
[JavaScript讨论组]

这个问题最近一直困扰着我,我感到自己无法理解Function对象的本质是什么。
如果说是一个普通的js变量。比如

var a = 5;

我可以把它理解为开辟了某个内存给变量a,并把内容赋值为5。
那么如果我定义了一个函数:

var fn = function() { console.log(this); };

它在内存中又是怎么存储的?

其实把我的问题再具体话一点,可以这样问:
1. Function对象中如何保存作用域链的上下文(context)
2. Function对象的函数体是以字符串的形式存储下来的吗?

再看看下面这个例子:

var fn;
(function(){
    var a = 5;
    fn = function () {
        console.log(a++);
    };
})();
fn();

这是个常见的闭包例子,就拿这个例子来说,Function对象是如何把变量a保存在自己的上下文环境中的呢?

首先感谢大家热情的回答,我再补充说明一下:
我主要的问题是Function在解释器引擎(比如Google V8)里是以怎样的形式实现的?是把函数体以字符串的形式存储下来,并在执行时以类似eval方法来调用它,或者还是其他方式?所以我的实际问题可能比较底层一点。

阿神
阿神

闭关修行中......

全部回复(3)
怪我咯

在JS中,Function对象就是Function对象。
在引擎中,随便它怎么实现都可以,只要遵照ECMAScript Spec就行了。
不过只要不是玩具引擎,就肯定不会把代码作为字符串存储,到执行到函数时再去解析的。

要理解这个问题,你需要定个目标,你要理解到什么样的深度呢?
我有个建议,就是不要把问题描述得多么底层。这样除了提高B格之外,其实无助于理解问题。这个问题是一个原理上的问题,不定非要你会C++、汇编才能理解。

你要知道Closure、This这些对象是如何传给函数的,即你说的闭包变量是如何传给函数的。你只需要参照ECMAScript Spec中「Creating Function Objects」、「Executable Code and Execution Contexts」这些章节就行了。
伪代码其实就是这样,对于下面的代码:

var fn;
(function(){
    var a = 5;
    fn = function () {
        console.log(a++);
    };
})();
fn();

经过解析,会变成类似下面的对象:

var fn;
var RANDOM_LAMBA_1={
  FormalParameterList:[],
  Scope:{fn:&fn},  //假设用&表示变量的地址
  Code:"\
   var a = 5;\
   fn ={\
      FormalParameterList:[],\
      Code:\"console.log(a++)\",\
      Scope:{a:&a}\
   };\
   return fn;\
  ",

};
RANDOM_LAMBA_1.Call(this,[]);
//Call方法大概如下:
function Call(thisObject,args) {
  var context={},fn=this; //this是Function对象
  fn.FormalParameterList.zip(args).forEach(function (a) {
    context[a[0]]=&a[1];
  });
  context.arguments=args;
  context.this=thisObject;//将this当成一个普通变量理解就行了
  with(context,fn.Scope) {
    eval(fn.Code);
  }
}

至于引擎中怎么实现的,想太多也没用,思而不学则殆。
理解这些至少需要理解 SouceCode => Tokens => AbstractSyntaxTree .....=> JIT Code 这个流程。将代码转换成抽象语法树的部分在最初JS加载解析时就已经执行了,不然怎么能不执行函数就能报告语法错误呢?虽然Spec用eval(stringSourceCode)这种方式抽述,但解释器实现显然最多只需要直接遍历已经Parse过后的Tree就行了,绝不会再去读取函数的FunctionBody的字符串解析执行的。如果还想更深入地理解,还不如直接去读编译原理呢。

伊谢尔伦

Javascript 中变量可以存放两种类型的值,一种为原始值(primitive value),如 Undefined, Null, Boolean, Number, String。这类值存放在栈内部,每赋值一次就创建一个新的拷贝。另一种为引用值(reference value),这类值存在堆内存中,只能通过引用赋值。
举例说明:

var a = 'test';//原始值
var b = function() {}; //引用值

我们来测试下:

var a = 'test';//原始值
var b = function() {}; //引用值
b.a = 'test';

function change(m, n) {
    m = 'change';
    n.a = 'change';
}

change(a, b);

现在变量 a 仍然是 test,但是 变量 b 的属性 a 的值则已经为 change,这也就是说前者相当于是拷贝了一份值,而后者则是引用赋值。
而闭包问题我是这样理解的,因为 Javascript 只有两种作用域,一是全局作用域,二是函数作用域,它是没有块级作用域的。所以闭包的出现就相当于利用一个匿名函数的壳模拟出一个块级作用域。举个更明显的闭包例子:

for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);  
        }, 1000);
    })(i);
}

联系上面的知识,这里往匿名函数内部传的参数将会被拷贝一份,也就是说循环没执行一次就拷贝变量 i 的值到匿名函数内部。
这里如果没有闭包的话:

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);  
    }, 1000);
}

由于变量 i 直接暴露在全局作用域内,当调用 console.log 函数开始输出时,这是循环已经结束,所以会输出10个10。
这是我对题主问题的理解,希望能对题主有帮助,可能还有不完善的地方,我打算写篇博文好好总结下。:)

阿神

我不懂 C++(Google V8 的实现语言),不过我看过为 Node.js 编写 C++ extensions 的文章,其中有提到过:V8 把 Javascript 代码编译成机器语言来执行,并且可以使用 C++ 来模拟编译的过程。于是我找了找类似的源代码,比如像下面这一段:

#include <v8.h>
#include <iostream>
#include <string>

using namespace v8;

int main(int argc, char* argv[]) {

// Create a stack-allocated handle scope.
HandleScope handle_scope;

// Create a new context.
Persistent<Context> context = Context::New();

//context->AllowCodeGenerationFromStrings(true);

// Enter the created context for compiling and
// running the hello world script.
Context::Scope context_scope(context);
Handle<String> source;
Handle<Script> script;
Handle<Value> result;


// Create a string containing the JavaScript source code.
source = String::New("function test_function() { var match = 0;if(arguments[0] == arguments[1]) { match = 1; } return match; }");

// Compile the source code.
script = Script::Compile(source);

// Run the script to get the result.
result = script->Run();
// Dispose the persistent context.
context.Dispose();

// Convert the result to an ASCII string and print it.
//String::AsciiValue ascii(result);
//printf("%s\n", *ascii);

Handle<v8::Object> global = context->Global();
Handle<v8::Value> value = global->Get(String::New("test_function"));
Handle<v8::Function> func = v8::Handle<v8::Function>::Cast(value);
Handle<Value> args[2];
Handle<Value> js_result;
int final_result;

args[0] = v8::String::New("1");
args[1] = v8::String::New("1");

js_result = func->Call(global, 2, args);
String::AsciiValue ascii(js_result);

final_result = atoi(*ascii);

if(final_result == 1) {

    std::cout << "Matched\n";

} else {

    std::cout << "NOT Matched\n";

}

return 0;

}

所以你想的没有错,看起来的确是保存为字符串再编译执行的。水平有限,只能帮到这里了,建议你找找为 Node.js 编写 C++ 扩展的文章,那些会对底层的编译/调用/执行等有更多深入的讲解。

另外我推荐你阅读一个博客系列:A Tour of V8,并且还有翻译质量不错的中文版,或许能回答你更多的疑问。

热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号