前言
本文是学习数据类型时做的知识整理笔记,发现了很多知识漏洞。
在 JavaScript 编程中,我们经常会遇到边界数据类型条件判断问题,很多代码只有在某种特定的数据类型下,才能可靠地执行。
尤其在大厂面试中,经常需要手写代码,因此很有必要提前考虑好数据类型的边界判断问题,并在你的 JavaScript 逻辑编写前进行前置判断,这样才能让面试官看到你严谨的编程逻辑和深入思考的能力,面试才可以加分。
因此本文将从数据类型的 概念、存储方式、检测方法、转换方法 几个方面,梳理并学习JavaScript 的数据类型的知识点。
数据类型概念
JavaScript 的数据类型有以下8种:
- 7 种基本数据类型(原始类型)
-
Undefined
- 用于被声明但未被赋值的值 -
Null
- 用于未知的值,代表“无”,“空”,“值未知” -
Boolean
- 用于true
和false
-
String
- 用于字符串:一个字符串可包含 0 个或多个字符,所以没有单独的单字符类型(C中的char
) -
Number
- 用于任何类型的数字:整数或浮点数,在±(2^53-1)
范围内的整数 -
Symbol
- 用于创建对象的唯一的标识符 -
BigInt
- 用于任意长度的整数
-
- 1种 复杂数据类型(引用类型)
-
Object
- 数据和功能的集合,能够存储多个值作为属性。-
Array
- 数组对象 -
RegExp
- 正则对象 -
Date
- 日期对象 -
Math
- 数学对象 -
Function
- 函数对象
-
-
存储方式
因为各种 JavaScript 的数据类型最后都会在初始化之后放在不同的内存中,因此上面的数据类型可以分成两类来存储:
-
基础类型 —— 存储在 栈内存
被引用或拷贝时,会创建一个完全相等的变量。
-
引用类型 —— 存储在 堆内存
存储的是地址,多个引用指向同一个地址,这里会涉及一个 “共享” 的概念。
为了理解 “共享” 的概念,我们来看两个例子:
例一:
let a = {
name: 'bnn',
age: 3
}
let b = a;
console.log(a.name);
b.name = 'jj';
console.log(a.name);
console.log(b.name);
答案:
console.log(a.name); // 'bnn'
console.log(a.name); // 'jj'
console.log(b.name); // 'jj'
在执行 b.name = 'jj'
后,为什么 a 和 b 的 name 属性都是 jj
了呢?
原因就是引用类型的 "共享",a 和 b 的引用指向了同一个地址,一个发生了改变,另一个也随之变化。
例二:
let a = {
name: 'jay',
age: 5
}
function change(o){
o.age = 10;
o = {
name: 'jj',
age: 3
}
return o;
}
let b = change(a);
console.log(b);
console.log(b.age);
console.log(a.age);
答案:
console.log(b); // {name: 'jj',age:'3'}
console.log(b.age); // 3
console.log(a.age); // 10
为什么 b 是 {name: 'jj',age:'3'}
,a.age
变成了 10 ?
因为函数传入了 a 对象,通过 o.age = 10
修改了 a 对象的 age 属性。
随后又将 o 变成了另一个地址 ,不再是传入的那个 a ,并返回,因此最后b的值就成了{name: 'jj',age:'3'}
。
而如果没有 return o
,b会是 undefined
。
数据类型检测
数据类型的判断方法有很多,下面重点来说三种经常遇到的数据类型检测方法。
typeof
下面通过代码来看一下typeof
:
typeof undefined // "undefined"
typeof null // "object"
typeof "0" // "string"
typeof 0 // "number"
typeof 10n // "bigint"
typeof true // "boolean"
typeof Symbol() // "symbol"
typeof [] // "object"
typeof {} // "object"
typeof console // "object"
typeof console.log // "function"
我们可以发现:
typeof
会以字符串的形式返回数据类型。-
typeof null
为什么是'object'
?这是 JavaScript 早期的一个 bug,并为了兼容性而保留下来了。
null
绝不是一个object
,它有自己的类型。如果需要判断
null
,直接通过=== null
即可。 typeof
不能判断function
以外的引用数据类型,除了function
其余都是object
。
instanceof
instanceof
操作符用于检测 构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
通过代码来看一下 instanceof
:
function Person() {};
let jj = new Person();
jj instanceof Person // true
class Fruit {}
let banana = new Fruit();
banana instanceof Fruit // true
在下面的代码中,为什么 instanceof
会返回 true
?a
明明不是通过 B()
创建的。
function A() {}
function B() {}
A.prototype = B.prototype = {};
let a = new A();
a instanceof B // true
a 的确不是通过 B()
创建的。
但是 instanceof
并不关心函数,而是关心函数与原型链匹配的 prototype
。
这里 a.__proto__ === A.prototype === B.prototype
。
所以 a instanceof B
返回 true
。
总之,对于 instanceof
来说,真正决定类型的是 prototype
,而不是构造函数。
instanceof
原理
在 ES6 中,instanceof
操作符会使用 Symbol.hasInstance
函数来确定关系。
以 Symbol. hasInstance
为键的函数会执行同样的操作,只是操作数对调了一下:
function Foo() {}
let f = new Foo();
console.log(Foo[Symbol.hasInstance](f)) // true
通常,instanceof
在检查中会将原型链考虑在内。此外,我们还可以在静态方法 Symbol.hasInstance
中设置自定义逻辑。
obj instanceof Class
算法的执行过程大致如下:
-
如果这有静态方法
symbol.hasInstance
,那就直接调用这个方法:例如:
// 设置 instanceOf 检查 // 假设具有 notLearn 属性的都不是 person,具有 learn 属性的都是 person class Person { static [Symbol.hasInstance](obj) { if (obj.notLearn) return false; if (obj.learn) return true; } } let sb = { notLearn: true }; sb instanceof Person; // false let me = { learn: true}; me instanceof Person; //true
-
大多数 class 没有
Symbol.hasInstance
。在这种情况下,标准逻辑是:使用obj instanceOf Class
检查Class.prototype
是否等于obj
的原型链中的原型之一。也就是说,一个一个地比较:
obj.__proto__ === Class.prototype? obj.__proto__.__proto__ === Class.prototype? obj.__proto__.__proto__.__proto__ === Class.prototype? ... // 如果任意一个答案为 true,则返回 true // 否则,如果检查到了原型链的尾端还是没有 true ,则返回 false
这里还要提到一个方法 objA.isPrototypeOf(objB)
如果 objA
处在 objB
的原型链中,则返回 true
所以,可以将 obj instanceof Class
检查改为 Class.prototype.isPrototypeOf(obj)
。
[] instanceof Object // true
Object.prototype.isPrototypeOf([]) // true
注意:Class
的 constructor
自身是不参与检查的!检查过程只和原型链以及 Class.prototype
有关。
Object.isPrototypeOf([]) // false
手写 instanceof
那么,如何自己实现一个 instanceof
呢?
function myInstanceof(left,right) {
// right 得是个构造函数,不能是箭头函数或实例对象。
if (typeof right !== 'function'){
throw new TypeError('Right-hand side of \'instanceof\' is not callable')
}
// left不能是基本类型。记得考虑 typeof null 这个特殊情况。
if(typeof left !== 'object'|| left === null) return false;
// Object.getPrototypeOf() 方法返回指定对象的原型
let proto = Object.getPrototypeOf(left);
while(true){ // 循环在原型链往下找
console.log(proto)
if(proto === null) return false;
// 找到相同的原型对象,返回 true
if (proto === right.prototype) return true;
// 没找到就继续往下一个原型找
proto = Object.getPrototypeOf(proto)
console.log(proto)
}
}
myInstanceof(new Number(1),Number); // true
myInstanceof(1,Number); // false
和 typeof
的区别
总结下面两点:
-
instanceof
可以准确判断复杂数据类型,但是不能正确判断基本数据类型。`` -
typeof
可以判断基本数据类型(null 除外),但是引用数据类型中,除了 function 类型以外,其他的也无法判断。
总之,不管单独用 typeof
还是 instanceof
,都不能满足所有场景的需求,而只能通过二者混写的方式来判断。而下面的第三种方法就能更好地解决数据类型检测问题。
Object.prototype.toString
toString()
是 Object 的原型方法,调用该方法,可以统一返回格式为"[object Xxx]"
的字符串,其中 Xxx 就是对象的类型。
对于 Object 对象,直接调用 toString()
就能返回 "[object Object]"
;而对于其他对象,则需要通过 call
来调用,才能返回正确的类型信息。
我们来看一下代码:
Object.prototype.toString({}) // "[object Object]"
Object.prototype.toString.call({}) // 同上结果,加上call也ok
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('1') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g) //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([]) //"[object Array]"
Object.prototype.toString.call(document) //"[object HTMLDocument]"
Object.prototype.toString.call(window) //"[object Window]"
可以看出,Object.prototype.toString.call()
可以很好地判断引用类型,甚至可以把 document 和 window 都区分开来。
但是在写判断条件的时候一定要注意,使用这个方法最后返回统一字符串格式为 "[object Xxx]"
,而这里字符串里面的 "Xxx" ,因为是类,是构造函数,第一个首字母要大写(注意:使用 typeof
返回的是小写)。区分函数和类的方法就是函数小写,类要大写
终极方法
那么如何实现一个全局通用的数据类型判断方法呢?
function getType(obj) {
//先进行 typeof 判断,如果是基本数据类型,直接返回
let type = typeof obj;
if (type !== 'object'){
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 返回
getType(Object) // "function" typeof 能判断,因此首字母小写
数据类型转换
我们经常会遇到 JavaScript 数据类型转换问题,有的时候需要我们 主动进行强制转换,而有的时候 JavaScript 会进行隐式转换,隐式转换的时候就需要我们多加留心。
我们先看一段代码:
'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) // 返回什么?
Number('') // 返回什么?
parseInt(''); // 返回什么?
{}+10 // 返回什么?
let obj = {
[Symbol.toPrimitive]() {
return 200;
},
valueOf() {
return 300;
},
toString() {
return 'Hello';
}
}
console.log(obj + 200); // 这里打印出来是多少?
上面这 12 个问题,就是在做数据类型转换时经常会遇到的 强制转换 和 隐式转换 的方式。
常用的类型转换
在学习强制转换和隐式转换之前,我们先来梳理一下四种常用的类型转换:
字符串转换
转换发生在 输出内容 的时候,也可以通过 String(value)
进行显式转换。原始类型值的string
类型转换通常是很明显的。
数字型转换
转换发生在进行 算数函数 和 表达式 时,也可以通过 Number(value)
进行显式转换。
数字型转换遵循以下 规则 :
值 | 变成 |
---|---|
undefined |
NaN |
null |
0 |
true 和 false
|
1 和 0 |
string |
去掉首尾空格后的数字字符串中含有的数字(包括0x开头的十六进制字符串)。如果剩余字符串为空,返回 0。如果不是以上格式的字符串,返回 NaN 。 |
布尔型转换
转换发生在进行 逻辑操作 时,也可以通过 Boolean(value)
进行显式转换。
布尔型转换遵循以下 规则:
值 | 变成 |
---|---|
0, null , undefined , NaN , ""
|
false |
其他值 | true |
对象 — 原始值转换
所有的对象在布尔上下文(context)中 均为 true
。所以对于对象,不存在 to-boolean 转换,只有字符串和数值转换。
对象转换的规则,会先调用内置的 [ToPrimitive]
函数,其 规则逻辑 如下:
如果部署了
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'
(字符串拼接)。
再看几个例子:
10 + {} // "10[object Object]"
{}
默认调用 valueOf
,是{}
,不是基础类型,继续转换;
调用 toString
,返回结果"[object Object]"
于是和10 进行+
运算,按照字符串拼接规则来。
{} + 10 // 10
因此我们发现,当对象作为操作数时,解释器总是优先调用 valueOf()
;而其他情况解释器总是认为我们想要的是字符串,所以会优先调用 toString()
因此,对象在前面,返回结果就是Number
;其他情况对象默认用 toString
。
[1,2,undefined,4,5] + 10 // "1,2,,4,510"
[1,2,undefined,4,5]
会默认先调用 valueOf
,结果还是这个数组,不是基础数据类型,继续转换;
调用toString
,返回"1,2,,4,5"
,然后再和10进行运算,还是按照字符串拼接规则。
下面我们来看一下强制类型转换和隐式转换:
强制类型转换
强制类型转换方式包括
Number()
parseInt()
parseFloat()
toString()
String()
Boolean()
这几种方法都是可以对数据类型进行强制转换的方法。具体如何转换要结合方法的规则和上面的转换规则。
比如在上面12个问题中:
Number(null)
的结果是 0,Number('')
的结果同样是 0,是因为用到了 Number()
进行强制转换。
parseInt('')
的结果是 NaN
,是因为用到了parseInt()
进行强制转换。
隐式转换
凡是通过逻辑运算符 (&&
、||
、 !
)、运算符 (+
、-
、*
、/
)、关系操作符 (>
、 <
、 <=
、>=
)、相等运算符 (==
) 或者 if
/while
条件的操作,如果遇到 两个数据类型不一样 的情况,都会出现 隐式类型转换 。
下面来看一下日常用得比较多的几个符号的隐式转换规则。
+
-
一元运算符
加号
+
应用于单个值,对数字没有任何作用。但是 如果运算元不是数字,加号
+
则会将其转化为数字。它的效果和
Number(...)
相同。负号运算符,是反转符号的一元运算符。
注意:一元运算符优先级高于二元运算符。
+'' // 0 +null // 0 +undefined // NaN +true // 1 +false // 0 -'' // -0 -null // -0 -undefined // NaN -true // -1 -false // -0
-
二元运算符
两边是数字,进行加法运算。
两边都是字符串,字符串拼接。
只要任意一个运算元是字符串,那么另一个运算元也将被转化为字符串。
一个是数字,另外一个是
undefined
、null
、布尔型或数字,则会将其转换成数字进行加法运算。-
二元
+
是唯一一个以上述方式支持字符串的运算符。其他算术运算符只对数字起作用,并且总是将其运算元转换为数字。1 + 2 // 3 '1' + '2' // '12' '1' + undefined // "1undefined" 规则3,undefined转换字符串 '1' + null // "1null" 规则3,null转换字符串 '1' + true // "1true" 规则3,true转换字符串 "" + 1 + 0 // "10" 规则3,首先将数字 1 转换为一个字符串:"" + 1 = "1",然后得到 "1" + 0,再次应用同样的规则得到最终的结果。 " -9 " + 5 // " -9 5" 规则3,带字符串的加法会将数字 5 加到字符串之后 2 + 2 + '1' // "41" 规则3,运算符是按顺序工作。第一个 + 将两个数字相加,所以返回 4,然后下一个 + 将字符串 1 加入其中,所以就是 4 + '1' = 41。 '1' + 1n // '11' 比较特殊字符串和BigInt相加,BigInt转换为字符串 1 + undefined // NaN 规则4,undefined转换数字相加NaN 1 + null // 1 规则4,null转换为0 1 + true // 2 规则4,true转换为1,二者相加为2 1 + 1n // 错误 不能把BigInt和Number类型直接混合相加 "" - 1 + 0 // "-1" 规则5,减法 - 只能用于数字,它会使空字符串 "" 转换为 0 " -9 " - 5 // -14 规则5,减法始终将字符串转换为数字,因此它会使 " -9 " 转换为数字 -9(忽略了字符串首尾的空格) " \t \n" - 2 = -2 // -2 规则5,减法始终将字符串转换为数字。字符串转换为数字时,会忽略字符串的首尾处的空格字符。在这里,整个字符串由空格字符组成,包括 \t、\n 以及它们之间的“常规”空格。因此,类似于空字符串,所以会变为 0。
整体来看,对于二元+
来说如果运算元中有字符串,隐式转换时更 倾向于转换成字符串 ,因为第三条规则中可以看到,在字符串和数字相加的过程中最后返回的还是字符串。
另外,要注意其他算术运算符只对数字起作用,并且总是将其运算元转换为数字。
==
、!==
当对不同类型的值进行比较时,JavaScript 会首先将其转化为数字
number
,再判定大小。-
null == undefined
null
和undefined
不能 转换为其他类型的值再进行比较。因此 除了它们之间互等外,不会等于任何其他的值!
null
和undefined
好CP,除了对方谁都不爱! -
只要任意一个运算元是
NaN
,则==
返回false
,!==
返回true
。NaN == NaN
返回false
。也就是说
NaN
无法做比较,不等于任何值,包括自己!NaN
谁都不爱,自己都不爱! 如果两个运算元都是对象,则比较他们是不是同一个对象,如果是,则
true
。-
如果一个运算元是对象,另一个操作数不是,则调用对象的
valueOf()
方法取得其原始值,再 根据前面的规则进行比较。false == 0 // true 规则1 false 转数字是0 true == 1 // true 规则1 true 转数字是1 true == 2 // false 规则1 true 转数字是1!true 转数字是1!是1! '' == 0 // true 规则1 空串是0 "5" == 5 // true 规则1 字符串转为数字 null == undefined // true 规则2 null == undefined undefined == 0 // false 规则2 null 和 undefined 都不会转为其他类型值! null == 0 // false 规则2 null 和 undefined 只有它们互等,其他都不等! "NaN" == NaN // false 规则3 NaN 不等于任何值! 5 == NaN // false 规则3 NaN 不等于任何值! NaN == NaN // false 规则3 NaN 不等于任何值!包括自己! NaN != NaN // true 规则3 NaN 谁都不爱!自己都不爱!这句话我已经说累了! var a = { value: 0, valueOf: function() { this.value++; return this.value; } }; // 注意这里a又可以等于1、2、3 console.log(a == 1 && a == 2 && a == 3); //true 规则5 调用对象的 `valueOf()`方法取得其原始值 // 注:但是执行过3遍之后,再重新执行a==3或之前的数字就是false,因为value已经加上去了,这里需要注意一下
<
、>
、 <=
、>=
null
被转化为0
,undefined
被转化为NaN
。-
在比较字符串的大小时,按字符(其实是Unicode 编码)逐个进行比较:
- 首先比较两个字符串的首位字符大小。
- 如果一方字符较大(或较小),则该字符串大于(或小于)另一个字符串。算法结束。
- 否则,如果两个字符串的首位字符相等,则继续取出两个字符串各自的后一位字符进行比较。
- 重复上述步骤进行比较,直到比较完成某字符串的所有字符为止。
- 如果两个字符串的字符同时用完,那么则判定它们相等,否则未结束(还有未比较的字符)的字符串更大。
null > 0 // false 规则1 在做大小与比较时,null被转成0 null >= 0 // true 规则1 在做大小与比较时,null被转成0 null == 0 // false 规则1 普通相等时,null == undefined且不等于其他值。只有在做大小与比较时,null被转成0 undefined > 0 // false 规则2 在做大小与比较时,undefined被转为NaN,NaN 无法作比较 undefined = 0 // false 规则2 在做大小与比较时,undefined被转为NaN,NaN谁都不等 undefined 《 0 // false 规则2 在做大小与比较时,undefined被转为NaN,NaN 无法作比较 "Z" > "A" // 规则2 "Asd" > "Asa" // 规则2 "Aaa" > "Aa" // 规则2
最后
如有问题,请大佬指正~
如有帮助,希望能够点赞收藏~
原文地址:https://juejin.cn/editor/drafts/6919422336584138760
参考资料:
- JavaScript高级程序设计(第4版)
- 现代 JavaScript 教程
- 若离大佬的 JavaScript 核心原理讲解
- 火锅boy的手写instanceof