语法
// 声明形式(文字)
var myObj = {
key: value,
};
// 构造形式
var myObj2 = new Object();
myObj2.key = value;
类型
主要类型:
基本类型
- string
- number
- boolean
- null
- undefined
引用类型
- object
内置对象
- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Error
这些内置对象从表现形式来说很像其他语言中的类型(type)或者类(class),比如 Java 中的 String 类。但是在 JavaScript 中,它们实际上只是一些内置函数。这些内置函数可以当作构造函数来使用,从而可以构造一个对应子类型的新对象。
var strPrimitive = "string"; // 字面量
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
var strObj = new String("string");
typeof strObj; // "object"
strObj instanceof String; // true
Object.prototype.toString.call(strObj); // "[object String]"
对象的内容
在引擎内部,这些值的存储方式是多种多样的,一般并不会存在对象容器内部。存储在对象容器内部的是这些属性的名称,它们就像指针(从技术角度来说就是引用)一样,指向这些值真正的存储位置。
var myObj = {
a: 2
};
myObj.a; // 2
myObj['a']; // 2
在对象中,属性名永远都是字符串。如果你使用string (字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串。即使是数字也不例外。
var myObj = {};
myObj[true] = "true";
myObj[1] = "1";
myObj[myObj] = "myObj";
myObj['true']; // "true"
myObj['1']; // "1"
myObj['[object Object]']; // "myObj"
可计算属性名
var prefix = 'foo';
var myObj = {
[prefix + 'bar']: 'hello',
[prefix + 'baz']: 'world'
};
myObj['foobar']; // "hello"
myObj['foobaz']; // "world"
属性与方法
由于函数很容易被认为是属于某个对象,在其他语言中,属于对象(也被称为“类”)的函数通常被称为“方法”。
从技术角度来说,函数永远不会“属于”一个对象,所以把对象内部引用的函数称为“方法”似乎有点不妥。
因为 this
是在运行时根据调用位置动态绑定的,所以函数和对象的关系最多也只能说是间接关系。
无论返回值是什么类型,每次访问对象的属性就是属性访问。如果属性访问返回的是一个函数,那它也并不是一个“方法”。属性访问返回的函数和其他函数没有任何区别(除了可能发生的隐式绑定 this
,就像我们刚才提到的)。
function foo () {
console.log('foo');
}
var someFoo = foo;
var myObj = {
someFoo: foo,
};
foo; // function foo() {...}
someFoo; // function foo() {...}
myObj.someFoo; // function foo() {...}
someFoo
和 myObject.someFoo
只是对于同一个函数的不同引用,并不能说明这个函数是特别的或者“属于”某个对象。如果 foo()
定义时在内部有一个 this
引用,那这两个函数引用的唯一区别就是 myObject.someFoo
中的 this
会被隐式绑定到一个对象。无论哪种引用形式都不能称之为“方法”。
数组
数组也支持 []
访问形式,数组有一套更加结构化的值存储机制(不过仍然不限制值的类型)。数组期望的是数值下标,也就是说值存储的位置(通常被称为索引)是整数。
var myArray = ['foo', 42, 'bar'];
myArray.length; // 3
myArray[0]; // "foo"
myArray[1]; // 42
// 数组也会是对象
myArray.baz = 'baz';
myArray.length; // 3
myArray.baz; // "baz"
// 如果试图向数组添加一个属性,但是属性名“看起来”像一个数字,那它会变成一个数值下标
myArray['3'] = 'baz';
myArray.length; // 4
myArray[3]; // "baz"
复制对象
对于 JSON 安全(也就是说可以被序列化为一个 JSON 字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)的对象来说,有一种巧妙的复制方法:
var myObj = JSON.parse(JSON.stringify(otherObj));
ES6 定义了 Object.assign()
方法来实现浅复制。Object.assign()
方法的第一个参数是目标 对象,之后还可以跟一个或多个源 对象。它会遍历一个或多个源对象的所有可枚举(enumerable)的自有键 (owned key)并把它们复制(使用 =
操作符赋值)到目标对象,最后返回目标对象,就像这样:
var newObj = Object.assign({}, oldObj);
属性描述符
var myObj = {
a: 2
};
Object.getOwnPropertyDescriptor(myObj, 'a');
// {value: 2, writable: true, enumerable: true, configurable: true}
在创建普通属性时属性描述符会使用默认值,可以使用 Object.defineProperty()
来添加一个新属性或者修改一个已有属性(如果它是 configurable
)并对特性进行设置。
不变性
有时候会希望属性或者对象是不可改变(无论有意还是无意)的,在 ES5 中可以通过很多种方法来实现。很重要的一点是,所有 的方法创建的都是浅不变形,也就是说,它们只会影响目标对象和它的直接属性。如果目标对象引用了其他对象(数组、对象、函数,等),其他对象的内容不受影响,仍然是可变的。
- 对象常量
var myObj = {};
Object.defineProperty(myObj, 'a', {
value: 2,
writable: false,
enumerable: true,
configurable: false
});
- 禁止扩展
禁止一个对象添加新属性并且保留已有属性
var myObject = {
a: 2
};
Object.preventExtensions(myObject);
myObj.b = 3;
myObj.b; // undefined
- 密封
Object.seal()
会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions()
并把所有现有属性标记为 configurable:false
。
所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)。
- 冻结
Object.freeze()
会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal()
并把所有“数据访问”属性标记为 writable:false
,这样就无法修改它们的值。
[[Get]]
var myObj = {
a: 2
};
myObj.a; // 2
在语言规范中,myObject.a
在 myObject
上实际上是实现了 [[Get]]
操作(有点像函数调用:[[Get]]()
)。对象默认的内置[[Get]]
操作首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性的值。如果没找到就按原型链继续找,如果无论如何都没有找到名称相同的属性,那[[Get]]
操作会返回值 undefined
。
[[Put]]
[[Put]]
被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性(这是最重要的因素)。
- 属性是否是访问描述符?如果是并且存在 setter 就调用 setter。
- 属性的数据描述符中 writable 是否是 false ?如果是,在非严格模式下静默失败,在严格模式下抛出 TypeError 异常。
- 如果都不是,将该值设置为属性的值。
Getter 和 Setter
在 ES5 中可以使用 getter 和 setter 部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。getter 是一个隐藏函数,会在获取属性值时调用。setter 也是一个隐藏函数,会在设置属性值时调用。
当你给一个属性定义 getter、setter 或者两者都有时,这个属性会被定义为“访问描述符”(和“数据描述符”相对)。对于访问描述符来说,JavaScript 会忽略它们的 value 和 writable 特性,取而代之的是关心 set 和 get (还有 configurable 和 enumerable )特性。
var myObj = {
get a() {
return 2;
}
};
Object.defineProperty(myObj, 'b', {
get: function() {
return this.a * 2;
},
enumerable: true
});
myObj.a; // 2
myObj.b; // 4
存在性
在不访问属性值的情况下判断对象中是否存在这个属性:
var myObj = {
a: 2
};
('a' in myObj); // true
('b' in myObj); // false
myObject.hasOwnProperty('a'); // true
myObject.hasOwnProperty('b'); // false
in 操作符会检查属性是否在对象及其 [[Prototype]]
原型链中。相比之下,hasOwnProperty()
只会检查属性是否在 myObject
对象中,不会检查 [[Prototype]]
链。
所有的普通对象都可以通过对于 Object.prototype
的委托来访问 hasOwnProperty()
,但是有的对象可能没有连接到 Object.prototype
(通过Object.create(null)
来创建)。在这种情况下,形如 myObejct.hasOwnProperty()
就会失败。这时可以使用一种更加强硬的方法来进行判断:Object.prototype.hasOwnProperty.call(myObject,"a")
,它借用基础的 hasOwnProperty()
方法并把它显式绑定到 myObject
上。
遍历
for..in
循环可以用来遍历对象的可枚举属性列表(包括 [[Prototype]]
链)。但是如何遍历属性的值呢?
对于数值索引的数组来说,可以使用标准的 for 循环来遍历值:
var myArray = [1, 2, 3];
for (var i = 0; i < myArray.length; i++) {
console.log(myArray[i]);
}
// 1 2 3
ES5 中增加了一些数组的辅助迭代器,包括 forEach()
、every()
和 some()
。每种辅助迭代器都可以接受一个回调函数并把它应用到数组的每个元素上,唯一的区别就是它们对于回调函数返回值的处理方式不同。
使用 for..in
遍历对象是无法直接获取属性值的,因为它实际上遍历的是对象中的所有可枚举属性,需要手动获取属性值。
那么如何直接遍历值而不是数组下标(或者对象属性)呢?幸好,ES6 增加了一种用来遍历数组的 for..of
循环语法:
for (var value of myArray) {
console.log(value);
}
// 1 2 3