Node.js Interview ---- 类型判断

Node.js Interview ---- 类型判断

相信很多人看过这个仓库.

Node.js Interviewelemefe.github.io


此文就是用来解答上述所问的第一篇类型判断.

为什么需要去考察JS的类型判断?

前置知识:

JS数据类型分为两大类:
- 基本数据类型: Number,String,Boolean,Undefined,Null,Symbol
- 复杂数据类型: Object

JS是一门动态类型语音,它不像Java等存在类型约束, 所以写JS时候一不小心就会发生类型转换.

console.log({}+[]) // 0

console.log("aa" == true) // false

console.log(1 + []) // "1"

const a = {
  i: 1,
  toString: function () {
    return a.i++;
  }
}
if (a == 1 && a == 2 && a == 3) {
  console.log('hello world!');
}

正因为JS没有类型约束,在开发大型项目时候不利于代码维护,代码阅读性不强, 所以社区提出了一些解决方法如flow和typescript.


JS的类型转换方式

JS的类型转换是一把双刃剑, 方便你书写代码同时又非常容易导致bug的产生.

JS类型转换分为两种:

  • 隐式类型转换
  • 强制类型转换


常见的隐式类型转换

我们平时经常涉及最多的隐式类型转换运算符大概就是+与==. +既可以操作字符串也可以操作数字, ==运算符与===最大区别也是它默认会进行类型转换.

JS既然允许类型转换自然有一套自己的法则:其实隐式类型转换主要涉及三种转换.

  • 通过ToPrimitive将值转换为原始值
ToPrimitive(input, PreferredType?)
- input是要转换的值,
- PreferredType是可选参数,可以是Number或String类型。它只是一个转换标志,转化后的结果并不一定是这个参数所值的类型,但是转换结果一定是一个原始值(或者报错)。
  1. 如果PreferredType被标记为Number,则会进行下面的操作流程来转换输入的值。
1、如果输入的值已经是一个原始值,则直接返回它
2、否则,如果输入的值是一个对象,则调用该对象的valueOf()方法,
   如果valueOf()方法的返回值是一个原始值,则返回这个原始值。
3、否则,调用这个对象的toString()方法,如果toString()方法返回的是一个原始值,则返回这个原始值。
4、否则,抛出TypeError异常。

2. 如果PreferredType被标记为String,则会进行下面的操作流程来转换输入的值。

1、如果输入的值已经是一个原始值,则直接返回它
2、否则,调用这个对象的toString()方法,如果toString()方法返回的是一个原始值,则返回这个原始值。
3、否则,如果输入的值是一个对象,则调用该对象的valueOf()方法,
   如果valueOf()方法的返回值是一个原始值,则返回这个原始值。
4、否则,抛出TypeError异常。

3. 如果PreferredType参数不存在,则按照下面规则

1、该对象为Date类型,则PreferredType被设置为String
2、否则,PreferredType被设置为Number

关于valueOf()和toString()解析规则:

每个对象都有toString()与valueOf()方法.

先说下valueOf().

1. Number、Boolean、String这三种构造函数生成的基础值的对象形式,通过valueOf转换后会变成相应的原始值
2. Date这种特殊的对象,其原型Date.prototype上内置的valueOf函数将日期转换为日期的毫秒的形式的数值
3. 除此之外返回的都为this,即对象本身

再来看看toString函数.

1. Number、Boolean、String、Array、Date、RegExp、Function这几种构造函数生成的对象,通过toString转换后会变成相应的字符串的形式,因为这些构造函数上封装了自己的toString方法
2. 除这些对象及其实例化对象之外,其他对象返回的都是该对象的类型,都是继承的Object.prototype.toString方法。


  • 通过ToNumber将值转换为数字
undefined => NaN
null => +0
布尔值 => true转换1,false转换为+0
数字 => 无需转换
字符串 => 有字符串解析为数字,例如:‘324’转换为324,‘qwer’转换为NaN.(又有一套解析规则)
对象 => 先进行 ToPrimitive(obj, Number)转换得到原始值,在进行ToNumber转换为数字
  • 通过ToString将值转换为字符串
undefined => 'undefined'
null => 'null'
布尔值 => 转换为'true' 或 'false'
数字 => 数字转换字符串,比如:1.765转为'1.765'
字符串 => 无须转换
对象 => 先进行 ToPrimitive(obj, String)转换得到原始值,在进行ToString转换为字符串


== 运算符隐式转换

比较运算 x==y, 其中 x 和 y 是值,返回 true 或者 false。这样的比较按如下方式进行:

1、若 Type(x) 与 Type(y) 相同, 则

    1* 若 Type(x) 为 Undefined, 返回 true。
    2* 若 Type(x) 为 Null, 返回 true。
    3* 若 Type(x) 为 Number, 则
  
        (1)、若 x 为 NaN, 返回 false。
        (2)、若 y 为 NaN, 返回 false。
        (3)、若 x 与 y 为相等数值, 返回 true。
        (4)、若 x 为 +0 且 y 为 −0, 返回 true。
        (5)、若 x 为 −0 且 y 为 +0, 返回 true。
        (6)、返回 false。
        
    4* 若 Type(x) 为 String, 则当 x 和 y 为完全相同的字符序列(长度相等且相同字符在相同位置)时返回 true。 否则, 返回 false。
    5* 若 Type(x) 为 Boolean, 当 x 和 y 为同为 true 或者同为 false 时返回 true。 否则, 返回 false。
    6*  当 x 和 y 为引用同一对象时返回 true。否则,返回 false。
  
