阅读 5024

JS做类型检测到底有几种方法?看完本文就知道了!

JS有很多数据类型,对于不同数据类型的识别和相互转换也是面试中的一个常考点,本文主要讲的就是类型转换和类型检测。

数据类型

JS中的数据类型主要分为两大类:原始类型(值类型)和引用类型。常见的数据类型如下图所示:

image-20200506103537269

原始数据类型存在栈中,引用类型在栈中存的是一个引用地址,这个地址指向的是堆中的一个数据对象。需要注意的是null在这里我们算在原始类型里面,但是你用typeof的时候会发现他是object,原因是就算他是一个对象,那他应该在栈中存一个引用地址,但是他是一个空对象,所以这个地址为空,也就是不对应堆中的任意一个数据,他在堆中没有数据,只存在于栈中,所以这里算为了原始类型。引用类型其实主要就是ObjectArrayFunction这些其实也都是Object派生出来的。关于这两种类型在内存中的更详细的知识可以看这篇文章。

image-20200506104330145

下面我们来看看这两种类型的区别:

原始类型

  1. 原始类型的值无法更改,要更改只能重新赋值。像下面这样尝试去修改是不行的,但是整个重新赋值可以。

    image-20200506104958681

    image-20200506105044457

  2. 原始类型的比较就是比较值,值相等,他们就相等

引用类型

  1. 引用类型的值是可以修改的,注意这个时候我们虽然修改了a里面的属性,但是a在栈上的引用地址并没有变化,变化的是堆中的数据。

    image-20200506105513907

  2. 引用类型的比较是比较他们的索引地址,而不是他们的值。比如下面两个对象,看着是一样的,但是他们的引用地址不一样,其实是不等的:

    image-20200506110135018

    要想让他们相等,得直接将b赋值为a,这样他们的引用地址一样,就是相等的。

    image-20200506110256501

类型转换

JS中当不同类型的数据进行计算的时候会进行类型转换,比如下面的例子:

image-20200506110621714

上面的例子中,我们用了加减来操作几个非数字的类型,这时候JS会进行隐式的类型转换,然后再进行加减运算。除了JS本身的隐式转换外,有时候我们还会主动进行类型转换,这就算是显示类型转换了。

隐式类型转换

转为字符串

经常出现在+运算中,并且其中有一个操作数不是数值类型

let s = 4 + 'px' + 5;
console.log(s);   // 4px5

s = 123e-2 + 'a';  
console.log(s);   // 1.23a
复制代码

转为数值

经常出现在数学运算中,表示连接字符串的+运算除外

let s = 'abc';
console.log(+s, -s); // NaN, NaN

s = ' 123 ';
console.log(+s, -s); // 123 -123

s = new Date();
console.log(+s, -s);  // 1588675647421 -1588675647421   (这个操作相当于取毫秒数)
复制代码

转为bool的场景

经常出现在if或者逻辑运算中

let s = 'abc';
if(s) {
  console.log(s);  // abc
}

console.log(!!s);  // true
复制代码

下面的值在进行bool转换时会转换为false,除此以外都是true:

  1. 0
  2. NaN
  3. ''(空字符串)
  4. null
  5. undefined

==运算符

当我们使用==进行比较时,如果两边的类型不同,JS会进行类型转换,然后再比较,===则不会进行类型转换,如果===两边的数据类型不同,直接返回false

image-20200506112606774

上面只是列举了其中几种情况,更多的情况可以参考下面这种表,这个表来自于MDN。这个表的内容比较多,有些是规范直接定义的,比如null == undefined,也没有太多逻辑可言。我们不确定时可以来查下这个表,但是实际开发中其实是不建议使用==的,因为如果你把这个转换关系记错了的话可能就会引入比较难排查的bug,一般推荐直接使用===

image-20200506111718423

转换规则

下面这几张表是一些转换规则,来自于《JS权威指南》:

image-20200505185955549

image-20200505190049837

