对象
语法
对象可以通过两种形式定义:声明(文字)形式和构造形式。
文字形式:
var myObj = {
key: value
// ...
};
构造形式:
var myObj = new Object();
myObj.key = value;
构造形式和文字形式生成的对象是一样的。唯一的区别是,在文字形式中可以添加多个键值对,但是在构造形式中必须逐个添加属性。
类型
对象是 JavaScript 的基础。
简单基本类型:
- string
- number
- boolean
- null
- undefined
- object
【注意】:
- 简单基本类型本身并不是对象。
- null 有时会被当作一种对象类型,但是这其实只是语言本身的一个 bug。实际上,null 本身是基础类型。
【关于注意中的第二点的原理】:
不同的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判断为 object 类型,null 的二进制表示是全 0,自然前三位也是 0,所以执行 typeof 时会返回“object”。
【错误纠正】:常见的错误说法“JavaScript 中万物皆是对象”。实际上,JavaScript 中有许多特殊的对象子类型,可以称之为复杂基本类型。函数就是对象的一个子类型,从技术角度来说是“可调用的对象”。在 JavaScript 中,函数是“一等公民”,因为它们本质上和普通的对象一样(只是可以调用),所以可以像操作其他对象一样操作函数。数组也是对象的一种类型,具备一些额外的行为。数组中内容的组织方式比一般的对象要稍微复杂一些。
内置对象
JavaScript 中还有一些对象子类型,通常被称为内置对象。
- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Error
这些内置对象从表现形式来说很像其他语言中的类型(type)或者类(class),比如 Java 中的 String 类。但是在 JavaScript 中,它们实际上只是一些内置函数。这些内置函数可以当作构造函数来使用,从而可以构造一个对应子类型的新对象。
【示例】:
var str = "I am a string";
typeof str; // "string"
str instanceof String; // false
var strObject = new String("I am a string");
typeof strObject; // "object"
strObject instanceof String; // true
【解释】:
原始值 “I am a string”,是一个字面量,不是一个对象。如果要在这个字面量上执行一些操作,比如获取长度、访问其中某个字符串等,那需要将其转换为 String 对象。JavaScript 装箱操作会自动把字面量转换成一个 String 对象。
【建议】:
JavaScript 社区中的大多数人都认为能够使用文字形式时就不要使用构造形式。
【注意】:
- null 和 undefined 没有对应的构造形式,只有文字形式。
- Date 只有构造形式,没有文字形式。
- 对于 Objecy、Array、Function 和 RegExp 来说,无论使用文字形式还是构造形式,它们都是对象,不是字面量。在某些情况下,相比用文字形式创建对象,构造形式可以提供一些额外选项。由于这两种形式都可以创建对象,所以首选更简单的文字形式。建议只在需要那些额外选项时使用构造形式。
- Error 对象很少在代码中显式创建,一般是在抛出异常时自动创建。也可以使用 new Error() 这种构造形式来创建,不过一般来说用不着。
内容
对象的内容是由一些存储在特定命名位置的(任意类型的)值组成的,我们称之为属性。
【简述】:
需要强调的一点是,当我们说“内容”时,似乎在暗示这些值实际上被存储在对象内部,但是这只是它的表现形式。在引擎内部,这些值的存储方式是多种多样的,一般并不会存在对象容器内部。存储在对象容器内部的是这些属性的名称,它们就像指针(从技术角度来说就是引用)一样,指向这些值真正的存储位置。
属性访问
- 属性访问:.操作符,例如 myObject.name。
- 键访问:[]操作符,例如 myObject["a"]。
【区别】:
属性访问需要满足标识符的命名规范,而键访问可以接受任意 UTF-8/Unicode 字符串作为属性名。例如,如果要引用名称为“Super-star”的属性,就只能使用键访问,因为“super-star”不是一个有效的标识符属性名。
【注意】:
在对象中,属性名都是都是字符串。如果使用字符串以外的其他值作为属性名,那么首先该属性名会被转换为一个字符串。需要额外注意的一点,在数组下标中使用的是数字,在对象属性名中数字会被转换成字符串,所以当心不要搞混对象和数组中数字的用法。
var myObject = {};
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
myObject["true"]; // "foo";
myObject["3"]; // "bar";
myObject["[object Object]"]; // "baz";
可计算属性名
键访问可以通过表达式来计算属性名。但是属性访问和文字形式声明对象则不行。
ES6 新增了可计算属性名,允许文字形式中使用 [] 包裹一个表达式来当作属性名。
【示例】:
var prefix = "foo";
// ES6
var myObject = {
[prefix + "bar"]: "hello",
[prefix + "baz"]: "world"
};
// ES6 之前
var myObject = {};
myObject[prefix + "bar"] = "hello";
myObject[prefix + "baz"] = "world";
myObject["foobar"]; // hello
myObject["foobaz"]; // world
属性与方法
【前言】:
如果访问的对象属性是一个函数,有些开发者喜欢使用不一样的叫法以作区分。由于函数很容易被认为是属于某个对象,在其他语言中,属于对象(也被称为“类”)的函数通常被称为“方法”,因此把“属性访问”说成是“方法访问”也就不奇怪了。有意思的是,JavaScript 的语法规范也作出了同样的区分。
【本书作者的见解】:
从技术角度来说,函数永远不会“属于”一个对象,所以把对象内部引用的函数称为“方法”似乎有点不妥。确实有些函数具有 this 引用,有时候这些 this 确实会指向调用位置的对象引用。但是这种用法从本质上来说并没有把一个函数变成一个“方法”,因为 this 是在运行时根据调用位置动态绑定的,所以函数和对象的关系最多也只能说是间接关系。无论返回值是什么类型,每次访问对象的属性就是属性访问。如果属性访问返回的是一个函数,那它也并不是一个“方法”。属性访问返回的函数和其他函数没有任何区别。
【举例】:
function foo() {
console.log("foo");
}
var someFoo = foo; // 对 foo 的变量引用
var myObject = {
someFoo: foo
};
var myObject2 = {
bar: function() {
console.log("bar");
}
};
var someBar = myObject.bar;
【解释】:
- someFoo 和 myObject.someFoo 只是对于同一个函数的不同引用,并不能说明这个函数是特别的或者“属于”某个对象。唯一的区别就在于 myObject.someFoo 中的 this 会被隐式绑定到一个对象。
- 即使在对象的文字形式中声明一个函数表达式,这个函数也不会“属于”这个对象。它们只是对于相同函数对象的多个引用。
数组
数组除了支持属性访问外,也支持键访问,并且有一套更加结构化的值存储机制(不限制值的类型)。数组期望的是数值下标,也就是说值存储的位置(通常被称为索引)是非负整数。
数组也是对象子类型,因此可以给数组添加属性。
【示例】:
var myArray = ["foo", 42, "bar"];
myArray.baz = "baz";
console.log(myArray.length); // 输出:3
console.log(myArray.baz); // 输出:"baz"
【解释】:
给数组添加属性,不会影响数组的长度。这是因为 JS 引擎对数组和对象的行为和用途进行了相应的优化,虽然数组也是对象,但其拥有更复杂的行为与存储机制。所以最好只用对象来存储键值对,只用数组来存储数值下标值对。
【注意】:
如果向数组添加一个“看起来”像是数字的属性,该属性会成为数组的一个数值下标,因此会修改数组的内容而不是添加一个属性。
var myArray = ["foo", 42, "bar"];
myArray["3"] = "baz";
console.log(myArray.length); // 输出:4
console.log(myArray[3]); // 输出:"baz"
属性描述符
在 ES5 之前,JavaScript 语言本身并没有提供可以直接检测属性特性的方法,比如判断属性是否是只读。但是从 ES5 开始,所有的属性都具备了属性描述符。
【示例】:
var myObject = {
a: 2
};
console.log(Object.getOwnPropertyDescriptor(myObject, "a"));
/**
输出:
{
value: 2,
writable: true,
enumerable: true,
configurable: true
}
*/
- 在创建普通属性时属性描述符会使用默认值,即 writable: true, enumerable: true, configurable: true。
- 可以使用 Object.defineProperty() 添加一个新属性或者修改一个已有属性(前提是该属性的 configurable: true)的属性描述符。
【示例】:
var myObject = {};
Object.defineProperty(myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
});
console.log(myObject.a); // 输出:2
1. Writable
writable 决定是否可以修改属性的值。
【示例】:
var myObject = {};
Object.defineProperty(myObject, "a", {
value: 2,
writable: false, // 不可写!
configurable: true,
enumerable: true
});
myObject.a = 3;
console.log(myObject.a); // 输出:2
// 案例二:
"use strict";
var myObject = {};
Object.defineProperty(myObject, "a", {
value: 2,
writable: false, // 不可写!
configurable: true,
enumerable: true
});
myObject.a = 3; // TypeError
【解释】:当 writable: false 时,对于属性值的修改静默(silently failed)失败。在严格模式下,会报 TypeError 错误。
2. Configurable
Configurable 决定属性是否可以配置。只要属性可配置,就可以使用 defineProperty() 方法来修改属性描述符。
【示例】:
var myObject = {
a: 2
};
myObject.a = 3;
console.log(myObject.a); // 输出:3
Object.defineProperty(myObject, "a", {
value: 4,
writable: true,
configurable: false, // 不可配置!
enumerable: true
});
console.log(myObject.a); // 输出:4;
myObject.a = 5;
console.log(myObject.a); // 输出:5
Object.defineProperty(myObject, "a", {
value: 6,
writable: true,
configurable: true,
enumerable: true
});
// TypeError
【解释】:
- 不管是不是处于严格模式,尝试修改一个不可配置的属性描述符都会出错。因此,把 configurable 修改成 false 是单向操作,无法撤销!
【注意】:在 configurable: false 时,仍旧可以把 writable 的值从 true 修改为 false,但是无法从 false 修改为 true。
【示例】:configurable: false 还会禁止删除该属性。
var myObject = {
a: 2
};
console.log(myObject.a); // 输出:2
delete myObject.a;
console.log(myObject.a); // 输出:undefined
Object.defineProperty(myObject, "a", {
value: 2,
writable: true,
configurable: false,
enumerable: true
});
console.log(myObject.a); // 输出:2;
delete myObject.a;
console.log(myObject.a); // 输出:2
【注意】:delete 只用来直接删除对象(可删除)属性。如果对象的某个属性是某个对象/函数的最后一个引用者,对这个属性执行 delete 操作之后,这个未引用的对象/函数就可以被垃圾回收。但是,不要把 delete 看作一个释放内存的工具,它就是一个删除对象属性的操作,仅此而已。
3. Enumerable
控制属性是否会出现在对象的属性枚举中,例如 for...in 循环。
如果把 enumerable 设置成 false,该属性就不会出现在枚举中,但是仍然可以正常地访问该属性。相对地,设置成 true 就会使其出现在枚举中。
【示例】:
var myObject = {
a: 2,
b: 3,
c: 4
};
Object.defineProperty(myObject, "a", {
value: 2,
writable: true,
configurable: false,
enumerable: false
});
for(item in myObject) {
console.log(item);
}
// 输出:b、c
不变性
属性或者对象不可改变。
所有方法创建的对象或属性都是浅不变性,也就是说,它们只会影响目标对象和它的直接属性。如果目标对象引用了其他对象(数组、对象、函数等),其他对象的内容不受影响,仍然是可变的。
1. 对象常量
结合 writable: false 和 configurable: false 可以创建一个真正的常量属性(不可修改、重定义或者删除)。
【示例】:
var myObject = {};
Object.defineProperty(myObject, "a", {
value: 2,
writable: false,
configurable: false
});
2. 禁止扩展
Object.preventExtensions() 禁止一个对象添加新属性并且保留已有属性。
【示例】:
var myObject = {
a: 2
};
Object.preventExtensions(myObject);
myObject.b = 3;
console.log(myObject.b); // 输出:undefined
3. 密封
Object.seal() 会创建一个“密封”的对象,禁止添加新属性,同时不允许配置已有属性。
【实质】:在现有对象上调用 Object.preventExtensions() 并把所有现有属性标记为 configurable: false。
【结果】:密封之后的对象不能添加新属性,也不能重新配置或者删除任何已有的属性,只能修改属性的值。
4. 冻结
Object.freeze() 会创建一个冻结对象,禁止添加新属性,不允许配置已有属性且修改属性的值。
【实质】:在现有对象上调用 Object.seal() 并把所有“数据访问”属性标记为 writable: false。
【注意】:当你深度冻结一个对象时(在一个对象上调用 Object.freeze(),然后遍历它引用的所有对象,并在这些对象上调用 Object.freeze()),一定要小心,因为这样做有可能会在无意中冻结其他共享对象。
[[Get]]
属性访问,例如 myObject.a,实际上在 myObject 对象中实现了 [[Get]] 操作。
【过程】:对象默认的内置 [[Get]] 操作首先在对象中查找是否有名称相同的属性,如果有就返回该属性的值。如果没有找到,则沿着原型链继续找。若还是没有,则返回值 undefined。
【注意】:属性访问和变量访问不同。如果在词法作用域中访问一个不存在的变量,则会抛出 ReferenceError 异常,而不会像属性访问那样返回 undefined。
[[Put]]
给对象属性赋值不会触发 [[Put]] 操作来设置或者创建这个属性。[[Put]] 操作触发取决于诸多因素。
【过程】:
判断对象中是否已经存在这个属性(最重要的因素)。如果已经存在:(暂时不讨论不存在的处理过程)
- 属性是否是访问描述符?如果是并且存在 setter 就调用 setter。
- 属性的数据描述符中 writable 是否是 false?如果是,在非严格模式下静默失败,在严格模式下抛出 TypeError 异常。
- 如果都不是,将该值设置为属性的值。
Getter 和 Setter
- getter:隐藏函数,会在获取属性值时调用。
- setter:隐藏函数,会在设置属性值时调用。
【注意】:在 ES5 中可以使用 getter 和 setter 部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。
【访问描述符】:给一个属性定义 getter、setter 或者两者都有时。对于访问描述符来说,JavaScript 会忽略它们的 value 和 writable 特性,取而代之的是关心 set 和 get 以及 configurable 和 enumerable 特性。
【示例】:
var myObject = {
// 给 a 定义一个 getter
get a() {
return 2;
}
};
Object.defineProperty(myObject, "b", {
// 给 b 设置一个 getter
get: function() {
return this.a * 2;
}
enumerable: true
});
console.log(myObject.a); // 输出:2
console.log(myObject.b); // 输出:4
【解释】:不管是对象文字形式中的 get a() {...},还是 defineProperty() 中的显式定义,二者都会在对象中创建一个不包含 value 的属性,对于这个属性的访问会自动调用一个隐藏函数,它的返回值会被当作属性访问的返回值。
【示例】:
var myObject = {
// 给 a 定义一个 getter
get a() {
return 2;
}
};
myObject.a = 3;
console.log(myObject.a); // 输出:2
【解释】:由于只定义了 a 的 getter,所以对 a 的值进行设置时 set 操作会忽略赋值操作,不会抛出错误。而且即便有合法的 setter,由于我们自定义的 getter 只会返回 2,所以 set 操作是没有意义的。为了让属性更合理,还应当定义 setter。但需要注意,setter 会覆盖单个属性默认的 [[Put]] 操作。
存在性
判断对象属性是否存在的两个方法,in 操作符以及 hasOwnProperty() 方法。
【示例】:
var myObject = {
a: 2
};
console.log("a" in myObject); // 输出:true
console.log("b" in myObject); // 输出:false
console.log(myObject.hasOwnPreperty("a")); // 输出:true
console.log(myObject.hasOwnPreperty("b")); // 输出:false
【解释】:
- in 操作符:检查属性是否在对象以及原型链中。
- hasOwnProperty():检查属性是否在对象中,不会检查原型链。
【其他】:
所有的普通对象都可以调用 hasOwnProperty()(基于 Object.prototype 的委托),但有的对象可能没有连接到 Object.property,例如 this 中所讲的“非常空”对象 Object.create(null),在这种情况下,调用 hasOwnProperty() 方法就会失败。此时,可以使用更加强硬的方法来进行判断,Object.prototype.hasOwnProperty.call(myObject, "a")。
【注意】:
in 操作符检查的是对象、数组的属性名而非属性值。