JavaScript基本数据类型+浅拷贝+深拷贝的实现

2. JavaScript数据类型

JS有八种基本的数据类型。

其中基本数据类型,也称原始数据类型。

原始数据类型

原始数据类型 表示
number 表示任何类型的数字,整数或者浮点数。范围是±(2(^53)-1)
bigint 表示任意长度的整数,使用时需要在数字后加上n, 示例:let a = 18888888n
string 表示字符串,一个字符串可以包含0个或者多个字符,没有单独的字符类型。
boolean 布尔值类型,表示true或者false
null 用于表示未知的值,只有一个null值的独立类型
undefined 用于表示未定义的值,只有一个undefined值得独立类型
symbol 唯一标识符,英文单词的意思就是标记。

有关symbol:

JavaScript 引入 Symbol 类型主要是为了解决对象属性名冲突的问题。

在 JavaScript 中,对象的属性名默认是字符串。如果你在没有充分了解一个对象的情况下给它添加新的属性,那么新的属性名可能会与已有的属性名冲突,从而导致原有的属性被覆盖,这可能会引发错误。

Symbol 类型的引入就是为了解决这个问题。每一个 Symbol 值都是唯一的,所以你可以用 Symbol 值作为对象的属性名,这样就可以保证新添加的属性不会与原有的属性冲突。

ini 复制代码
let a = Symbol() //
let obj= {
  [a]: 'values'
}
console.log(obj[a])

Object:非原始数据类型 (引用数据类型)

Object用于更复杂的数据结构。以下类型都是对象:

  • Function(函数)
  • Array(数组)
  • Date(日期)
  • RegExp(正则表达式)

原始数据类型和非原始数据类型的差异

当定义一个变量的时候,JavaScript 引擎会为变量分配两种内存:栈内存和堆内存。

静态值在编译阶段有固定的大小,静态值有:

原始值:NullUndefinedBooleanNumberStringSymbolBigInt 引用值:是对象的引用。 静态值有固定的大小,不能改变。JavaScript 引擎为它们分配一片固定的内存,并存储在上。

就是说原始数据类型存储在上。

JavaScript 将对象(Object) 存储在堆(heap)上。

ini 复制代码
let person = {
    name: "John",
    age: 25
};

JavaScript 引擎在堆内存上创建了一个新的对象,同时它和栈内存上的 person 变量连接。因此,我们说 person 变量是对象的引用。

JavaScript 的引用数据类型(如对象、数组和函数)是通过内部使用指针来实现的。但是,这些指针对 JavaScript 开发者是不可见的,因为 JavaScript 不提供直接操作指针的能力。当你创建一个对象并将其赋值给一个变量时,这个变量实际上存储的是一个指向对象在内存中位置的引用,而不是对象本身。当你将这个变量赋值给另一个变量时,你是在复制这个引用,而不是对象本身。这就是为什么在 JavaScript 中,如果你修改了一个对象,那么所有引用这个对象的变量都会看到这个修改。

ini 复制代码
let obj1 = { value: 1 };
let obj2 = obj1;
obj2.value = 2;
​
console.log(obj1.value); // 输出 2

就是说引用数据类型存储在堆上

那么栈和堆到底是什么?

栈(Stack)和堆(Heap)是计算机内存中的两种重要的数据结构,它们在存储数据时有着不同的特性和用途。

  1. 栈(Stack):栈是一种后进先出(LIFO)的数据结构,它在计算机内存中占据一块连续的区域。栈用于存储原始数据类型(如 number、string、boolean、null、undefined、symbol)和引用类型的地址。栈上的数据在函数执行完毕后会被自动清除,所以栈的空间通常比较小。
  2. 堆(Heap):堆是一种更灵活的数据结构,它在内存中占据的区域不连续。堆用于存储引用数据类型(如 object、array、function)的实际数据。堆上的数据的生命周期由垃圾回收器控制,当没有引用指向一个对象时,这个对象就会被垃圾回收器清除。

在 JavaScript 中,当你创建一个对象并将其赋值给一个变量时,这个变量实际上存储的是一个指向对象在堆内存中位置的引用,这个引用存储在栈内存中。当你将这个变量赋值给另一个变量时,你是在复制这个引用,而不是对象本身。

复制值