image-20200505190124871

显式类型转换

显式类型转换是我们自己写代码明确转换的类型,可以使代码看起来更清晰,是实际开发时推荐的做法。

image-20200506113002845

转字符串

显式转换为字符串可以使用toString方法,它的执行结果通常和String()方法一致。Number类型的toString方法还支持参数,可以指定需要转换的进制。下面的图是一些原始类型的toString()nullundefined没有toString方法,调用会报错:

image-20200506113217062

Number类型的toString方法支持进制:

image-20200506113346662

转数值

转为数值就很简单了,经常在用,就是这两个全局方法:parseIntparseFloat

对象转字符串

对象转换为字符串和数值会稍微麻烦点,下面我们单独来探究下。对象转为字符串主要有三种方法:

  1. value.toString()

    这个前面讲过了

  2. '' + value。这个是前面提到过的隐式转换,但是value是对象的话会按照下面的顺序进行转换:

    1. 先调用value.valueOf方法,如果值是原始值,则返回
    2. 否则调用value.toString方法,如果值是原始值,则返回
    3. 否则报错TypeError
  3. String(value)。这个是前面提到的显式转换,流程跟前面类似,但是调用toStringvalueOf的顺序不一样。

    1. 先调用value.toString方法,如果值是原始值,则返回
    2. 否则调用value.valueOf方法,如果值是原始值,则返回
    3. 否则报错TypeError

需要注意的是,Date对象有点特殊,他始终调用toString方法。

下面我们写一段代码来验证下:

Object.prototype.valueOf = function() {
  return 'aaa';
}

Object.prototype.toString = function() {
  return 'bbb';
}

let a = {};
let b = '' + a;
let c = String(a);

console.log(b);
console.log(c);
复制代码

上述代码输出是,跟我们预期一样:

image-20200506160225229

对象转数值

对象类型转为数值主要有两种方法:

  1. +value
  2. Number(value)

这两种的执行逻辑是一样的:

  1. 先调用valueOf方法,如果值是原始值,就返回
  2. 否则,调用toString方法,然后将toString的返回值转换为数值

照例写个例子看下:

Object.prototype.valueOf = function() {
  return {};
}

Object.prototype.toString = function() {
  return 'bbb';
}

let a = {};
let b = +a;
let c = Number(a);

console.log(b);
console.log(c);
复制代码

上述代码的输出都是NaN,这是因为我们toString方法返回的bbb没办法转化为正常数值,强行转就是NaN:

image-20200506160750545

类型检测

类型检测是我们经常遇到的问题,面试时也经常问到各种类型检测的方法,下面是几种常用的类型检测的方法。

typeof

做类型检测最常用的就是typeof了:

let a;
typeof a;   // undefined

let b = true;
typeof b;   // boolean

let c = 123;
typeof c;   // number

let d = 'abc';
typeof d;   // string

let e = () => {};
typeof e;   // function

let f = {};
typeof f;   // object

let g = Symbol();
typeof g;   // symbol
复制代码

instanceof

typeof最简单,但是他只能判断基本的类型,如果是对象的话,没法判断具体是哪个对象。instanceof可以检测一个对象是不是某个类的实例,这种检测其实基于面向对象和原型链的,更多关于instanceof原理的可以看这篇文章。下面来看个例子:

let a = new Date();
a instanceof Date;  // true
复制代码

constructor

constructor的原理其实跟前面的instanceof有点像,也是基于面向对象和原型链的。一个对象如果是一个类的实例的话,那他原型上的constructor其实也就指向了这个类,我们可以通过判断他的constructor来判断他是不是某个类的实例。具体的原理在前面提到的文章也有详细说明。还是用上面那个例子:

let a = new Date();
a.constructor === Date;  // true
复制代码

使用constructor判断的时候要注意,如果原型上的constructor被修改了,这种检测可能就失效了,比如:

function a() {}
a.prototype = {
  x: 1
}

