面试题
先看一个经典面试题:把下面的listdata里的val值倒序排列,即把listdata里面的val值变为4 3 2 1。
let listdata = {
val: 1,
next: {
val: 2,
next: {
val: 3,
next: {
val: 4,
next: null
}
}
}
}
本题考察的是如何反向创建链表,答案如下:
let result = {}
while(listdata) {
// 赋值
result.val = listdata.val
// 为第一个next赋值null
result.next = result.next ?? null
// 将result.next指向result,这里会发生循环引用
result.next = JSON.parse(JSON.stringify(result))
// 改变循环
listdata = listdata.next
}
console.log(JSON.stringify(result.next))
// {"val":4,"next":{"val":3,"next":{"val":2,"next":{"val":1,"next":null}}}}
为什么要用
JSON.parse(JSON.stringify())深拷贝result数据呢?这里涉及到引用传递的相关知识,不妨先去掉看看有什么问题。
let result = {}
while(listdata) {
result.val = listdata.val
result.next = result.next ?? null
result.next = result
listdata = listdata.next
}
console.log(JSON.stringify(result.next))
报错了,报错信息如下:
console.log(JSON.stringify(result.next))
^
TypeError: Converting circular structure to JSON
这是因为result.next存在循环引用,所以JSON.stringify()才报错。
为什么会这样呢?
将
result.next指向result,这里明显是有问题的,存在循环引用。如何解决呢?我们只能将result深拷贝一份出去,让result.next指向这个深拷贝对象就可以了。如果想要深入了解,请继续往下看,下面我们来仔细讨论引用传递这个话题。
引用传递
接着上面的 listdata,看看下面的例子:
...
// 赋值完成之后呢,testdata 和 listdata 都共享一个引用地址
let testdata = listdata
// 给listdata重新赋值
listdata = {}
console.log(JSON.stringify(testdata))
// {"val":1,"next":{"val":2,"next":{"val":3,"next":{"val":4,"next":null}}}}
console.log(listdata)
// {}
显然,通过上述示例,我们可以看到给 listdata 重新赋值后,并不会改变原地址的值。
为什么呢?
listdata只是一个标识符,之前的值为一个引用数据类型的地址,比如:listdata: 0x00。现在只是给它赋值了一个新的地址listdata: 0x01,该地址存储的是{}。
那如果修改 listdata.val 呢,会发生什么?
...
let testdata = listdata
listdata.val = 'test'
console.log(JSON.stringify(testdata))
// {"val":"test","next":{"val":2,"next":{"val":3,"next":{"val":4,"next":null}}}}
可以看到 testdata 的值发生了变化,即原地址的值变了。
这里发生了什么呢?
其实我们可以将
.看做是一个操作符,listdata.val看成一个表达式。执行listdata.val = 'test'时,会从左到右计算表达式的值。listdata.val的计算结果:取listdata地址里的val属性;'test'(也是一个表达式)的计算结果为:'test',最后才是赋值操作。所以呢,该语句的执行结果就是将listdata地址里的val属性赋值为'test',即修改了原地址里的值。
那如果是基本数据类型呢?
let a = 3 // js引擎开辟了一块内存 00x0,存储3,标识符a指向00x0
let b = a // b也指向00x0
a = 2 // 重新开辟了一块内存 00x1,存储2,标识符a指向00x1
console.log(b) // 所以呢b指向00x0,为3
我们常说的 Immutable(不可更改)指的就是 00x0, 00x1 里的原始值 3 2 无法更改。GC 也是通过引用实现的标记-清除算法,来清理内存。
形成 or 实参
示例1:
var a = 1;
// 函数声明时的参数x,为形参
function foo(x) {
x = 2;
}
// 函数调用时传递的参数a,为实参
foo(a);
console.log(a); // 仍为1, 未受x = 2赋值所影响
示例2:
var obj = {x : 1};
function foo(o) {
// 形参o和实参obj共享一个地址,下述语句,修改了该地址的值
o.x = 3;
}
foo(obj);
console.log(obj.x); // 3, 被修改了!
通过上述两个示例可以看到,实参为对象时,会修改实参的值。其实,无论是基本类型,还是引用类型在进行参数传递时,传递的都是实参的副本(浅拷贝对象)。直接修改副本的值,如示例1、示例3,是不会影响实参的;只有通过操作符
.时(o.x),才会影响原地址的值。因为副本和实参都指向同一个地址。
示例3:
var obj = {x : 1};
function foo(o) {
// 形参o是实参obj的副本,修改o不会影响obj
o = 3;
}
foo(obj);
console.log(obj.x); // 1
总结
在使用引用数据类型进行赋值或传参时,如非必要,不要影响实参的值。可传递实参的深拷贝对象:
object类型深拷贝方法:JSON.parse(JSON.stringify())Iterator(如: Array, Map, Set)可使用扩展运算符:...