我的编程空间,编程开发者的网络收藏夹
学习永远不晚

如何实现JavaScript的共享传递和按值传递

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

如何实现JavaScript的共享传递和按值传递

这期内容当中小编将会给大家带来有关如何实现JavaScript的共享传递和按值传递,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。

关于JavaScript如何将值传递给函数,在互联网上有很多误解和争论。大致认为,参数为原始数据类时使用按值传递,参数为数组、对象和函数等数据类型使用引用传递。

按值传递 和 引用传递参数 主要区别简单可以说:

  • 按值传递:在函数里面改变传递的值不会影响到外面

  • 引用传递:在函数里面改变传递的值会影响到外面

但答案是 JavaScript  对所有数据类型都使用按值传递。它对数组和对象使用按值传递,但这是在的共享传参或拷贝的引用中使用的按值传参。这些说有些抽象,先来几个例子,接着,我们将研究JavaScript在  函数执行期间的内存模型,以了解实际发生了什么。

按值传参

在 JavaScript 中,原始类型的数据是按值传参;对象类型是跟Java一样,拷贝了原来对象的一份引用,对这个引用进行操作。但在 JS  中,string 就是一种原始类型数据而不是对象类。

let setNewInt = function (i) {  ii = i + 33;  };  let setNewString = function (str) {  str += "cool!";  };  let setNewArray = function (arr1) {  var b = [1, 2];  arr1 = b;  };  let setNewArrayElement = function (arr2) {  arr2[0] = 105;  };  let i = -33;  let str = "I am ";  let arr1 = [-4, -3];  let arr2 = [-19, 84];  console.log('i is: ' + i + ', str is: ' + str + ', arr1 is: ' + arr1 + ', arr2 is: ' + arr2);  setNewInt(i);  setNewString(str);  setNewArray(arr1);  setNewArrayElement(arr2);  console.log('现在, i is: ' + i + ', str is: ' + str + ', arr1 is: ' + arr1 + ', arr2 is: ' + arr2);

运行结果

i is: -33, str is: I am , arr1 is: -4,-3, arr2 is: -19,84  现在, i is: -33, str is: I am , arr1 is: -4,-3, arr2 is: 105,84

这边需要注意的两个地方:

1)***个是通过 setNewString 方法把字符串 str 传递进去,如果学过面向对象的语言如C#,Java 等,会认为调用这个方法后 str  的值为改变,引用这在面向对象语言中是 string 类型的是个对象,按引用传参,所以在这个方法里面更改 str 外面也会跟着改变。

但是 JavaScript 中就像前面所说,在JS 中,string 就是一种原始类型数据而不是对象类,所以是按值传递,所以在 setNewString  中更改 str 的值不会影响到外面。

2)第二个是通过 setNewArray 方法把数组 arr1 传递进去,因为数组是对象类型,所以是引用传递,在这个方法里面我们更改 arr1  的指向,所以如果是这面向对象语言中,我们认为***的结果arr1 的值是重新指向的那个,即 [1, 2],但***打印结果可以看出 arr1  的值还是原先的值,这是为什么呢?

共享传递

Stack Overflow上Community Wiki  对上述的回答是:对于传递到函数参数的对象类型,如果直接改变了拷贝的引用的指向地址,那是不会影响到原来的那个对象;如果是通过拷贝的引用,去进行内部的值的操作,那么就会改变到原来的对象的。

可以参考博文 JavaScript Fundamentals (2) – Is JS call-by-value or  call-by-reference?

function changeStuff(state1, state2)  {  state1.item = 'changed';  state2 = {item: "changed"};  }  var obj1 = {item: "unchanged"};  var obj2 = {item: "unchanged"};  changeStuff(obj1, obj2);  console.log(obj1.item); // obj1.item 会被改变  console.log(obj2.item); // obj2.item 不会被改变

缘由: 上述的 state1 相当于 obj1, 然后 obj1.item = 'changed',对象 obj1 内部的 item  属性进行了改变,自然就影响到原对象 obj1 。类似的,state2 也是就 obj2,在方法里 state2  指向了一个新的对象,也就是改变原有引用地址,这是不会影响到外面的对象(obj2),这种现象更专业的叫法:call-by-sharing,这边为了方便,暂且叫做  共享传递。

内存模型

JavaScript 在执行期间为程序分配了三部分内存:代码区,调用堆栈和堆。 这些组合在一起称为程序的地址空间。

如何实现JavaScript的共享传递和按值传递

代码区:这是存储要执行的JS代码的区域。

调用堆::这个区域跟踪当前正在执行的函数,执行计算并存储局部变量。变量以后进先出法存储在堆栈中。***一个进来的是***个出去的,数值数据类型存储在这里。

