这套题还不错,感兴趣的猿可以试一试:前端开发工程师
什么是原始值和引用值?
ECMAScript 变量可以包含两种不同类型的数据:原始值和引用值。
原始值就是最简单的数据
原始值一共有6种:
- Undefined
- Null
- Boolean
- Number
- String
- Symbol
引用值是由多个值构成的对象
在操作对象时,实际上操作的是对该对象的引用。
变量赋值
通过变量把原始值赋值到另一个变量时:
- 原始值会被复制到新的位置,创建一个完全独立的副本。
- 引用值也会被复制到新的位置,但是它复制的只是一个指针,指向了存储在堆内存中的对象。两个变量实际上指向的是同一个对象。
函数传参时,值会被赋值到函数中的一个局部变量。效果同变量赋值一致。(书里的说法是函数传参是按值的。不过讲了大段篇幅也没有很清晰。)
书里的例子还是很明了的:
const obj = new Object()
obj.name = 'nick'
function test(target) {
target.name = 'jack'
target = new Object()
target.name = 'tony'
}
test(obj)
console.log(obj.name) // jack
判断数据类型
适合通过 typeof 来判断的有四种原始值类型
- 字符串
- 数值
- 布尔值
- undefined
typeof的坑
明明有Null类型…这是JS的一个“特性”
typeof null // "object"
instanceof
- 通过instanceof 检测原始值,始终会返回false
- 所有引用值都是Object的实例
深浅拷贝
基于原始值和引用值的特点,如果我们想要拷贝一个完全独立的引用值的副本出来,一旦这个对象的层级超过两层,就没法用常规的方式拷贝,这个就衍生出来了深浅拷贝的问题~
obj = {
a: {
b: [{
c: 1
}]
},
d: undefined,
e: Symbol()
}
那些可能有问题的“常规拷贝”方式
- 数组的slice和concat方法会返回一个新的数组,而不改变原数组,但是如果原数组的元素是对象,新数组的元素仍然指向原对象的引用。
- Object.assign 以及 扩展运算符也是同理
- JSON.stringify和JSON.parse 。这个适合大部分的场景,拷贝一些基础的数据,如Number, String, Boolean, Array, Object。stringify的过程会忽略undefined、function、Symbol
function clone(jsonObj) {
return JSON.parse(JSON.stringify(jsonObj))
}
clone(obj)
// {
// a: {
// b: [{
// c: 1
// }]
// }
// }
递归深拷贝
function clone(jsonObj) {
let buf;
if (jsonObj instanceof Array) {
buf = [];
let i = jsonObj.length;
while (i--) {
buf[i] = clone(jsonObj[i]);
}
return buf;
} else if (jsonObj instanceof Object) {
if (typeof jsonObj === 'function') {
return jsonObj
}
buf = {};
for (let k in jsonObj) {
buf[k] = clone(jsonObj[k]);
}
return buf;
} else {
return jsonObj;
}
}
- 原始值直接赋值,引用值则分为数组和普通对象来遍历
- 较JSON.parse 支持了undefined、Symbol、function
- 仍然可能会有问题:如果混入了对象的实例,for in 会遍历包含原型链上的属性。如果我们期望copy对应的实例,结果将会变得不可控
总结
当我们在JS中操作一个引用值,就需要考虑到对引用值的修改是否会引起预期之外的副作用~