深入探究JavaScript的类型判断(从基础到精通)
JavaScript 语言具有多种数据类型,它们可以大致分为两大类:基本数据类型(Primitive Data Types)和引用数据类型(Reference Data Types)。
一、数据类型
基本数据类型(Primitive Data Types) 包括:
- Undefined: 表示变量已被声明但未被初始化时的值。
- Null: 代表一个刻意的空值或缺失的值。
- Boolean: 只有两个值,
true
或false
。 - Number: 用于表示整数和浮点数,包括Infinity、-Infinity和NaN。
- String: 用于表示文本,由零个或多个字符组成。
- Symbol: ES6 引入的新类型,表示独一无二的、不可变的数据类型,主要用于对象的属性键。
- BigInt: ES10 引入,用于表示任意大小的整数。
引用数据类型(Reference Data Types) 包括:
Object: 一种复杂数据结构,可以包含多个键值对,包括但不限于普通对象、数组、函数等。
Array: 特殊类型的对象,用于存储有序的元素集合。
Function: 在JavaScript中,函数也是对象,可以作为值传递,拥有方法和属性
上面的数据类型如果说有不熟悉的,那一般是Symbol和BigInt,下面我简要说一下:
- Symbol:
最重要的特征就是唯一性,例如:
let a = Symbol(1) let b = Symbol(1) console.log(a === b) // false
用Symbol() 返回的东西,具有唯一性。
- BigInt:
Number
类型的安全整数范围(-2^53 到 2^53),超出这个范围的数进行计算会出现精度丢失的问题,算不准确,于是就出现了BigInt.
特点
- 创建: BigInt 可以通过在整数末尾添加
n
来创建,例如123n
。你也可以使用BigInt()
函数将字符串转换为 BigInt,如BigInt("123")
。 - 运算: BigInt 和 Number 类型在进行算术运算时需要特别注意类型匹配。两个 BigInt 类型可以直接进行加减乘除等运算,但 BigInt 和 Number 直接运算会导致错误,需要先将 Number 转换为 BigInt。
- 比较: BigInt 和 Number 之间可以进行宽松的相等性比较(==),但严格相等性比较(===)会因为类型不同而返回 false。严格比较时,需要确保类型一致。
- 不支持: BigInt 不支持一元运算符
++
和--
,也不适用于Math对象的方法,以及不能用于某些JavaScript原生对象的属性,比如数组的长度。 - 字符串转换: BigInt 转换为字符串时,会保持其完整的数值,不会发生精度丢失。
示例
// 创建 BigInt const largeNum = 1234567890123456789012345678901234567890n; // 运算 const anotherLargeNum = 9876543210987654321098765432109876543210n; const sum = largeNum + anotherLargeNum; // 比较 console.log(largeNum === BigInt('1234567890123456789012345678901234567890')); // true console.log(largeNum == 1234567890123456789012345678901234567890); // true, 松散比较 console.log(largeNum === 1234567890123456789012345678901234567890); // false, 严格比较类型不同 // 字符串转换 console.log(largeNum.toString()); // '1234567890123456789012345678901234567890'
二、类型判断时会产生的疑问
1. typeof()类型判断
console.log(typeof (null));//object console.log(typeof (undefined));//undefined console.log(typeof (true));//boolean console.log(typeof (20));//number console.log(typeof ("abc"));//string console.log(typeof (Symbol()));//symbol console.log(typeof (34n));//bigint console.log(typeof ([]));//object console.log(typeof ({}));//object console.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); // 输出: true console.log(myDog instanceof Animal); // 输出: true console.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); // 同样可能输出: false let bool = true; console.log(bool instanceof Boolean); // 输出: false console.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)); // true console.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)); // 输出: true console.log(Array.isArray(notArr)); // 输出: false
这个函数非常有用,尤其是在处理可能是多种类型输入的动态数据时,能够确保你正确地识别并处理数组类型的数据。
三、结语:
在JavaScript的世界里,准确无误地判断数据类型是编写健壮、可维护代码的基础。本文从基础出发,系统梳理了JavaScript的各大数据类型,重点解析了在类型判断时常见的疑惑与误区,尤其深入探讨了typeof
、instanceof
以及Object.prototype.toString.call()
这三种类型判断方法的原理与实践,最后还提到了Array.isArray()
这一专门用于数组类型判断的便捷工具。
通过本篇内容的学习,你不仅掌握了每种判断方法的适用场景与限制,还理解了如何利用Object.prototype.toString.call()
这一终极武器来实现精确无误的类型识别。记住,每种方法都有其独特的价值和潜在的陷阱,合理选择才能在实战中游刃有余。
以上就是深入探究JavaScript的类型判断(从基础到精通)的详细内容,更多关于JavaScript类型判断的资料请关注脚本之家其它相关文章!
最新评论