js数据类型的前世今生( 数据类型判断、浅拷贝、深拷贝)

75 阅读8分钟

js数据类型的前世今生( 数据类型判断、浅拷贝、深拷贝)

js数据类型

首先我们需要知道的是js数据类型分为 基本数据类型引用数据类型 ,我们说的深拷贝或浅拷贝都是针对的 引用数据

基本数据类型
  • String类型
  • Null类型
  • Undefined类型
  • Boolean
  • 数字类型(Number、BigInt)
  • 符号类型(Symbol)
引用数据类型
  • Object
  • Data
  • Function
  • RegExp
  • Array
  • ....
两者的区别

基本数据类型因为其占用空间小,大小固定等特性,js在保存时会直接将其保存到栈内存中,而引用数据类型则因为占据空间大、占用内存不固定,直接保存到栈内存会程序运行的性能,所以引用数据类型会直接在堆内存中创建,栈内存保存指针,通过指针在堆内存中获得实体。

可以参考这张图:

16fae6c92f0345b4_tplv-t2oaga2asx-zoom-in-crop-mark_4536_0_0_0.webp

所以一般说到深拷贝和浅拷贝,我们针对的是引用数据类型

怎样判断我们的数据类型是哪种?
1. typeof

typeof 运算符返回一个字符串,表示操作数的类型。

我们一般用于检测我们的基本数据类型

console.log(typeof 42); // "number"console.log(typeof 'blubber'); // "string"console.log(typeof true);// "boolean"console.log(typeof undeclaredVariable); //  "undefined"// 由于js历史原因,typeof null时,会返回“object”
console.log(typeof null); // "object" 

下表罗列了**typeof**的返回值

类型结果
Undifined“undifined”
Boolean"boolean"
Number"Number"
BigInt"bigInt"
String"string"
Symbol"Symbol"
Null"object" 原因
Function"function"
其他任何对象"Object"
2.instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

instanceof返回一个Boolean值

let obj = {}
obj instanceof Object //  truelet arr = []
arr instanceof Array //  trueconst fun = function() {}
fun instanceof Function // trueconst date = new Date()
date instanceof Date   // true

判断基本数据类型时,一定要通过new关键词实例化才是正确的

let str1 ='abc'
let str2 = new String('abc');
​
str1 instanceof String // false
str2 instanceof String // true

所以一般**instanceof**我们用于判断引用数据类型

3.Object.prototype.toString()

该方法统一返回 [object Xxx]的字符串,调用该方法时我们需要使用call方法改变this指向问题,否则永远指向Object.prototype

// 未使用call方法
Object.prototype.toString([1, 2, 3]); // "[object Object]"
Object.prototype.toString({}); // "[object Object]"
Object.prototype.toString(new Date()); // "[object Object]"// 使用call方法
// 引用类型
Object.prototype.toString.call([])           // '[object Array]'
Object.prototype.toString.call({})           // '[object Object]'
Object.prototype.toString.call(function(){}) // "[object Function]'
Object.prototype.toString.call(/test/g)       // '[object RegExp]'
Object.prototype.toString.call(new Date())   // '[object Date]'
Object.prototype.toString.call(new Error())  // '[object Error]'
Object.prototype.toString.call(new Map())    // '[object Map]'
Object.prototype.toString.call(new Set())    // '[object Set]'
Object.prototype.toString.call(new WeakMap()) // '[object WeakMap]'
Object.prototype.toString.call(new WeakSet()) // '[object WeakSet]'
Object.prototype.toString.call(document)     // '[object HTMLDocument]'
Object.prototype.toString.call(window)       // '[object Window]'// 原始类型
Object.prototype.toString.call(1)           // '[object Number]'
Object.prototype.toString.call('1')         // '[object String]'
Object.prototype.toString.call(true)        // '[object Boolean]'
Object.prototype.toString.call(1n)          // '[object BigInt]'
Object.prototype.toString.call(null)        // '[object Null]'
Object.prototype.toString.call(undefined)   // '[object Undefined]'
Object.prototype.toString.call(Symbol('a')) // '[object Symbol]'
4.我们封装一个自己的方法(取长补短)
function getDataType(data) {
  // 先处理基本数据类型
  // 处理null
  if (data === null) return null;
  // 处理非null情况 (基本数据类型、function)
  let type = typeof data;
  if (type !== 'object') return type;
  // 处理引用类型
  let reg = /^[object (\S*)]$/;
  // 统一返回小写
  return Object.prototype.toString.call(data).replace(reg, '$1').toLocaleLowerCase();
}

测试一下

// 基本数据类型
console.log(getDataType('a'));
console.log(getDataType(null));
console.log(getDataType(undefined));
console.log(getDataType(true));
console.log(getDataType(1));
console.log(getDataType(Symbol('symbol')));
// 引用数据类型
console.log(getDataType({}));
console.log(getDataType([1, 2, 3]));
console.log(getDataType(function () {}));
console.log(getDataType(new Date()));
console.log(getDataType(new RegExp()));

