正如你所知道的,数据类型是作为js的入门知识点,在整个js的学习过程中也是尤为重要的。数据类型看起来简单,但是围绕着其衍生的边界数据类型判断问题、深拷贝浅拷贝问题对于新手而言是难以理解的。

一、数据类型

JavaScript 是一种弱类型或者说动态类型,这就意味着你不需要提前声明变量的类型,在程序运行的过程中,类型会被自动确定。这就意味着你可以使用同一个变量保存不同类型的数据.

js内存分为栈内存(stack)和堆内存(heap)

  • 栈内存:是一种特殊的线性表,它具有后进先出的特性,存放基本类型。
  • 堆内存:存放引用类型(在栈内存中存一个基本类型值保存对象在堆内存中的地址,用于引用这个对象)。

数据类型根据存储方式分为两类:

  • 基本数据类型(简单数据类型、原始数据类型):值存储在栈内存中,被引用或拷贝时,会创建一个完全相等的变量。占用空间小、大小固定,通过按值来访问,属于被频繁使用的数据。
  • 引用数据类型(复杂数据类型):地址存储在栈内存中,值存在了堆内存中,多个引用会指向同一个地址。占据空间大、占用内存不固定。如果存储在栈中,将会影响程序运行的性能; 引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。 当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

未命名文件 (2).png
根据上面的标准划分数据类型,常见的有:

  • 基本数据类型:String、Number、Boolean、Undefined、Null、Symbol、BigInt
  • 复杂数据类型:Object、Array、Date、Function、RegExp等

未命名文件 (1).png

二、数据类型的检测

通常的数据类型的检测有三种方法:

  • typeof
  • instanceof

    2.1 typeof

    使用typeof进行基础数据类型(null除外)检测,但是对于引用数据类型,除了function外,其它的均无法进行判断。
    1. typeof "yichuan"; //"string"
    2. typeof 18; //"number"
    3. typeof undefined; //undefined
    4. typeof true; //boolean
    5. typeof Symbol(); //"symbol"
    6. typeof null; //"object"
    7. typeof []; //"object"
    8. typeof {}; //"object"
    9. typeof console; //"object"
    10. typeof console.log; //"function"

    2.2 instanceof

    使用instanceof是通过原型链进行查找,可以准确地判断复杂引用数据类型,但是不能准确判断基础数据类型。 ```javascript let Fun = Function(){}; let fun = new Fun(); fun instanceof Fun;//true

let str = new String(“yichuan”); str instanceof String;//true

let str = “yichuan”; str instanceof String;//false

  1. <a name="v7A0o"></a>
  2. ### 2.3 Object.prototype.toString.call()
  3. Object.prototype.toString方法返回对象的类型字符串,因此可用来判断一个值的类型。因为实例对象有可能会自定义toString方法,会覆盖Object.prototype.toString,所以在使用时,最好加上call。所有的数据类型都可以使用此方法进行检测,且非常精准。
  4. ```javascript
  5. Object.prototype.toString.call("yichuan");//["object String"]
  6. Object.prototype.toString.call(18);//["object Number"]
  7. Object.prototype.toString.call(true);//["object Boolean"]
  8. Object.prototype.toString.call(null);//["object Null"]
  9. Object.prototype.toString.call(new Symbol());//["object Symbol"]
  10. Object.prototype.toString.call({});//["object Object"]
  11. Object.prototype.toString.call([]);//["object Array"]
  12. Object.prototype.toString.call(/123/g);//["object RegExp"]
  13. Object.prototype.toString.call(function(){});//["object Function"]
  14. Object.prototype.toString.call(new Date());//["object Date"]
  15. Object.prototype.toString.call(document);//["object HTMLDocument"]
  16. Object.prototype.toString.call(window);//["object Window"]

我们可以看到此输出的结果都是[“object Xxxx”]首字母大写。

2.4 通用的数据类型判断方法

  1. function getType(obj){
  2. //先判断输入的数据判断返回结果是否为object
  3. if(typeof obj !== "object"){
  4. return typeof obj;
  5. }
  6. // 对于typeof返回object的,再进行具体的判断,使用正则返回结果,切记正则中间有个空格哦
  7. return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/,"$1");
  8. }

切记:

  • 使用typeof返回的类型是小写
  • 使用toString返回的类型是大写
    1. getType("yichuna");//"string"
    2. getType(18);//"number"
    3. getType(true);//"boolean"
    4. getType(undefined);//"undefined"
    5. getType();//"undefined"
    6. getType(null);//"Null"
    7. getType({});//"Object"
    8. getType([]);//"Array"
    9. getType(function(){});//"Function"
    10. getType(new Date());//"Date"
    11. getType(/123/g);//"RegExp"

    三、数据类型转换

