深入理解 JavaScript 函数调用机制:值传递与共享对象
本文深入解析 JavaScript 中的函数参数传递机制,澄清值传递与引用传递的误区。通过对比 C++ 概念,引入 Call by Sharing(共享传递)模型,并结合 Google V8 源码分析对象引用、严格相等判断及局部句柄实现,阐明变量共享指针而非修改指针本身的特性。
JavaScript 函数调用机制概述
定义
在 JavaScript 学习过程中,许多开发者对函数参数的传递方式感到困惑。为了探究本质,我们尝试从源码中寻找答案。在此之前,需要明确几个核心概念,暂时摒弃传统的“值传递”或“引用传递”等固有叫法,回归英文术语进行探讨。
call by reference && call by value && call by sharing
前两者分别对应我们熟悉的 C++ 中的引用传递和值传递。第三种概念较为晦涩,官方解释为“接收对象引用的副本”。用通俗的话来说:
可以将 Object 理解为键(key)的集合,Object 对 key 指向的数据具有引用性质(此处不深究是指针实现还是 C++ 引用实现)。函数接收的是一个变量的副本,该变量包含了 Object 的引用,这本质上是一种值传递。
因此,在函数传参时,接收到的对象形参实际上是实参的副本。直接更改形参的指向是不可行的;但由于 Object 内部的 key 本身都是引用,所以修改 key 所指向的数据是可行的。
通过代码验证参数传递行为
证明
通过几段简单的代码即可验证上述观点。
Code 1: 函数能修改 key 指向的数据
let func = obj => { obj.name = 'Dosk' };
let obj = {name : 'Alxw'};
console.log(obj); //{ name: 'Alxw' }
func(obj)
console.log(obj); //{ name: 'Dosk' }
Code 2: 函数不能修改 obj
let func = obj => { obj = {} };
let obj = {name : 'Alxw'};
console.log(obj); //{ name: 'Alxw' }
func(obj)
console.log(obj); //{ name: 'Alxw' }
Code 3: 内部 obj 和外部 === 结果相等
let def = {name : 'Alxw'};
let func = obj => { console.log(obj === def) };
func(def); //true
第三段代码可能引发疑问:既然 obj 是 def 的副本,为什么 === 操作的结果为真?通常认为 === 对于 Object 比较的是内存地址,如果是复制应该返回 false。
为了解答这一疑问,我们需要回到 Google V8 的源码中查看具体实现。
深入 Google V8 源码分析
深入 Google V8
查看源码中关于严格等于操作的代码部分:
bool Object::StrictEquals(Object* that) {
if (this->IsNumber()) {
if (!that->IsNumber()) return false;
return NumberEquals(this, that);
} else if (this->IsString()) {
if (!that->IsString()) return false;
return String::cast(this)->Equals(String::cast(that));
} else if (this->IsSimd128Value()) {
if (!that->IsSimd128Value()) return false;
return Simd128Value::cast(this)->Equals(Simd128Value::cast(that));
}
return this == that;
}
从逻辑上看,这似乎对应最后一种情况。理论上,如果 def 和 obj 是不同的对象,应该返回 false,这是否推翻了前文所述?其实不然,这里忽略了一个关键点:Google V8 在实例化 Object 时是动态实例化的。在编译型语言中,动态实例化只能在堆内存上进行,即只能通过指针引用。证明这一结论涉及 Local、Handle 等类的实现细节,较为繁琐。一个更简单的证明方式是:在源码中搜索所有调用 Object::StrictEquals 的地方,发现都是直接传入而没有取地址操作。
有人可能会问:既然值传递的变量包含 Object 的引用,理论上应该能修改 Object 才对,为什么第三段代码不能修改 obj 本身?
道理很简单:我们在 JavaScript 语言逻辑层面的操作,实际上只是调用 Google V8 实例方法而已,无法直接操作到这一底层层面(当然,潜在的 BUG 除外)。
重新定义 Call by Sharing
重新定义
至此,我们可以对 call by sharing 进行重新解释:
传递时确实是值传递,但内容包含了 Object 的指针,且无法修改这个指针,它是多个变量共享的。
基于源码接口的补充证明
另一种简单的证明
让我们查看源码。
V8_DEPRECATE_SOON("Use maybe version",
Local<Value> Call(Local<Value> recv, int argc,
Local<Value> argv[]));
V8_WARN_UNUSED_RESULT MaybeLocal<Value> Call(Local<Context> context,
Local<Value> recv, int argc,
Local<Value> argv[]);
上面的是即将弃用的接口,碰巧我看到的这个版本代码包含大量的这种即将弃用的代码,看看就好。重点是第二个接口,它是函数唯一的调用接口。其中的 Local<Value> 最终会调用 C++ 的位复制,因此可以简单证明这是值传递。
可能是重点
别忘了,我们定义的变量都是类似 Handle<Object> 这种形式的,所以它们之间的对象才是共享的。我们所说的 JavaScript 里面的变量并不直接指的是 Object 的实例。
最后的最后
总之,理解起来可能很费劲甚至可能有错误,但在 JavaScript 语言层次上能够确定这一特性,这才是重要的。
以上所述是小编给大家介绍的JavaScript中的函数调用,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对本站网站的支持!
JavaScript异步发送HTTP请求时怎么优雅地处理错误
2026-11-05 17:10:44
5款主流JS在线运行工具对比:CodePen、JSFiddle与StackBlitz怎么选
2026-11-05 17:04:04
JavaScript入门书籍避坑:这些书不适合初学者
2026-11-05 16:31:15
JS使用Math.round实现保留两位小数的原理与代码
2026-11-05 15:35:55
JavaScript 原生 Date 对象怎么实现日期加减
2026-11-05 14:30:00
