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)
相关推荐
一斤代码1 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子1 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年1 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子1 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina2 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路2 小时前
React--Fiber 架构
前端·react.js·架构
伍哥的传说3 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409193 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app
我在北京coding3 小时前
element el-table渲染二维对象数组
前端·javascript·vue.js
布兰妮甜3 小时前
Vue+ElementUI聊天室开发指南
前端·javascript·vue.js·elementui