3.1 强制类型转换

常见的强制类型转换方法有:

  • Number()
  • String()
  • Boolean()
  • parseInt()
  • parseFloat()
  • toString()

    3.2 Number()方法的强制转换规则

  • 布尔值 true和false分别被转换为1和0

  • 数字 返回本身
  • null 返回0
  • undefined 返回NaN
  • 字符串
    • 如果字符串中只包含数字,则将其转换为十进制
    • 如果字符串中只包含有有效的浮点格式,将其转换为浮点数值
    • 如果是空字符串,将其转换为0
    • 如果不是以上格式的字符串,则均返回NaN
  • Symbol 抛出异常

    3.3 Boolean()方法的强制转换规则

    undefined、null、false、””、0(包括+0、-0)、NaN转换出来都是false,其余类型转换都是true。特别注意:Boolean({})转换为true

    3.4 隐式类型转换==

  • 如果类型相同,无需进行类型转换

  • 如果其中一个操作值为null或undefined,那么另一个操作符必须是null或undefined才会返回true,否则均返回false
  • 如果其中一个值是Symbol类型,那么返回false
  • 如果其中一个操作知为Boolean,那么转为number
  • 两个操作值均为string和number类型,那么将字符串转为number
  • 如果一个操作值为object,且另一个为string、number或symbol,就会把object转为原始数据类型判断

小试牛刀:

  1. null == undefined; //true
  2. null == 0;//false
  3. "" == null;//false
  4. "" == 0;//true 会转为number类型再进行判断
  5. "123" == 123;//true
  6. 0 == false;//true
  7. 1 == true;//true

3.5 隐式类型转换+

“+”号操作符,不仅可以用于数字相加,还可以用于字符串拼接。

  • 如果其中一个是字符串,另外一个是number、undefined、null或boolean,则调用toString()方法进行字符串拼接
  • 如果是纯字符串、数组、正则等,则默认调用对象的转换方法会存在优先级,然后进行拼接
  • 如果字符串和bigInt进行相加,会先将bigInt转为字符串
  • 如果number类型与undefined相加,则得到NaN ```javascript 1 + 2;//3 1 + “2”;//“12”

“1” + undefined;//“1undefined” “1” + null;//“1null” “1” + true;//“1true” “1” + 1n;//“11” 字符串和bigInt进行相加,会先将bigInt转为字符串

1 + undefined;//NaN undefined会先转为NaN 1 + null;//1 null转为0 1 + true;//2 1 + 1n;//Error

  1. <a name="EwGK9"></a>
  2. ### 3.6 object的转换规则
  3. - 如果部署了[Symbol.toPrimitive]()方法,优先调用再返回
  4. - 调用valueOf(),如果转换为基础类型则返回
  5. - 调用toString(),如果转换为基础数据类型则返回
  6. - 如果都没有返回基础数据类型,则会报错
  7. <a name="YlixH"></a>
  8. ## 四、深拷贝和浅拷贝
  9. 在js的编程中经常需要进行数据进行复制,那么什么时候使用深拷贝、什么时候使用浅拷贝呢,是开发过程中需要思考的?如何提升自己手写js的能力,以及对一些边界情况的深入思考能力呢?
  10. 有两个重要问题:
  11. - 拷贝一个很多嵌套的对象要如何实现呢?
  12. - 深拷贝写成什么程度才能让面试官满意呢?
  13. <a name="segBI"></a>
  14. ### 4.1 浅拷贝的原理和实现
  15. 自己创建一个新的对象,来接受要重新复制或引用的对象值。
  16. - 如果对象属性是基本数据类型,复制的就是基本数据类型的值给新对象;
  17. - 如果对象属性是引用数据类型,赋值的则是内存中的地址,如果其中一个对象改变了这个内存中的地址,肯定会影响另外一个对象
  18. <a name="wUGFp"></a>
  19. #### 4.1.1 Object.assign
  20. Object.assign是es6中object的一个方法,该方法可以用于js对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。
  21. ```javascript
  22. Object.assign(target,...sources);//target目标对象,sources待拷贝的对象

注意:

  • Object.assign不会拷贝对象的继承属性
  • Object.assign不会拷贝对象的不可枚举属性

例如:

  1. let obj = {};
  2. let obj1 = {
  3. name:"yichuan",
  4. scores:{
  5. math:100,
  6. Chinese:100
  7. }
  8. };
  9. Object.assign(obj,obj1);
  10. console.log(obj);//{name:"yichuan",scores:{math:100,Chinese:100}}

