前言:js中的数据类型我一开始以为挺简单的,后来发现好多坑。。我已经载到上面很多次了。今天就吐血整理一下关于数据方面的。比如说隐式类型转换、==的问题以及数据劫持的问题。话不多说,正式开始吧。
js之数据
1、基本数据类型和引用数据类型
首先数据类型有基本数据类型和引用数据类型之分,前者有Number、String、Undefined、布尔、Null还有es6中的Symbol。
引用数据类型也就是对象,有Function、Array、基本包装类型、Date等等。
判断数据类型的几种方法
(1) typeof操作符
typeof操作符返回的类型只有基本类型和Object
typeof 1; //"number"
typeof []; //"object"
(2)instanceof
instanceof可以用来进一步判断是哪种引用类型
[] instanceof Array; //true
[] instanceof Object; //true
[] instanceof Function; //false
(3)Object.prototype.toString.call()
还可以用Object.prototype.toString.call()来判断对象的类型
Object.prototype.toString.call([]); //"[object Array]"
Object.prototype.toString.call("123"); //"[object String]"
(4)constructor
[].constructor==Array; //true
[].constructor==Object; //false
基本数据类型和引用类型的区别
(1)基本数据类型存放在栈中,数据大小确定,内存空间大小可以分配,它们是直接按值存放的,所以可以直接按值访问;
引用类型是存放在堆内存中的对象,变量其实是保存的在栈内存中的一个指针(保存的是堆内存中的引用地址),这个指针指向堆内存,每个空间大小不一样,要根据情况进行特定的配置。
(2)动态的属性。引用类型可以为变量动态添加属性,基本类型不可以。
(3)复制和传参的区别。如果从一个变量向另一个变量复制基本类型值,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上;当从一个变量向另一个变量复制引用类型的值时,同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上引用同一个对象。
深浅拷贝问题
由于数据类型的区别,在这里涉及到深浅拷贝的问题。深浅拷贝是对于对象来说的。浅拷贝只是拷贝了属性的一层;而深拷贝是层层递归的拷贝。
这里抛开基本类型不谈,先开看一下引用类型的赋值、浅拷贝和深拷贝
var obj={
name:'xx',
age:20,
friends:['xs','se','ty']
}
// 浅拷贝(注意浅拷贝不是赋值)
function shallowCopy(obj){
var newObj={};
for(props in obj){
newObj[props]=obj[props];
}
return newObj;
}
// 深拷贝
function deepCopy(obj){
var newObj= obj instanceof Array===true ? []:{};
for(props in obj){
if(typeof obj[props] === 'object'){
newObj[props]=deepCopy(obj[props]);
}else{
newObj[props]=obj[props];
}
}
return newObj;
}
//然后我们对对象进行拷贝
//1、赋值操作
var obj1=obj;
obj1.name="hh";
console,log(obj.name);//'hh',此时的操作是赋值,实际上是把obj的地址传给了obj1,二者指向一个对象,修改obj
也修改了obj1
//2.浅拷贝
var obj2=shallowCopy(obj);
//2.1 修改拷贝对象的基本类型值
obj2.name="nn";
console.log(obj.name) ; //"hh"
console.log(obj2.name); //"nn" 此时obj2的name被修改并没有影响到obj
//2.2 修改拷贝对象的引用类型值
obj2.friends.push('ws');
console.log(obj.friends); // ["xs", "se", "ty", "ws"]
console.log(obj1.friends);// ["xs", "se", "ty", "ws"]
console.log(obj2.friends);// ["xs", "se", "ty", "ws"] 此时我们注意到修改拷贝对象中的引用类型值仍然会引起原对象的改变,是因为它们仍然是指向内存中的同一个地址,也是我们所说的只拷贝了一层
//3. 深拷贝
var obj3=deepCopy(obj);
obj3.friends.push("wewe");
console.log(obj3.friends);//["xs", "se", "ty", "ws", "wewe"]
console.log(obj.friends);//["xs", "se", "ty", "ws"] 此时改变拷贝对象中引用类型的值也不会引起原对象的改变,因为它被层层递归深度拷贝了。
2、==问题和隐式类型转换
首先来几道问题
// 1、==问题 (此处有点坑)
[]=={}; //false
[]==''; //true
''=={}; //false
//2、运算符(+ - )的隐式类型转换问题 (此处也是坑)
//2.1 基本数据类型的+ -
'20'+19; //'2019'
19+'20'; //'1920'
19+ +'20'; //39
19+ -'20'; //-1
'20'-19; //1
+'20s'+19; //NaN
// 2.2 引用数据类型的+ -
{} + {} // "[object Object][object Object]"
[1,2,3] + [] // "1,2,3" + ""
[] + {} // "" + "[object Object]"
{} + [] // 0 ---> {}被当做一个块,相当于执行 ({},+[]),返回值为小括号最后面的表达式的返回值
{q:1} + [] // 0
+{q:1}+[] //NaN
// 3. 关于!的问题 (此处真的是神坑)
[] == 0 // true
![] == 0 // true
[] == ![] // true
[] == [] // false 比较地址
{} == {} // false 比较地址
{} == !{} // false
看起来是不是很疑惑,其实这中间涉及到的就是隐式类型转换之中的toString以及valueof以及Number。
首先我们要明确不同的操作符引起的类型转换到底用到了哪些隐式类型转换
2.1 ==操作符
(1)如果==两边有一个操作符为布尔值,比较相等之前先将其转换为数值,false转换成0,true转换成1
(2)如果操作数有一个是字符串,另一个是数值,比较相等之前将字符串转换成数值
(3)当一个对象和一个基本类型进行比较的时候,首先调用对象的valueOf()方法,基本包装类型会返回对应的基本数据类型。但是对于对象和数组返回的都是本身,这时候调用的是对象的toString方法。
(4)如果两个操作数都是对象,则比较它们的引用地址是否指向同一个对象
(5)null和undefined是相等的
(6)要比较相等之前,不能将null和undefined转换成其他任何值
【注意这里所有的类型转成数值都是用的Number()方法,而不是parseInt()或者parseFloat()】
来看一下对象的toString方法会将对象转换成什么
[].toString(); //''
({}).toString(); // '[object Object]' 加()的原因是浏览器会将{}解析成代码块,需要包裹一个()
因此一开始提出的问题也就不难解释,我们来回顾一下与==相等的几个柿子:
[]=={}; //false ,两个对象比较引用地址的值,显然不一样
[]==''; //true ,有一边是对象,调用toString将[]转换成'',此时二者相等
''=={}; //false,有一边是对象,调用toString将[]转换成 '[object Object]' ,此时二者不相等
[] == 0 // true,调用toString将[]转换成'',然后将空字符串转成数值0,
[] == [] // false 此处这样理解,==两边都是对象,则比较引用地址
{} == {} // false 比较引用地址
此外注意几个与Undefined和null相关的,因为这两个不遵守规则,,还要注意NaN与任何数都不相等,与自身也不相等。
null==0; //false
undefined==0;//false
undefined==null;//true
NaN==NaN; //false
2.2 + - 操作符
+ -既可以作为单目操作符也可以作为双目操作符。
2.2.1 一元操作符
一元操作符就是对一个操作数进行操作。
自增和自减
除了数值可以进行自增和自减,其他类型可以用Number()将其转换成数值再进行++或–的操作,举几个栗子叭
//1 字符串
var str="123";
++str; //124,是因为Number将其转换成数值123
var str1="123s";
++str1; //NaN
// 2 数组
var arr=[];
++arr; //1, 是因为[]首先被valueof转换成自身,然后被toString转换成空字符串,然后Number将其转换成数值0
var arr1=[1,2,3];
++arr1; // NaN
//3 对象
var obj={name:1};
++obj; //NaN
var obj1={
valueOf:function(){
return 1;
}
}
++obj1; //2 ,此处重写了对象的valueOf方法,返回数值1
一元+和一元-
一元+和一元-*也都可以作为单目操作符,他们会引起js的隐式数据类型转换,规则和前面的一样,也是将非数值操作数使用Number()方法将其转换成为数值。再来看几个栗子叭
+'1'; //1
+'12w'; //NaN
+[1]; //1
+[1,2]; //NaN
-[1]; //-1
+{}; //NaN
var obj={
valueOf:function(){
return 2;
}
}
-obj; //-2
2.2.2 + - 双目操作符
+操作符
(1)当+两边都为数值时,直接相加
(2)当+的两边有一边是字符串的时候,会将其另一边也转换成字符串拼接起来
(3)当+两边的类型有一边或者两边为对象、数值、布尔,调用他们的toString()方法将其转换成字符串再拼接。
【这一段摘自红宝书,但是这里经过试验我认为首先将对象用valueOf取得原始值,如果是基本类型,则可以用+的规则,如果是对象,再调用对象的toString()方法将其转换成字符串,不如我们来看几个栗子】
(4)undefined和null转换成字符串undefined和null
//栗子1:
var a={
valueOf(){
return 1;
}
}
var b={
valueOf(){
return 2;
}
}
console.log(a+b); // 3,这里证明了进行+运算的时候首先调用的valueOf
//栗子2:
var c={
toString(){
return 1;
}
}
var d={
toString(){
return 2;
}
}
console.log(c+d); //3 ,这里先调用了对象的valueOf返回对象本身,然后调用返回的对象本身身上的toString
//栗子3:
var d={
valueOf(){
return 1;
}
}
var e={
valueOf(){
return d;
}
}
console.log(d+e); //1[object Object] ,这里首先调用valueOf,d对象返回基本类型可以直接操作,而e对象返回对象d,不可以直接操作,再调用e对象的toString转换成字符串[object Object]再进行拼接
//进一步探究这个例子
var e={
valueOf(){
return d;
},
toString(){
return '123';
}
}
console.log(d+e); // '1123'
//栗子4:
var a={
valueOf(){
return 1;
},
toString(){
return '你猜对了吗a';
}
}
var b={
valueOf(){
return a;
},
toString(){
return '你猜对了吗b';
}
}
console.log(b+a); //你猜对了吗b1
console.log(a+b); // 1你猜对了吗b
这几个栗子证明了首先取得某对象valueOf的返回值,如果是基本类型则使用该值,如果是引用类型,则再取得某对象的toString值。
-操作符
(1)当-两遍都为数值数,执行减法操作
(2)当其中有一个操作数为字符串、布尔、null和undefined,则调用Number()方法将其转换成数值,再进行减法操作
(3)如果一个操作数是对象,则先用valueOf再用toString再转换字符串为数值。
了解了+和-的运算规则,然后我们再来看前面的几道题:
'20'+19; //'2019' ,+的一边是字符串,将其转换成字符串拼接
19+'20'; //'1920', +的一边是字符串,将其转换成字符串拼接
19+ +'20'; //39, 单目+的优先级高于双目+,所以先对单目+'20'进行类转换成20,然后再数值相加
19+ -'20'; //-1 ,同上,先对单目-'20'进行类转换成-20,然后再数值相加
'20'-19; //1 ,-的两边有一边的字符串,将其转换成数值20,再相减
+'20s'+19; //NaN,先计算+'20s'得到NaN
{} + {} // "[object Object][object Object]",+的两边或一边是对象,调用toString转换成字符串拼接
[1,2,3] + [] // "1,2,3" + ""
[] + {} // "[object Object]"
{} + [] // 0 ---> {}被当做一个块,相当于执行 ({},+[]),返回值为小括号最后面的表达式的返回值
{q:1} + [] // 0
+{q:1}+[] //NaN
//补充一下undefined的操作
'123'+undefined; //"123undefined"
'123'-undefined; //NaN
'21'-null; // 21
2.3 ! 操作符
!取非操作符也是一个涉及到隐式类型转换的比较典型的操作符,实际上* 、/、>等等都会涉及到,但是我今天真的写不动了,讲几个比较典型的吧。所以!是我写的最后一个操作符了。
采用!关系运算符的时候,先用Boolean()将其转换成布尔类型
(1)以下几种情况转换成Boolean类型会返回false
0 、- 0、NaN、undefined、null、''、false
然后对其取非,就会返回true
(2)除了这几种情况,比如引用类型、+0,非空字符串、非0数值等等都会返回true。然后对其取非就返回false
然后接下来我们再来看前面的题目
![] == 0 // true 主要是对引用类型取非都会将引用类型用Boolean()转换成true,然后取非就是false。
[] == ![] // true 此处可以这样理解,![]的级别高于==,所以先进行取非运算得到false,然后[]==false为true(这个是因为会将[] 用toString转换成空字符串,然后将布尔值转换成0.将空字符串也转换为数值0,所以是相等的)
{} == !{} // false ,根据上面对[]==![]的理解,首先将右边的!{}转换成false,问题变成了{}==false,此时仍然是用toString对象转换,得到[object Object],此时二者显然不相等。
到此为止对隐式类型的转换是不是就掌握了呢。当然还需要回头看一下Number()方法的转换规则什么的。这里我先贴出来,当做再复习理顺一下。
2.4 Number()方法
(1)如果是Boolean 值,true 和false 将分别被转换为1 和0。
(2)如果是数字值,只是简单的传入和返回。
(3)null返回0,undefined返回NaN
(4)空字符串返回0,纯数字字符串返回数值,否则转换成NaN
(5)则调用对象的valueOf()方法,然后依照前面的规则转换返回的值。如果转换的结果是NaN,则调用对象的toString()方法,然后再次依照前面的规则转换返回的字符串值。
3、数据劫持等相关问题
首先来上一道经典的面试题
3.1 经典题目1
var a=?;
a==1&&a==2&&a==3; //true
求将a补充完整
(注意到此时用var声明了a,重写对象的属性显然不能使用),不过注意到此时用的还是==,结合我们上面所提到的,所以可以采用重写对象的valueOf方法。
var a={
count:1,
valueOf(){
return this.count++;
}
}
a==1&&a==2&&a==3; //true
3.2 经典题目2
“如何实现 a === 1 && a === 2 && a === 3 的结果为true”
注意到此时用的是全等,不会进行隐式类型转换,所以另辟蹊径采用数据劫持
可以用几种方法来做,可以采用原生的Object.defineProperty,也可以采用es6的proxy
【该方法一定注意不要用var来声明a,因为var声明之后对象的可读写属性等默认为false不能再被更改】
var count=1;
Object.defineProperty(window, 'a', {
get(){
return count++;
}
});
a===1&&a==2&a===3; //true
3.3 经典题目3
我们知道{}==’'返回的是false,如何让它返回true呢
这个问题还是采用的==,我们此时还是仍然采用隐式类型转换。
var obj = {
valueOf() { return '' }
};
console.log(obj == ''); // true
当然也可以采用数据劫持
obj={};
Object.defineProperty(window, 'obj', {
get(){
return '';
}
});
console.log(obj == ''); // true
两种方法不同的是,后者只是在涉及隐式类型转换的时候(也就是调用valueOf的时候)才会返回空字符串,正常打印的时候还是打印的空对象,而前者只要是访问对象的时候就会返回空字符串。