输出结果

string
null
undefined
boolean
number
symbol
object
array
function
date
regexp

浅拷贝与深拷贝

前置知识

由上文介绍的引用数据类型,我们可以得知,引用数据类型保存的时候,仅保存其内存地址,所以当我们改变同一个内存地址的一个值时,所有被赋值的引用数据类型都会发生变化

const student1 = {
  name: '小在',
  age: 18,
};
const student2 = student1;student1.age = 20;
​
console.log(student1);  //{ name: '小在', age: 20 }
console.log(student2);  //{ name: '小在', age: 20 }
console.log(student1 == student2); // true

这种情况下,在实际业务中肯定会存在很多问题,所以我们保存两份数据,每个份数据互不干扰

浅拷贝

顾名思义,浅拷贝,就是浅浅的拷贝一层,像上面的student1,所有的value值是基本数据类型,我们通过浅拷贝就可以实现

1.Object.assign
const student1 = {
  name: '小在',
  age: 18,
};
const student2 = Object.assign({}, student1);student1.age = 20;
​
console.log(student1);//{ name: '小在', age: 20 }
console.log(student2);//{ name: '小在', age: 18 }
​
​
const subject1 = ['语文', '数学', '英语'];
const subject2 = Object.assign([], subject1);
subject1[0] = '化学';
​
console.log(subject1);  //[ '化学', '数学', '英语' ]
console.log(subject2);  //[ '语文', '数学', '英语' ]
2.扩展运算符 ...
const student1 = {
  name: '小在',
  age: 18,
};
const student2 = { ...student1 };student1.age = 20;
​
console.log(student1);//{ name: '小在', age: 20 }
console.log(student2);//{ name: '小在', age: 18 }
​
const subject1 = ['语文', '数学', '英语'];
const subject2 = [...subject1];
subject1[0] = '化学';
​
console.log(subject1);  //[ '化学', '数学', '英语' ]
console.log(subject2);  //[ '语文', '数学', '英语' ]
3.数组的一些自身方法Array.from() 、slice、concat
const subject1 = ['语文', '数学', '英语'];
const subject2 = Array.from(subject1);
const subject3 = subject1.slice(0);
const subject4 = [].concat(subject1);
subject1[0] = '化学';
​
console.log(subject1); //[ '化学', '数学', '英语' ]
console.log(subject2); //[ '语文', '数学', '英语' ]
console.log(subject3); //[ '语文', '数学', '英语' ]
console.log(subject4); //[ '语文', '数学', '英语' ]

浅拷贝存在一种问题,就是当我们的引用数据类型里面又包含一个引用数据类型,那么还会出现指针指向同一个内存的问题

const student1 = {
  name: '小在',
  age: 18,
  area: {
    province: '北京',
    city: '北京市',
    region: '朝阳区',
  },
};
const student2 = { ...student1 };
​
student1.area.province = '上海';
student1.area.city = '上海市';
student1.area.region = '徐汇区';
​
console.log(student1);
// {
//  name: '小在',
//  age: 18,
//  area: { province: '上海', city: '上海市', region: '徐汇区' }
// }
console.log(student2);
// {
//  name: '小在',
//  age: 18,
//  area: { province: '上海', city: '上海市', region: '徐汇区' }
// }
深拷贝

当出现

1.JSON.parse(JSON.stringify(obj))

既然基本数据类型不存在指针问题,可以单独存储到内存中,那么我们就可以将引用数据类型先转化为基本数据类型,然后再转回到引用数据,就可以解决

const student1 = {
  name: '小在',
  age: 18,
  area: {
    province: '北京',
    city: '北京市',
    region: '朝阳区',
  },
};
const student2 = JSON.parse(JSON.stringify(student1));
​
student1.area.province = '上海';
student1.area.city = '上海市';
student1.area.region = '徐汇区';
​
console.log(student1);
// {
//  name: '小在',
//  age: 18,
//  area: { province: '上海', city: '上海市', region: '徐汇区' }
// }console.log(student2);
// {
//  name: '小在',
//  age: 18,
// area: { province: '北京', city: '北京市', region: '朝阳区' }
// }
​
​
// 但是这样有一个问题,当我们的对象中有symbol,undefined,function时,此方法无法转化
const student3 = {
  name: '小在',
  age: 18,
  class: undefined,
  id: Symbol('001'),
  skill: function () {
    console.log('say hello');
  },
};
const student4 = JSON.parse(JSON.stringify(student3));
​
console.log(student3);
// {
//  name: '小在',
//  age: 18,
//  class: undefined,
//  id: Symbol(001),
//  skill: [Function: skill]
// }console.log(student4);
// { name: '小在', age: 20 }
2.我们封装一个自己的方法

先上最终效果,我们在一步一步讲

