[译][removed]如何判断值的类型

原文链接:http://www.adobe.com/devnet/archive/html5/articles/categorizing-values-in-javascript.html

本文中,我将会解释JavaScript一共有几种类型的值,以及如何判断一个值的类型。这将有助于你更好地理解JavaScript是如何工作的。也能帮你实现更高级的编程任务,比如在需要处理各种不同类型的传入值的地方,写一个完善的库进行处理。有了本文的知识,你就能够避免因两个不同值之间存在细微的差别而引起的bug。

我将会给你展示四种判断值的类型的方法:通过隐藏属性[[Class]],通过typeof运算符,通过instanceof运算符,以及通过函数Array.isArray()。我还会解释在进行类型判断时,为什么内置构造函数的原型对象会有一些不可思议的分类结果。

1.回顾基础知识

 

  • 原始值和对象值
  • 包装对象类型
  • 内部属性
  • 术语:原型和原型对象的区别
  • constructor属性

2.判断值的类型

  • [[Class]]
  • typeof
  • instanceof
  • Array.isArray()

3.内置原型对象

  • Object.prototype
  • Function.prototype
  • Array.prototype
  • RegExp.prototype
  • Date.prototype
  • Number.prototype
  • String.prototype
  • Boolean.prototype

4.建议

  • 原型对象看作是其类型的原始成员
  • 使用哪种分类机制

 

5.接下来该学习什么

回顾基础知识

在开始文章的正式部分之前,你需要复习一些必要的知识

原始值和对象值

JavaScript中的所有值不是原始值就是对象值。

原始值:下面的值都是原始值

  • undefined
  • null
  • 布尔值
  • 数字
  • 字符串

原始值是不可变的。你不能给它们添加属性:

> 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)”很不幸的可能会有点歧义:

  1. 一方面,对象之间有“谁是谁的原型”这个关系。每个对象都有一个隐藏属性[[Prototype]],指向了自己的原型(可以是对象值或者原始值null)。一个对象的原型是该对象的延续。如果在该对象上访问一个属性,却没有找到,则会继续去它的原型上找。多个对象可能有相同的原型。
  2. 另一方面,如果一个对象obj是由一个构造函数Foo实例化的,那么构造函数Foo会有一个属性Foo.prototype,对象obj的原型就指向这个属性。

说的再清晰点,开发者们通常把(1)中对象的“[[prototypes]]”内部属性称之为原型,把(2)中函数的“prototype”属性称之为原型对象。有三个方法可以用来操作对象的原型:

  • Object.getPrototypeOf(obj)返回对象obj的原型:
> Object.getPrototypeOf({}) === Object.prototype
true
  • Object.create(proto)创建一个原型是proto的空对象:
> Object.create(Object.prototype)
{}

Object.create()还可以做更多的其他事,但已经超出本文的介绍范围了。

  • proto.isPrototypeOf(obj)返回true,如果proto是obj的原型(或者原型的原型,依次类推):
> Object.prototype.isPrototypeOf({})
true

"constructor"属性

给定一个构造函数Foo,它的原型对象Foo.prototype会有一个属性Foo.prototype.constructor,该属性的值又指向回Foo。对于大部分用户定义的函数来说,其prototype属性以及prototype属性constructor属性都会自动生成。(译者注:通过Function.prototype.bind创建的绑定函数没有原型对象,typeof function(){}.bind({}).prototype; //"undefined")

> function Foo() { }
> Foo.prototype.constructor === Foo
true
> RegExp.prototype.constructor === RegExp
true

构造函数的所有实例都会从其原型对象上继承constructor属性。因此,你可以用它来判断一个实例是由哪个构造函数创建的:

> 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"

在JavaScript代码中,唯一可以访问该属性的方法就是通过默认的toString()方法,通常是这样调用的:

Object.prototype.toString.call(value)

可能返回的值有:

  • “[object Undefined]”:如果值是undefined;
  • “[object Null]”:如果值是null;
  • “[object " + value.[[Class]] + "]”:如果值是一个对象;
  • “[object " + value.[[Class]] + "]”:如果值是一个原始值(它会被转换成对应的包装类型,从而返回和上面相同的结果);

例子:

> 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) {
    var str = Object.prototype.toString.call(x);
    return /^\[object (.*)\]$/.exec(str)[1];
}

