讲清楚之 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是按值传递还是按引用传递?

讲清楚之 javascript 参数传值的更多相关文章

  1. C# asp.net页面通过URL参数传值中文乱码问题解决办法

    1.编码string state=Server.UrlEncode(stateName.Text.Trim());Response.Redirect("aaa.aspx?state=&quo ...

  2. JavaScript URL传值过程中遇到的问题及知识点总结

    JavaScript URL传值过程中遇到的问题及知识点总结 Web系统开发过程中经常用到URL进行传值,刚刚接触时不太会解析,会出现中文乱码问题等. 1.父子页面之间的传值(在一个页面中以加载ifr ...

  3. 讲清楚之 javascript中的this

    讲清楚之 javascript中的this 这一节来探讨this. 在 javascript 中 this 也是一个神的存在,相对于 java 等语言在编译阶段确定,而在 javascript 中, ...

  4. struts(二) ---中参数传值

    struts中参数传值的方式有 种: 第一种:直接通过属性来传值 第二种: 第三种:

  5. delphi数组作为参数传值

    在函数中如果数组的个数不定,可以使用开放数组参数 实参可以接受静态数组和动态数组 procedure p1(a:array of Byte); begin ShowMessage( IntToHex( ...

  6. Get和Post的参数传值

    1. get是从服务器上获取数据,post是向服务器传送数据. 2. get是把参数数据队列加到提交表单的ACTION属性所指的URL中,值和表单内各个字段一一对应,在URL中可以看到.post是通过 ...

  7. html的URL参数传值问题

    在URL中的参数传值时,例如:www.nihao.com?id=001 ,= 两边不能有空格,不然PHP在通过$_GET['id']获取时会出现传值为空.

  8. C++ 参数传值 与 传引用

    参数传值 在 C++ 中,函数参数的传递有两种方式:传值和传引用.在函数的形参不是引用的情况下,参数传递方式是传值的.传引用的方式要求函数的形参是引用.“传值”是指,函数的形参是实参的一个拷贝,在函数 ...

  9. C# oracle to_date 日期型 参数传值

    C#操作oracle,date字段,使用参数传值 例子一,获取三小时前的记录 public static DataTable Query() {     const string sSql = &qu ...

随机推荐

  1. 关于UI自动化IOS元素定位方法说明

    1. 元素属性介绍 下图是通过weditor定位的微博的"我的钱包",各属性如下图: className:元素类型,如:XCUIElementTypeButton isEnable ...

  2. 矩池云上安装yolov4 darknet教程

    这里我是用PyTorch 1.8.1来安装的 拉取仓库 官方仓库 git clone https://github.com/AlexeyAB/darknet 镜像仓库 git clone https: ...

  3. 字符集编码(四):UTF

    在前面文章<字符集编码(中):Unicode>中我们聊了 Unicode 标准并提到其有三种实现形式:UTF-16.UTF-8 和 UTF-32,本篇我们就具体聊聊这三种 UTF 是怎么实 ...

  4. yum 安装的历史与撤销(yum history undo )

    我们可以使用yum history 来查看yum的历史记录 想撤销安装则输入 yum history undo <ID号> 即可撤销yum的安装/升级

  5. NET经典书籍必读

    C#与.NET框架,入门 + 进阶 + 精通,外加并发编程实例,10本C#图书,一本都不能少. 1.<Learning hard C#学习笔记> 作者:李志  书号:978-7-115-3 ...

  6. 简悦+Logseq 搭建本地化个人知识库

    最近在少数派上看到了 简悦 +Logseq 个人知识库搭建 | 从零开始完全指南 - 少数派, 一时间感觉打开了新世界,其实我很早就买了简悦 2.0,但由于一直没有很好的使用场景,外加配置实在过于复杂 ...

  7. pycharm远程调试、开发(详细操作)

    如果仅是远程开发,新建 ssh Interpreter 并 apply tools -> deployment -> browser remote host 即可 1.服务器侧准备 准备调 ...

  8. logging 日志配用

    第一步,创建一个logger: 第二步,创建一个handler,用于写入日志文件: 第三步,再创建一个handler,用于输出到控制台: 第四步,定义handler的输出格式: 第五步,将logger ...

  9. Java的浅克隆和深克隆

    如何实现对象的克隆 (1)实现 Cloneable 接口并重写 Object 类中的 clone() 方法: (2)实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真 ...

  10. 如何解析EML(邮件)格式的文件以及一款小巧的EML邮件阅读工具

    在理解EML格式的时候,先回顾一下历史,这样有助于理解邮件的格式,比如邮件传输时为何会有多种编码方式.此外,理解EML格式也有助于理解HTTP协议. 历史溯源 由于历史原因,我们目前看到的大部分的网络 ...