function deepClone(targe) {
  // 特殊处理null
  if (targe === null) return targe;
  // 基本数据类型
  if (typeof targe !== 'object') return targe;
  // 特殊处理date
  if (targe instanceof Date) return new Date(targe);
  // 特殊处理正则
  if (targe instanceof RegExp) return new RegExp(targe);
  // 判断targe类型为object或array,动态创建{}或[]
  const cloneTarge = new targe.constructor();
  // 使用Reflect.ownKeys属性可获取包括Symbol类型在内的可枚举属性
  Reflect.ownKeys(targe).forEach((key) => {
    cloneTarge[key] = deepClone(targe[key]);
  });
  return cloneTarge;
}

先写一个最简单的拷贝方法,通过for...in遍历值,赋给新值

function deepClone(target) {
  const newTarget = {};
  for (const key in target) {
    newTarget[key] = target[key];
  }
  return newTarget;
}

这样只是最基础的浅拷贝,我们通过递归解决一下对象内部的引用类型

function deepClone(target) {
  // 特殊处理null
  if (target === null) return target;
  // 基本数据类型
  if (typeof target !== 'object') return target;
  const newTarget = {};
  for (const key in target) {
    newTarget[key] = deepClone(target[key]);
  }
  return newTarget;
}

我们再加一些特殊的引用数据类型处理,如DateRegExp

function deepClone(target) {
  // 特殊处理null
  if (target === null) return target;
  // 基本数据类型
  if (typeof target !== 'object') return target;
  // 特殊处理date
  if (target instanceof Date) return new Date(target);
  // 特殊处理正则
  if (target instanceof RegExp) return new RegExp(target);
  const newTarget = {};
  for (const key in target) {
    newTarget[key] = deepClone(target[key]);
  }
  return newTarget;
}

我们在处理一下引用数据类型为数组的情况,可以通过target.constructor来为Object或Array

function deepClone(target) {
  // 特殊处理null
  if (target === null) return target;
  // 基本数据类型
  if (typeof target !== 'object') return target;
  // 特殊处理date
  if (target instanceof Date) return new Date(target);
  // 特殊处理正则
  if (target instanceof RegExp) return new RegExp(target);
  // 判断targe类型为object或array,动态创建{}或[]
  const newTarget = new target.constructor();
  for (const key in target) {
    newTarget[key] = deepClone(target[key]);
  }
  return newTarget;
}

当对象的键位symbol类型时,我们也需要处理一下

function deepClone(target) {
  // 特殊处理null
  if (target === null) return target;
  // 基本数据类型
  if (typeof target !== 'object') return target;
  // 特殊处理date
  if (target instanceof Date) return new Date(target);
  // 特殊处理正则
  if (target instanceof RegExp) return new RegExp(target);
  // 判断targe类型为object或array,动态创建{}或[]
  const newTarget = new target.constructor();
  // 使用Reflect.ownKeys属性可获取包括Symbol类型在内的可枚举属性
  Reflect.ownKeys(target).forEach((key) => {
    newTarget[key] = deepClone(target[key]);
  });
  return newTarget;
}

我们验证一下

const student1 = {
  name: '小在',
  age: 18,
  area: {
    province: '北京',
    city: '北京市',
    region: '朝阳区',
  },
  class: undefined,
  id: Symbol('001'),
  skill: function () {
    console.log('say hello');
  },
  birthday: new Date(),
  regExp: /^1/,
  specialty: ['游泳', '跑步'],
};
let symbolIDCard = Symbol('IDCard');
student1[symbolIDCard] = '10086';
​
let student2 = deepClone(student1);
student1.area.province = '上海';
​
console.log(student1);
// {
//  name: '小在',
//  age: 18,
//  area: { province: '上海', city: '北京市', region: '朝阳区' },
//  class: undefined,
//  id: Symbol(001),
//  skill: [Function: skill],
//  birthday: 2022-10-20T10:32:39.645Z,
//  regExp: /^1/,
//  specialty: [ '游泳', '跑步' ],
//  [Symbol(IDCard)]: '10086'
// }console.log(student2);
// {
//  name: '小在',
//  age: 18,
//  area: { province: '北京', city: '北京市', region: '朝阳区' },
//  class: undefined,
//  id: Symbol(001),
//  skill: [Function: skill],
//  birthday: 2022-10-20T10:32:39.645Z,
//  regExp: /^1/,
//  specialty: [ '游泳', '跑步' ],
//  [Symbol(IDCard)]: '10086'
// }
3.使用第三方插件库lodash
import { cloneDeep } from 'lodash';
​
let student2 = deepClone(student1);

参考文章

1.JavaScript 数据类型和数据结构

2.Reflect.ownKeys

3.typeofdeveloper.mozilla.org/zh-CN/docs/…

4.instanceof

5.lodash的cloneDeep方法

6.轻松拿下 JS 浅拷贝、深拷贝