对象
0 Views 前端 with
本文字数:2,795 字 | 阅读时长 ≈ 10 min

对象

0 Views 前端 with
本文字数:2,795 字 | 阅读时长 ≈ 10 min

JavaScript中的对象有字面形式(比如 var a = { .. })和构造形式(比如 var a = new Array(..))。字面形式更常用,不过有时候构造形式可以提供更多选项。

语法

对象可以通过两种形式定义: 声明(文字) 形式和构造形式。
对象的文字语法大概是这样:

1
2
3
4
var myObj = {
key: value
// ...
};

构造形式大概是这样:

1
2
var myObj = new Object();
myObj.key = value;

构造形式和文字形式生成的对象是一样的。 唯一的区别是, 在文字声明中你可以添加多个键 / 值对, 但是在构造形式中你必须逐个添加属性。

类型

对象是 JavaScript 的基础。 在 JavaScript 中一共有六种主要类型(术语是“语言类型”)

注意, 简单基本类型(string、 boolean、 number、 null 和 undefined) 本身并不是对象。null 有时会被当作一种对象类型, 但是这其实只是语言本身的一个 bug, 即对 null 执行typeof null 时会返回字符串 “object”。 实际上, null 本身是基本类型。

有一种常见的错误说法是“JavaScript 中万物皆是对象”, 这显然是错误的。

实际上, JavaScript 中有许多特殊的对象子类型, 我们可以称之为复杂基本类型。函数就是对象的一个子类型(从技术角度来说就是“可调用的对象”)。 JavaScript 中的函数是“一等公民”, 因为它们本质上和普通的对象一样(只是可以调用), 所以可以像操作其他对象一样操作函数(比如当作另一个函数的参数)。

数组也是对象的一种类型, 具备一些额外的行为。 数组中内容的组织方式比一般的对象要稍微复杂一些。

内置对象

JavaScript 中还有一些对象子类型, 通常被称为内置对象。

这些内置对象从表现形式来说很像其他语言中的类型(type) 或者类(class), 比如 Java 中的 String类。

但是在 JavaScript 中, 它们实际上只是一些内置函数。 这些内置函数可以当作构造函数(由 new 产生的函数调用——参见第 2 章) 来使用, 从而可以构造一个对应子类型的新对象。 举例来说:

1
2
3
4
5
6
7
8
var strPrimitive = "I am a string"; 
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
var strObject = new String( "I am a string" );
typeof strObject; // "object"
strObject instanceof String; // true
// 检查 sub-type 对象
Object.prototype.toString.call( strObject ); // [object String]

从代码中可以看到, strObject 是由 String 构造函数创建的一个对象。

原始值 “I am a string” 并不是一个对象, 它只是一个字面量, 并且是一个不可变的值。如果要在这个字面量上执行一些操作, 比如获取长度、 访问其中某个字符等, 那需要将其转换为 String 对象

幸好, 在必要时语言会自动把字符串字面量转换成一个 String 对象, 也就是说你并不需要显式创建一个对象

思考下面的代码:

1
2
3
var strPrimitive = "I am a string";
console.log( strPrimitive.length ); // 13
console.log( strPrimitive.charAt( 3 ) ); // "m"

使用以上两种方法, 我们都可以直接在字符串字面量上访问属性或者方法, 之所以可以这样做, 是因为引擎自动把字面量转换成 String 对象, 所以可以访问属性和方法。

同样的事也会发生在数值字面量上, 如果使用类似 42.359.toFixed(2) 的方法, 引擎会把对象转换成
new Number(42)。 对于布尔字面量来说也是如此。

null 和 undefined 没有对应的构造形式, 它们只有文字形式。 相反, Date 只有构造, 没有文字形式

对于 Object、 Array、 Function 和 RegExp(正则表达式) 来说, 无论使用文字形式还是构造形式, 它们都是对象, 不是字面量。 在某些情况下, 相比用文字形式创建对象, 构造形式可以提供一些额外选项。 由于这两种形式都可以创建对象, 所以我们首选更简单的文字形式。 建议只在需要那些额外选项时使用构造形式。

Error 对象很少在代码中显式创建, 一般是在抛出异常时被自动创建。 也可以使用 new Error(..) 这种构造形式来创建, 不过一般来说用不着。

内容

对象的内容是由一些存储在特定命名位置的(任意类型的) 值组成的,我们称之为属性。

需要强调的一点是, 当我们说“内容” 时, 似乎在暗示这些值实际上被存储在对象内部,但是这只是它的表现形式。 在引擎内部, 这些值的存储方式是多种多样的, 一般并不会存在对象容器内部。 存储在对象容器内部的是这些属性的名称, 它们就像指针(从技术角度来说就是引用) 一样, 指向这些值真正的存储位置。

思考下面的代码:

1
2
3
4
5
var myObject = {
a: 2
};
myObject.a; // 2
myObject["a"]; // 2

