
JavaScript 中的值分类
这篇文章将介绍 JavaScript 中的值分类的方法:通过隐藏属性[[Class]]、 typeof
操作符、 instanceof
操作符和函数 Array.isArray()
。我们查看 JavaScript 对象内置构造方法的属性,这会产生意外的分类结果。
前置知识
为了深入讨论这个话题,让我们回顾一些前置知识。
基础类型与对象(object)
JavaScript 中的所有值都是基础类型或对象两者中的一个。
基础类型,下面这些就是基础类型:
- undefined
- null
- Booleans
- Numbers
- Strings
基础类型都是不可变的,你不能向它们添加属性:
var str = 'abc';
str.foo = 123; // 试着添加属性 "foo"
// -> 123
str.foo; // 没有改变
// -> undefined
基础类型之间的比较是值比较,如果它们相等的话,那么它们的内容相同:
'abc' === 'abc'; // true
对象,所有的非基础类型的值都是对象。对象是可变的:
var 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
不是如此:
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(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
这不同于对象转换为数字或字符串:如果对象包装这些基础值,那么转换的结果就是包装的原始值。
本文作者 Axel Rauschmayer,转载请注明来源链接:
原文链接:https://2ality.com/2013/01/categorizing-values.html
本文链接:https://tie.pub/2019/06/categorizing-values-in-javascript/