javascript中的数据类型判断及原理探究

avatar
前端工程师 @豌豆公主

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情

javascript中的数据类型判断及原理探究

目录

  • 为什么要做数据类型检测(安全防范机制) e.g.
  • 四种方式检测数据类型
  • 便捷api
  • 从vue封装的函数,去窥见数据类型判断的封装
  • 总结

安全防范机制

  • 10 + val = ?
  • val.map(v=>v) ?
  • try catch, 默认值 ?.

typeof

定义

typeof操作符返回一个字符串,表示未经计算的操作数的类型(此处有彩蛋)。

长图来袭

image.png

typeof 原理探究

  • 从上面可以总结出,对于基本数据类型,typeof可以完美判断,但是对于null取判断出了object。对于复杂数据类型,几乎都是object, 但是function(){}, 和Symbol ,却判断出是function。完美来探究下typeof原理

  • js在底层存储变量时,会在变量的机器码的低位1-3位存储其类型信息

000:对象
010:浮点数
100:字符串
110:布尔
1:整数
null:所有机器码均为0
undefined:用 −2^30 整数来表示
`typeof通过判断前三位的值来判断变量的数据类型`
  1. 所以这就很好的解释了typeof null 为什么是object。因为null的所有机器码都是0
  2. 为什么function(){}判断出来的是function。我们从ECMA262标准去寻找答案

image.png

规范里说,这个对象,如果实现了call方法,那么他就是function类型

我们来看看函数是否实现了call方法

image.png

确实是实现了call,所以typeof function(){} function

  1. 如果我们输出一个未定义的变量,会发生什么?

image.png 如果我们使用typeof去判断这个未定义的变量,会发生什么?

image.png

规范里这样解释到

image.png

总结

typeof 是根据变量在机器码的低位1-3位存储其类型信息来做类型判断的,所以他是未经计算的操作数的类型判断!!!。除了null, typeof判断基本数据类型没什么问题,但是对于引用数据类型,就显得力不从心了,下面介绍一个几乎万能判断法Object.prototype.toString.call()

Object.prototype.toString.call()

定义

toString()  方法返回一个表示该对象的字符串()。