运行上面的函数:

> getClass(null);
"Null"

> getClass({});
"Object"

>getClass([]);
"Array"

> (function () { return getClass(arguments) }())
"Arguments"

> function Foo() {}
> getClass(new Foo());
"Object"

typeof

typeof可以用来判断原始值的类型,以及区分对象值和原始值。语法如下:

typeof value

它会返回下面表1中的某个字符串:

表1.typeof返回的值
操作数的类型返回值
undefined“undefined”
null“object”
布尔值“boolean”
数字“number”
字符串“string”
函数“function”
其他“object”


typeof在操作null时会返回“object”,这是JavaScript语言本身的bug。不幸的是,永远不可能被修复了,因为太多已有的代码已经依赖了这样的表现。还需要注意的是,函数也是object类型,但typeof区分了它们。可数组却没有区分,也被视为object。所有这些怪异表现让判断一个值的类型是否是object变的复杂:

function isObject(x) {
    return x !== null && (typeof x === "object" || typeof x === "function");
}

instanceof

instanceof可以检测一个值是否是某个构造函数的实例:

value instanceof Type

该运算符会检查Type.prototype是否处于值value的原型链上。也就是说,如果你自己实现了一个instanceof函数,可以下下面这样(没有进行参数的类型检测,比如参数Type为null的情况下会出错):

function myInstanceof(value, Type) {
    return Type.prototype.isPrototypeOf(value);
}

如果是原始值,instanceof总会返回false:

> "" instanceof String
false
> "" instanceof Object
false

Array.isArray

Array.isArray()之所以存在,是因为在浏览器中:每个框架都有自己的全局运行环境。例如:框架A和框架B(无论谁包含谁),框架A中的代码可以向框架B中的代码船只。但框架B中的代码不能使用instanceof Array来检测框架A中传来的这个值是否是数组。因为B中的构造函数Array和A中的Array是不同的:

<!DOCTYPE html>
<html>
<head>
	<script type="text/javascript">
		function test(arr) {
			var 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 type="text/javascript">
var iframeWin = frames[0];
iframeWin.document.write('<script>window.parent.test([])</' + 'script>');
</script>
</body>
</html>

因此,ECMAScript 5才引入了Array.isArray(),它使用了内部属性[[Class]]来判断一个值是否是数组。可是,上面讲的问题不只针对于数组,其他的类型同样也有这个问题,但我们没有Function.isFunction()等等。

内置原型对象

内置类型的原型对象是很特殊的值:它们的行为很像是该类型的实例,但如果用instanceof检测的话,得出的结果是它们不是该类型的实例。其他的一些用来判断类型的方法也同样有问题。如果搞清楚了这是为什么,你就能更加深入地理解相关知识。

Object.prototype

Object.prototpe是一个空对象:它没有任何可迭代的自身属性:

> 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没有原型(译者注:也可以说,有原型,但值是null)。它是唯一一个内置的没有原型的对象(译者注:Object.create(null)也可以创建没有原型的对象)。

> Object.getPtototypeOf(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.ptototype不是Function的实例。

> Function.prototype instanceof Function
false

这是因为Function.prototype的原型链中没有它自身。它的上一级原型是Object.prototype(译者注:和Object一样,必须这么规定,否则原型链会有死循环):

> 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是一个空数组:

> Array.prototype
[]
> Array.prototype.length
0

[[Class]]内部属性也判断它为数组:

> getClass(Array.prototype)
"Array"

Array.isArray()也同样,因为它本来就是基于[[Class]]实现的:

Array.isArray(Array.prototype);
true

但是,instanceof不行:

> Array.prototype instanceof Array
false

为了减少重复语句,在文章剩余下部分中,我不会再提到“原型对象不是自身类型的实例”了。

RegExp.prototype

RegExp.prototype是一个可以匹配任意字符串的正则:

> RegExp.prototype.test("abc");
true
> RegExp.prototype.test("");
true

[[Class]]的值为“RegExp”

> 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也是一个Date对象:

> getClass(new Date());
'Date'
> getClass(Date.prototype)
'Date'

Date对象包装的是一个数字。引用自ECMAScript5.1规范:

一个Date对象包含了一个数字,该数字表示了一个特定时间点的时间,单位为毫秒。这个数字称之为时间值。一个时间值也可以是NaN,表明这个Date对象不表示一个特定时间点的时间。

ECMAScript中的时间是用从协调世界时1970年1月1日开始的毫秒数来表示的。
两种常用的获取时间值的方法是通过调用其valueOf方法或者将该日期对象强制转换为数字:

> var d = new Date();    //现在的时间

> d.valueOf()
1347035199049
> Number(d)
1347035199049

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("")
''

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

这和对象转换为数字和字符串的表现不同。如果一个对象包装了数字或字符串类型的原始值,转换的结果就是那些被包装的原始值。

建议

下面是针对在JavaScript中如何判断一个值的类型的建议。

将原型对象看作是其类型的原始成员

原型对象总是其类型的原始成员吗?不是的,只有内置类型是这样的。更有用的想法是把它们看成是模拟类的东西:它们的属性会被所有的实例继承(通常是方法)。

使用哪种分类机制

在考虑使用哪种分类机制的时候,你要先想想需要处理的值是否可能来自其他框架。

如果没这个问题的话,则使用typeof和instanceof,没必要使用[[Class]]和Array.isArray()。但你必须熟悉typeof的怪异表现:null被认为是一个“object”以及存在两个非原始值的类型“object”和“function”。例如,下面是一个用来判断是否是对象值的函数。

function isObject(v) {
    return (typeof v === "object" && v !== null) || typeof v === "function";
}

尝试使用这个方法:

> isObject({});
true
> isObject([]);
true

> isObject("");
false
> isObject(undefined);
false

如果你写的代码可能接受来自其他框架的值,那么instanceof并不适合。你必须考虑使用[[Class]]或者Array.isArray()。另一个替代办法是使用一个对象的constructor属性的值,但这并不是一个靠得住的解决方案,因为并不是所有的对象都记录着自己的构造函数,也并不是所有的构造函数都有名字,另外还有命名冲突的风险。下面的函数演示了如何获取一个对象的构造函数的名字:

function getConstructorName() {
    if(obj.constructor && obj.constructor.name)
    {
        return obj.constructor.name;
    } else {
        return "";
    }
}

另一个需要指出的是:函数的name属性(比如obj.constructor.name)并不是标准的一部分,例如IE就不支持。尝试运行一下:

> getConstructorName({})
'Object'
> getConstructorName([])
'Array'
> getConstructorName(/abc/)
'RegExp'

> function Foo() {}
> getConstructorName(new Foo())
'Foo'

如果将一个原始值传给getConstructorName(),你会得到该原始值对应的包装类型:

> getConstructorName("")
'String'

这是因为原始值从临时生成的包装对象上获取到了constructor属性:

> "".constructor === String.prototype.constructor
true

通过本文,你已经学习了如何在JavaScript中判断一个值的类型。但不幸的是,你必须非常了解这部分知识的细节才能很好的完成这项任务,因为有两个相关的运算符是有缺陷的:typeof的奇怪表现(比如在操作null时返回“object”)以及instanceof不能识别其他框架中的对象。本文也讲了如何处理这种缺陷。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值