如果要访问 myObject 中 a 位置上的值, 我们需要使用 . 操作符或者 [] 操作符。 .a 语法通常被称为“属性访问”, [“a”] 语法通常被称为“键访问”。 实际上它们访问的是同一个位置, 并且会返回相同的值 2, 所以这两个术语是可以互换的。

这两种语法的主要区别在于 . 操作符要求属性名满足标识符的命名规范, 而 [“..”] 语法可以接受任意 UTF-8/Unicode 字符串作为属性名。 举例来说, 如果要引用名称为 “SuperFun!” 的属性,那就必须使用 [“Super-Fun!”] 语法访问, 因为 Super-Fun! 并不是一个有效的标识符属性名

此外, 由于 [“..”] 语法使用字符串来访问属性, 所以可以在程序中构造这个字符串, 比如说:

1
2
3
4
5
6
7
8
9
var myObject = {
a:2
};
var idx;
if (wantA) {
idx = "a";
}
// 之后
console.log( myObject[idx] ); // 2

在对象中, 属性名永远都是字符串。 如果你使用 string(字面量) 以外的其他值作为属性
名, 那它首先会被转换为一个字符串。 即使是数字也不例外, 虽然在数组下标中使用的的确是数字, 但是在对象属性名中数字会被转换成字符串, 所以当心不要搞混对象和数组中数字的用法:

1
2
3
4
5
6
7
var myObject = { };
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"

属性与方法

如果访问的对象属性是一个函数, 有些开发者喜欢使用不一样的叫法以作区分。 由于函数很容易被认为是属于某个对象, 在其他语言中, 属于对象(也被称为“类”) 的函数通常被称为“方法”, 因此把“属性访问” 说成是“方法访问” 也就不奇怪了。

有意思的是, JavaScript 的语法规范也做出了同样的区分。

从技术角度来说, 函数永远不会“属于” 一个对象, 所以把对象内部引用的函数称为“方法” 似乎有点不妥。

确实, 有些函数具有 this 引用, 有时候这些 this 确实会指向调用位置的对象引用。 但是这种用法从本质上来说并没有把一个函数变成一个“方法”, 因为 this 是在运行时根据调用位置动态绑定的, 所以函数和对象的关系最多也只能说是间接关系。

无论返回值是什么类型, 每次访问对象的属性就是属性访问。 如果属性访问返回的是一个函数, 那它也并不是一个“方法”。 属性访问返回的函数和其他函数没有任何区别(除了可能发生的隐式绑定 this, 就像我们刚才提到的)。

数组

数组也支持 [] 访问形式, 不过就像我们之前提到过的, 数组有一套更加结构化的值存储机制(不过仍然不限制值的类型)。 数组期望的是数值下标, 也就是说值存储的位置(通常被称为索引) 是整数, 比如说 0 和 42:

1
2
3
4
var myArray = [ "foo", 42, "bar" ];
myArray.length; // 3
myArray[0]; // "foo"
myArray[2]; // "bar"

数组也是对象, 所以虽然每个下标都是整数, 你仍然可以给数组添加属性:

1
2
3
4
var myArray = [ "foo", 42, "bar" ];
myArray.baz = "baz";
myArray.length; // 3
myArray.baz; // "baz"

可以看到虽然添加了命名属性(无论是通过 . 语法还是 [] 语法), 数组的 length 值并未发生变化。

注意: 如果你试图向数组添加一个属性, 但是属性名“看起来” 像一个数字, 那它会变成一个数值下标(因此会修改数组的内容而不是添加一个属性)

1
2
3
4
var myArray = [ "foo", 42, "bar" ];
myArray["3"] = "baz";
myArray.length; // 4
myArray[3]; // "baz"

遍历

for..in 循环可以用来遍历对象的可枚举属性列表(包括 [[Prototype]] 链)。 但是如何遍历属性的值呢?

对于数值索引的数组来说, 可以使用标准的 for 循环来遍历值:

1
2
3
4
5
var myArray = [1, 2, 3];
for (var i = 0; i < myArray.length; i++) {
console.log( myArray[i] );
} /
/ 1 2 3

这实际上并不是在遍历值, 而是遍历下标来指向值, 如 myArray[i]。

ES5 中增加了一些数组的辅助迭代器,包括 forEach(..)、 every(..) 和 some(..)。 每种辅助迭代器都可以接受一个回调函数并把它应用到数组的每个元素上,唯一的区别就是它们对于回调函数返回值的处理方式不同。

使用 for..in 遍历对象是无法直接获取属性值的,因为它实际上遍历的是对象中的所有可枚举属性,你需要手动获取属性值。这在ES6中新增了方法

小结

JavaScript中的对象有字面形式(比如 var a = { .. })和构造形式(比如 var a = new Array(..))。字面形式更常用,不过有时候构造形式可以提供更多选项。

许多人都以为“JavaScript中万物都是对象”,这是错误的。对象是6个(或者是7个,取决于你的观点)基础类型之一。对象有包括 function 在内的子类型,不同子类型具有不同的行为,比如内部标签 [object Array]表示这是对象的子类型数组。

Sep 02, 2018