> For the complete documentation index, see [llms.txt](https://lizh.gitbook.io/knowledge/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lizh.gitbook.io/knowledge/javascript-xi-lie/06-si-zhong-pan-duan-shu-ju-lei-xing-fang-fa-de-you-que-dian.md).

# 四种判断数据类型方法的优缺点

JavaScript 是一种**弱类型**或者说**动态**语言。这意味着你不用提前声明变量的类型，在程序运行过程中，类型会被自动确定。这也意味着你可以使用同一个变量保存不同类型的数据：

```js
let a = 42
a = "bar"
a = true
a = {}
```

最新的 ECMAScript 标准定义了 8 种数据类型:

* **基本类型（7种）**

  * [Boolean](https://developer.mozilla.org/zh-CN/docs/Glossary/Boolean)
  * [Null](https://developer.mozilla.org/zh-CN/docs/Glossary/Null)
  * [Undefined](https://developer.mozilla.org/zh-CN/docs/Glossary/undefined)
  * [Number](https://developer.mozilla.org/zh-CN/docs/Glossary/Number)
  * [BigInt](https://developer.mozilla.org/zh-CN/docs/Glossary/BigInt)
  * [String](https://developer.mozilla.org/zh-CN/docs/Glossary/字符串)
  * [Symbol](https://developer.mozilla.org/zh-CN/docs/Glossary/Symbol)

  基本类型也称为简单类型，存储于栈内存中，数据大小确定，内存空间大小可以分配，是**按值存储的，可以直接访问**。
* **引用类型（1种）**

  * [Object](https://developer.mozilla.org/zh-CN/docs/Glossary/Object)

  `Object` 是唯一的复杂数据类型。 `Object、Function 、Array、RegExp、Date` 这些引用类型值最终都可以归结为 `Object` 复杂数据类型。

  引用类型存储于堆内存中。而存储在变量处的值，是一个指针，指向存储对象的内存处，**即按址访问**。

除 Object 以外的所有类型都是不可变的（值本身无法被改变）。例如，与 C 语言不同，JavaScript 中字符串是不可变的（如：JavaScript 中对字符串的操作一定返回了一个新字符串，原始字符串并没有被改变）。我们称这些类型的值为"原始值”。

鉴于ECMAScript 是弱类型的语言，因此需要有一种手段来检测给定变量的数据类型。对于这个问题，JavaScript 也提供了多种方法，但遗憾的是，没有一种是完美，每种方法都有自己的优缺点。

```js
let objT = {
    str: '',
    num: 1,
    bool: true,
    undef: undefined,
    nul: null,
    symbol: Symbol(),
    bigInt: BigInt(10),

    fun: new Function(),

    arr: [],
    numN: new Number(),
    boolN: new Boolean(),
    strN: new String(),
    date: new Date(),
    reg: new RegExp(),
}
```

### typeof

**`typeof`** 操作符返回一个字符串，表示未经计算的操作数的类型。

```js
typeof variable
```

返回该类型的字符串(全小写字母)形式表示，包括以下 8 种：string、number、boolean、symbol、bigint、undefined、function、object 等。

有些时候，typeof 操作符会返回一些令人迷惑但技术上却正确的值：

* 对于 null ，返回`'object '`

  ```js
  // JavaScript 诞生以来便如此
  typeof null === 'object';
  ```

  在 JavaScript 最初的实现中，JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 `null` 代表的是空指针（大多数平台下值为 0x00），因此，null 的类型标签是 0，`typeof null` 也因此返回`'object'`。

  曾有一个 ECMAScript 的修复提案（通过选择性加入的方式），但[被拒绝了](http://wiki.ecmascript.org/doku.php?id=harmony:typeof_null)。该提案会导致 `typeof null === 'null'`。
* 引用类型Function，返回`'function'`

  ```js
  let func1 = new Function()
  let func2 = function () {}

  typeof func1 //"function"
  typeof func2 //"function"
  ```
* 对于其他基本类型，均可以返回正确的结果
* 对于其他引用类型，一律返回`'object'`
* 除 Function 外的所有构造函数的类型都是`'object'`

  ```js
  // 除 Function 外的所有构造函数的类型都是 'object'
  let str = new String('String');
  let num = new Number(100);
  let func = new Function();

  typeof str; // 返回 'object'
  typeof num; // 返回 'object'
  typeof func; // 返回 'function'
  ```
* 对正则表达式字面量的类型判断在某些浏览器中不符合标准：

  ```js
  typeof /s/ === 'function'; // Chrome 1-12 , 不符合 ECMAScript 5.1
  typeof /s/ === 'object'; // Firefox 5+ , 符合 ECMAScript 5.1
  ```
* 当前所有的浏览器都暴露了一个类型为 `undefined` 的非标准宿主对象 [`document.all`](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/all)。

  ```js
  typeof document.all === 'undefined';
  ```

  尽管规范允许为非标准的外来对象自定义类型标签，但它要求这些类型标签与已有的不同。`document.all` 的类型标签为 `'undefined'` 的例子在 Web 领域中被归类为对原 ECMA JavaScript 标准的“故意侵犯”。

在 ECMAScript 2015 之前，`typeof` 总能保证对任何所给的操作数返回一个字符串。即便是没有声明的标识符，`typeof` 也能返回 `'undefined'`。使用 `typeof` 永远不会抛出错误。

但在加入了块级作用域的 [let](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/let) 和 [const](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/const) 之后，在其被声明之前对块中的 `let` 和 `const` 变量使用 `typeof` 会抛出一个 [ReferenceError](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError)。块作用域变量在块的头部处于“[暂存死区](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/let#Temporal_Dead_Zone_and_errors_with_let)”，直至其被初始化，在这期间，访问变量将会引发错误。

```js
for (let k in objT) {
    console.log(typeof objT[k])
}

let objT = {
    str: '', //string
    num: 1, //number
    bool: true, //boolean
    undef: undefined, //undefined
    nul: null, //object
    symbol: Symbol(), //symbol
    bigInt: BigInt(10), //bigint

    fun: new Function(), //function

    //以下都为object
    arr: [],
    numN: new Number(),
    boolN: new Boolean(),
    strN: new String(),
    date: new Date(),
    reg: new RegExp()
}
```

### instanceof

**`instanceof`** **运算符**用于检测构造函数的 `prototype` 属性是否出现在某个实例对象的原型链上。即用来判断 object 是否为 constructor 的实例，如果 object 是 constructor 的实例，则返回 true，否则返回 false。表达式为：

```js
object instanceof constructor
```

* 不能用来判断7种基本类型值，因为基本数据类型没有原型链

  ```js
  objT.str instanceof String //false
  objT.num instanceof Number //false
  objT.bool instanceof Boolean //false
  objT.undef instanceof undefined //Uncaught TypeError: Right-hand side of 'instanceof' is not an object
  objT.nul instanceof null //Uncaught TypeError: Right-hand side of 'instanceof' is not an object
  objT.symbol instanceof Symbol //false
  objT.bigInt instanceof BigInt //false
  ```
* 不能正确判断 Object 类型

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

  ```js
  [] instanceof Array // true
  [] instanceof Object; // true

  function Person(){}
  new Person() instanceof Person //true
  new Person() instanceof Object;// true
  ```
* prototype 属性的值是可变的

  ```js
  function A( ){} 
  let a = new A()
  a instanceof A //true

  //修改构造函数的prototype
  A.prototype = null
  a instanceof A //false

  //修改对象的原型链
  b.__proto__ = null
  b instanceof A //false
  ```

  构造函数的 `prototype` 属性的值有可能会改变，改变之后的值很有可能不存在于 `object` 的原型链上。

  对象 `obj` 的原型链的也是可以修改的。虽然在目前的ES规范中，我们只能读取对象的原型而不能改变它，但借助于非标准的 `__proto__` 伪属性，是可以实现的。
* 不能用于跨窗口判断

  在浏览器中，我们的脚本可能需要在多个 frame 或 window 之间的交互。多个窗口意味着多个全局环境，不同的全局环境拥有不同的全局对象，从而拥有不同的内置类型构造函数。这可能会引发一些问题。比如：

  ```js
  document.body.appendChild(document.createElement('iframe'))
  let xArray = window.frames[0].Array
  let arr = new xArray(1,2,3)

  arr instanceof xArray //true
  arr instanceof Array //false
  ```

  针对数组的这个问题，ES5 提供了 Array.isArray() 方法 。该方法用以确认某个对象本身是否为 Array 类型，而不区分该对象在哪个环境中创建。

### constructor

**constructor**属性返回一个指向创建了该对象原型的函数引用。需要注意的是，该属性的值是那个函数本身。所有对象都会从它的原型上继承一个 `constructor` 属性。

```js
obj.constructor === Constructor
```

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

  ```js
  objT.undef.constructor //Cannot read property 'constructor' of undefined
  objT.nul.constructor //Cannot read property 'constructor' of null
  ```
* 其他基本数据类型访问时，JavaScript会自动调用其构造函数来生成一个对象

  ```js
  (0).constructor === Number // true
  true.constructor === Boolean // true
  'a'.constructor === String // true

  // 相当于
  (new Number(0)).constructor === Number
  (new Boolean(true)).constructor === Boolean
  (new String('a')).constructor === String
  ```
* 对象的constructor是可变的

  ```js
  function A( ){} 
  let a = new A()
  a.constructor === A //true

  //修改对象的constructor
  a.constructor = null
  a.constructor === A //false
  ```
* 不能用于跨窗口判断

  与instanceof类似，constructor在不同的窗口拥有不同的内置构造函数。

  ```js
  document.body.appendChild(document.createElement('iframe'))
  let xArray = window.frames[0].Array
  let arr = new xArray(1,2,3)

  arr.constructor === Array //false
  arr.constructor === xArray //true
  ```

### toString

`toString`方法是最为可靠的类型检测手段，它会将当前对象转换为字符串并输出。 `toString`属性定义在`Object.prototype`上，因而所有对象都拥有`toString`方法。 但`Array`, `Date`等对象会重写从`Object.prototype`继承来的`toString`， 所以最好用`Object.prototype.toString`来检测类型。

```js
//目前是最可靠的了，它总能返回正确的值
Object.prototype.toString.call(variable) 
```

* 无法检测用户自定义类型

  `toString`也不是完美的，它无法检测用户自定义类型。 因为`Object.prototype`是不知道用户会创造什么类型的， 它只能检测ECMA标准中的那些内置类型。

  ```js
  function A( ){} 
  let a = new A()

  Object.prototype.toString.call(a) //[object Object]
  ```
* 避免了跨窗口问题

  因为返回值是字符串，也避免了跨窗口问题。当然IE弹窗中还是有Bug，不必管它了。

### 常见问题

#### 内部属性 \[\[Class]] 是什么？

每个对象在创建时都会带有一个 \[\[class]] 的内部属性，这个属性无法直接访问，可通过 `Object.prototype.toString()` 来查看。

```javascript
Object.prototype.toString.call([])     // '[object Array]'
Object.prototype.toString.call(/a/i )  // '[object RegExp]'
```

一般来说，对象的内部 \[\[class]] 属性和创建该对象的**构造函数**是相对应的，但是也有例外，如，`null|undefined`。总的来说， \[\[class]] 属性代表的是数据的类型值。

对象的 \[\[class]] 属性：

```javascript
Object.prototype.toString.call({})         // '[object Object]'
Object.prototype.toString.call(new Date()) // '[object Date]'
```

基本类型值的 \[\[class]] 属性：

```javascript
Object.prototype.toString.call("abc")     // '[object String]'
Object.prototype.toString.call(1234)      // '[object Number]'
Object.prototype.toString.call(true)      // '[object Boolean]'

Object.prototype.toString.call(null)      // '[object Null]'
Object.prototype.toString.call(undefined) // '[object Undefined]'
```

### 总结

* `typeof` 只适用于基本数据类型，对于 `null` 还有 Bug；
* `instanceof` 适用于构造函数创建的对象，它是基于原型链运作的；
* `constructor` 适用于 ECMA 内置 JavaScript 类型和构造函数创建的对象，无法判断 null、undefined；
* `toString` 适用于 ECMA 内置 JavaScript类型（包括基本数据类型和内置对象）的类型判断；
* `instanceof`和`constructor`是不安全的，且都有跨窗口问题。

**总之，如果你要判断的是基本数据类型或 JavaScript 内置对象，使用 toString； 如果要判断的是自定义类型，请使用 instanceof 或 constructor**。

### 参数链接

[判断JS数据类型的四种方法](https://www.cnblogs.com/onepixel/p/5126046.html)

[如何检查 JavaScript 变量类型？](https://harttle.land/2015/09/18/js-type-checking.html)