字符串形如=》[object, [type],其中[type]会返回es定义的对象类型,包含"Undefined","Null","Arguments", “Array”, “Boolean”, “Date”, “Error”, “Function”, “JSON”, “Math”, “Number”, “Object”, “RegExp”, 和 “String”

原理探究

  1. Object.prototype.toString() 没什么好说的,就是返回表示改对象的字符串,为什么要使用call呢?

由于Object.prototype.toString()本身允许被修改,像Array、Boolean、Number的toString就被重写过,所以需要调用Object.prototype.toString.call(arg)来判断arg的类型,call将arg的上下文指向Object,所以arg执行了Object的toString方法。

至于call,就是改变对象的this指向,当一个对象想调用另一个对象的方法,可以通过call或者apply改变其this指向,将其this指向拥有此方法的对象,就可以调用该方法了

代码实践

function type() {}
console.log(type.toString()); // function type() {}

由于Function重写了toString() ,本来使用toString会返回表示该对象类型,但是被重写后,返回了函数本身。

如果我们把Function重写的toString() 删掉,看会发生什么

function type() {}
delete Function.prototype.toString;
console.log(type.toString()); // [object Function] 

删掉Function重写的toString(),会返回我们希望的结果。但是我们往往不会用delete去删除,所以就采用call方法将arg的上下文指向Object,所以arg执行了Object的toString方法。

function type() {}
console.log(Object.prototype.toString.call(type)); // [object Function]

对自定义类型的判断

  • 假设我们声明了一个类,并且去做函数的构造调用
  class A {}
  let a = new A();
  console.log(Object.prototype.toString.call(A)); // [object Function]
  console.log(Object.prototype.toString.call(a)); // [object Object]

显然,对于对自定义类型的判断,Object.prototype.toString.call 出现了问题,下面会给出解决方案

总结

万精油Object.prototype.toString.call()的实现关键,其实抛开一切,就是一个关键的方法 toString()。他本身的功能就是返回对象的字符串,形如[object, [[class]]],也就是数据的类型。但是很多类型,都改写了toString方法,我们本来可以delete 改写的方法。但是更为明智的是使用Object去做类型判断

instanceof

定义

  • instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

代码实践

  class A {}
  let B = new A();
  console.log(B instanceof A); // true

根据定义可知,instanceof 检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。看过这篇文章的都知道,new的实现,就有一个原型的关联这一段代码

instanceof原理实现

function _instanceof(explame, classFunction) {
    let classFuncPrototype = classFunction.prototype;
    proto = Object.getPrototypeOf(explame);
    while (true) {
      if (classFuncPrototype == proto) {
        return true;
      }
      if (proto == null) {
        return false;
      }
      proto = Object.getPrototypeOf(proto);
    }
  }
  let arr = [];
  console.log(_instanceof(arr, Array));

原型链接巩固

console.log([].__proto__ === Array.prototype); // true
console.log([].__proto__ === Object.prototype); // false
console.log([].__proto__.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null

你我本无缘,全靠__proto__ 。 对象尽头,终为null。

instanceof的弊端

  1. 类型判断不准确
let arr = []
arr instanceof Object // true

因为instanceof玩的是原型链,所以数组类型 也是对象类型 2. 可以随意更改原型链,导致判断不准

function Fn() {
  this.x = 100
}
Fn.prototype = Object.create(Array.prototype)
let f = new Fn()
console.log(f instanceof Array)

instanceof扩展

根据instanceof的定义,我们只能检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。这在h很大程度上限制了我们的判断。我们可以使用Symbol.hasInstance来扩展instanceof

Symbol.hasInstance

  • Symbol.hasInstance用于判断某对象是否为某构造器的实例。因此你可以用它自定义 instanceof 操作符在某个类上的行为。
  class A {
    constructor() {}
  }
  let a = new A();
  console.log(a instanceof A); // true
  console.log(A[Symbol.hasInstance](a)); // true

以上两种写法是等价,如果我们要使用instanceof来做字符串的检测呢?

let c = '';
  console.log(c instanceof String); // false
  console.log(String[Symbol.hasInstance](c)); // false

下面让我们自定义 instanceof 操作符在某个类上的行为

// 判断字符串
class isStr {
    static [Symbol.hasInstance](x) {
      return typeof x === 'string';
    }
  }
  console.log('' instanceof isStr); // true
  
  // 判断数组
 class isArray {
    static [Symbol.hasInstance](x) {
      return Array.isArray(x);
    }
  }
  console.log([] instanceof isArray); // true

constructor

定义

  • constructor 属性返回 Object 的构造函数(用于创建实例对象)这个属性(constructor)指向该对象的基本对象构造函数类型。注意,此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串。

  • 简单来说:有了constructor属性,可以得知某个实例对象,到底是哪一个构造函数产生的。如果不能确定constructor属性是什么函数,有一个办法,通过name属性,从实例得到构造函数的名称。

  class B {
    constructor() {}
  }
  let b = new B();
  console.log('B', B); // [class B]
  console.log('B', b.constructor); // [class B]
  console.log('B', b.constructor === B); // true
  console.log('B', b.constructor.name); // B

判断数据类型,长图来袭击

image.png

null 和 undefined

这俩兄弟 null 和 undefined 是光杆司令,没有相应的构造函数。

image.png

弊端

  • 可以改

image.png

便捷api

isNaN

与 JavaScript 中其他的值不同,NaN不能通过相等操作符(== 和 ===)来判断 ,因为 NaN == NaN 和 NaN === NaN 都会返回 false。 因此,isNaN 就很有必要了。

当算术运算返回一个未定义的或无法表示的值时,NaN就产生了。但是,NaN并不一定用于表示某些值超出表示范围的情况。将某些不能强制转换为数值的非数值转换为数值的时候,也会得到NaN

例如,0 除以 0 会返回NaN —— 但是其他数除以 0 则不会返回NaN

tips

  • isNaN函数会首先尝试将参数进行类型转换(所以用他只判断NaN即可,其他的不太保险)

image.png

  • polyfill (利用NaN自身永不相等于自身这一特征)

var isNaN = function(value) {
    var n = Number(value);
    return n !== n;
};

Object.is()

  • Object.is()  方法判断两个值是否为同一个值
  • demo

image.png

  • 让null 和undefined 终于找回了自己

Array.isArray()

  • Array.isArray()  用于确定传递的值是否是一个 Array

从vue封装的函数,去窥见数据类型的方式(内部查看)

实现一个万能数据类型检测的方法

 let class2type = {};
  let toString = class2type.toString;
  [
    'Boolean',
    'Number',
    'String',
    'Function',
    'Array',
    'Date',
    'RegExp',
    'Object',
    'Error',
    'Symbol',
  ].forEach((val) => {
    class2type[`object ${val}`] = val.toLowerCase();
  });
  console.log('class2type', class2type);
  function toType(obj) {
    if (obj == null) {
      return obj + '';
    }
    return typeof obj === 'object' || typeof obj === 'function'
      ? class2type[toString.call(obj)]
      : typeof obj;
  }
  window.toType = toType;

总结

typeof

  • typeof 判断基本数据类型是完全可以的,但是对于null会返回object。这是由于js在底层存储变量时,会在变量的机器码的低位1-3位存储其类型信息。恰巧,null的所有机器码都为0。
  • typeof在判断复杂数据类型时,基本都返回了object,但是再判断function(){}时,却返回了function.根据规范可知,由于Function 实现了call方法,所以返回function

instanceof

  • instanceof 用于判断对象是否是某个构造函数的实例,走的是原型链。由于null 和 undefined是光杆司令,所以使用instanceof,会报错。他有个缺陷就是不能判断基本数据类型。但是我们可以使用Symbol.hasInstance 自定义 instanceof 操作符在某个类上的行为,这样就变得灵活起来

toString()

  • toString 与身俱来就是做类型检测的,但是无奈被大多数类型改写了toString。所以我们把希望给予 Object上,使用Object.prototype.toString.call() 去做类型校验。但是也有一个问题,就是对于自定义类型,无法做出正确判断。

是结束,也是开始

  • 判断数据类型,大多是将上面的方式组合起来,那个方便用那个。学了数据类型判断,后续就要输出类型转化的相关文章了