一、数据类型概况
1.1 原始类型
原始类型:undefined,null,boolean,string,number,symbol
原始类型是没有函数可以调用的,比如 undefined.toString()。

但是很多人就会有疑问为什么 '1'.toString() 可以转换,其实这种情况 '1' 已经被强制转换了,调用的是 String 类型的 toString 方法,其实 String 类型是对象类型。
其实原始类型在 JS 中还有很多坑,比如 0.1 + 0.2 !== 0.3,这个是因为 JS 中 number 类型是浮点类型的,所以会出现这样的 bug。
还有一个问题,就是 null 本身是原始类型,为什么使用 typeof null 的时候,判断出来的是 object 呢?因为 JS 最初的版本使用的 32 位系统,考虑性能问题,使用了低位存储变量,000 开头表示对象,null 表示为全零,所以错误的判断成了 object。
1.2 对象类型
JS 中除了原始类型,其他的都是对象类型。
原始类型存储的是值,但是对象类型存储的是地址(指针),当我们创建一个对象时,JS 就会在内容寸分配一个空间来存放值。
const test = [];
当我们定义个数组,赋值给 test 的时候,内存会给数组分配一个地址(可以理解为门牌号),暂时定义为 #001,test 变量就指向到这个 #001 的地址。
const a = [];
const b = a;
b.push(1);
这个时候 a 指向 #001 这个地址,b 也指向这个地址,当我们给 b 添加一个元素的时候,其实是给这个地址上添加了值,所以就 a 和 b 两个值其实都发生了变化。
我们再来看一个问题,当我们将一个对象类型当做函数参数传入之后会有什么效果。
function test(o) {
o.age = 18;
o = {
name: 'test2'
};
return o;
}
var obj = {
name: 'test'
};
console.log('外部的对象', obj); // {name: "test"}
const obj2 = test(obj);
console.log('被修改的外部对象', obj); // {name: "test", age: 18}
console.log('新的对象', obj2); // {name: "test2"}
为什么在函数中修改传入的对象类型参数,会将外部引用的变量也改变呢,如果给变量重新赋值,输出的为什么又是另外一个变量,为什么不会对外部的变量进行改变呢?
我们抱着疑惑来看一下为什么,具体原因是:
- 当我们给函数传参时,其实传递的是对象的地址副本。
- 在函数中修改地址副本,所以引用这个地址的变量值都会改变。
- 当我们重新给变量进行赋值时,其实将这个变量引用到了一个新的地址上。
二、类型判断
我们在想判断变量类型时,可以使用 typeof 来判断数据类型,但是判断出来的类型有的时候不尽如人意。
如:
typeof undefined; //undefined
typeof Symbol(); // 'symbol'
typeof 123; //number
typeof '123'; //string
typeof true; //boolean
typeof [1, 2, 3]; //object
typeof { id: 11 }; //object
typeof null; //object
typeof console.log; //function
使用 typeof 判断对象类型时,除了 function 以外,其他的判断出来的结果都是 object,并不是我们想要的。
既然使用 typeof 判断对象类型时的结果不尽如人意,那么我们就有了另外的方法去判断对象类型。
使用操作符 instanceof,instanceof 是通过原型链来判断。
function Person() {}
const person = new Person();
person instanceof Person; // true
var str = 'hello world';
str instanceof String; // false
var str1 = new String('hello world');
str1 instanceof String; // true
使用 instanceof 虽然可以通过原型链去判断对象类型,但是在判断原始类型的时候会有问题,如果不是通过构造函数 new XX() 创建的,就会判断失败。
我们可以自己写一个用 instanceof 判断原始类型的方法:
class PrimitiveString {
static [Symbol.hasInstance](x) {
return typeof x === 'string';
}
}
console.log('hello world' instanceof PrimitiveString); // true
你可能不知道 Symbol.hasInstance 是什么东西,其实就是一个能让我们自定义 instanceof 行为的东西,以上代码等同于 typeof 'hello world' === 'string',所以结果自然是 true 了。
instanceof 经过我们自己的改造可以判断原始类型之后,其实判断函数也会有问题。
const a = function() {};
a instanceof Function; //true
a instanceof Object; //true
在判断函数时,就会发生上面的结果。因为 function 的实例类型既是 Function,也是 Object。
所以我们就要想另外一种方法了,可以使用 Object.prototype.toString.call() 获取一个对象的类型,通过借用 Objcet 原型链上的 toString() 方法,将对象或者常规的变量进行字符串格式化,然后判断类型。
var toString = Object.prototype.toString;
toString.call(new Date()); // [object Date]
toString.call(new String()); // [object String]
toString.call(Math); // [object Math]
toString.call(/s/); // [object RegExp]
toString.call([]); // [object Array]
toString.call(undefined); // [object Undefined]
toString.call(null); // [object Null]
三、类型转换
js 中的数据类型可以进行转换,转换的方式也有很多,但是大体上主要是分为显式转换和隐式转换。
3.1 显式转换
-
Number 方法
- 数字:转换后还是数字
- 字符串:如果可以被解析为数值,则为相应的数值,如果不能,则是 NaN,如果是空字符串那就是 0
- 布尔值:true 为 1,false 为 0
- undefined:NaN
- null:0 6.对象:先执行 object 原型上的 valueOf 方法,看是否能转换,如果不可以再执行原型链上的 toString,看是否可以转换,如果不可以报错
-
String 方法
- 数字:转换成对应的字符串
- 字符串:还是对应的字符串
- 布尔值:true 为'true',false 为'false'
- undefined:undefined
- null:null
- 对象:先执行 toString 方法,看是否能转换,如果不可以再执行 valueOf,看是否可以转换,如果不可以报错
在对 object 类型进行转换时,我们会调用 toString 和 valueOf 方法,这个时候我们也可以自己重写 toString 和 valueOf 方法。
-
Boolean 方法
NaN,null,undefined,0,-0,"",false 都会转换成 false,其他的都会被转换成 true。
3.2 隐式转换
在使用四则运算和比较运算符时都会进行隐式转换。
四则运算:
加法运算符不同于其他几个运算符,它有以下几个特点:
- 如果两个值都是 number 类型,那就直接相加
- 如果两个值有一方是字符串,那就将另外一方也转换成字符串
- 如果一方不是字符串或者数字(对象,数组),那么会将它们转换为数字或者字符串,在转换的过程中会调用 valueOf 和 toString 方法,对于+号,是先调用 valueOf,然后 toString。
常规的操作:
1 + '1'; // '11'
true + true; // 2
4 + [1, 2, 3]; // "41,2,3"
我们再来一些比较奇葩的转换:
[] + [] // "",将空数组转换成了 ""
'a' + + 'b' // aNaN,会先看 + 'b' 这个表达式,转换为 NaN,所以 'a' + NaN 就是aNaN
{} + [] // 0,这个时候 {} 在 +号之前,会被看做是一个代码块,最终结果是直接转换 [] 为 0
[] + {} // "[object Object]",[] 转换为 0,{} 调用对象的 toString 方法,转换成字符串,因为一方是字符串,所以 [] 进行了两次转换,[] -> 0 -> ''。
{} + {} // "[object Object][object Object]"
true + true // 2
1 + {a:1} // "1[object Object]"
除了加法运算以外,其他的运算,只要有一放是数字,那么另一方就会被转换成数字。
4 * '3'; // 12
1 - '1'; // 0
4 * []; // 0
4 * [1, 2]; // NaN
比较运算符:
- 如果是对象,就通过 valueOf 和 toString 转换对象
- 如果是字符串,就通过 unicode 字符索引来比较
let a = {
valueOf() {
return 0;
},
toString() {
return '1';
}
};
a > -1; // true
a 是对象,所以先通过 valueOf 转换成原始值再进行比较。
参考文献:
- 掘金小册——前端面试之道
其他心法目录:
后续会将自己总结的内功心法大全慢慢放出来,其中包括 JS 作用域,执行上下文,this 指向问题,原型链,闭包...,供自己和他人学习。
- JavaScript 内功心法——词法作用域及动态作用域
- JavaScript 内功心法——变量提升及函数提升
- JavaScript 内功心法——执行上下文
- JavaScript 内功心法——作用域链
- JavaScript 内功心法——EventLoop
- JavaScript 内功心法——原型与原型链
阅读完后两部曲
- 喜欢的小伙伴点个赞吧,感觉对身边人有帮助的,麻烦动动手指,分享一下。非常感谢各位花时间阅读完,同时很感谢各位的点赞和分享。
- 希望各位关注一下我的公众号吧,新的文章第一时间发到公众号,公众号主要发一些个人随笔、读书笔记、还有一些技术热点和实时热点,并且还有非常吸引人的我个人自费抽奖活动哦~