例如:

  1. var corn = 95  

  2. let lion = 100 


如何实现JavaScript的共享传递和按值传递

在这里,变量 corn 和 lion 值在执行期间存储在堆栈中。

堆:是分配 JavaScript 引用数据类型(如对象)的地方。 与堆栈不同,内存分配是随机放置的,没有 LIFO策略。  为了防止堆中的内存漏洞,JS引擎有防止它们发生的内存管理器。

class Animal {}  // 在内存地址 0x001232 上存储 new Animal() 实例  // tiger 的堆栈值为 0x001232  const tiger = new Animal()  // 在内存地址 0x000001 上存储 new Objec实例  // `lion` 的堆栈值为 0x000001  let lion = {  strength: "Very Strong"  }
如何实现JavaScript的共享传递和按值传递   

Here,lion 和 tiger 是引用类型,它们的值存储在堆中,并被推入堆栈。它们在堆栈中的值是堆中位置的内存地址。

激活记录(Activation Record),参数传递

我们已经看到了 JS 程序的内存模型,现在,让我们看看在 JavaScript 中调用函数时会发生什么。

// 例子一  function sum(num1,num2) {  var result = num1 + num2  return result  }  var a = 90  var b = 100  sum(a, b)

每当在 JS 中调用一个函数时,执行该函数所需的所有信息都放在堆栈上。这个信息就是所谓的激活记录(Activation Record)。

这个 Activation Record,我直译为激活记录,找了好多资料,没有看到中文一个比较好的翻译,如果朋友们知道,欢迎留言。

激活记录上的信息包括以下内容:

  • SP 堆栈指针:调用方法之前堆栈指针的当前位置。

  • RA 返回地址:这是函数执行完成后继续执行的地址。

  • RV 返回值:这是可选的,函数可以返回值,也可以不返回值。

  • 参数:将函数所需的参数推入堆栈。

  • 局部变量:函数使用的变量被推送到堆栈。

我们必须知道这一点,我们在js文件中编写的代码在执行之前由 JS 引擎(例如 V8,Rhino,SpiderMonke y等)编译为机器语言。

所以以下的代码:

let shark = "Sea Animal"

会被编译成如下机器码:

01000100101010  01010101010101

上面的代码是我们的js代码等价。 机器码和 JS 之间有一种语言,它是汇编语言。 JS 引擎中的代码生成器在最终生成机器码之前,首先是将 js  代码编译为汇编代码。

为了了解实际发生了什么,以及在函数调用期间如何将激活记录推入堆栈,我们必须了解程序是如何用汇编表示的。

为了跟踪函数调用期间参数是如何在 JS 中传递的,我们将例子一的代码使用汇编语言表示并跟踪其执行流程。

先介绍几个概念:

ESP:(Extended Stack Pointer)为扩展栈指针寄存器,是指针寄存器的一种,用于存放函数栈顶指针。与之对应的是  EBP(Extended Base Pointer),扩展基址指针寄存器,也被称为帧指针寄存器,用于存放函数栈底指针。

EBP:扩展基址指针寄存器(extended base pointer) 其内存放一个指针,该指针指向系统栈最上面一个栈帧的底部。

EBP 只是存取某时刻的 ESP,这个时刻就是进入一个函数内后,cpu 会将ESP的值赋给 EBP,此时就可以通过 EBP  对栈进行操作,比如获取函数参数,局部变量等,实际上使用 ESP 也可以。

// 例子一  function sum(num1,num2) {  var result = num1 + num2  return result  }  var a = 90  var b = 100  var s = sum(a, b)

我们看到 sum 函数有两个参数 num1 和 num2。函数被调用,传入值分别为 90 和 100 的 a 和 b。

记住:值数据类型包含值,而引用数据类型包含内存地址。

在调用 sum 函数之前,将其参数推入堆栈

ESP->[......]  ESP->[ 100 ]  [ 90 ]  [.......]

然后,它将返回地址推送到堆栈。返回地址存储在EIP 寄存器中:

ESP->[Old EIP]  [ 100 ]  [ 90 ]  [.......]

接下来,它保存基指针

ESP->[Old EBP]  [Old EIP]  [ 100 ]  [ 90 ]  [.......]

然后更改 EBP 并将调用保存寄存器推入堆栈。

ESP->[Old ESI]  [Old EBX]  [Old EDI]  EBP->[Old EBP]  [Old EIP]  [ 100 ]  [ 90 ]  [.......]

为局部变量分配空间:

ESP->[ ]  [Old ESI]  [Old EBX]  [Old EDI]  EBP->[Old EBP]  [Old EIP]  [ 100 ]  [ 90 ]  [.......]

