本文深入解析JavaScript中原始值与对象的根本区别,涵盖可变性、引用机制及包装类型陷阱。详细阐述typeof与instanceof运算符的行为差异,特别是null的typeof bug、函数与数组的对象属性,以及原始值方法调用的原型链原理。
JavaScript 值分类:原始值与对象
尽管许多编程语言宣称“一切皆是对象”,但在 JavaScript 中,并非所有的值都是对象。
在 JavaScript 中,值主要分为两大类:原始值(primitive)和对象(object)。
JavaScript 对这两种值的定义如下:
以下类型的值属于原始值:
1. 字符串 2. 数字:在 JavaScript 中,所有数字均为浮点数 3. 布尔值 4. null 5. undefined
其余所有值均属于对象(object)。对象还可以进一步细分:
1. 原始值的包装器:Boolean, Number, String。这些通常很少直接使用。
2. 使用字面量创建的对象。以下字面量会生成对象,同时也可以通过构造函数创建对象。您可以使用字面量来创建对象。
• [] 等价于 new Array() • {} 等价于 new Object() • function() {} 等价于 new Function() • /\s*/ 等价于 new RegExp("\\s*") 3. 日期对象:new Date("2011-12-24")
我们可以通过枚举原语和定义对象来区分原语和对象。但也可以从定义的角度来描述原语和对象。让我们先从对象开始讨论。
对象的可变性与引用机制
1. 对象是可变的:
代码如下:
> var obj = {};
> obj.foo = 123; // 添加属性和值
123
> obj.foo // 读取属性,返回属性的值
123
2. 每个对象都有自己唯一的标识符,因此通过字面量或构造函数创建的对象与任何其他对象都不相等,我们可以通过 === 进行比较。
代码如下:
> {} === {}
false
对象是通过引用来比较的,只有当两个对象具有相同的标识时,才认为这两个对象相等。
代码如下:
> var obj = {};
> obj === obj
true
3. 变量保存的是对象的引用,因此如果两个变量引用了相同的对象——当我们修改其中一个变量的属性时,另一个变量也会随之改变。
代码如下:
> var var1 = {};
> var var2 = var1;
> var1.foo = 123; // 修改变量 var1 的属性
123
> var2.foo // var2 也发生了改变
123
原始值的不可变性与值比较
正如预期的那样,原始值和对象的行为不同:
1. 原始值是不可变的;你不能给它们添加属性:
代码如下:
> var str = "abc";
> str.foo = 123; // 尝试添加属性(此操作将被忽略)
123
> str.foo // 读取属性的值,返回 undefined
undefined
2. 原始值没有内部标识,原始值是按值进行比较的:比较两个原始值的依据是它们的内容,如果两个原始值的内容相同,则认为这两个原始值相同。
代码如下:
> "abc" === "abc"
true
这意味着,一个原始值的标识就是它的值,JavaScript 引擎不会为原始值分配唯一标识。
最后两个事实结合起来的意思是:我们无法区分一个变量到底是对象的引用,还是原始值的副本。
原始值与包装类型的陷阱
陷阱:原始值和它们的包装类型 规则:尽可能忽略包装类型。在其他编程语言如 Java 中,你很少会注意到它们。
原始值类型 boolean, number 以及 string 都有自己对应的包装类型 Boolean, Number 和 String。包装类型的实例都是对象值,两种类型之间的转换也很简单:
• 转换为包装类型:new String("abc") • 转换为原始类型:new String("abc").valueOf() 原始值类型以及它们相应的包装器类型有很多不同点,例如:
代码如下:
> typeof "abc"
'string'
> typeof new String("abc")
'object'
> "abc" instanceof String
false
> new String("abc") instanceof String
true
> "abc" === new String("abc")
false
包装类型的实例是一个对象,因此和 JavaScript 中的对象一样,包装类型也无法进行值的比较(只能比较引用)。
代码如下:
> var a = new String("abc");
> var b = new String("abc");
> a == b
false // 虽然 a 和 b 有相同的内容,但是依然返回 false
> a == a
true
原始值的方法调用与原型链
原始值没有自己的方法 包装对象类型很少被直接使用,但它们的原型对象定义了许多其对应的原始值也可以调用的方法。例如,String.prototype 是包装类型 String 的原型对象。它的所有方法都可以使用在字符串原始值上。包装类型的方法 String.prototype.indexOf 在字符串原始值上也有,它们并不是两个拥有相同名称的方法,而的确就是同一个方法:
代码如下:
> "abc".charAt === String.prototype.charAt
true
在数字的包装类型 Number 的原型对象有 toFixed 方法,即 Number.prototype.toFixed,但是当我们写如下代码时却发生错误:
代码如下:
> 5.toFixed(3)
SyntaxError: Unexpected token ILLEGAL
此错误是解析错误(SyntaxError),5 后面跟着一个点号(.),这个点被当作了小数点,而小数点后面应该是一个数,以下代码可以正常运行:
代码如下:
> (5).toFixed(3)
"5.000"
> 5..toFixed(3)
"5.000"
使用 typeof 和 instanceof 进行分类
值的分类:typeof 和 instanceof 如果你想要对值进行分类,你需要注意原始值和对象之间的区别。typeof 运算可以用来区分原始值和对象。instanceof 可以用来区分对象,而且,instanceof 对于所有的原始值都返回 false。
typeof typeof 可以用来判断原始值的类型,以及区分对象值和原始值:
代码如下:
> typeof "abc"
'string'
> typeof 123
'number'
> typeof {}
'object'
> typeof []
'object'
typeof 返回以下字符串:
| 参数 | 结果 |
|---|---|
| undefined | "undefined" |
| null | "object" |
| 布尔值 | "boolean" |
| 数字 | "number" |
| 字符串 | "string" |
| 函数 | "function" |
| 其他 | "object" |
注释:
• typeof 在操作 null 时会返回 "object",这是 JavaScript 语言本身的 bug。不幸的是,这个 bug 永远不可能被修复了,因为太多已有的代码已经依赖了这样的表现。这并不意味着,null 实际上就是一个对象[4] 。
• typeof 还可以让检查一个变量是否已声明,而不会抛出异常。没有任何一个函数可以实现此功能,因为你不能把一个未声明的变量传递给函数的参数。
代码如下:
> typeof undeclaredVariable
'undefined'
> undeclaredVariable
ReferenceError: undeclaredVariable is not defined
• 函数也是对象类型;这可能是很多人无法理解的,但有时候却是非常有用的。
• 数组是一个对象。
更多关于 typeof 的信息 [5] 和 [6]。
instanceof instanceof 可以检测一个值是否是某个构造函数的实例:
代码如下:
value instanceof Constructor
如果上面的表达式返回 true,则表示 value 是 Constructor 的一个实例。它等价于:
代码如下:
Constructor.prototype.isPrototypeOf(value)
大多数对象是 Object 的实例,因为原型链的末端(prototype chain)是 Object.prototype。原始值不是任何对象的实例:
代码如下:
> "abc" instanceof Object
false
> "abc" instanceof String
false