对于原始数据类型来说,复制的时候会拷贝栈上的值的副本,并且赋值给新的变量。

ini 复制代码
let age = 25;
let newAge = age; 
console.log(age, newAge); // 25 25

这两个变量互不影响。

但是对于引用值来说,复制的值指向的是同一个对象,因此操作的也是同一个对象。

当我们将一个引用值从一个变量赋值给另一个变量,JavaScript 引擎创建一个引用,因此两个变量都是指向堆内存中的同一个对象。意味着,你修改其中一个,另一个也会被修改。也就是浅拷贝

ini 复制代码
let person = {
    name: "John",
    age: 25
}
​
let member = person;
member.age = 26;
​
console.log(person); // {name: 'John', age: 26}
console.log(member); // {name: 'John', age: 26}
​

浅拷贝

浅拷贝和深拷贝都是相对于引用数据类型而言的。使用=直接赋值得到的是引用值。

javascript 复制代码
// 数组浅拷贝, 直接使用等于号?
// 不行,直接使用 = 会直接赋值引用的地址,会导致两个一起改变
let arr1 = [1, 2, 3, 4]
let arr2 = arr1
arr2[1] = 5
console.log(arr2) // 1,5,3,4
console.log(arr1) // 1,5,3,4

实现数组浅拷贝的几种方式

1. 使用扩展运算符

scss 复制代码
// 1. 使用扩展运算符
let arr1 = [1, 2, 3, 4]
let arr2 = [...arr1]
arr2[2] = 5
console.log(arr2) // 1 ,2 ,5, 4
console.log(arr1) // 1, 2, 3, 4

2. 使用.slice()方法

scss 复制代码
// 2. 使用.slice()方法
let arr1 = [1, 2, 3, 4]
let arr2 = arr1.slice()
arr2[2] = 5
console.log(arr2) // 1 ,2 ,5, 4
console.log(arr1) // 1, 2, 3, 4

3. 使用.assign()方法,注意assign()方法挂载在Object这一原型对象上

注意使用方式的不同。

scss 复制代码
// 3. 使用.assign()方法
let arr1 = [1, 2, 3, 4]
let arr2 = []
Object.assign(arr2, arr1)
arr2[2] = 5
console.log(arr2) // 1 ,2 ,5, 4
console.log(arr1) // 1, 2, 3, 4

4. 使用Array.from()方法

对于数组或者其他可迭代的对象都可以使用该方法实现浅拷贝。

javascript 复制代码
// 4. 使用Array.from()方法
let arr1 = [1, 2, 3, 4]
let arr2 = Array.from(arr1)
arr2[2] = 5
console.log(arr2) // 1 ,2 ,5, 4
console.log(arr1) // 1, 2, 3, 4

实现对象浅拷贝的集中方式

1. 扩展运算符

javascript 复制代码
// 1. 使用扩展运算符
let obj1 = { name: 'hh', age: 18 }
let obj2 = { ...obj1 }
obj2.age = 20
console.log(obj1) // { name: 'hh', age: 18 }
console.log(obj2) // { name: 'hh', age: 20 

2. Object.assign()方法

javascript 复制代码
// 2. 使用Object.assign()方法
let obj1 = { name: 'hh', age: 18 }
let obj2 = {}
Object.assign(obj2, obj1)
obj2.age = 20
console.log(obj1) // { name: 'hh', age: 18 }
console.log(obj2) // { name: 'hh', age: 20 }

注意对象不能使用Array.from()方法实现浅拷贝。

✨✨✨深度拷贝的实现

可以使用第三方库如lodash、rfdc等进行深度拷贝。

1. 使用JSON.stringify()和JSON.parse()实现数组和对象深拷贝

javascript 复制代码
// 1. 深度拷贝第一种实现方式 stringify+parse
function deepCopyIfy(obj) {
  return JSON.parse(JSON.stringify(obj))
}
​
let obj1 = { name: 'ZhangsSan', age: 18, info: { gender: 'female', address: 'unknow' } }
let obj2 = deepCopyIfy(obj1)
obj2.info.gender = 'male'
console.log(obj1)
// {
//   name: 'ZhangsSan',
//   age: 18,
//   info: { gender: 'female', address: 'unknow' }
// }
console.log(obj2)
// {
//   name: 'ZhangsSan',
//   age: 18,
//   info: { gender: 'male', address: 'unknow' }
// }