let b = new a();
b.constructor === a;    // 注意这时候是 false
复制代码

上面为false的原因是,constructor这个属性其实是挂在a.prototype下面的,我们在给a.prototype赋值的时候其实覆盖了之前的整个prototype,也覆盖了a.prototype.constructor,这时候他其实压根就没有这个属性,如果我们非要访问这个属性,只能去原型链上找,这时候会找到Object:

image-20200506172606821

要避免这个问题,我们在给原型添加属性时,最好不要整个覆盖,而是只添加我们需要的属性,上面的改为:

a.prototype.x = 1;
复制代码

如果一定要整个覆盖,记得把constructor加回来:

a.prototype = {
  constructor: a,
  x: 1
}
复制代码

duck-typing

duck-typing翻译叫“鸭子类型”,名字比较奇怪,意思是指一个动物,如果看起来像鸭子,走起路来像鸭子,叫起来也像鸭子,那我们就认为他是只鸭子。就是说我们通过他的外观和行为来判断他是不是鸭子,而不是准确的去检测他的基因是不是鸭子。这种方式在科学上当然是不严谨的,但是在部分场景下却是有效的。用编程语言来说,就是看某个对象是不是具有某些特定的属性和方法,来确定他是不是我们要的对象。比如有些开源库判断一个对象是不是数组会有下面的写法:

function isArray(object) {
  return object !== null && 
    typeof object === 'object' && 
    'splice' in object && 
    'join' in object
}

isArray([]);  // true
复制代码

这就是通过检测目标对象是不是包含Array应该有的方法来判断他是不是一个Array。这就是所谓的看着像鸭子,那就是鸭子。但是一个具有splicejoin方法的对象也能通过这个检测,所以这样是不准确的,只是部分场景适用。

Object.prototype.toString.call

Object.prototype.toString.call是比较准确的,可以用来判断原生对象具体是哪个类型:

Object.prototype.toString.call(new Array());   // [object Array]
Object.prototype.toString.call(new Date());    // [object Date]
复制代码

这个方法返回的是[object XXX],这个XXX是对应的构造函数名字。但是他只能检测原生对象,对于自定义类型是没有用的:

function a() {}
let b = new a();

Object.prototype.toString.call(b);   // [object Object]
复制代码

可以看到对于自定义类a的实例b,我们得到仍然是[object Object],而不是我们预期的[object a]

一些原生方法: Array.isArray,Number.isInteger

JS为了解决类型检测的问题,也引入了一些原生方法来提供支持,比如Array.isArrayNumber.isInteger等。Array.isArray可以用来检测一个对象是不是数组:

Array.isArray([]);   // true
Array.isArray(123);  // false
复制代码

Number.isInteger可以用来检测一个对象是不是整数:

Number.isInteger(1);     // true
Number.isInteger(-1);    // true
Number.isInteger(-1.1);  // false
Number.isInteger('aaa'); // false
复制代码

如果有原生检测的方法我们当然推荐使用原生方法了,但是目前原生方法并没有那么多和全面,很多时候还是要用前面的方法来检测类型。

小节

JS其实没有一种完美的方法来检测所有的类型,具体的检测方法需要我们根据实际情况来进行选择和取舍。下面是几种方法的总结:

image-20200506180011564

总结

  1. JS有两种数据类型,原始类型和引用类型,引用类型主要就是对象。
  2. 当我们使用+,逻辑判断或者==时会有隐式的类型转换。
  3. 有时候隐式的类型转换会出现我们不想要的结果,如果我们确定要进行判断或者类型转换,最好使用显式的,比如使用===,而不是==
  4. 对象转为字符串和数值可能需要调valueOftoString方法,调用顺序需要看具体场景。
  5. JS没有一个完美的类型检测方法,我们最好根据需要选择具体的检测方法。

文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是作者持续创作的动力。

作者博文GitHub项目地址: github.com/dennis-jian…

作者掘金文章汇总:juejin.im/post/684490…