这里执行加法:

mov ebp+4, eax ; 100  add ebp+8, eax ; eaxeax = eax + (ebp+8)  mov eax, ebp+16  ESP->[ 190 ]  [Old ESI]  [Old EBX]  [Old EDI]  EBP->[Old EBP]  [Old EIP]  [ 100 ]  [ 90 ]  [.......]

我们的返回值是190,把它赋给了 EAX。

mov ebp+16, eax

EAX 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。

然后,恢复所有寄存器值。

[ 190 ] DELETED  [Old ESI] DELETED  [Old EBX] DELETED  [Old EDI] DELETED  [Old EBP] DELETED  [Old EIP] DELETED  ESP->[ 100 ]  [ 90 ]  EBP->[.......]

并将控制权返回给调用函数,推送到堆栈的参数被清除。

[ 190 ] DELETED  [Old ESI] DELETED  [Old EBX] DELETED  [Old EDI] DELETED  [Old EBP] DELETED  [Old EIP] DELETED  [ 100 ] DELETED  [ 90 ] DELETED  [ESP, EBP]->[.......]

调用函数现在从 EAX 寄存器检索返回值到 s 的内存位置。

mov eax, 0x000002 ; // s 变量在内存中的位置

我们已经看到了内存中发生了什么以及如何将参数传递汇编代码的函数。

调用函数之前,调用者将参数推入堆栈。因此,可以正确地说在 js  中传递参数是传入值的一份拷贝。如果被调用函数更改了参数的值,它不会影响原始值,因为它存储在其他地方,它只处理一个副本。

function sum(num1) {  num1 = 30  }  let n = 90  sum(n)  // `n` 仍然为 90

让我们看看传递引用数据类型时会发生什么。

function sum(num1) {  num1 = { number:30 }  }  let n = { number:90 }  sum(n)  // `n` 仍然是 { number:90 }

用汇编代码表示:

n -> 0x002233  Heap: Stack:  002254 012222  ... 012223 0x002233  002240 012224  002239 012225  002238  002237  002236  002235  002234  002233 { number: 90 }  002232  002231 { number: 30 }  Code:  ...  000233 main: // entry point  000234 push n // n 值为 002233 ,它指向堆中存放 {number: 90} 地址。 n 被推到堆栈的 0x12223 处.  000235 ; // 保存所有寄存器  ...  000239 call sum ; // 跳转到内存中的`sum`函数  000240  ...  000270 sum:  000271 ; // 创建对象 {number: 30} 内在地址主 0x002231  000271 mov 0x002231, (ebp+4) ; // 将内存地址为 0x002231 中 {number: 30} 移动到堆栈 (ebp+4)。(ebp+4)是地址 0x12223 ,即 n 所在地址也是对象 {number: 90} 在堆中的位置。这里,堆栈位置被值 0x002231 覆盖。现在,num1 指向另一个内存地址。 000272 ; // 清理堆栈  ...  000275 ret ; // 回到调用者所在的位置(000240)

我们在这里看到变量n保存了指向堆中其值的内存地址。 在sum 函数执行时,参数被推送到堆栈,由 sum 函数接收。

sum 函数创建另一个对象 {number:30},它存储在另一个内存地址 002231 中,并将其放在堆栈的参数位置。 将前面堆栈上的参数位置的对象  {number:90} 的内存地址替换为新创建的对象 {number:30} 的内存地址。

这使得 n 保持不变。因此,复制引用策略是正确的。变量 n 被推入堆栈,从而在 sum 执行时成为 n 的副本。

此语句 num1 = {number:30} 在堆中创建了一个新对象,并将新对象的内存地址分配给参数 num1。 注意,在 num1 指向 n  之前,让我们进行测试以验证:

// example1.js  let n = { number: 90 }  function sum(num1) {  log(num1 === n)  num1 = { number: 30 }  log(num1 === n)  }  sum(n)  $ node example1  true  false

是的,我们是对的。就像我们在汇编代码中看到的那样。最初,num1 引用与 n 相同的内存地址,因为n被推入堆栈。

然后在创建对象之后,将 num1 重新分配到对象实例的内存地址。

让我们进一步修改我们的例子1:

function sum(num1) {  num1.number = 30  }  let n = { number: 90 }  sum(n)  // n 成为了 { number: 30 }

这将具有与前一个几乎相同的内存模型和汇编语言。这里只有几件事不太一样。在 sum 函数实现中,没有新的对象创建,该参数受到直接影响。

...  000270 sum:  000271 mov (ebp+4), eax ; // 将参数值复制到 eax 寄存器。eax 现在为 0x002233  000271 mov 30, [eax]; // 将 30 移动到 eax 指向的地址

