这套题还不错,感兴趣的猿可以试一试:前端开发工程师

什么是原始值和引用值?

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中操作一个引用值,就需要考虑到对引用值的修改是否会引起预期之外的副作用~