讲清楚之 javascript 参数传值
讲清楚之 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 的值。
为什么会出现a、x在指向同一个对象后,对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 的属性,会导致 x、a 指向的对象被修改,因为它们指向同一个堆地址。
参考:
《javascript高级程序设计》
讲清楚之 javascript 参数传值的更多相关文章
- C# asp.net页面通过URL参数传值中文乱码问题解决办法
1.编码string state=Server.UrlEncode(stateName.Text.Trim());Response.Redirect("aaa.aspx?state=&quo ...
- JavaScript URL传值过程中遇到的问题及知识点总结
JavaScript URL传值过程中遇到的问题及知识点总结 Web系统开发过程中经常用到URL进行传值,刚刚接触时不太会解析,会出现中文乱码问题等. 1.父子页面之间的传值(在一个页面中以加载ifr ...
- 讲清楚之 javascript中的this
讲清楚之 javascript中的this 这一节来探讨this. 在 javascript 中 this 也是一个神的存在,相对于 java 等语言在编译阶段确定,而在 javascript 中, ...
- struts(二) ---中参数传值
struts中参数传值的方式有 种: 第一种:直接通过属性来传值 第二种: 第三种:
- delphi数组作为参数传值
在函数中如果数组的个数不定,可以使用开放数组参数 实参可以接受静态数组和动态数组 procedure p1(a:array of Byte); begin ShowMessage( IntToHex( ...
- Get和Post的参数传值
1. get是从服务器上获取数据,post是向服务器传送数据. 2. get是把参数数据队列加到提交表单的ACTION属性所指的URL中,值和表单内各个字段一一对应,在URL中可以看到.post是通过 ...
- html的URL参数传值问题
在URL中的参数传值时,例如:www.nihao.com?id=001 ,= 两边不能有空格,不然PHP在通过$_GET['id']获取时会出现传值为空.
- C++ 参数传值 与 传引用
参数传值 在 C++ 中,函数参数的传递有两种方式:传值和传引用.在函数的形参不是引用的情况下,参数传递方式是传值的.传引用的方式要求函数的形参是引用.“传值”是指,函数的形参是实参的一个拷贝,在函数 ...
- C# oracle to_date 日期型 参数传值
C#操作oracle,date字段,使用参数传值 例子一,获取三小时前的记录 public static DataTable Query() { const string sSql = &qu ...
随机推荐
- vue 中contenteditable="true"添加可编辑属性后v-model双向绑定失效的解决办法
在项目中会遇到需要编辑单元格的双向绑定问题,v-model双向绑定会在添加contenteditable="true"属性后失效解决方法如下,亲测好用(v-html和@blur实现 ...
- B站(bilibili) 等级升级脚本-云函数版
前言 云函数运行会莫名其妙的报错(我也不知道为什么,我这边测试有时候报错,有时候不会),并非代码有错,建议在本地运行. 由于之前写的代码有些bug,后面又又又又又又又更新了. 最后更新于2022-1- ...
- ASP.NET Core 6框架揭秘实例演示[18]:HttpClient处理管道
我们知道ASP.NET的核心就是由中间件组成的请求处理管道,HttpClient也采用了类似的设计.HttpClient管道由一组HttpMessageHandler对象构成,这些HttpMessag ...
- 使用 yapi-to-typescript 生成接口响应数据的 TS 类型声明
TS 是越写越爽,但是很不爽的是接口响应数据一堆,每次要去手写很麻烦. 最近正好后端接口文档从一个垃圾工具切换到了 Yapi,然后去搜了下可以通过接口文档自动生成 TS 类型声明,就自己搞了下,还是很 ...
- itertools.chain()and itertools.product()操作+pandas.DataFrame.transform
- 人生苦短,我用JRebel
昨天看到团子推送的一篇关于热部署的文章,其中介绍了自研的Sonic插件在公司内部的应用.同时晒出来一张对比图: 团子表示我们的插件要比同类插件优秀哦.不过我定睛一看,好家伙,第一列的JRebel在图中 ...
- vue3-异步组件的使用
我们通常会在实际开发中,将项目进行分包处理,以此加快项目的速度 即使用异步组件 //异步组件,分包处理 import {defineAsyncComponent} from "vue&quo ...
- Makefile 入门(加减乘除实现)
Makefile 入门(加减乘除实现) 准备 使用任意Linux发行版即可,本文使用WSL Ubuntu. 开始之前,需要安装必要的工具: sudo apt install make g++ 开始 1 ...
- ZYNQ 驱动初始化套路--UART串口
@ 目录 UART 串口 GPIO之 MIO控制 LED 套路如下: 1.定义硬件信息结构体 namePs_Config *Config; 2.定义驱动信息结构体 namePs name_Ps; 3. ...
- docker基础命令,常用操作
docker基础命令 使用docker镜像 获取镜像 从docker registry获取镜像的命令是docker pull.命令格式是: docker pull [选项][docker regist ...