面试官:JavaScript的数据类型你了解多少?
前言
作为JavaScript的入门知识点,Js数据类型在整个JavaScript的学习过程中其实尤为重要。最常见的是边界数据类型条件判断问题。
我们将通过这几个方面来了解数据类型:
-
概念 -
检测方法 -
转换方法
概念
undefined
、Null
、Boolean
、String
、Number
、Symbol
、BigInt
为基础类型;
Object
为引用类型,其中包括Array
、RegExp
、Date
、Math
、Function
这几种常见的类型。
数据类型大致分为两类来进行存储。
基础类型存储在栈内存,被引用或拷贝时,会创建一个完全相等的变量。
引用类型存储在堆内存,存储的是地址,多个引用指向同一个地址,这里会涉及一个“共享”的概念。
示例题一:
let a = {
name:"maomin",
age:"20"
}
let b = a;
console.log(a.name); // "maomin"
b.name = "haojie";
console.log(a.name); // "haojie"
console.log(b.name); // "haojie"
这里就体现了引用类型的“共享”的特性,即这两个值都存在同一块内存中共享,一个发生了改变,另外一个也随之跟着变化。
示例题二:
let a = {
name: 'maomin',
age: 20
}
function change(o) {
o.age = 24;
o = {
name: 'haojie',
age: 30
}
return o;
}
let b = change(a);
console.log(b.age); // 30
console.log(a.age); // 24
函数传参进来的 o,传递的是对象在堆中的内存地址值,通过调用 o.age = 24
确实改变了 a 对象的 age 属性;但是代码中{name:'haojie',age:30}
却又把 o 变成了另一个内存地址,将{name: 'haojie', age: 30}
存入其中,最后返回 b 的值就变成了 {name: 'haojie', age: 30}
。
其实,上面两个例子很显明地阐述了在Vue.js组件中data
属性必须是一个函数而不是一个对象,每个实例可以维护一份被返回对象的独立的拷贝。
数据类型检测
第一种检测方法:typeof
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof [] // 'object'
typeof {} // 'object'
typeof console // 'object'
typeof console.log // 'function'
可以看到,前 6 个都是基础数据类型,而为什么第 6 个 null
的typeof
是 object
呢?这里要和你强调一下,虽然 typeof null
会输出 object
,但这只是 JS 存在的一个悠久 Bug,不代表 null
就是引用数据类型,并且 null
本身也不是对象。因此,null
在 typeof
之后返回的是有问题的结果,不能作为判断 null
的方法。如果你需要在 if 语句中判断是否为null
,直接通过 ===null
来判断就好。
此外还要注意,引用数据类型 Object
,用 typeof
来判断的话,除了 function
会正确判断以外,其余都是 object
,是无法判断出来的。
第二种检测方法:instanceof
我们 new 一个对象,那么这个新对象就是它原型链继承上面的对象了,通过 instanceof
我们能判断这个对象是否是之前那个构造函数生成的对象,这样就基本可以判断出这个新对象的数据类型。
let Car = function() {}
let benz = new Car();
benz instanceof Car; // true
let car = new String('maomin');
car instanceof String; // true
let str = 'haojie';
str instanceof String; // false
我们自己可以实现一个 instanceof 的底层实现:
function myInstanceof(target, typeObj) {
// 这里先用typeof来判断基础数据类型,如果是,直接返回false
if(typeof target !== 'object' || target === null) return false;
// getProtypeOf是Object对象自带的API,能够拿到参数的原型对象
let proto = Object.getPrototypeOf(target);
while(true) { //循环往下寻找,直到找到相同的原型对象
if(proto === null) return false;
if(proto === typeObj.prototype) return true;//找到相同原型对象,返回true
proto = Object.getPrototypeof(proto);
}
}
// 验证一下自己实现的myInstanceof
console.log(myInstanceof(new String('maomin'), String)); // true
console.log(myInstanceof('maomin', String)); // false
看到上述代码的实现,我们会总结这两个方法的差异性:
-
instanceof
可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型; -
typeof
也存在弊端,它虽然可以判断基础数据类型(null
除外),但是引用数据类型中,除了function
类型以外,其他的也无法判断。
第三种检测方法: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]"
好,下面我们将实现一个完善的数据类型检测方法。
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返回
数据类型转换
在日常的业务开发中,经常会遇到 JavaScript 数据类型转换问题,有的时候需要我们主动进行强制转换,而有的时候 JavaScript 会进行隐式转换,隐式转换的时候就需要我们多加留心。
'123' == 123 // true
'' == null // false
'' == 0 // true
[] == 0 // true
[] == '' // true
[] == ![] // true
null == undefined // true
Number(null) // 0
Number('') // 0
parseInt(''); // NaN
{}+10 // 10
let obj = {
[Symbol.toPrimitive]() {
return 200;
},
valueOf() {
return 300;
},
toString() {
return 'Hello';
}
}
console.log(obj + 200); // 400
上面的代码相信你并不陌生,基本涵盖了我们平常容易疏漏的一些情况,这就是在做数据类型转换时经常会遇到的强制转换和隐式转换的方式,
强制类型转换
强制类型转换方式包括Number()
、parseInt()
、parseFloat()
、toString()
、String()
、Boolean()
,这几种方法都比较类似,通过字面意思可以很容易理解,都是通过自身的方法来进行数据类型的强制转换。
Number() 方法的强制转换规则
-
如果是布尔值,
true
和false
分别被转换为 1 和 0; -
如果是数字,返回自身;
-
如果是
null
,返回0
; -
如果是
undefined
,返回NaN
; -
如果是字符串,遵循以下规则:如果字符串中只包含数字(或者是
0X / 0x
开头的十六进制数字字符串,允许包含正负号),则将其转换为十进制;如果字符串中包含有效的浮点格式,将其转换为浮点数值;如果是空字符串,将其转换为 0;如果不是以上格式的字符串,均返回NaN
; -
如果是
Symbol
,抛出错误; -
如果是对象,并且部署了
[Symbol.toPrimitive]
,那么调用此方法,否则调用对象的valueOf()
方法,然后依据前面的规则转换返回的值;如果转换的结果是NaN
,则调用对象的toString()
方法,再次依照前面的顺序转换返回对应的值。
Number(true); // 1
Number(false); // 0
Number('0111'); //111
Number(null); //0
Number(''); //0
Number('1a'); //NaN
Number(-0X11); //-17
Number('0X11') //17
其中,上面分别列举了比较常见的 Number
转换的例子,它们都会把对应的非数字类型转换成数字类型,而有一些实在无法转换成数字的,最后只能输出NaN
的结果。
parseInt() 方法的强制转换规则
考虑到用 Number()
函数转换字符串时相对复杂且有点反常规,通常在需要得到整数时可以优先使用 parseInt()
函数。parseInt()
函数更专注于字符串是否包含数值模式。字符串最前面的空格会被忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号,parseInt()
立即返回 NaN
。这意味着空字符串也会返回 NaN
(这一点跟 Number()
不一样,它返回 0)。如果第一个字符是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。比如,"1234blue"
会被转换为 1234
,因为"blue"
会被完全忽略。类似地,"22.5"
会被转换为22
,因为小数点不是有效的整数字符。
假设字符串中的第一个字符是数值字符,parseInt()
函数也能识别不同的整数格式(十进制、八进制、十六进制)。换句话说,如果字符串以"0x
"开头,就会被解释为十六进制整数。如果字符串以"0
"开头,且紧跟着数值字符,在非严格模式下会被某些实现解释为八进制整数。
let num1 = parseInt("1234blue"); // 1234
let num2 = parseInt(""); // NaN
let num3 = parseInt("0xA"); // 10,解释为十六进制整数
let num4 = parseInt(22.5); // 22
let num5 = parseInt("70"); // 70,解释为十进制值
let num6 = parseInt("0xf"); // 15,解释为十六进制整数
不同的数值格式很容易混淆,因此 parseInt()也接收第二个参数,用于指定底数(进制数)。如果知道要解析的值是十六进制,那么可以传入 16 作为第二个参数,以便正确解析:
let num = parseInt("0xAF", 16); // 175
事实上,如果提供了十六进制参数,那么字符串前面的"0x
"可以省掉:
let num1 = parseInt("AF", 16); // 175
let num2 = parseInt("AF"); // NaN
在这个例子中,第一个转换是正确的,而第二个转换失败了。区别在于第一次传入了进制数作为参数,告诉 parseInt()
要解析的是一个十六进制字符串。而第二个转换检测到第一个字符就是非数值字符,随即自动停止并返回 NaN
。
通过第二个参数,可以极大扩展转换后获得的结果类型。比如:
let num1 = parseInt("10", 2); // 2,按二进制解析
let num2 = parseInt("10", 8); // 8,按八进制解析
let num3 = parseInt("10", 10); // 10,按十进制解析
let num4 = parseInt("10", 16); // 16,按十六进制解析
因为不传底数参数相当于让 parseInt()
自己决定如何解析,所以为避免解析出错,建议始终传给它第二个参数。多数情况下解析的应该都是十进制数,此时第二个参数就要传入 10。
parseFloat() 方法的强制转换规则
parseFloat()
函数的工作方式跟parseInt()
函数类似,都是从位置 0 开始检测每个字符。同样,它也是解析到字符串末尾或者解析到一个无效的浮点数值字符为止。这意味着第一次出现的小数点是有效的,但第二次出现的小数点就无效了,此时字符串的剩余字符都会被忽略。因此,"22.34.5"
将转换成 22.34
。
parseFloat()
函数的另一个不同之处在于,它始终忽略字符串开头的零。十六进制数值始终会返回 0。因为parseFloat()
只解析十进制值,因此不能指定底数。最后,如果字符串表示整数(没有小数点或者小数点后面只有一个零),则 parseFloat()
返回整数。下面是几个示例:
let a = parseFloat(true); // NaN
let num1 = parseFloat("1234blue"); // 1234,按整数解析
let num2 = parseFloat("0xA"); // 0
let num3 = parseFloat("22.5"); // 22.5
let num4 = parseFloat("22.34.5"); // 22.34
let num5 = parseFloat("0908.5"); // 908.5
let num6 = parseFloat("3.125e7"); // 31250000
Boolean() 方法的强制转换规则
除了undefined
、null
、 false
、 ''
、0(包括 +0,-0)
、NaN
转换出来是false
,其他都是 true
。
Boolean(0) //false
Boolean(null) //false
Boolean(undefined) //false
Boolean(NaN) //false
Boolean(1) //true
Boolean(13) //true
Boolean('12') //true
toString() 方法的强制转换规则
这个方法唯一的用途就是返回当前值的字符串等价物。比如:
let age = 11;
let ageAsString = age.toString(); // 字符串"11"
let found = true;
let foundAsString = found.toString(); // 字符串"true"
toString()
方法可见于数值、布尔值、对象和字符串值。(没错,字符串值也有 toString()
方法,该方法只是简单地返回自身的一个副本。)null
和 undefined
值没有toString()
方法。
多数情况下,toString()
不接收任何参数。不过,在对数值调用这个方法时,toString()
可以接收一个底数参数,即以什么底数来输出数值的字符串表示。默认情况下,toString()返回数值的十
进制字符串表示。而通过传入参数,可以得到数值的二进制、八进制、十六进制,或者其他任何有效基
数的字符串表示,比如:
let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"
这个例子展示了传入底数参数时,toString()输出的字符串值也会随之改变。数值 10 可以输出为
任意数值格式。注意,默认情况下(不传参数)的输出与传入参数 10 得到的结果相同。
String() 方法的强制转换规则
如果你不确定一个值是不是 null
或 undefined
,可以使用 String()
转型函数,它始终会返回表示相应类型值的字符串。String()函数遵循如下规则。
-
如果值有
toString()
方法,则调用该方法(不传参数)并返回结果。 -
如果值是
null
,返回"null"
。 -
如果值是
undefined
,返回"undefined"
。
下面看几个例子:
let value1 = 10;
let value2 = true;
let value3 = null;
let value4;
console.log(String(value1)); // "10"
console.log(String(value2)); // "true"
console.log(String(value3)); // "null"
console.log(String(value4)); // "undefined"
这里展示了将 4 个值转换为字符串的情况:一个数值、一个布尔值、一个 null 和一个 undefined。数值和布尔值的转换结果与调用toString()
相同。因为 null
和 undefined
没有 toString()
方法,所以 String()
方法就直接返回了这两个值的字面量文本。
用加号操作符给一个值加上一个空字符串
""
也可以将其转换为字符串。
隐式类型转换
凡是通过逻辑运算符 (&&
、 ||
、!
)、运算符 (+
、-
、*
、/
)、关系操作符 (>
、 <
、 <=
、>=
)、相等运算符 (==
) 或者 if/while
条件的操作,如果遇到两个数据类型不一样的情况,都会出现隐式类型转换。
下面主要说一下日常用得比较多的“==
”和“+
”这两个符号的隐式转换规则。
'==' 的隐式类型转换规则
-
如果类型相同,无须进行类型转换;
-
如果其中一个操作值是
null
或者undefined
,那么另一个操作符必须为null
或者undefined
,才会返回true
,否则都返回false
; -
如果其中一个是
Symbol
类型,那么返回false
; -
两个操作值如果为
string
和number
类型,那么就会将字符串转换为number
; -
如果一个操作值是
boolean
,那么转换成number
; -
如果一个操作值为
object
且另一方为string
、number
或者symbol
,就会把object
转为原始类型再进行判断(调用 object 的valueOf/toString
方法进行转换)。
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已经加上去了,这里需要注意一下
'+' 的隐式类型转换规则
'+'
号操作符,不仅可以用作数字相加,还可以用作字符串拼接。仅当 '+'
号两边都是数字时,进行的是加法运算;如果两边都是字符串,则直接拼接,无须进行隐式类型转换。
除了上述比较常规的情况外,还有一些特殊的规则,如下所示。
-
如果其中有一个是字符串,另外一个是 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,字符串拼接
整体来看,如果数据中有字符串,JavaScript 类型转换还是更倾向于转换成字符串,因为第三条规则中可以看到,在字符串和数字相加的过程中最后返回的还是字符串,这里需要关注一下。
Object 的转换规则
对象转换的规则,会先调用内置的[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'(字符串拼接)
// 再看两个特殊的case:
10 + {}
// "10[object Object]",注意:{}会默认调用valueOf是{},不是基础类型继续转换,调用toString,返回结果"[object Object]",于是和10进行'+'运算,按照字符串拼接规则来,参考'+'的规则C
[1,2,undefined,4,5] + 10
// "1,2,,4,510",注意[1,2,undefined,4,5]会默认先调用valueOf结果还是这个数组,不是基础数据类型继续转换,也还是调用toString,返回"1,2,,4,5",然后再和10进行运算,还是按照字符串拼接规则,参考'+'的第3条规则
总结
-
**数据类型的基本概念:**这是必须掌握的知识点,作为深入理解 JavaScript 的基础。
-
数据类型的判断方法:
typeof
和instanceof
,以及Object.prototype.toString
的判断数据类型、手写 instanceof 代码片段,这些是日常开发中经常会遇到的。 -
**数据类型的转换方式:**两种数据类型的转换方式,日常写代码过程中隐式转换需要多留意,如果理解不到位,很容易引起在编码过程中的 bug,得到一些意想不到的结果。
摘要
《JavaScript高级程序设计》(第四版)
备注
- END -
欢迎关注我的公众号
前端历劫之路
回复关键词
电子书
,即可获取12本前端热门电子书。回复关键词
红宝书第4版
,即可获取最新《JavaScript高级程序设计》(第四版)电子书。我创建了一个技术交流、文章分享群,群里有很多大厂的前端大佬,关注公众号后,点击下方菜单
了解更多
即可加我微信,期待你的加入。作者:Vam的金豆之路
微信公众号:前端历劫之路