num1 是(ebp+4),包含 n 的地址。值被复制到 eax 中,30 被复制到 eax 指向的内存中。任何寄存器上的花括号 [] 都告诉 CPU  不要使用寄存器中找到的值,而是获取与其值对应的内存地址号的值。因此,检索 0x002233 的 {number: 90} 值。

看看这样的答案:

原始数据类型按值传递,对象通过引用的副本传递。

具体来说,当你传递一个对象(或数组)时,你无形地传递对该对象的引用,并且可以修改该对象的内容,但是如果你尝试覆盖该引用,它将不会影响该对象的副本-  即引用本身按值传递:

function replace(ref) {  ref = {}; // 这段代码不影响传递的对象  }  function update(ref) {  ref.key = 'newvalue'; // 这段代码确实会影响对象的内容  }  var a = { key: 'value' };  replace(a); // a 仍然有其原始值,它没有被修改的  update(a); // a 的内容被更改

从我们在汇编代码和内存模型中看到的。这个答案***正确。在 replace 函数内部,它在堆中创建一个新对象,并将其分配给 ref 参数,a  对象内存地址被重写。

update 函数引用 ref 参数中的内存地址,并更改存储在存储器地址中的对象的key属性。

总结

根据我们上面看到的,我们可以说原始数据类型和引用数据类型的副本作为参数传递给函数。不同之处在于,在原始数据类型,它们只被它们的实际值引用。JS 不允许我们获取他们的内存地址,不像在C与C++程序设计学习与实验系统,引用数据类型指的是它们的内存地址。

上述就是小编为大家分享的如何实现JavaScript的共享传递和按值传递了,如果刚好有类似的疑惑,不妨参照上述分析进行理解。如果想知道更多相关知识,欢迎关注编程网行业资讯频道。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

如何实现JavaScript的共享传递和按值传递

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Java如何实现值传递

这篇文章主要介绍了Java如何实现值传递,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。值传递是在一个函数中,调用另外一个函数,其中含有参数传递的情况。往往值传递就是把数据传递
2023-06-03

php函数是如何按值传递参数的

本篇内容主要讲解“php函数是如何按值传递参数的”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“php函数是如何按值传递参数的”吧!向函数传递参数的方式有四种,分别是值传递、引用传递、默认参数和可
2023-06-20

Blazor如何实现组件嵌套传递值

这篇文章主要讲解了“Blazor如何实现组件嵌套传递值”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Blazor如何实现组件嵌套传递值”吧!实现创建一个Blazor Server空的应用程序
2023-07-05

SpringMVC中如何实现前台向后台传递值

今天就跟大家聊聊有关SpringMVC中如何实现前台向后台传递值,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。一、概述。在很多企业的开法中常常用到SpringMVC+Spring+H
2023-05-31

如何实现PHP与JavaScript的交互?(PHP和JavaScript之间如何传递数据和通信?)

本教程详细介绍PHP与JavaScript的交互方法。通过AJAX,JavaScript可向服务器发送请求,更新页面内容而不影响其他部分。服务器端推送(SSE)允许服务器实时推送数据,而WebSocket则实现双向实时通信。交互中,数据通过请求传递,包括文本和对象。PHP和JavaScript可使用这些技术实现动态交互,创建功能强大的网络应用程序,提升用户体验。
如何实现PHP与JavaScript的交互?(PHP和JavaScript之间如何传递数据和通信?)
2024-04-02

vue如何实现兄弟组件的数据传递

本篇内容介绍了“vue如何实现兄弟组件的数据传递”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!在main.js里面设置data{eventH
2023-07-04

vbs如何实现循环产生的参数传递

小编给大家分享一下vbs如何实现循环产生的参数传递,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!Set objFSO = CreateObject("Script
2023-06-08

如何使用AQ实现异步消息传递和处理

要使用AQ(Advanced Queuing)实现异步消息传递和处理,首先需要在Oracle数据库中创建一个队列和一个相关联的队列表。然后,可以将消息放入队列中,并编写PL/SQL过程来处理这些消息。下面是使用AQ实现异步消息传递和处理的
如何使用AQ实现异步消息传递和处理
2024-03-02

C++ Cartographer源码中关于传感器的数据传递如何实现

今天小编给大家分享一下C++ Cartographer源码中关于传感器的数据传递如何实现的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了
2023-07-05

Vue的指令中如何实现传递更多参数

本篇内容主要讲解“Vue的指令中如何实现传递更多参数”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Vue的指令中如何实现传递更多参数”吧!基本原理本文介绍的指令扩展方法,主要以闭包为基础,并且使
2023-06-30

编程热搜

目录