改变目标对象的值:我们可以看到下面改变了目标对象的值,会引起待拷贝对象的值的改变。

  1. let obj = {};
  2. let obj1 = {
  3. name:"yichuan",
  4. scores:{
  5. math:100,
  6. Chinese:100
  7. }
  8. };
  9. Object.assign(obj,obj1);
  10. console.log(obj);//{name:"yichuan",scores:{math:100,Chinese:90}}
  11. obj.scores.Chinese = 10;
  12. console.log(obj);//{name:"yichuan",scores:{math:100,Chinese:90}}
  13. console.log(obj1);//{name:"yichuan",scores:{math:100,Chinese:90}}

不可拷贝不可枚举属性

  1. let obj1 = {
  2. user:{
  3. name:"yichuan",
  4. age:18
  5. },
  6. idCard:Symbol(1)
  7. };
  8. Object.defineProperty(obj1,"innumerable",{
  9. value:"不可枚举属性",
  10. enumerable:false
  11. });
  12. let obj2 = {};
  13. Object.assign(obj2,obj1);
  14. obj1.user.name = "onechuan";
  15. console.log("obj1",obj1);//{user: {…}, idCard: Symbol(1), innumerable: '不可枚举属性'}
  16. console.log("obj2",obj2);//{user: {…}, idCard: Symbol(1)} 我们可以看到并没有innumerable属性

4.1.2 展开运算符

  1. /* 对象的拷贝 */
  2. let obj1 = {
  3. user:{
  4. name:"yichuan",
  5. age:18
  6. },
  7. school:"实验小学"
  8. };
  9. let obj2 = {...obj1};
  10. obj2.school = "五道口男子技校";
  11. console.log(obj1);//{school: "实验小学",user: {name: 'yichuan', age: 18}}
  12. obj2.user.age = 19;
  13. console.log(obj2);//{school: "实验小学",user: {name: 'yichuan', age: 19}}
  14. /* 数组的拷贝 */
  15. let arr = ["red","green","blue"];
  16. let newArr = [...arr];
  17. console.log(arr);//['red', 'green', 'blue']
  18. console.log(newArr);//['red', 'green', 'blue']

4.1.3 concat拷贝数组

数组的concat方法其实也是浅拷贝

  1. let arr = ["red","green","blue"];
  2. let newArr = arr.concat();
  3. newArr[1] = "black";
  4. console.log(arr);//["red","green","blue"];
  5. console.log(newArr);//["red","black","blue"];

4.1.4 slice拷贝数组

slice方法仅针对数组类型,arr.slice(begin,end);

  1. let arr = ["red","green","blue"];
  2. let newArr = arr.slice();
  3. newArr[1] = "black";
  4. console.log(arr);//["red","green","blue"];
  5. console.log(newArr);//["red","black","blue"];

4.1.5 手写浅拷贝

  • 对基本数据类型进行最基本的拷贝
  • 对引用数据类型开辟新的存储,并且拷贝一层对象属性
    1. function shallowClone(target){
    2. //先要判断是否为对象数据类型
    3. if(typeof target === "object" && target !== null){
    4. //判断输入的是object类型还是数组类型
    5. const cloneTarget = Array.isArray(target) ?[]:{};
    6. //遍历目标对象元素
    7. for(let prop in target){
    8. //判断cloneTarget对象上是否有此属性,没有进行拷贝
    9. if(!cloneTarget.hasOwnProperty(prop)){
    10. cloneTarget[prop] = target[prop]
    11. }
    12. }
    13. return cloneTarget;
    14. }
    15. return target;
    16. }

4.2 深拷贝的原理和实现

前面我们知道浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值。对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有对象完全复制过来存放。

深拷贝就是将一个对象从内存中完整地拷贝出来给目标对象,并在堆内存中开辟新的空间进行存储新对象的值,且新对象的值改变不会影响原对象,也就是实现了二者的隔离。

4.2.1 JSON.stringify()

