资讯专栏INFORMATION COLUMN

讲清楚之 javascript 参数传值

itvincent / 3382人阅读

摘要:讲清楚之参数传值参数传值是指函数调用时,给函数传递配置或运行参数的行为,包括通过进行传值。所以对的赋值会改变上下文栈中标识符保存的具体值此时如果使用的是按引用传递,则变量所指向的对象因该也被赋值为。

讲清楚之 javascript 参数传值

参数传值是指函数调用时,给函数传递配置或运行参数的行为,包括通过call、apply 进行传值。

在实际开发中,我们总结javascript参数传值分为基本数据类型按值传递(String、Numbe、Boolean、Null、undefind),引用数据类型按引用传递(Object, 包括Array、Function、Data)。这篇文章将要纠正这一误解: 实质上引用类型是按共享传递的。

在探索传值问题前,我们先看一下 javascript 在声明变量时是怎样分配内存的

原始值:存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。这是因为这些原始类型占据的空间是固定的,所以可将他们存储在较小的内存区域 – 栈中。这样存储便于迅速查寻变量的值。

引用值:存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存地址。这是因为:引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。相反,放在变量的栈空间中的值是该对象存储在堆中的地址。地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。

基本类型是将原始值保存在栈中。引用类型是将数据保存在堆中,然后在栈中建立堆地址的引用。在javascript中是不允许直接访问保存在堆内存中的对象的,所以在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值,这就是传说中的按引用访问。而原始类型的值则是可以直接访问到的。不同的内存分配机制也带来了不同的访问机制。

而基本类型和引用类型在变量复制的时候也存在区别:

原始值:在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的 value 而已。

引用值:在将一个保存着对象内存地址的变量复制给另一个变量时,会把这个内存地址赋值给新变量,也就是说这两个变量都指向了堆内存中的同一个对象,他们中任何一个作出的改变都会反映在另一个身上。复制是不会产生新的堆内存消耗。

所以我们总结 javascript 中所有函数参数都是按值传递,都是把形参复制给实参,只是基本数据类型复制的是原始值,而引用类型复制的是堆内存的地址

引用类型的这种求值策略就是 按共享传递(call by sharing).

理论知识梳理完了,下面举几个例子分析一下具体的情形.

基本数据类型传值
let a = 1
function foo(x) {
    console.log(x)
}
foo(a)
console.log(a)
// 2
// 1

变量 a 的值直接复制到了foo函数的实参x上,此时变量x是变量 a 的一个副本。它们独立的在各自的上下文栈中保存了值‘1’,且相互之间互不影响,我们对 a、x的读写操作,操作的是他们各自的值。

示例图中,在全局上下文和foo的上下文中各自保存了值1.

引用类型传值
let a = {
    abc: 1
}
function foo(x) {
    x.abc = 2
    console.log(x.abc)
}
foo(a)
console.log(a.abc)
// 2
// 2

根据编码经验我们会很错误的得出,上面的栗子是按引用传递。对象a的引用被传递到函数foo内部, 函数内部变量x指向全局变量a,从而实现了引用的传递,所以变量x和变量a读写的是同一个对象。我们用一张图来揭示:

但其实是按共享传递。按引用传递是我们对 javascript 求值策略的误解,如果是按引用传递那下面这个例子就懵比了:

let a = {
    abc: 1
}
function foo(x) {
    console.log(x) // {abc: 1}
    x = 2
    console.log(x) // 2
}
foo(a)
console.log(a.abc) // 1

foo 函数执行时第一个打印输出 ‘{abc: 1}’, 第二个打印输出 ‘2’, 调用 foo 函数后的console.log(a.abc)一句打印输出‘1’。

按引用传递的话。表达式console.log(a.abc)是在 foo 函数执行后执行的,此时的a应该已经被赋值为 2, a.abc是不存在的应该打印输出 ‘undefind’。 但是却打印输出了 1,说明在 foo 函数的内部执行x = 2是并没有修改外层对象 a 的值。

为什么会出现ax在指向同一个对象后,对x赋值又没有改变原对象的值呢?

因为这里对象传递给实参是按共享传递(call by sharing)的,根据引用类型变量复制的特点(上面描述过):

foo 函数执行时, 形参 x 的值是传进去的对象 a 的内存地址引用,即在变量对象创建阶段x保存的是一个对象的堆内存地址。此时 a、x 都指向同一对象。 接着在函数的执行阶段,代码的第二行将原始数据类型 2 赋值给 x,导致 x 不再保存原先的堆内存地址转而保存一个原始值,再次访问 x 的时候是访问对 x 最后一次赋值的原始值。

所以对 x 的赋值会改变上下文栈中标识符 x 保存的具体值

此时如果使用的是按引用传递 ,则变量 a 所指向的对象因该也被赋值为 2。但其实对 x 赋值为一个基本数据类型并没有使原对象为一个字面量值,这就说明引用类型并不是按引用传值,不是遵从按引用规则来处理值的写入。

需要区分给对象属性赋值与直接给对象赋值的区别:

let a = {
    abc: 1
}
function foo(x) {
    x.abc = 99
    console.log(x) // {abc: 99}
    x = 2
    console.log(x) // 2
}
foo(a)
console.log(a) // {abc: 99}

在 foo 函数内部修改对象 x 的属性,会导致 xa 指向的对象被修改,因为它们指向同一个堆地址。

参考:

《javascript高级程序设计》

javascript传递参

JS是按值传递还是按引用传递?

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/107840.html

相关文章

  • 讲清楚之 javascript 函数

    摘要:中函数是一等公民。小明小明调用函数时,传递给函数的值被称为函数的实参值传递,对应位置的函数参数名叫作形参。所以不推荐使用构造函数创建函数因为它需要的函数体作为字符串可能会阻止一些引擎优化也会引起浏览器资源回收等问题。 函数 之前几节中围绕着函数梳理了 this、原型链、作用域链、闭包等内容,这一节梳理一下函数本身的一些特点。 javascript 中函数是一等公民。 并且函数也是对象,...

    Keagan 评论0 收藏0
  • 讲清楚之执行上下文

    摘要:栈底为全局上下文,栈顶为当前正在执行的上下文。位于栈顶的上下文执行完毕后会自动出栈,依次向下直至所有上下文运行完毕,最后浏览器关闭时全局上下文被销毁。 讲清楚之执行上下文 标签 : javascript 什么是执行上下文? 当 JavaScript 代码执行一段可执行代码时,会创建对应的上下文(execution context)并将该上下文压入上下文栈(context stack...

    3fuyu 评论0 收藏0

发表评论

0条评论

itvincent

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<