在进行深拷贝的时候经常需要判断数组和对象,这里提供几个常用方法。
首先,typeof
可以判断除了 null
之外所有的基本类型,以及函数。返回值为 7 个字符串:
string
boolean
number
symbol (ES2015 新增)
bigint (ES2020 新增)
undefined
function
注意:基本类型中的 null
会被判断为 object
:
typeof null === 'object'
在 JavaScript 中不同的对象在底层都表示为二进制,其中前三位表示类型信息,二进制前三位是 000 的话会被判断为 object 类型,null 的二进制表示是全 0, 自然前三位也是 0, 所以执行 typeof 时会返回“object”。
因此 typeof
在判断基本类型 null
,引用类型 Array
、 Object
、基本类型的包装类型以及函数实例 (new + 函数)
时,得到的都是 object
,局限性很大。下面介绍几种判断数组的常用方法。
1. 使用Array.isArray()
Array.isArray()
用于确定传递的值是否是一个 Array 。这个方法是 Array 的静态方法,不能在实例上访问,只能通过 Array 进行调用。
const arr = [1, 2]
const obj = {a: 1}
Array.isArray(arr) // true
Array.isArray(obj) // false
2. 使用instanceof判断Array和Object
instanceof用来判断一个变量是否为一个构造函数创建的实例,代码形式为obj1 instanceof obj2(obj1是否是obj2的实例)判断方法是根据对象的原型链依次向下查询,如果obj2的原型属性存在obj1的原型链上,(obj1 instanceof obj2)值为true。
注意:不管判断数组还是对象,都必须 xxx instanceof Array
arr = [1, 2, 3, 4, 5]
obj = {name: "dby", age: 23}
arr instanceof Array // true,即为数组
obj instanceof Array // false,即为对象
在 JS 中 Array 继承了 Object,所以数组也会被判断为对象,因此 xxx instanceof Object
无法区分数组和对象
arr = [1, 2, 3, 4, 5]
obj = {name: "dby", age: 23}
Array instanceof Object // true,数组继承了Obejct的原型对象
arr instanceof Object // true,数组也会被判断为对象
obj instanceof Object // true
通过上面的分析,有的同学应该会发现,instanceof判断数组和对象其实还是有限制的,xxx instanceof Array
其实只能区分数组,因为JS中有12种常见的内置对象Arguments, Boolean, Date, Error, Function, JSON, Math, Number, Array, Object, RegExp, String,这些对象全都继承了Object()的原型对象,而instanceof又会一直沿着原型链去查找,所以当xxx instanceof Array
返回false的时候,只能确定不是数组,但无法确定是不是形如{name: "dby", age: 23}
的对象。
String instanceof Object // true
Function instanceof Object // true
这里再补充一下instanceof
的原理,其实就是沿着原型链向上查找。
function new_instance_of(leftValue, rightValue) {
let rightProto = rightValue.protytype, // 获取构造函数原型对象
leftValue = leftValue__proto__; // 获取实例原型指针
while(leftValue !== null) {
if(leftValue === rightProto) {
return true; // 如果实例proto指向构造函数原型对象
}
leftValue = leftValue.__proto__; // 如果不是,就继续沿着原型链向上查找
}
return false; // 实例proto最终指向null,如果还是没有就返回false
}
3. 根据实例上的constructor指针判断
构造函数的prototype上有一个constructor指针,这个指针指回构造函数。
Array.prototype.constructor // ƒ Array() { [native code] }
Object.prototype.constructor // ƒ Object() { [native code] }
通过构造函数创建出来的实例对象上的__proto__指针会指向构造函数的prototype,因此通过__proto__可以访问到constructor,根据constructor就能判断这个实例是由哪个构造函数创建的。
const arr = [1, 2]
const obj = {a: 1}
arr.constructor == Array // true
arr.constructor == Object // false
obj.constructor == Object // true
obj.constructor == Array // false
需要注意的是,像上面这样xxx.constructor
的写法,等价于xxx.__proto__.constructor
,表示查找创建这个实例的构造函数的prototype,例如arr的构造函数是Array(),obj的构造函数是Object()。但是通过观察arr的原型会发现,arr的__proto__里面还有__proto__,如下图所示(太长了,中间省略):
第一个__proto__里的constructor指向构造函数Array,第二个__proto__里的constructor指向构造函数Object,这就说明构造函数Array实际上是继承了构造函数Object,所以在原型中可以访问到。当然这里我们只关心第一层__proto__里的constructor,如果要访问更里面的constructor,可以使用xxx.__proto__.proto__.constructor
。顺便这也解释了为什么arr instanceof Object
也会返回true的原因。
4. 直接尝试调用数组的操作方法
刚才观察了数组的原型对象,发现上面挂载了很多操作数组的属性方法,那么只要在变量上调用数组的操作方法,就能知道这个变量是不是数组,如下所示:
const a = {a: 1}
a.length // undefined,即对象上没有length属性
a.slice // undefined,即对象上没有slice方法
这里slice后面没加括号,函数不会执行,只会返回表达式,但是因为对象上没有这个方法,所以是undefined。为什么这边不让函数执行呢?下面是加了括号的结果:
a.slice() // Uncaught TypeError: a.slice is not a function
可以看到执行就报错了,提示方法不存在,所以当作一个属性去访问的时候不会报错。如果slice作用在数组上会先浅拷贝,并根据传入的参数分割数组。
5. 使用Object.prototype.toString.call()
使用toString()给对象转字符串的时候,发现返回的不是对象的内容,而是数据类型,如下所示:
const a = {a: 1}
a.toString() // "[object Object]"
Object.prototype.toString()方法其实返回一个对象的内部属性[[class]]
由此得到启发,只要让别的类型的数据也能调用对象上的这个方法,就可以判断类型了。实现这个很简单,改变Object.prototype.toString()这个函数的this指向就可以了,可以用call()方法来实现。这个功能非常强大,各种类型都能判断。
Object.prototype.toString.call() // "[object Undefined]"
Object.prototype.toString.call([1, 2]) // "[object Array]"
Object.prototype.toString.call(new Object()) // "[object Object]"
Object.prototype.toString.call(666) // "[object Number]"
Object.prototype.toString.call("666") // "[object String]"
Object.prototype.toString.call(/\s/g) // "[object RegExp]"
如果将对象的内容转为字符串,可以使用 JSON 序列化,即 JSON.stringify() 。