2、若 x 为 null 且 y 为 undefined, 返回 true。
3、若 x 为 undefined 且 y 为 null, 返回 true。
4、若 Type(x) 为 Number 且 Type(y) 为 String,返回比较 x == ToNumber(y) 的结果。
5、若 Type(x) 为 String 且 Type(y) 为 Number,返回比较 ToNumber(x) == y 的结果。
6、若 Type(x) 为 Boolean, 返回比较 ToNumber(x) == y 的结果。
7、若 Type(y) 为 Boolean, 返回比较 x == ToNumber(y) 的结果。
8、若 Type(x) 为 String 或 Number,且 Type(y) 为 Object,返回比较 x == ToPrimitive(y) 的结果。
9、若 Type(x) 为 Object 且 Type(y) 为 String 或 Number, 返回比较 ToPrimitive(x) == y 的结果。
10、返回 false。


常见的强制类型转换


  • String()
处理非字符串到字符串的强制类型转换


  • Boolean()
可以被强制类型转换为false的值:
undefined
null
false
+0, -0, NaN
""


  • Number()
将非数字值当作数字来使用
其中 true转换为1, false转换为0, undefined转换为NaN, null转换为0, 对字符串的处理遵循数字常量的相关规则, 处理失败时返回NaN


那么有哪些方式进行类型判断呢?


  • typeof操作符

typeof 是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括以下 7 种:number、boolean、symbol、string、object、undefined、function 等。

typeof ''; // string 有效
typeof 1; // number 有效
typeof Symbol(); // symbol 有效
typeof true; //boolean 有效
typeof undefined; //undefined 有效
typeof null; //object 无效
typeof [] ; //object 无效
typeof new Function(); // function 有效
typeof new Date(); //object 无效
typeof new RegExp(); //object 无效

是的通过上述代码你会发下一些问题: 通过这个方法并不能达到我们想要的类型验证.

1. 对于基本类型,除 null 以外,均可以返回正确的结果。
2. 对于引用类型,除 function 以外,一律返回 object 类型。
3. 对于 null ,返回 object 类型。
4. 对于 function 返回  function 类型。


  • instanceof

instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型

[] instanceof Array; // true
{} instanceof Object;// true
new Date() instanceof Date;// true
 
function Person(){};
new Person() instanceof Person;
 
[] instanceof Object; // true
new Date() instanceof Object;// true
new Person instanceof Object;// true

虽然 instanceof 能够判断出 [ ] 是Array的实例,但它认为 [ ] 也是Object的实例,为什么呢?

我们简单分析一下 [ ]、Array、Object 三者之间的关系:

从 instanceof 能够判断出 [ ].__proto__ 指向 Array.prototype,而 Array.prototype.__proto__ 又指向了Object.prototype,最终 Object.prototype.__proto__ 指向了null,标志着原型链的结束。因此,[]、Array、Object 就在内部形成了一条原型链:

所以: instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。

instanceof还有一个问题: 它假定只有一个全局执行环境。如果网页中包含多个iframe,那实际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的构造函数。如果你从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数。

// ES6的 Array.isArray可以解决上述问题.


  • constructor

基本认识: 当一个函数 F被定义时,JS引擎会为F添加 prototype 原型,然后再在 prototype上添加一个 constructor 属性,并让其指向 F 的引用。如下所示:

当执行 var f = new F() 时,F 被当成了构造函数,f 是F的实例对象,此时 F 原型上的 constructor 传递到了 f 上,因此 f.constructor == F.

可以看出,F 利用原型对象上的 constructor 引用了自身,当 F 作为构造函数来创建对象时,原型上的 constructor 就被遗传到了新创建的对象上.

同样,JavaScript 中的内置对象在内部构建时也是这样做的:

缺陷:

1. null 和 undefined 是无效的对象,因此是不会有 constructor 存在的,这两种类型的数据需要通过其他方式来判断。

2. 函数的 constructor 是不稳定的,这个主要体现在自定义对象上,当开发者重写 prototype 后,原有的 constructor 引用会丢失,constructor 会默认为 Object.因此,为了规范开发,在重写对象原型时一般都需要重新给 constructor 赋值,以保证对象实例的类型不被篡改。
  • toString(推荐使用)

toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。

对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。


Object.prototype.toString.call('') ;   // [object String]
Object.prototype.toString.call(1) ;    // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window 是全局对象 global 的引用


参考资料
判断JS数据类型的四种方法 - 一像素 - 博客园www.cnblogs.com图标http://yanhaijing.com/es5/#nullyanhaijing.com

编辑于 2018-10-06

文章被以下专栏收录

    知乎编程专栏里更值得关注和学习的web前端开发专栏!专栏主要技术栈:JavaScript,HTML,CSS,nodejs,Vuejs,reactjs,linux,Java,不辣不辣等各种开发工具