JavaScript 中的值分类
这篇文章将介绍 JavaScript 中的值分类的方法:通过隐藏属性[[Class]]、 typeof
操作符、 instanceof
操作符和函数 Array.isArray()
。我们查看 JavaScript 对象内置构造方法的属性,这会产生意外的分类结果。
前置知识
为了深入讨论这个话题,让我们回顾一些前置知识。
基础类型与对象(object)
JavaScript 中的所有值都是基础类型或对象两者中的一个。
基础类型,下面这些就是基础类型:
- undefined
- null
- Booleans
- Numbers
- Strings
基础类型都是不可变的,你不能向它们添加属性:
const str = 'abc'
str.foo = 123 // 试着添加属性 "foo"
// -> 123
str.foo // 没有改变
// -> undefined
基础类型之间的比较是值比较,如果它们相等的话,那么它们的内容相同:
'abc' === 'abc' // true
对象,所有的非基础类型的值都是对象。对象是可变的:
const obj = {}
obj.foo = 123 // 试着添加属性 "foo"
// -> 123
obj.foo // 属性 "foo" 被添加
// -> 123
对象是引用比较。每一个对象都拥有自己的标识,如果两个对象是真实的同一个,那么它们相同:
{} === {}; // false
var obj = {};
obj === obj; // true
包装对象类型,基础类型布尔(boolean)、数值(number)和字符串(string) 有对应的包装对象类型 Boolean
、Number
和 String
。后者的实例是对象,且基础类型不同于它们的包装对象:
typeof new String('abc') // 'object'
typeof 'abc' // 'string'
new String('abc') === 'abc' // false
包装对象很难直接使用,但是它们的原型对象是基础类型的方法。举例来说,String.prototype
是包装对象 String
的原型对象,所有的方法对字符串也是有效的。比如包装方法 String.prototype.indexOf
。基础字符串拥有相同的方法。不是字面上的相同方法,确实是相同的方法:
String.prototype.indexOf === ''.indexOf // true
内部属性
内部属性是 JavaScript 中不能直接访问的属性,但会影响它如何工作。内部属性的名字开始大写字母且包含在双中括号内。举例来说,[[Extensible]] 表示一个布尔标签代表着确定是否可以添加属性到一个对象。它的值可以间接操作:通过 Object.isExtensible()
读取它的值,通过 Object.preventExtensions()
设置它的值为 false
。一旦它的值为 false
,就没有办法改变它的值为 true
。
术语:原型与原型对象
在 JavaScript 中,术语 prototype(原型) 有一点设计过重:
- 一方面,对象之间存在原型联系。每个对象都有隐含属性[[Prototype]]指向它的 prototype(原型) 或者
null
。原型是对象的延续。如果引用对象的属性却没有找到,将继续搜索前者(即在原型上查找)。不同的对象可以有相同的原型。 - 另一方面,如果一个类型是构造方法
Foo
的实例,那么该构造方法拥有原型对象Foo.prototype
的属性。
为了对比,我们称为“原型”和“原型对象”。有三个方法帮助我们处理原型:
- 方法
Object.getPrototypeOf(obj)
返回对象obj
的原型:
Object.getPrototypeOf({}) === Object.prototype // true
- 方法
Object.create(proto)
创建一个原型为proto
的空对象。
Object.create(Object.prototype) // {}
- 方法
proto.isPrototypeOf(obj)
是用来确定对象proto
是否是对象obj
的原型。如果proto
是obj
的原型(或原型的原型),方法proto.isPrototypeOf(obj)
返回true
。
Object.prototype.isPrototypeOf({}) // true
属性“constructor”(构造方法)
给一个构造方法 Foo
,原型对象 Foo.prototype
有一个指向 Foo
的属性 Foo.prototype.constructor
,它为每个功能函数默认设置该属性。
function Foo() {}
Foo.prototype.constructor === Foo // true
RegExp.prototype.constructor === RegExp // true
构造函数的所有实例继承了原型对象的属性。然后,我们可以使用它查明哪个构造函数创建了实例:
new Foo().constructor // [Function: Foo]
;/abc/.constructor // [Function: RegExp]
值分类
让我们看看四种分类值的方法:
- [[Class]] 是一个内部属性,表示将对象分类的一个字符串;
typeof
是一个对基础类型分类的操作符,并帮助我们区别对象;instanceof
是对对象进行分类的操作符;Array.isArray()
是用来判断值是否是数组的函数。
[[Class]] 属性
[[Class]] 是一个值为下面字符串的内部属性:
“Arguments”, “Array”, “Boolean”, “Date”, “Error”, “Function”, “JSON”, “Math”, “Number”, “Object”, “RegExp”, “String”
通过默认方法 toString()
是 JavaScript 代码中唯一访问它的方式,方法 Object.prototype.toString()
是通用的且返回:
- “[object Undefined]” 如果
this
是undefined
, - “[object Null]” 如果
this
是null
, - “[object ” + obj.[[Class]] + ”]” 如果
this
是一个对象obj
. - 基础类型被转换为对象,就像之前的规则一样处理。
举例:
Object.prototype.toString.call(undefined)
// -> '[object Undefined]'
Object.prototype.toString.call(Math)
// -> '[object Math]'
Object.prototype.toString.call({})
// -> '[object Object]'
于是,下面的函数返回值 x
的 [[Class]] 属性:
function getClass(x) {
const str = Object.prototype.toString.call(x)
return /^\[object (.*)\]$/.exec(str)[1]
}
下面是该函数的应用:
getClass(null) // 'Null'
getClass({}) // 'Object'
getClass([]) // 'Array'
getClass(JSON) // 'JSON'
;(function () {
return getClass(arguments)
})() // 'Arguments'
function Foo() {}
getClass(new Foo()) // 'Object'
typeof 操作符
操作符 typeof
可以分类基础类型,并且帮助我们来区别基础类型和对象。
typeof value
起决于操作值 value
,操作返回下面字符串中的一个:
操作值 | 结果 |
---|---|
undefined | ”undefined” |
null | ”object” |
Boolean 值 | ”boolean” |
Number 值 | ”number” |
String 值 | ”string” |
Function 值 | ”function” |
所有其它值 | ”object” |
typeof
操作 null
返回 "object"
是一个 bug。这无法被修复,因为修复意味着破话现有代码。注意,函数也是对象,但是 typeof
区分了它们。另一方面,数组却被它视为对象。
instanceof 操作符
instanceof
检查值是否是一个类型的实例:
value instanceof Type
该操作符查看 Type.prototype
并且检查谁是 value
的原型。如果我们自己实现接口 instanceof
,它看起来就像这样(去除一些错误检查,例如类型是 null
):
function myInstanceof(value, Type) {
return Type.prototype.isPrototypeOf(value)
}
对基础类型使用 instanceof
总是返回 false
:
'' instanceof String // false
'' instanceof Object // false
Array.isArray()
Array.isArray()
的出现是因为浏览器的一个特殊问题:每一个 frame
都有自己的全局环境。举一个例子:给定 frame
A 和 frame
B(它们在同一个页面)。frame
A 可以添加一个值到 frame
B 的代码中。然后 B 的代码不能使用 instanceof Array
来检查这个值是否是一个数组,因为它是 B 的数组不同于 A 的数组。例子:
<html>
<head>
<script>
// test() is called from the iframe
function test(arr) {
const iframeWin = frames[0]
console.log(arr instanceof Array) // false
console.log(arr instanceof iframeWin.Array) // true
console.log(Array.isArray(arr)) // true
}
</script>
</head>
<body>
<iframe></iframe>
<script>
// Fill the iframe
const iframeWin = frames[0]
iframeWin.document.write('<script>window.parent.test([])</' + 'script>')
</script>
</body>
</html>
因此,ECMAScript 5 推出了使用 [[Class]] 来确定值是否是数组的 Array.isArray()
函数。意图使 JSON.stringify()
变得安全。但是所有的类型与 instanceof
结合使用时依然存在相同的问题。
内置的原型对象
内置类型中的原型对象是特殊值:它们是类型的原始成员,却不是它的实例。这使得分类变得古怪。通过了解这些怪异,我们能够更深了解类型分类。
Object.prototype
Object.prototype
就像一个空对象:他没有任何可枚举的自有属性(它的方法都是不可枚举的)。
Object.prototype // {}
Object.keys(Object.prototype) // []
出乎意料的是,Object.prototype
是一个对象,但却不是 Object
的实例。一方面,typeof
和 [[Class]] 都分出它是一个对象:
getClass(Object.prototype) // 'Object'
typeof Object.prototype // 'object'
另一方面,instanceof
不认为它是 Object
的实例:
Object.prototype instanceof Object // false
如果为了使上面的结果为 true
,Object.prototype
将必须在它自己的原型链上。但是那将造成链的循环,这就是为什么 Object.prototype
没有原型。它是唯一没有原型的内置对象。
Object.getPrototypeOf(Object.prototype) // null
这种悖论适用于所有内置原型对象:除了 instanceof
之外,它们被所有机制视为其类型的实例。
预期,[[Class]],typeof
和 instanceof
在大多数其它对象上达成一致:
getClass({}); // 'Object'
typeof {}; // object'
{} instanceof Object; // true
Function.prototype
Function.prototype
本身就是一个函数。它接受任何参数并返回 undefined
:
Function.prototype('a', 'b', 1, 2) // undefined
出乎意料,Function.prototype
是一个函数,但不是 Function
的实例:一方面,typeof
检查内置方法 [[Call]] 是否存在,说明 Function.prototype
是一个函数:
typeof Function.prototype // 'function'
[[Class]] 属性也说明相同的事:
getClass(Function.prototype) // 'Function'
另一方面,instanceof
说明 Function.prototype
不是 Function
的实例:
Function.prototype instanceof Function // false
那是因为它的原型链中没有 Function.prototype
。相反,它的原型是 Object.prototype
:
Object.getPrototypeOf(Function.prototype) === Object.prototype
// -> true
预期,使用其它方法,它们没有意外:
typeof function () {}; // 'function'
getClass(function () {}); // 'Function'
function () {} instanceof Function; // true
Function
也是一个函数:
typeof Function // 'function'
getClass(Function) // 'Function'
Function instanceof Function // true
Array.prototype
Array.prototype
本身是一个空数组:它以这种方法显示且长度为 0。
Array.prototype // []
Array.prototype.length // 0
[[Class]] 也证明它是数组:
getClass(Array.prototype) // 'Array'
也可以使用基于 [[Class]] 的函数 Array.isArray()
:
Array.isArray(Array.prototype) // true
当然,instanceof
不是如此:
// eslint-disable-next-line unicorn/no-instanceof-array
Array.prototype instanceof Array // false
在本章节的其余部分中,我们不再提及原型对象不是其类型的实例。
RegExp.prototype
RegExp.prototype
是一个匹配所有内容的正则表达式:
RegExp.prototype.test('abc') // true
RegExp.prototype.test('') // true
String.prototype.match
也接受 RegExp.prototype
,它通过 [[Class]] 检查其参数是否是正则表达式。对于正则表达式和原型对象,该检查是令人信服的:
getClass(/abc/) // 'RegExp'
getClass(RegExp.prototype) // 'RegExp'
RegExp.prototype
等价于“空正则表达式”。这种表达式有两种创建方式:
new RegExp('') // 通过构造方法方式
;/(?:)/ // 字面量方式
你应该只用 RegExp
构造方法创建动态正则表达式。通过文字表达空的正则表达式很复杂,因为你不能使用 //
来描述它。空的非捕获组 (?:)
的行为与空的正则表达式相同:它匹配所有内容,并且不会在匹配中创建捕获。
new RegExp('').exec('abc')
// -> [ '', index: 0, input: 'abc' ]
;/(?:)/.exec('abc')
// -> [ '', index: 0, input: 'abc' ]
对比:一个空的组不仅匹配索引 0,而且捕获了位于索引 1 的第一组:
/()/.exec("abc")
// ->
[ '', // index 0
'', // index 1
index: 0,
input: 'abc' ]
有趣的是,通过构造函数创建的空正则表达式和 RegExp.prototype
两者都显示为空的字面量:
new RegExp('') // /(?:)/
RegExp.prototype // /(?:)/
Date.prototype
Date.prototype
也是一个日期类型:
getClass(new Date()) // 'Date'
getClass(Date.prototype) // 'Date'
日期包装数字。这里引用ECMAScript 5.1 规范:
- Date 对象包含一个数字,表示一个毫秒内的特定时刻。这样的数字称为时间值。时间值也可以是
NaN
,表示 Date 对象不表示特定的时刻。 - ECMAScript 规定时间是自 UTC 时间 1970 年 1 月 1 日 00:00:00 起,以毫秒为单位。
访问时间值的两种常用方法是调用方法 valueOf
或通过将日期强制转换为数字:
const d = new Date() // now
d.valueOf() /// 1347035199049
Number(d) // 1347035199049
另外可以使用 getTime()
和 Date.now()
函数来获取时间值,Date.now()
函数返回自 1970 年 1 月 1 日 00:00:00 UTC 到当前时间的毫秒数。
const now = new Date()
now.getTime() // 1560500640506
Date.now() // 1560500676490
Date.prototype
的时间值是 NaN
:
Date.prototype.valueOf() // NaN
Number(Date.prototype) // NaN
Date.prototype
显示的是无效的时间,可以通过 NaN
创建相同的时间:
Date.prototype // Invalid Date
new Date(Number.NaN) // Invalid Date
Number.prototype
Number.prototype
与 new Number(0)
大约相同:
Number.prototype.valueOf() // 0
当转换为数字将返回包装的原始值:
;+Number.prototype // 0
对比:
;+new Number(0) // 0
String.prototype
String.prototype
与 new String('')
大约相同:
String.prototype.valueOf() // ''
当转换为数字将返回包装的原始值:
`${String.prototype}` // ''
对比:
`${new String(0)}` // ''
Boolean.prototype
Boolean.prototype
与 new Boolean(false)
大约相同:
Boolean.prototype.valueOf() // false
布尔对象(Boolean)可以强制转换为布尔值,但是结果总是 true
,那是因为任何对象转换为布尔值都是 true
。
!!Boolean.prototype // true
!!new Boolean(false) // true
!!new Boolean(true) // true
这不同于对象转换为数字或字符串:如果对象包装这些基础值,那么转换的结果就是包装的原始值。
文章由 吳文俊 翻译,原文地址 Categorizing values in JavaScript,转载请注明来源。