一、JS数据类型
基本数据类型
引用数据类型
存储方式
- 基础数据类型存储在栈内存,被引用或者拷贝时,会创建一个完全相等的变量。
- 引用类型存储在堆内存,存储的是地址,多个引用指向同一个地址。
这段代码打印出来,为:30,24。因为 function 传入的是内存地址,而 return 会创建一个新的内存地址。(没有return,b的值会是undefined)
b: { name: ‘Kath’, age: 30}; a: {name: ‘Julia’, age: 24}
数据类型检测
typeof
null的数据类型并非object,判断方法:null === null
instanceof
当new一个新对象时,这个新对象的原型链( prototype
)继承它new的对象。通过 instanceof 我们可以判断这个对象是不是构造函数生成的对象。
手动实现 instanceof
两种判断数据类型的差异
- typeof 可以判断基础数据类型(null 除外),不能判断除了 function 以外的引用数据类型。
- instanceof 可以判断复杂数据引用类型(通过构造函数的 new),但不能判断基础数据类型。
Object.prototype.toString
推荐使用
除 object({})以外,其它都需要加上 call。
toString 方法返回具体数据类型function getType(obj){
let type = typeof obj;
if (type !== "object") { // 先进行typeof判断,如果是基础数据类型,直接返回
return type;
}
// 对于typeof返回结果是object的,再进行如下的判断,正则返回结果
return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1'); // 注意正则中间有个空格
}
/* 代码验证,需要注意大小写,哪些是typeof判断,哪些是toString判断?思考下 */
getType([]) // "Array" typeof []是object,因此toString返回
getType('123') // "string" typeof 直接返回
getType(window) // "Window" toString返回
getType(null) // "Null"首字母大写,typeof null是object,需toString来判断
getType(undefined) // "undefined" typeof 直接返回
getType() // "undefined" typeof 直接返回
getType(function(){}) // "function" typeof能判断,因此首字母小写
getType(/123/g) //"RegExp" toString返回
数据类型转换
'123' == 123 // false or true?
'' == null // false or true?
'' == 0 // false or true?
[] == 0 // false or true?
[] == '' // false or true?
[] == ![] // false or true?
null == undefined // false or true?
Number(null) // 返回什么?0
Number('') // 返回什么?0
parseInt(''); // 返回什么?NaN
parseInt(null); // NaN
{}+10 // 返回什么?10
let obj = {
[Symbol.toPrimitive]() {
return 200;
},
valueOf() {
return 300;
},
toString() {
return 'Hello';
}
}
console.log(obj + 200); // 这里打印出来是多少?400
强制类型转换
Number()
布尔—true和false分别被转换成 1 和 0
- 数字—返回自身(不仅仅是十进制)
- null—返回 0
- undefined—返回 NaN
- 字符串—如果字符串中只包含数字,则将其转换成十进制
- 如果字符串中包含有效的浮点格式,将其转换成浮点数值
- 空字符串,转换成 0
- 不是以上格式的字符串,均返回 NaN
- Symbol—抛出错误
- 对象—如果部署了[Symbol.toPrimitive],那么调用此方法,否则调用对象的 valueOf() 方法
parseInt()
parseFloat()
toString()
String()
Boolean()
运算符
- 仅当‘+’两边都是数字时,进行加法运算。如果两边都是字符串,则直接拼接,不进行隐式转换。
- 如果其中一个是字符串,另一个是 undefined、null或者布尔,则调用 toString() 方法进行字符串拼接;如果是纯对象、数组、正则等,则默认调用对象的转换方法会存在优先级,再进行拼接。
- 如果其中一个是数字,另一个是undefined、null、布尔或者数字,则会将其转换成数字进行加法运算,对象情况参考上一条。
如果一个是字符串,一个是数字,则按照字符串规则进行拼接。
1 + 2 // 3 常规情况
'1' + '2' // '12' 常规情况
// 下面看一下特殊情况
'1' + undefined // "1undefined" 规则1,undefined转换字符串
'1' + null // "1null" 规则1,null转换字符串
'1' + true // "1true" 规则1,true转换字符串
'1' + 1n // '11' 比较特殊字符串和BigInt相加,BigInt转换为字符串
1 + undefined // NaN 规则2,undefined转换数字相加NaN
1 + null // 1 规则2,null转换为0
1 + true // 2 规则2,true转换为1,二者相加为2
1 + 1n // 错误 不能把BigInt和Number类型直接混合相加
'1' + 3 // '13' 规则3,字符串拼接
object的转换规则
- 如果部署了 Symbol.toPrimitive 方法,优先调用再返回;
- 调用 valueOf(),如果转换为基础类型,则返回;
- 调用 toString(),如果转换为基础类型,则返回;
var obj = {
value: 1,
valueOf() {
return 2;
},
toString() {
return '3'
},
[Symbol.toPrimitive]() {
return 4
}
}
console.log(obj + 1); // 输出5
// 因为有Symbol.toPrimitive,就优先执行这个;如果Symbol.toPrimitive这段代码删掉,则执行valueOf打印结果为3;如果valueOf也去掉,则调用toString返回'31'(字符串拼接)
// 再看两个特殊的case:
10 + {}
// "10[object Object]",注意:{}会默认调用valueOf是{},不是基础类型继续转换,调用toString,返回结果"[object Object]",于是和10进行'+'运算,按照字符串拼接规则来,参考'+'的规则
[1,2,undefined,4,5] + 10
// "1,2,,4,510",注意[1,2,undefined,4,5]会默认先调用valueOf结果还是这个数组,不是基础数据类型继续转换,也还是调用toString,返回"1,2,,4,5",然后再和10进行运算,还是按照字符串拼接规则,参考'+'的第3条规则
-
- *
- /
关系操作符
- >
- <
- <=
=
相等运算符
==
- 如果类型相同,无需进行类型转换
- 如果有一方为 null 或者 undefined,另一个也必须为 null 或 undefined,否则返回 false。
- 其中一个为 Symbol 类型,返回 false
- 两个操作值为 string 或 number 类型,那么会把字符串转换成 number
- 如果一个操作值是 boolean,那么转换成 number(
true
转换成 1,false
转换成 0) - 如果一个操作值是 object,且另一方为 string、number 或者 symbol,就会把 object 转换成原始类型再判断。
if/while 条件null == undefined // true 规则2
null == 0 // false 规则2
'' == null // false 规则2
'' == 0 // true 规则4 字符串转隐式转换成Number之后再对比
'123' == 123 // true 规则4 字符串转隐式转换成Number之后再对比
0 == false // true e规则 布尔型隐式转换成Number之后再对比
1 == true // true e规则 布尔型隐式转换成Number之后再对比
var a = {
value: 0,
valueOf: function() {
this.value++;
return this.value
}
};
// 注意这里a又可以等于1、2、3
console.log(a == 1 && a == 2 && a ==3); //true f规则 Object隐式转换
// 注:但是执行过3遍之后,再重新执行a==3或之前的数字就是false,因为value已经加上去了,这里需要注意一下
深浅拷贝
浅拷贝
原理:自己创建一个新对象来重新赋值或者引用原来的对象值,如果对象属性是基本数据类型,则是复制的值给新对象,如果属性是引用数据类型,则复制的是内存中的地址(这会导致只真正拷贝了第一层的数据)。
方法一: object.assign(目标对象,源对象1,源对象2、、、)
不会拷贝对象的继承属性
- 不会拷贝对象的不可枚举属性(enumerable = false)
- 可拷贝 Symbol 类型的属性
方法二:扩展运算符方式...
- 对象和数组皆可用,数组使用和 arr.slice() 相同的效果。
方法三:concat 拷贝数组
- arr2 = arr1.concat
手动实现浅拷贝:
const shallowClone = (target) => {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? []: {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = target[prop];
}
}
return cloneTarget;
} else {
return target;
}
}
总结:浅拷贝只会拷贝一层对象(改变第一层对象不会改变源对象),而赋值操作只是将指针改变,引用的仍然是同一个对象,所以赋值操作的对象第一层改变值源对象也会改变。
方法四:slice 拷贝数组
不会影响和改变原始数组,arr.slice(begin, end)
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr); //[ 1, 2, { val: 1000 } ]
深拷贝
原理:将一个对象从内存中完整的拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。
方法一:JSON.stringify使用方法:先用 JSON.stringify(obj) 将对象转换成 JSON 字符串,再用 JSON.parse() 将之转换成 对象。
- 注意:
- 拷贝的对象中的值如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失。
- 拷贝 Date 引用类型会变成字符串
- 无法拷贝不可枚举的属性(enumerable为false)
- 无法拷贝对象的原型链
- 拷贝 RegExp 引用类型会变成空对象
{}
- 对象中含有 NaN、Infiniy 以及 -Infinity,JSON 序列化结果会变成 null
- 无法拷贝对象的循环应用,即对象成环(obj[key] = obj)
方法二:基础版(手写递归实现)
let obj1 = {
a:{
b:1
}
}
function deepClone(obj) {
let cloneObj = {}
for(let key in obj) { //遍历
if(obj[key] && typeof obj[key] ==='object') {
cloneObj[key] = deepClone(obj[key]) //是对象就再次调用该函数递归
} else {
cloneObj[key] = obj[key] //基本类型的话直接复制值
}
}
return cloneObj
}
let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2); // {a:{b:1}}
注意:
- 这个深拷贝函数并不能复制不可枚举的属性以及 Symbol 类型;
- 这种方法只是针对普通的引用类型的值做递归复制,而对于 Array、Date、RegExp、Error、Function 这样的引用类型并不能正确地拷贝;
- 对象的属性里面成环,即循环引用没有解决。
方法三:改进版(改进后递归实现)
- 针对能够遍历对象的不可枚举属性以及 Symbol 类型,可以使用 Reflect.ownKeys 方法。(Reflect 详见 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect)
- 当参数为 Date,RegExp 类型,则直接生成一个新的实例返回。
- 利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性,顺便结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链。
- 利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏,作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap 存储的值。 ```javascript const isComplexDataType = obj => (typeof obj === ‘object’ || typeof obj === ‘function’) && (obj !== null) const deepClone = function (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) let allDesc = Object.getOwnPropertyDescriptors(obj) //遍历传入参数所有键的特性 let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc) //继承原型链 hash.set(obj, cloneObj) for (let key of Reflect.ownKeys(obj)) { cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== ‘function’) ? deepClone(obj[key], hash) : obj[key] } return cloneObj } // 下面是验证代码 let obj = { num: 0, str: ‘’, boolean: true, unf: undefined, nul: null, obj: { name: ‘我是一个对象’, id: 1 }, arr: [0, 1, 2], func: function () { console.log(‘我是一个函数’) }, date: new Date(0), reg: new RegExp(‘/我是一个正则/ig’),
}; Object.defineProperty(obj, ‘innumerable’, { enumerable: false, value: ‘不可枚举属性’ } ); obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj)) obj.loop = obj // 设置loop成循环引用的属性 let cloneObj = deepClone(obj) cloneObj.arr.push(4) console.log(‘obj’, obj) console.log(‘cloneObj’, cloneObj) ```