然而,这种方式根据不同类型的值的表现各有区别。会导致一些数据被忽略,一些数据被转换。

javascript 复制代码
// 1. 深度拷贝第一种实现方式 stringify+parse
function deepCopyIfy(obj) {
  return JSON.parse(JSON.stringify(obj))
}
​
let obj1 = {
  name: 'ZhangsSan',
  age: 18,
  info: {
    gender: 'female',
    address: 'unknow',
    notNumber: NaN, // 会被忽略 转换为null
    date: new Date('2022-12-31T23:59:59'), // Date会转换为字符串
    undefined: undefined, // 被忽略 甚至不会被添加到新对象上
    infinity: Infinity, // 被忽略 转换为 null
    regExp: /.*/ //正则表达式会被忽略 转换为一个空对象 {}
  }
}
let obj2 = deepCopyIfy(obj1)
obj2.info.gender = 'male'
console.log(obj1)
// {
//   name: 'ZhangsSan',
//   age: 18,
//   info: {
//     gender: 'female',
//     address: 'unknow',
//     notNumber: NaN,
//     date: 2022-12-31T15:59:59.000Z,
//     undefined: undefined,
//     infinity: Infinity,
//     regExp: /.*/
//   }
// }
console.log(obj2)
// {
//   name: 'ZhangsSan',
//   age: 18,
//   info: {
//     gender: 'male',
//     address: 'unknow',
//     notNumber: null,
//     date: '2022-12-31T15:59:59.000Z',
//     infinity: null,
//     regExp: {}
//   }
// }
​

2. 使用全局structedNode()方法进行深拷贝。

javascript 复制代码
// 2. 全局的structedClone()方法将给定的值进行深拷贝
let obj1 = {
  name: 'ZhangsSan',
  age: 18,
  info: {
    gender: 'female',
    address: 'unknow',
    notNumber: NaN, // 会被忽略
    date: new Date('2022-12-31T23:59:59'), // Date会转换为字符串
    testUn: undefined,
    undefined: undefined, // 被忽略
    infinity: Infinity, // 被忽略
    regExp: /.*/ //正则表达式会被忽略
  }
}
let obj2 = structuredClone(obj1)
console.log(obj2)

该方法可以实现深度拷贝。然而,此方法存在浏览器兼容性的问题。例如在360浏览器中将报错显示undefined

手动实现深拷贝

手动实现主要利用的是递归的思想。但是注意要处理几种特例,一是:若拷贝的是基本数据类型或者null,那么直接返回就可以。二是:如果处理的对象是Date,或者RegExp类型的,那么生成一个新的相同类型对象并且返回。三是:要注意循环引用的情况,需要加一个weakmap,防止重复复制相同的引用,导致爆栈。

javascript 复制代码
// 手动实现深拷贝
function deepCopy(obj, cache = new WeakMap()) {
  // 1. 特例处理
  // 1.1 假如obj是null或者不是一个引用类型,直接返回值
  if (obj === null || typeof obj !== 'object') {
    return obj
  }

  // 1.1 处理循环引用的问题
  if (cache.has(obj)) {
    return cache.get(obj)
  }

  // 1.2 处理 Date对象
  if (obj instanceof Date) {
    return new Date(obj.getTime())
  }

  // 1.3 处理正则表达式对象
  if (obj instanceof RegExp) {
    return new RegExp(obj)
  }

  // 2. 假如obj是数组或者对象,就要进行递归复制
  // 2.1 首先判断一下obj是数组还是对象,方便初始化值
  let newObj = Array.isArray(obj) ? [] : {}

  // 将newObj放到chche中
  cache.set(obj, newObj)

  // 2.2 使用迭代器来遍历这个数组或者对象
  Object.keys(obj).forEach((item) => (newObj[item] = deepCopy(obj[item], cache)))

  return newObj
}

let obj1 = { name: 'nihao', age: 10, arr: [1, 2, 3, 4], date: new Date(), reg: /test/g }
obj1.next = obj1
// let arr1 = [1, 2, 3, [3, 4]]
let obj2 = deepCopy(obj1)
// let arr2 = deepCopy(arr1)
console.log(obj2)
// console.log(arr2)
相关推荐
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax