深入探索JavaScript类型判断:基础与进阶技巧

发表时间: 2024-05-20 10:29

JavaScript 语言具有多种数据类型,它们可以大致分为两大类:基本数据类型(Primitive Data Types)和引用数据类型(Reference Data Types)。

一、数据类型

基本数据类型(Primitive Data Types) 包括:

  1. Undefined: 表示变量已被声明但未被初始化时的值。
  2. Null: 代表一个刻意的空值或缺失的值。
  3. Boolean: 只有两个值,true 或 false。
  4. Number: 用于表示整数和浮点数,包括Infinity、-Infinity和NaN。
  5. String: 用于表示文本,由零个或多个字符组成。
  6. Symbol: ES6 引入的新类型,表示独一无二的、不可变的数据类型,主要用于对象的属性键。
  7. BigInt: ES10 引入,用于表示任意大小的整数。

引用数据类型(Reference Data Types) 包括:

  1. Object: 一种复杂数据结构,可以包含多个键值对,包括但不限于普通对象、数组、函数等。
  2. Array: 特殊类型的对象,用于存储有序的元素集合。
  3. Function: 在JavaScript中,函数也是对象,可以作为值传递,拥有方法和属性

上面的数据类型如果说有不熟悉的,那一般是SymbolBigInt,下面我简要说一下:

  1. Symbol

最重要的特征就是唯一性,例如:

let a = Symbol(1) let b = Symbol(1) console.log(a === b)  // false

Symbol() 返回的东西,具有唯一性。

  • BigInt

Number 类型的安全整数范围(-2^53 到 2^53),超出这个范围的数进行计算会出现精度丢失的问题,算不准确,于是就出现了BigInt.

特点

  1. 创建: BigInt 可以通过在整数末尾添加 n 来创建,例如 123n。你也可以使用 BigInt() 函数将字符串转换为 BigInt,如 BigInt("123")。
  2. 运算: BigInt 和 Number 类型在进行算术运算时需要特别注意类型匹配。两个 BigInt 类型可以直接进行加减乘除等运算,但 BigInt 和 Number 直接运算会导致错误,需要先将 Number 转换为 BigInt。
  3. 比较: BigInt 和 Number 之间可以进行宽松的相等性比较(==),但严格相等性比较(===)会因为类型不同而返回 false。严格比较时,需要确保类型一致。
  4. 不支持: BigInt 不支持一元运算符 ++ 和 --,也不适用于Math对象的方法,以及不能用于某些JavaScript原生对象的属性,比如数组的长度。
  5. 字符串转换: BigInt 转换为字符串时,会保持其完整的数值,不会发生精度丢失。

示例

// 创建 BigIntconst largeNum = 1234567890123456789012345678901234567890n;// 运算const anotherLargeNum = 9876543210987654321098765432109876543210n;const sum = largeNum + anotherLargeNum;// 比较console.log(largeNum === BigInt('1234567890123456789012345678901234567890')); // trueconsole.log(largeNum == 1234567890123456789012345678901234567890); // true, 松散比较console.log(largeNum === 1234567890123456789012345678901234567890); // false, 严格比较类型不同// 字符串转换console.log(largeNum.toString()); // '1234567890123456789012345678901234567890'

二、类型判断时会产生的疑问

1.typeof()类型判断

console.log(typeof (null));//objectconsole.log(typeof (undefined));//undefinedconsole.log(typeof (true));//booleanconsole.log(typeof (20));//numberconsole.log(typeof ("abc"));//stringconsole.log(typeof (Symbol()));//symbolconsole.log(typeof (34n));//bigintconsole.log(typeof ([]));//objectconsole.log(typeof ({}));//objectconsole.log(typeof (function () { }));//function

我们看上面的代码会产生两个疑问:

为什么对null的类型判断为object?

这个行为实际上是JavaScript设计初期的一个决策,后来成为了语言的一部分,被视为一个历史遗留问题。在JavaScript的最初设计中,类型信息是通过值的内部表示来区分的,特别是通过值的头部比特位。对于当时的实现来说,null的内部二进制表示是全零,这与对象类型在内存中的某些标记模式相吻合(判断其二进制前三位是否为0,是则为object,否则为原始类型),尤其是当引擎检查值的头部比特以快速区分基本类型和引用类型时,全零可能被错误地解释为了一个空对象的标记。至于后来为什么不改,则是因为大量的企业已经用JavaScript写了大量的项目,改动后,全部项目都会报错,基于这种考虑就没动。因此在以后判断类型是否为object时,需要将null排除。

为什么单独function判断类型为function,而非object?

在JavaScript中,函数(Function)是一种特殊的对象,这意味着它本质上继承了对象的特性,可以拥有属性和方法。然而,出于对语言设计和实用性考虑,typeof操作符特意将函数类型区分对待,当应用于函数时,它返回的是"function"而不是"object"。

2. instanceof类型判断

在JavaScript中,instanceof 是一个操作符,用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。它的使用方式与Java中的instanceof相似,但概念上更符合JavaScript的原型继承模型。其基本语法如下:

object instanceof Constructor
  • object:需要检查的对象。
  • Constructor:一个构造函数或者函数对象。

如果object是通过Constructor或其任意父类(通过原型链)构造的,那么instanceof操作符返回true;否则,返回false。

例如:

function Animal() {}function Dog() {}Dog.prototype = new Animal();let myDog = new Dog();console.log(myDog instanceof Dog); // 输出: trueconsole.log(myDog instanceof Animal); // 输出: trueconsole.log(myDog instanceof Object); // 输出: true,因为所有对象都最终继承自Object

在这个例子中,myDog对象是通过Dog构造函数创建的,而Dog的原型链上包含了Animal,因此myDog既是Dog的实例,也是Animal的实例。同时,由于JavaScript中所有对象都继承自Object,所以myDog也是Object的实例。

原始类型(如string、number、boolean、null、undefined、symbol、bigint)不是对象,因此不能直接使用instanceof来判断这些类型。如果你尝试对原始类型值使用instanceof,它们会被临时转换为对应的包装对象(如new String()、new Number()、new Boolean()),然后再进行检查,但这通常不是你想要的行为,并且对于null和undefined这样的值,这样做会直接导致错误。

例如:

let str = "some text";console.log(str instanceof String); // 可能意外地输出: false,因为字符串不是String对象的实例// 实际上,"some text" 在进行 instanceof 检查前会被转换为 String("some text"),但这是临时的包装对象,检查后即被销毁。let num = 2;console.log(num instanceof Number); // 同样可能输出: falselet bool = true;console.log(bool instanceof Boolean); // 输出: falseconsole.log(null instanceof Object); // 抛出 TypeError: null is not an object (evaluating 'null instanceof Object')console.log(undefined instanceof Object); // 抛出 TypeError: undefined is not an object (evaluating 'undefined instanceof Object')

面试题补充:请写出instanceof的判断原理。

如果面试官出这种题,那对你算是非常温柔了。

function myinstanceof(object, constructor) {     // 当对象不为null时进入循环,因为null没有__proto__    while (object !== null) {        // 如果对象的原型等于构造函数的prototype属性,说明该对象是构造函数的实例        if (object.__proto__ === constructor.prototype) {             return true;         } else {             // 如果当前对象不是实例,继续向上查找其原型链            object = object.__proto__;         }    }     // 遍历完原型链都没有找到匹配,说明不是该构造函数的实例    return false; }console.log(myinstanceof({}, Object));  // trueconsole.log(myinstanceof({}, Array));  // false

3. Object.prototype.toString.call( )

Object.prototype.toString.call() 是JavaScript中一个强大的方法,用于获取任何值的类型信息。这个方法能够返回一个表示该值的字符串,这个字符串格式通常为"[object Type]",其中Type是JavaScript中的类型名称。相比于前两种判断存在的瑕疵和不准确,这种更为完美和准确。

console.log(Object.prototype.toString.call({}));      // "[object Object]"console.log(Object.prototype.toString.call([]));     // "[object Array]"console.log(Object.prototype.toString.call(new Date)); // "[object Date]"console.log(Object.prototype.toString.call(null));    // "[object Null]"console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"..................

在面试时,很多大厂面试官会为难你,问:有什么办法可以判断类型?

你回答:Object.prototype.toString.call( ),

他又问你为什么这个方法可以判断类型?

要回答好这个问题,就需要我们对Object.prototype.toString.call( )有着更为深入的理解:

这个方法要分两部分理解:Object.prototype.toString和call()

  • Object.prototype.toString

先来看官方文档上写的Object.prototype.toString


翻译:

调用tostring方法时,会执行以下步骤:

1.如果该值未定义,则返回“[object Undefined]”

2.如果this值为null,则返回“[object Null]”

3.让o成为调用To Obiect的结果,将this值作为参数传递。(将 o 作为 To Object(this) 的执行结果)

4.定义class为o的内部属性 [[Class]] 的值

5.返回String值,该值是由三个String“[object”、class 和 “]” 连接起来的结果。

你可以将以上步骤理解为以下步骤:

1.检查值是否为undefined:如果调用toString的方法的对象是undefined,则返回"[object Undefined]"。

2.检查值是否为null:如果该值是null,则返回"[object Null]"。

3.转换为对象(To Object操作):对于非null和非undefined的值,首先通过抽象操作To Object将其转换为对象(如果还不是对象)。这意味着原始值(如数字、字符串等)会先转换为它们的包装对象,然后继续后续步骤。

4.获取内部属性[[Class]]:获取转换后的对象的内部属性[[Class]]的值。这个属性由JavaScript引擎维护,代表了对象的类型信息,比如"Array"、"Date"、"Object"等。

5.构造并返回结果字符串:最后,将字符串"[object ", class的值,以及"]"拼接起来,形成并返回最终的类型字符串,如"[object Array]"、"[object Date]"等。

然而用Object.prototype.toString()远远不足以有效的判断类型,尽管它很强大:


Object.prototype.toString()判断类型时,通常不会按照预期工作,尤其是当直接在原始值(如字符串、数字、布尔)上尝试时,因为这样调用的this并没有绑定到你想要检查的对象上。

其原因在于第三个步骤To Obiect,官方文档对此如下描述:


它会new一个对象,而不是单纯的字面量,此时其this指向发生改变,其内部属性[[class]]为指向对象的内部属性object。

因此,需要call()的帮助改变其this指向

  • call

不了解call的,我简单举个例子来说明一下效果:

var object = {     a: 11} function foo() {     console.log(this.a) } foo.call(obj)  // 11

想通过func函数输出1,可以通过call方法将func里的this指向object。

我们可以尝试模拟call方法的行为,写如下代码:

var object = {     a: 11}; function foo() {     console.log(this.a); } Function.prototype.mycall = function(context) {    // 检查调用mycall的是否为一个函数    if (typeof this !== 'function') {        throw new TypeError(this + ' is not a function');    }        // 使用Symbol来避免属性名冲突    const fn = Symbol('key');        // 将当前函数(this指向的func)赋值给context的一个唯一属性    context[fn] = this;        // 调用这个新添加的函数,此时this会被隐式绑定到context上    context[fn]();        // 删除临时添加的属性,以清理环境    delete context[fn];};foo.mycall(object);  // 输出: 11

核心原理可以概括为: call方法通过在指定的context对象上临时引用并调用目标函数,实现了对该函数内部this的隐式绑定,从而使得函数能够在预期的上下文中执行。

具体来说:

  • 临时绑定:它本质上是在context对象上创建一个属性(通常使用一个不易冲突的属性名,如使用Symbol),并将目标函数赋值给这个属性。(Symbol作用在于,防止别人调用你写的方法时,用同名的变量名)
  • 调用函数:接着,通过context上的这个属性间接调用目标函数。由于是通过对象属性的方式来调用的,JavaScript的函数调用规则决定了此时函数内的this将绑定到该对象(即context)上。
  • 清理:为了不污染context对象,调用结束后通常还会删除之前添加的临时属性,即清除本来就不存在context里的属性。

看完这两部分的解释,再来做一个总结:

使用Object.prototype.toString.call()时,当参数为Boolean,Number和String类型,call()先将这个原始值转换为其对应的包装对象,即new String('11'),然后再调用Object.prototype.toString方法,当执行到第三步to object时,发现参数为对象,则将对象赋给变量o。在这个过程中this指向因为call的纠正作用没有发生改变,因此,其内部属性[[class]]没有发生改变。

加上了call,你可以理解为以下情况:

Object.prototype.toString.call('11'); // 输出: "[object String]" Object.prototype.toString.call(new String('11')); // 输出同样为: "[object String]"

写出完整步骤: 当执行Object.prototype.toString.call('11')时,其内部过程大致如下:

  • 字符串字面量'11'作为call的第一个参数,使得toString方法内部的this指向了一个临时创建的String对象(即new String('1'))。
  • 该方法检查this不是null或undefined,继续执行。
  • 将这个临时的字符串对象视为操作对象O。
  • 从O中获取其内部属性[[Class]],得到值"String"。
  • 组合并返回字符串"[object String]",表示这是一个字符串类型的对象。

4.Array.isArray(x)

Array.isArray(x) 是JavaScript的一个内建函数,用于检测x是否为一个数组。这个方法提供了最直接和可靠的方式来判断一个变量是否是数组类型,相比使用instanceof或typeof等方法更准确,因为它不会受到不同全局执行环境(如iframe、Web Workers)中Array构造函数不同的影响。

使用示例:

let arr = [1, 2, 3];let notArr = "I am not an array";console.log(Array.isArray(arr)); // 输出: trueconsole.log(Array.isArray(notArr)); // 输出: false

这个函数非常有用,尤其是在处理可能是多种类型输入的动态数据时,能够确保你正确地识别并处理数组类型的数据。

三、结语:

在JavaScript的世界里,准确无误地判断数据类型是编写健壮、可维护代码的基础。本文从基础出发,系统梳理了JavaScript的各大数据类型,重点解析了在类型判断时常见的疑惑与误区,尤其深入探讨了typeof、instanceof以及Object.prototype.toString.call()这三种类型判断方法的原理与实践,最后还提到了Array.isArray()这一专门用于数组类型判断的便捷工具。

通过本篇内容的学习,你不仅掌握了每种判断方法的适用场景与限制,还理解了如何利用Object.prototype.toString.call()这一终极武器来实现精确无误的类型识别。记住,每种方法都有其独特的价值和潜在的陷阱,合理选择才能在实战中游刃有余。