很多问题看似复杂,没有章法,事实上却有着千丝万缕的联系,陈道长此次阐述因为数据类型不同而引发的问题,本文主要探讨JS函数参数传递规则、浅拷贝、深拷贝的原理。
变量类型和存储
首先要明确js中变量的特点,JS变量本身没有类型,只有值有类型。这句话怎么理解呢,先看下面这段代码。
let a = 42
typeof a //'number'
注意返回的是'number',不是number,typeof检测的不是a的类型,而是42的类型,也就是a是没有类型的,只有a的值有类型。
JS总共有7种数据类型:Number、String、Boolean、Null、Symbol、Undefined、Object。Object是引用类型,其他的是基本类型。至于数组和函数属于Object的子类型。不过typeof 一个函数的时候 会返回'function',这是为了彰显函数是一等公民的地位。它要特殊一点。
我们在声明一个变量时,会给变量进行赋值,变量在存储的时候也有区别。基本类型值存放在栈中,可以直接访问。引用类型值存放在堆内存中。很关键的一点:JS是不允许直接访问内存的,所以当一个变量的值是Object时,它保存的只是一个指针,指向的是Object存放的内存地址。
定义一个var a = {{类型值}};b = a
,会出现下面两种情况:1、当类型值是基本类型时,a和b值虽然相同,但确是两个独立的变量。2、当类型值是Object时,a和b存储的是一样的指针,指向了共同的地址。所以再修改a或者b时,1会有各自的变化,2会两个变化完还是一样。
JS函数参数
事实上,参数传递就是受到数据类型的影响。JS的函数有几个特性:1、参数没有个数限制,不管你函数里要用几个,它却可以接收任意多个,因为不用它操心,来的这么些参数都放到了一个参数数组里。另外参数值也没有类型限制,非常的开放,然后函数内再通过arguments对象去访问这个数组,拿到参数,所以参数不跟函数直接打交道,就好比中间有个传话的。所以这也是为什么es6之前中不能直接给函数参数指定默认值。
es6新搞了个rest参数,它搭配了一个数组,变量多余的参数会存放到这个数组里,就不用arguments对象来获取了。
// arguments变量的写法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort();
}
// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();
参数传递问题
重点来了!函数参数传递其实就是把函数外的值复制给函数内部的值,也就是按照上面的规则来
参数来自外部,要传递到函数里,找个中间变量,JS是参数是按“值”传递的,这个“值”就是变量的值。当变量的值属于基本类型,这个“值”就是普通值,当变量的值是引用类型时,这个“值”时引用类型的地址。
传递的参数值是基本类型值会复制给一个局部变量。传递的参数是引用类型的值,复制的是地址。
深浅拷贝
为啥存在浅拷贝和深拷贝,原因和上面一样。当我们想把对象a赋值给变量b,你会发现b和a指向的是一个地方。b改变的时候a也会变。这就是因为变量存储的只是个指针,引用类型的值是放在内存中,没法直接访问。
普通复制就是浅拷贝,新对象修改时,老对象也会发生变化。
但是我们想把b复制给a后,然后两个对象互不干扰,完成这样的复制,这就是深拷贝。
深拷贝的原理就是把a对象的每个属性的值遍历一遍,然后把它复制给一个中间值,它作为普通的值进行复制到b对象的属性。这就回到了基本变量的复制问题。
当然一个对象的属性也可能是对象,例如:obj = [{a:1},{b:2}]
,我们想把obj赋值给新对象,这个时候就得递归遍历了,把对象中a属性的值1和b属性的值2都给取到,然后把1和2再复制到新的对象里去,这个时候新对象就不会影响到之前的对象了。
说的有点绕,就是要切断新对象和老对象的关系,但是咱们又没办法直接操作内存,就只能通过引用取到老对象中存的数据值,拿出来之后放到新对象里。这就是深拷贝的思想。
总结
由于js中数据类型不同,变量保存的值也有两种存储方式,在堆里面和在栈面,存储方式不同,也就导致了读取方式不同,也就导致了复制操作的结果不同,而参数传递和深浅拷贝都是数据复制的操作。所以两者受到的约束也是相同的,也就要遵循变量读取的规则。
本文是陈少棠原创,收录在《齐云札记》,转载请标明原作。