正如你所知道的,数据类型是作为js的入门知识点,在整个js的学习过程中也是尤为重要的。数据类型看起来简单,但是围绕着其衍生的边界数据类型判断问题、深拷贝浅拷贝问题对于新手而言是难以理解的。
一、数据类型
JavaScript 是一种弱类型或者说动态类型,这就意味着你不需要提前声明变量的类型,在程序运行的过程中,类型会被自动确定。这就意味着你可以使用同一个变量保存不同类型的数据.
js内存分为栈内存(stack)和堆内存(heap)
- 栈内存:是一种特殊的线性表,它具有后进先出的特性,存放基本类型。
- 堆内存:存放引用类型(在栈内存中存一个基本类型值保存对象在堆内存中的地址,用于引用这个对象)。
数据类型根据存储方式分为两类:
- 基本数据类型(简单数据类型、原始数据类型):值存储在栈内存中,被引用或拷贝时,会创建一个完全相等的变量。占用空间小、大小固定,通过按值来访问,属于被频繁使用的数据。
- 引用数据类型(复杂数据类型):地址存储在栈内存中,值存在了堆内存中,多个引用会指向同一个地址。占据空间大、占用内存不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
根据上面的标准划分数据类型,常见的有:
- 基本数据类型:String、Number、Boolean、Undefined、Null、Symbol、BigInt
- 复杂数据类型:Object、Array、Date、Function、RegExp等
未命名文件 (1).png
二、数据类型的检测
通常的数据类型的检测有三种方法:
- typeof
- instanceof
2.1 typeof
使用typeof进行基础数据类型(null除外)检测,但是对于引用数据类型,除了function外,其它的均无法进行判断。
- typeof "yichuan"; //"string"
- typeof 18; //"number"
- typeof undefined; //undefined
- typeof true; //boolean
- typeof Symbol(); //"symbol"
- typeof null; //"object"
- typeof []; //"object"
- typeof {}; //"object"
- typeof console; //"object"
- typeof console.log; //"function"
2.2 instanceof
使用instanceof是通过原型链进行查找,可以准确地判断复杂引用数据类型,但是不能准确判断基础数据类型。
- 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
2.3 Object.prototype.toString.call()
Object.prototype.toString方法返回对象的类型字符串,因此可用来判断一个值的类型。因为实例对象有可能会自定义toString方法,会覆盖Object.prototype.toString,所以在使用时,最好加上call。所有的数据类型都可以使用此方法进行检测,且非常精准。
- Object.prototype.toString.call("yichuan");//["object String"]
- Object.prototype.toString.call(18);//["object Number"]
- Object.prototype.toString.call(true);//["object Boolean"]
- Object.prototype.toString.call(null);//["object Null"]
- Object.prototype.toString.call(new Symbol());//["object Symbol"]
- Object.prototype.toString.call({});//["object Object"]
- Object.prototype.toString.call([]);//["object Array"]
- Object.prototype.toString.call(/123/g);//["object RegExp"]
- Object.prototype.toString.call(function(){});//["object Function"]
- Object.prototype.toString.call(new Date());//["object Date"]
- Object.prototype.toString.call(document);//["object HTMLDocument"]
- Object.prototype.toString.call(window);//["object Window"]
我们可以看到此输出的结果都是["object Xxxx"]首字母大写。
2.4 通用的数据类型判断方法
- function getType(obj){
- //先判断输入的数据判断返回结果是否为object
- if(typeof obj !== "object"){
- return typeof obj;
- }
- // 对于typeof返回object的,再进行具体的判断,使用正则返回结果,切记正则中间有个空格哦
- return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/,"$1");
- }
切记:
- 使用typeof返回的类型是小写
- 使用toString返回的类型是大写
- getType("yichuna");//"string"
- getType(18);//"number"
- getType(true);//"boolean"
- getType(undefined);//"undefined"
- getType();//"undefined"
- getType(null);//"Null"
- getType({});//"Object"
- getType([]);//"Array"
- getType(function(){});//"Function"
- getType(new Date());//"Date"
- 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转为原始数据类型判断
小试牛刀:
- null == undefined; //true
- null == 0;//false
- "" == null;//false
- "" == 0;//true 会转为number类型再进行判断
- "123" == 123;//true
- 0 == false;//true
- 1 == true;//true
3.5 隐式类型转换+
"+"号操作符,不仅可以用于数字相加,还可以用于字符串拼接。
- 如果其中一个是字符串,另外一个是number、undefined、null或boolean,则调用toString()方法进行字符串拼接
- 如果是纯字符串、数组、正则等,则默认调用对象的转换方法会存在优先级,然后进行拼接
- 如果字符串和bigInt进行相加,会先将bigInt转为字符串
- 如果number类型与undefined相加,则得到NaN
- 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
3.6 object的转换规则
- 如果部署了Symbol.toPrimitive方法,优先调用再返回
- 调用valueOf(),如果转换为基础类型则返回
- 调用toString(),如果转换为基础数据类型则返回
- 如果都没有返回基础数据类型,则会报错
四、深拷贝和浅拷贝
在js的编程中经常需要进行数据进行复制,那么什么时候使用深拷贝、什么时候使用浅拷贝呢,是开发过程中需要思考的?如何提升自己手写js的能力,以及对一些边界情况的深入思考能力呢?
有两个重要问题:
- 拷贝一个很多嵌套的对象要如何实现呢?
- 深拷贝写成什么程度才能让面试官满意呢?
4.1 浅拷贝的原理和实现
自己创建一个新的对象,来接受要重新复制或引用的对象值。
- 如果对象属性是基本数据类型,复制的就是基本数据类型的值给新对象;
- 如果对象属性是引用数据类型,赋值的则是内存中的地址,如果其中一个对象改变了这个内存中的地址,肯定会影响另外一个对象
4.1.1 Object.assign
Object.assign是es6中object的一个方法,该方法可以用于js对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。
- Object.assign(target,...sources);//target目标对象,sources待拷贝的对象
注意:
- Object.assign不会拷贝对象的继承属性
- Object.assign不会拷贝对象的不可枚举属性
例如:
- let obj = {};
- let obj1 = {
- name:"yichuan",
- scores:{
- math:100,
- Chinese:100
- }
- };
- Object.assign(obj,obj1);
- console.log(obj);//{name:"yichuan",scores:{math:100,Chinese:100}}
改变目标对象的值:我们可以看到下面改变了目标对象的值,会引起待拷贝对象的值的改变。
- let obj = {};
- let obj1 = {
- name:"yichuan",
- scores:{
- math:100,
- Chinese:100
- }
- };
- Object.assign(obj,obj1);
- console.log(obj);//{name:"yichuan",scores:{math:100,Chinese:90}}
- obj.scores.Chinese = 10;
- console.log(obj);//{name:"yichuan",scores:{math:100,Chinese:90}}
- console.log(obj1);//{name:"yichuan",scores:{math:100,Chinese:90}}
不可拷贝不可枚举属性
- let obj1 = {
- user:{
- name:"yichuan",
- age:18
- },
- idCard:Symbol(1)
- };
- Object.defineProperty(obj1,"innumerable",{
- value:"不可枚举属性",
- enumerable:false
- });
- let obj2 = {};
- Object.assign(obj2,obj1);
- obj1.user.name = "onechuan";
- console.log("obj1",obj1);//{user: {…}, idCard: Symbol(1), innumerable: '不可枚举属性'}
- console.log("obj2",obj2);//{user: {…}, idCard: Symbol(1)} 我们可以看到并没有innumerable属性
4.1.2 展开运算符
- /* 对象的拷贝 */
- let obj1 = {
- user:{
- name:"yichuan",
- age:18
- },
- school:"实验小学"
- };
- let obj2 = {...obj1};
- obj2.school = "五道口男子技校";
- console.log(obj1);//{school: "实验小学",user: {name: 'yichuan', age: 18}}
- obj2.user.age = 19;
- console.log(obj2);//{school: "实验小学",user: {name: 'yichuan', age: 19}}
- /* 数组的拷贝 */
- let arr = ["red","green","blue"];
- let newArr = [...arr];
- console.log(arr);//['red', 'green', 'blue']
- console.log(newArr);//['red', 'green', 'blue']
4.1.3 concat拷贝数组
数组的concat方法其实也是浅拷贝
- let arr = ["red","green","blue"];
- let newArr = arr.concat();
- newArr[1] = "black";
- console.log(arr);//["red","green","blue"];
- console.log(newArr);//["red","black","blue"];
4.1.4 slice拷贝数组
slice方法仅针对数组类型,arr.slice(begin,end);
- let arr = ["red","green","blue"];
- let newArr = arr.slice();
- newArr[1] = "black";
- console.log(arr);//["red","green","blue"];
- console.log(newArr);//["red","black","blue"];
4.1.5 手写浅拷贝
- 对基本数据类型进行最基本的拷贝
- 对引用数据类型开辟新的存储,并且拷贝一层对象属性
- function shallowClone(target){
- //先要判断是否为对象数据类型
- if(typeof target === "object" && target !== null){
- //判断输入的是object类型还是数组类型
- const cloneTarget = Array.isArray(target) ?[]:{};
- //遍历目标对象元素
- for(let prop in target){
- //判断cloneTarget对象上是否有此属性,没有进行拷贝
- if(!cloneTarget.hasOwnProperty(prop)){
- cloneTarget[prop] = target[prop]
- }
- }
- return cloneTarget;
- }
- return target;
- }
4.2 深拷贝的原理和实现
前面我们知道浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值。对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有对象完全复制过来存放。
深拷贝就是将一个对象从内存中完整地拷贝出来给目标对象,并在堆内存中开辟新的空间进行存储新对象的值,且新对象的值改变不会影响原对象,也就是实现了二者的隔离。
4.2.1 JSON.stringify()
其实在实际开发过程使用最简单的深拷贝就是使用JSON.stringify()配合JSON.parse()。但其实是有缺陷的,不影响简单使用。注意:
- let obj1 = {
- user:{
- name:"yichuan",
- age:18
- },
- school:"实验小学"
- };
- let obj2 = JSON.parse(JSON.stringify(obj1));
- console.log(obj1);//{school: "实验小学",user: {name: 'yichuan', age: 18}}
- console.log(obj2);//{school: "实验小学",user: {name: 'yichuan', age: 18}}
- obj2.school = "门头沟学员";
- obj2.user.age = 19;
- console.log(obj1);//{school: "实验小学",user: {name: 'yichuan', age: 18}}
- console.log(obj2);//{school: "门头沟学院",user: {name: 'yichuan', age: 19}}
4.2.2 简易手写深拷贝
作为简易版手写深拷贝,只能完成基础的拷贝功能,也存在一些缺陷:
- 不能拷贝不可枚举的属性以及symbol类型
- 只能针对普通的引用类型的值做递归复制
- 对象的属性里面成环,即循环引用没有得到妥善解决
- function deepClone(obj){
- const cloneObj = {};
- //遍历对象键名
- for(let key in obj){
- //判断是否为对象类型
- if(typeof obj[key]==="object"){
- //是对象就再次调用函数进行递归拷贝
- cloneObj[key] = deepClone(obj[key]);
- }else{
- //是基本数据类型的话,就直接进行复制值
- 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}}
4.2.3 优化版手写深拷贝
对于上面简易版的深拷贝,很显然面试官是不买账的,为此我们针对递归进行升级处理。
- 针对能够遍历对象的不可枚举属性以及Symbol类型,我们可以使用Reflect.ownKeys方法
- 当参数为Date、RegExp类型,则直接生成一个新的实例返回
- 利用Object的getOwnPropertyDescriptors方法可以获得对象的所有属性,以及对应的特性,顺便结合Object.create()方法创建新对象,并继承传入原对象的原型链
- 利用WeakMap类型作为Hash表,因为WeakMap是弱引用类型,可以有效防止内存泄漏,作为检测循环引用有很大的帮助。如果存在循环,则引用直接返回WeakMap存储的值
- const isComplexDataType = (obj) => (typeof obj === 'object' || typeof obj === 'function') && obj !== null;
- function deepClone(obj, hash = new WeakMap()) {
- //判断是否为日期类型
- if (obj.constructor === Date) return new Date(obj);
- //判断是否正则对象
- if (obj.constructor === RegExp) return new RegExp(obj);
- //如果循环引用了,就使用weakMap进行解决
- if (hash.has(obj)) return hash.get(obj);
- const allDesc = Object.getOwnPropertyDescriptors(obj);
- //遍历传入参数所有键的特性
- const cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc);
- //继承原型链
- hash.set(obj, cloneObj);
- for (const key of Reflect.ownKeys(obj)) {
- cloneObj[key] = isComplexDataType(obj[key]) && typeof obj[key] !== 'function' ? deepClone(obj[key]) : obj[key];
- }
- return cloneObj;
- }
- const obj1 = {
- num: 2021,
- str: 'jue',
- bool: true,
- nul: null,
- arr: ['ref', 'green', 'blue'],
- date: new Date(0),
- reg: new RegExp('/123/g'),
- user: {
- name: 'yichuan',
- age: 18
- },
- school: '实验小学'
- };
- const obj2 = deepClone(obj1);
- obj1.user.age = 19;
- 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}}
5参考学习
《如何写出一个惊艳面试官的深拷贝?》
《JavaScript基本数据类型和引用数据类型》
《Javascript核心原理精讲》
6写在最后
其实在实际开发和使用过程中,很多人对于深拷贝的细节问题理解并不是很透彻,如果能够更深层次的研究细节,你就会发现此部分内容对于了解更深层次js的底层原理有所帮助。这篇文章是作为对数据类型、数据类型的检测、数据类型强制和隐藏转换、深浅拷贝的简要总结,希望对大家有所帮助。