正如你所知道的,数据类型是作为js的入门知识点,在整个js的学习过程中也是尤为重要的。数据类型看起来简单,但是围绕着其衍生的边界数据类型判断问题、深拷贝浅拷贝问题对于新手而言是难以理解的。
一、数据类型
JavaScript 是一种弱类型或者说动态类型,这就意味着你不需要提前声明变量的类型,在程序运行的过程中,类型会被自动确定。这就意味着你可以使用同一个变量保存不同类型的数据.
js内存分为栈内存(stack)和堆内存(heap)
- 栈内存:是一种特殊的线性表,它具有后进先出的特性,存放基本类型。
- 堆内存:存放引用类型(在栈内存中存一个基本类型值保存对象在堆内存中的地址,用于引用这个对象)。
数据类型根据存储方式分为两类:
- 基本数据类型(简单数据类型、原始数据类型):值存储在栈内存中,被引用或拷贝时,会创建一个完全相等的变量。占用空间小、大小固定,通过按值来访问,属于被频繁使用的数据。
- 引用数据类型(复杂数据类型):地址存储在栈内存中,值存在了堆内存中,多个引用会指向同一个地址。占据空间大、占用内存不固定。如果存储在栈中,将会影响程序运行的性能; 引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。 当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
根据上面的标准划分数据类型,常见的有:
- 基本数据类型:String、Number、Boolean、Undefined、Null、Symbol、BigInt
- 复杂数据类型:Object、Array、Date、Function、RegExp等
二、数据类型的检测
通常的数据类型的检测有三种方法:
- 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是通过原型链进行查找,可以准确地判断复杂引用数据类型,但是不能准确判断基础数据类型。 ```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
<a name="v7A0o"></a>
### 2.3 Object.prototype.toString.call()
Object.prototype.toString方法返回对象的类型字符串,因此可用来判断一个值的类型。因为实例对象有可能会自定义toString方法,会覆盖Object.prototype.toString,所以在使用时,最好加上call。所有的数据类型都可以使用此方法进行检测,且非常精准。
```javascript
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()
-
3.2 Number()方法的强制转换规则
布尔值 true和false分别被转换为1和0
- 数字 返回本身
- null 返回0
- undefined 返回NaN
- 字符串
- 如果字符串中只包含数字,则将其转换为十进制
- 如果字符串中只包含有有效的浮点格式,将其转换为浮点数值
- 如果是空字符串,将其转换为0
- 如果不是以上格式的字符串,则均返回NaN
-
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 ```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
<a name="EwGK9"></a>
### 3.6 object的转换规则
- 如果部署了[Symbol.toPrimitive]()方法,优先调用再返回
- 调用valueOf(),如果转换为基础类型则返回
- 调用toString(),如果转换为基础数据类型则返回
- 如果都没有返回基础数据类型,则会报错
<a name="YlixH"></a>
## 四、深拷贝和浅拷贝
在js的编程中经常需要进行数据进行复制,那么什么时候使用深拷贝、什么时候使用浅拷贝呢,是开发过程中需要思考的?如何提升自己手写js的能力,以及对一些边界情况的深入思考能力呢?
有两个重要问题:
- 拷贝一个很多嵌套的对象要如何实现呢?
- 深拷贝写成什么程度才能让面试官满意呢?
<a name="segBI"></a>
### 4.1 浅拷贝的原理和实现
自己创建一个新的对象,来接受要重新复制或引用的对象值。
- 如果对象属性是基本数据类型,复制的就是基本数据类型的值给新对象;
- 如果对象属性是引用数据类型,赋值的则是内存中的地址,如果其中一个对象改变了这个内存中的地址,肯定会影响另外一个对象
<a name="wUGFp"></a>
#### 4.1.1 Object.assign
Object.assign是es6中object的一个方法,该方法可以用于js对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。
```javascript
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()。但其实是有缺陷的,不影响简单使用。
注意:
- 拷贝的对象值中如果有函数、undefined、symbol等类型,经过JSON.stringify()序列化后字符串中这几个键值对会消失
- 拷贝Date引用类型会变成字符串
- 无法拷贝不可枚举的属性
- 无法拷贝对象的原型链
- 拷贝RegExp引用类型会变成空对象
对象中含有NaN、Infinity以及-Infinity,JSON序列化的结果是变成了null
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类型
- 只能针对普通的引用类型的值做递归复制
对象的属性里面成环,即循环引用没有得到妥善解决 ```javascript 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}}
<a name="Nl0xl"></a>
#### 4.2.3 优化版手写深拷贝
对于上面简易版的深拷贝,很显然面试官是不买账的,为此我们针对递归进行升级处理。
- 针对能够遍历对象的不可枚举属性以及Symbol类型,我们可以使用Reflect.ownKeys方法
- 当参数为Date、RegExp类型,则直接生成一个新的实例返回
- 利用Object的getOwnPropertyDescriptors方法可以获得对象的所有属性,以及对应的特性,顺便结合Object.create()方法创建新对象,并继承传入原对象的原型链
- 利用WeakMap类型作为Hash表,因为WeakMap是弱引用类型,可以有效防止内存泄漏,作为检测循环引用有很大的帮助。如果存在循环,则引用直接返回WeakMap存储的值
```javascript
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}}
参考学习
- 《如何写出一个惊艳面试官的深拷贝?》
- 《JavaScript基本数据类型和引用数据类型》
- 《Javascript核心原理精讲》
写在最后
其实在实际开发和使用过程中,很多人对于深拷贝的细节问题理解并不是很透彻,如果能够更深层次的研究细节,你就会发现此部分内容对于了解更深层次js的底层原理有所帮助。这篇文章是作为对数据类型、数据类型的检测、数据类型强制和隐藏转换、深浅拷贝的简要总结,希望对大家有所帮助。