数据类型
基本类型:共7种,也被称为值类型,是一种既非对象也无方法的数据。包括:string、number、bigint、boolean、null、undefined、symbol。
除了 null 和 undefined之外,所有基本类型都有其对应的包装对象: String 为字符串基本类型。 Number 为数值基本类型。 BigInt 为大整数基本类型。 Boolean 为布尔基本类型。 Symbol 为字面量基本类型。 这个包裹对象的valueOf()方法返回基本类型值。
引用类型:对象(Object)、数组(Array)、函数(Function)、Date、RegExp、基本包装类型(String、Number、Boolean、BigInt、Symbol)以及单体内置对象(Global、Math)
二者区别:
- 基本类型
- 基本类型的值是不可改变的。
- 基本类型的值保存在栈中。
- 基本类型的比较是值的比较。
- 保存与复制的是值本身。
- 使用typeof检测数据的类型。
- 引用类型
- 引用类型可以拥有属性和方法,并且是可以动态改变的.
- 引用类型的值是同时保存在栈内存和堆内存中的对象。
- 引用类型的比较是地址的比较。
- 保存与复制的是指向对象的一个指针。
- 使用instanceof检测数据类型。
对于引用类型的变量,==和===只会判断引用的地址是否相同,而不会判断对象具体里属性以及值是否相同。因此,如果两个变量指向相同的对象,则返回true. 如果是不同的对象,即使包含相同的属性和值,也会返回false 示例:
var arrRef = ["Hi!"];
var arrRef2 = arrRef;
console.log(arrRef === arrRef2); // true
var arr1 = ["Hi!"];
var arr2 = ["Hi!"];
console.log(arr1 === arr2); // false
示例:
var a,b;
a = "zyj";
b = a;
a.toUpperCase();
console.log(a); // zyj
console.log(b); // zyj
a = "呵呵"; // 改变 a 的值,并不影响 b 的值
console.log(a); // 呵呵
console.log(b); // zyj
示例:
var a = {name:"percy"};
var b;
b = a;
a.name = "zyj";
console.log(b.name); // zyj
b.age = 22;
console.log(a.age); // 22
var c = {
name: "zyj",
age: 22
};
类型转换
显式调用Boolean(value)、Number(value)、String(value)完成的类型转换,叫做显示类型转换。
比较操作或者加减乘除四则运算操作时,常常会触发 JavaScript 的隐式类型转换。隐式类型转换时,绝大多数情况下都是优先转为number型。
js内部用于实现类型转换的4个函数是:
- ToPrimitive ( input [ , PreferredType ] ), 转换为原始对象,即基本类型,依赖于valueOf和toString的实现(先valueOf,再toString)
- ToBoolean ( argument ),除了以下(undefined、null、-0/+0、NaN、'')五个值的转换结果为false,其他的值全部为true
- ToNumber ( argument )
- ToString ( argument )
比较运算
JavaScript 为我们提供了严格比较与类型转换比较两种模式,严格比较(===)只会在操作符两侧的操作对象类型一致,并且内容一致时才会返回为 true,否则返回 false。 而更为广泛使用的 == 操作符则会首先将操作对象转化为相同类型,再进行比较。对于 <= 等运算,则会首先转化为原始对象(Primitives),然后再进行对比。
x == y, 算法流程如下:
- x或y中有一个为NaN, 则返回false;
- x或y都为null或undefined中的一种,则返回true(null == undefined),否则返回false(null == 0);
- x或y类型不一致,且为String,Number,Boolean中的一种,则x、y转为Number再比较;
- x或y中有一个为Object,则将其转为原始类型,再进行比较;
优先比类型,再比null与undefined,再比string和number,再比boolean与any,再比object与string、number、symbol;
以上如果转为原始类型比较,则进行类型转换,直到类型相同再比较值的大小。这就是==的隐式转换对比
示例:
[] == ![] //true
1. 基于运算符的优先级,运算![],[]转为boolean后为真值,取反后,变为:[]==false
2. 任何类型与boolean比较,boolean转为number,即 []==0
3. []为对象,转为原始值,ToPrimitive先valueOf返回[],再toString返回''
4. 最后string转number,变为 0==0
加法运算
遇到算数运算符(- 、* 、/ 和 %)的时候会在运算之前将参与运算的双方转换成数字。而加法(+)运算有些特殊,只要其中一个操作数是字符串,那么它就执行连接字符串的操作。
加法(+)的算法如下:
- +号左右分别进行取值,进行ToPrimitive()操作,转为原始值;
- 分别获取左右转换之后的值,如果存在String,则对其进行ToString处理后进行拼接操作;
- 其他的都进行ToNumber处理;
- 在转换时ToPrimitive,除去Date为string外,都按照ToPrimitive 类型为Number进行处理;
示例:
1+'2'+false
1.左边取原始值,依旧是Number
2.中间为String,则都进行toString操作
3.左边转换按照toString的规则,返回'1',得到结果temp值'12'
4.右边布尔值和temp同样进行1步骤
5.temp为string,则布尔值也转为string'false'
6.拼接两者 得到最后结果 '12false'
对象转换
只有在 JavaScript 表达式或语句中需要用到数字或字符串时,对象才被隐式转换。 当需要将对象转换成数字时,需要以下三个步骤:
- 调用 valueOf()。如果结果是原始值(不是一个对象),则将其转换为一个数字。
- 否则,调用 toString() 方法。如果结果是原始值,则将其转换为一个数字。
- 否则,抛出一个类型错误。
示例:
> 3 * { valueOf: function () { return 5 } }
15
类型判断
- 判断数组
- 使用
Array.isArray()
判断数组 - 使用
[] instanceof Array
判断是否在Array的原型链上,即可判断是否为数组 [].constructor === Array
通过其构造函数判断是否为数组- 也可使用
Object.prototype.toString.call([])
判断值是否为[object Array]
来判断数组
- 判断对象
Object.prototype.toString.call({})
结果为[object Object]
则为对象{} instanceof Object
判断是否在Object的原型链上,即可判断是否为对象{}.constructor === Object
通过其构造函数判断是否为对象
- 判断函数
- 使用
typeof function
判断func是否为函数 - 使用
func instanceof Function
判断func是否为函数 - 通过
func.constructor === Function
判断是否为函数 - 也可使用
Object.prototype.toString.call(func)
判断值是否为[object Function]
来判断func
- 判断null
- 最简单的是通过
null===null
来判断是否为null Object.prototype.__proto__===a
判断a是否为原始对象原型的原型,即nulltypeof (a) == 'object' && !a
通过typeof判断null为对象,且对象类型只有null转换为Boolean为false
- 判断NaN
isNaN(any)
直接调用此方法判断是否为非数值
深浅拷贝
- 浅拷贝: 拷贝的是对象的指针,修改内容互相影响
- 深拷贝:整个对象拷贝到另一块内存空间中,修改内容不互相影响
名词 | 和原数据是否指向同一对象 | 第一层数据为基本数据类型 | 原数据中包含子对象 |
---|---|---|---|
赋值 | 是 | 改变会使原数据一同改变 | 改变会使原数据一同改变 |
浅拷贝 | 否 | 改变不会使原数据一同改变 | 改变会使原数据一同改变 |
深拷贝 | 否 | 改变不会使原数据一同改变 | 改变不会使原数据一同改变 |
如下例子:对对象直接复制后,导致原对象值发生改变
let a = {
age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2
解决办法一:
let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1
解决办法二:
let a = {
age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1
解决办法三:
let a = ['ant', 'bison', 'camel', 'duck', 'elephant']
let b = a.slice(1, 5)
console.log(b) // ["bison", "camel", "duck", "elephant"]
浅拷贝只解决了第一层的问题,但是如果遇到嵌套对象,就不行了,就得用深拷贝。
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = { ...a }
a.jobs.first = 'native'
console.log(b.jobs.first) // native
解决办法一:
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
但是JSON.parse(JSON.stringify(object))方法也是有局限性的:
- 会忽略undefined和symbol
- 不能序列化函数
- 不能解决循环引用的对象
解决办法二:自己实现深拷贝函数(考虑了对象、数组、Symbol类型以及多层嵌套)
function deepClone(obj) {
function isObject(o) {
return (typeof o === 'object' || typeof o === 'function') && o !== null
}
if (!isObject(obj)) {
throw new Error('非对象')
}
let isArray = Array.isArray(obj)
let newObj = isArray ? [] : {}
Reflect.ownKeys(obj).forEach(key => {
newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
})
return newObj
}
const e = Symbol("e")
const f = Symbol.for("f")
let obj = {
a: [1, 2, 3],
b: {
c: 2,
d: 3
}
}
obj[e] = 'localSymbol'
obj[f] = 'globalSymbol'
let newObj = deepClone(obj)
newObj.b.c = 1
console.log(newObj) // { a: [ 1, 2, 3 ], b: { c: 2, d: 3 }, [Symbol(e)]: 'localSymbol', [Symbol(f)]: 'globalSymbol' }
console.log(newObj[e] === obj[e]) // true
console.log(obj.b.c) // 2
上述函数的问题是没有考虑循环引用
以及来自原型链上的属性
的拷贝。
let obj = {
a: [1, 2, 3],
b: {
c: 2,
d: 3
}
}
obj.e = obj
let newObj = deepClone(obj)
console.log(newObj.e) // 2
>输出
RangeError: Maximum call stack size exceeded
let childObj = Object.create(obj)
let newObj = deepClone(childObj)
console.log('原对象:')
for(let key in childObj){
console.log(childObj[key])
}
console.log('新对象:')
for(let key in newObj){
console.log(newObj[key])
}
>输出
原对象:
[ 1, 2, 3 ]
{ c: 2, d: 3 }
新对象:
解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系。 当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
这里使用Reflect.ownKeys()
获取所有的键值,同时包括 Symbol
。
for...in
获取当前对象及其原型链上的所有可枚举属性Object.keys
获取当前对象上的所有可枚举属性Object.getOwnPropertyNames
获取当前对象上的所有可枚举和不可枚举属性Object.getOwnPropertySymbols
获取当前对象上所有Symbol属性Reflect.ownKeys
获取当前对象上所有可枚举、不可枚举属性以及Symbol属性Object.getPrototypeOf
获取对象原型链上一级的对象Reflect.getPrototypeOf
获取对象原型链上一级的对象
所有通过Object和Reflect方法获取对象的属性,都无法访问到对象原型链上的属性。
Object.keys
是获取到对象属性的所有方法中范围最小的一种方法Reflect.ownKeys
是获取到对象属性的所有方法中范围最大的一种方法- 此外
Reflect.ownKeys = Object.getOwnPropertyNames + Object.getOwnPropertySymbols
function deepClone(obj, wm = new WeakMap()) {
function isObject(o) {
return (typeof o === 'object' || typeof o === 'function') && o !== null
}
if (!isObject(obj)) {
throw new Error('非对象')
}
if (wm.has(obj)) return wm.get(obj); // 新增代码,查哈希表
let isArray = Array.isArray(obj)
let newObj = isArray ? [] : {}
wm.set(obj, newObj); // 新增代码,哈希表设值
Object.getOwnPropertySymbols(obj).forEach(symKey => {
newObj[symKey] = isObject(obj[symKey]) ? deepClone(obj[symKey], wm) : obj[symKey]
})
//使用for in替换Reflect.ownKeys
for( let key in obj){
newObj[key] = isObject(obj[key]) ? deepClone(obj[key],wm) : obj[key]
}
return newObj
}
测试一下:
const e = Symbol('e')
const f = Symbol.for('f')
const g = Symbol.for('g')
let obj = {
a: [1, 2, 3],
b: {
c: 2,
d: 3,
},
}
obj[e] = 'localSymbol'
obj[f] = 'globalSymbol'
let childObj = Object.create(obj)
childObj[g] = 'globalSymbol_'
let newObj = deepClone(childObj)
console.log('原对象:')
for (let key in childObj) {
console.log(childObj[key])
}
while (childObj) { // 循环
Object.getOwnPropertySymbols(childObj).forEach(symKey => {
console.log(childObj[symKey])
})
childObj = Object.getPrototypeOf(Object(childObj))
}
console.log('新对象:')
for (let key in newObj) {
console.log(newObj[key])
}
while (newObj) { // 循环
Object.getOwnPropertySymbols(newObj).forEach(symKey => {
console.log(newObj[symKey])
})
newObj = Object.getPrototypeOf(Object(newObj))
}
>输出
原对象:
[ 1, 2, 3 ]
{ c: 2, d: 3 }
globalSymbol_
localSymbol
globalSymbol
新对象:
[ 1, 2, 3 ]
{ c: 2, d: 3 }
globalSymbol_
此时,上述函数可以深拷贝当前对象或数组的所有可枚举属性、Symbol类型键,以及该对象原型链上的所有可枚举属性。但仍然有一个问题,就是不能拷贝原型链上的Symbol类型键。
需要使用Object.getPrototypeOf
来循环获取上一级对象的Symbol类型键属性。
function deepClone(obj, wm = new WeakMap()) {
function isObject(o) {
return (typeof o === 'object' || typeof o === 'function') && o !== null
}
if (!isObject(obj)) {
throw new Error('非对象')
}
if (wm.has(obj)) return wm.get(obj) // 新增代码,查哈希表
let isArray = Array.isArray(obj)
let newObj = isArray ? [] : {}
wm.set(obj, newObj) // 新增代码,哈希表设值
while (obj) {
Reflect.ownKeys(obj).forEach(key => {
if(obj.propertyIsEnumerable(key)){
newObj[key] = isObject(obj[key]) ? deepClone(obj[key],wm) : obj[key]
}
})
obj = Object.getPrototypeOf(Object(obj))
}
return newObj
}
至此,该函数可以深拷贝当前对象和它原型链上的所有可枚举属性及Symbol属性,结果大家可以去验证。
总结:
浅拷贝
- Object.assign()
- 扩展运算符 ...
- Array.prototype.slice()/Array.prototype.concat()
深拷贝
- JSON.parse(JSON.stringify())
- lodash的深拷贝函数
参数传递
- 基本类型传值调用(值拷贝)
- 引用类型传共享调用(指针拷贝)
1.关键点是函数传参时,传入的是实参的拷贝,而不是实参本身。所以,基本类型传递的是变量的值的拷贝,而引用类型传递对象的指针的拷贝,其指针也是变量的值。 所以传共享调用也可以说是传值调用。
2.值拷贝后,对值修改,自然不会影响原值。指针拷贝后,与原指针指向的是同一个对象,如果函数内修改对象的属性,刚原对象属性自然也变,但如果直接对指针拷贝赋予新值,即修改它的指向,则不会影响到原指针指向的原对象。
示例:
function changeStuff(a, b, c) {
a = a * 10;
b.item = "changed";
c = {item: "changed"};
}
var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};
changeStuff(num, obj1, obj2);
console.log(num); // 10
console.log(obj1.item); //changed
console.log(obj2.item); //unchanged
可以看到,变量 a 的值就是 num 值的拷贝,变量 b c 分别是 obj1 obj2 的指针的拷贝。 函数的参数其实就是函数作用域内部的变量,函数执行完之后就会销毁。
变量 a 的值的改变,并不会影响变量 num。 而 b 因为和 obj1 是指向同一个对象,所以使用 b.item = "changed"; 修改对象的值,会造成 obj1 的值也随之改变。 由于是对 c 重新赋值了,所以修改 c 的对象的值,并不会影响到 obj2。