其实在实际开发过程使用最简单的深拷贝就是使用JSON.stringify()配合JSON.parse()。但其实是有缺陷的,不影响简单使用。
注意:

  • 拷贝的对象值中如果有函数、undefined、symbol等类型,经过JSON.stringify()序列化后字符串中这几个键值对会消失
  • 拷贝Date引用类型会变成字符串
  • 无法拷贝不可枚举的属性
  • 无法拷贝对象的原型链
  • 拷贝RegExp引用类型会变成空对象
  • 对象中含有NaN、Infinity以及-Infinity,JSON序列化的结果是变成了null

    1. let obj1 = {
    2. user:{
    3. name:"yichuan",
    4. age:18
    5. },
    6. school:"实验小学"
    7. };
    8. let obj2 = JSON.parse(JSON.stringify(obj1));
    9. console.log(obj1);//{school: "实验小学",user: {name: 'yichuan', age: 18}}
    10. console.log(obj2);//{school: "实验小学",user: {name: 'yichuan', age: 18}}
    11. obj2.school = "门头沟学员";
    12. obj2.user.age = 19;
    13. console.log(obj1);//{school: "实验小学",user: {name: 'yichuan', age: 18}}
    14. console.log(obj2);//{school: "门头沟学院",user: {name: 'yichuan', age: 19}}

    4.2.2 简易手写深拷贝

    作为简易版手写深拷贝,只能完成基础的拷贝功能,也存在一些缺陷:

  • 不能拷贝不可枚举的属性以及symbol类型

  • 只能针对普通的引用类型的值做递归复制
  • 对象的属性里面成环,即循环引用没有得到妥善解决 ```javascript function deepClone(obj){ const cloneObj = {}; //遍历对象键名 for(let key in obj){ //判断是否为对象类型 if(typeof obj[key]===”object”){

    1. //是对象就再次调用函数进行递归拷贝
    2. cloneObj[key] = deepClone(obj[key]);

    }else{

    1. //是基本数据类型的话,就直接进行复制值
    2. cloneObj[key] = obj[key];

    }

    } return cloneObj; }

const obj1 = { user:{ name:”yichuan”, age:18 }, school:”实验小学” }

let obj2 = deepClone(obj1); obj1.user.age = 19; console.log(obj2);//{school: “实验小学”,user: {name: ‘yichuan’, age: 18}}

  1. <a name="Nl0xl"></a>
  2. #### 4.2.3 优化版手写深拷贝
  3. 对于上面简易版的深拷贝,很显然面试官是不买账的,为此我们针对递归进行升级处理。
  4. - 针对能够遍历对象的不可枚举属性以及Symbol类型,我们可以使用Reflect.ownKeys方法
  5. - 当参数为Date、RegExp类型,则直接生成一个新的实例返回
  6. - 利用Object的getOwnPropertyDescriptors方法可以获得对象的所有属性,以及对应的特性,顺便结合Object.create()方法创建新对象,并继承传入原对象的原型链
  7. - 利用WeakMap类型作为Hash表,因为WeakMap是弱引用类型,可以有效防止内存泄漏,作为检测循环引用有很大的帮助。如果存在循环,则引用直接返回WeakMap存储的值
  8. ```javascript
  9. const isComplexDataType = (obj) => (typeof obj === 'object' || typeof obj === 'function') && obj !== null;
  10. function deepClone(obj, hash = new WeakMap()) {
  11. //判断是否为日期类型
  12. if (obj.constructor === Date) return new Date(obj);
  13. //判断是否正则对象
  14. if (obj.constructor === RegExp) return new RegExp(obj);
  15. //如果循环引用了,就使用weakMap进行解决
  16. if (hash.has(obj)) return hash.get(obj);
  17. const allDesc = Object.getOwnPropertyDescriptors(obj);
  18. //遍历传入参数所有键的特性
  19. const cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc);
  20. //继承原型链
  21. hash.set(obj, cloneObj);
  22. for (const key of Reflect.ownKeys(obj)) {
  23. cloneObj[key] = isComplexDataType(obj[key]) && typeof obj[key] !== 'function' ? deepClone(obj[key]) : obj[key];
  24. }
  25. return cloneObj;
  26. }
  27. const obj1 = {
  28. num: 2021,
  29. str: 'jue',
  30. bool: true,
  31. nul: null,
  32. arr: ['ref', 'green', 'blue'],
  33. date: new Date(0),
  34. reg: new RegExp('/123/g'),
  35. user: {
  36. name: 'yichuan',
  37. age: 18
  38. },
  39. school: '实验小学'
  40. };
  41. const obj2 = deepClone(obj1);
  42. obj1.user.age = 19;
  43. console.log(obj2);//{arr: ['ref', 'green', 'blue'],bool: true,date: Thu Jan 01 1970 08:00:00 GMT+0800 (中国标准时间) ,nul: null,num: 2021,reg: /\/123\/g/,school: "实验小学",str: "jue",user: {name: 'yichuan', age: 18}}

参考学习

  • 《如何写出一个惊艳面试官的深拷贝?》
  • 《JavaScript基本数据类型和引用数据类型》
  • 《Javascript核心原理精讲》

    写在最后

    其实在实际开发和使用过程中,很多人对于深拷贝的细节问题理解并不是很透彻,如果能够更深层次的研究细节,你就会发现此部分内容对于了解更深层次js的底层原理有所帮助。这篇文章是作为对数据类型、数据类型的检测、数据类型强制和隐藏转换、深浅拷贝的简要总结,希望对大家有所帮助。