前言
继续整理<你不知道的JavaScript>读书笔记,本文是对象部分。
对象
语法
对象的文字语法大概是这样:
1 | var myObj = { |
构造形式大概是这样:
1 | var myObj = new Object(); |
构造形式和文字形式生成的对象是一样的。
- 唯一的区别是,在文字声明中你可以添加多个键 / 值对,但是在构造形式中你必须逐个添加属性。
类型
对象是 JavaScript 的基础。在 JavaScript 中一共有六种主要类型(术语是“语言类型”):
- string
- number
- boolean
- null
- undefined
- object
简单基本类型( string 、 boolean 、 number 、 null 和 undefined )本身并不是对象。
实际上, null 本身是基本类型。但是对 null 执行typeof null
时会返回字符串 object
。
内置对象
- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Error
1 | var strPrimitive = "I am a string"; |
使用以上两种方法,我们都可以直接在字符串字面量上访问属性或者方法,因为引擎自动把字面量转换成 String 对象,所以可以访问属性和方法。
同理,数字和布尔值也是如此。
null 和 undefined 没有对应的构造形式,它们只有文字形式。相反, Date 只有构造,没有文字形式。
对于Object 、 Array 、 Function 和 RegExp
- 首选更简单的文字形式。
- 建议只在需要那些额外选项时使用构造形式。
Error 对象很少在代码中显式创建,一般是在抛出异常时被自动创建。
内容
1 | var myObject = { |
.a
语法通常被称为“属性访问”["a"]
语法通常被称为“键访问”。
主要区别在于
.
操作符要求属性名满足标识符的命名规范[".."]
语法可以接受任意UTF-8/Unicode
字符串作为属性名。
可计算属性名
ES6 增加了可计算属性名,可以在文字形式中使用 [] 包裹一个表达式来当作属性名:
1 | var prefix = "foo"; |
属性与方法
如果访问的对象属性是一个函数,属于对象(也被称为“类”)的函数通常被称为“方法”。
数组
数组也支持 [] 访问形式,数组期望的是数值下标,也就是说值存储的位置(通常被称为索引)是整数,比如说 0 和 42:
1 | var myArray = [ "foo", 42, "bar" ]; |
数组也是对象,所以虽然每个下标都是整数,你仍然可以给数组添加属性:
1 | var myArray = [ "foo", 42, "bar" ]; |
但这并不是一个好主意。数组和普通的对象都根据其对应的行为和用途进行了优化,所以最好
只用对象来存储键 / 值对,只用数组来存储数值下标 / 值对。
复制对象
1 | function anotherFunction() { /*..*/ } |
如何准确地表示 myObject 的复制呢?
首先,我们应该判断它是浅复制还是深复制。
- 对于浅拷贝来说,复制出的新对象中 a 的值会复制旧对象中 a 的值,也就是 2,但是新对象中 b 、 c 、 d 三个属性其实只是三个引用,它们和旧对象中 b 、 c 、 d引用的对象是一样的。
- 对于深复制来说,除了复制 myObject 以外还会复制 anotherObject 和 anotherArray 。
- 这时问题就来了, anotherArray 引用了 anotherObject 和myObject ,所以又需要复制 myObject ,这样就会由于循环引用导致死循环。
有一种巧妙的复制方法:
1 | var newObj = JSON.parse( JSON.stringify( someObj ) ); |
当然,这种方法需要保证对象是 JSON 安全的,所以只适用于部分情况。
- 如果你的对象里有函数,函数无法被拷贝下来
- 无法拷贝someObj对象原型链上的属性和方法
浅复制
ES6 定义了 Object.assign(..)
方法来实现浅复制。 Object.assign(..)
方法的第一个参数是目标对象,之后还可以跟一个或多个源对象。
它会遍历一个或多个源对象的所有可枚举(enumerable,参见下面的代码)的自有键(owned key,很快会介绍)并把它们复制(使用 = 操作符赋值)到目标对象,最后返回目标对象,就像这样:1
2
3
4
5var newObj = Object.assign( {}, myObject );
newObj.a; // 2
newObj.b === anotherObject; // true
newObj.c === anotherArray; // true
newObj.d === anotherFunction; // true
由于 Object.assign(..) 就是使用 = 操作符来赋值,所
以源对象属性的一些特性(比如 writable )不会被复制到目标对象。
属性描述符
从 ES5 开始,所有的属性都具备了属性描述符。
1 | var myObject = { |
writable
(可写)、enumerable
(可枚举)和configurable
(可配置)。
可以使用 Object.defineProperty(..)
来添加一个新属性或者修改一个已有属性(如果它是 configurable )并对特性进行设置。
1 | var myObject = {}; |
1. Writable
- writable 决定是否可以修改属性的值。
1 | var myObject = {}; |
2. Configurable
- 只要属性是可配置的,就可以使用
defineProperty(..)
方法来修改属性描述符:
1 | var myObject = { |
不管是不是处于严格模式,尝试修改一个不可配置的属性描述符都会出错。注意:如你所见,把 configurable 修改成false 是单向操作,无法撤销!
如果对象的某个属性是某个对象 / 函数的最后一个引用者,对这个属性执行 delete 操作之后,这个未引用的对象 / 函数就可以被垃圾回收。但是,不要把 delete 看作一个释放内存的工具(就像 C/C++ 中那样),它就是一个删除对象属性的操作,仅此而已。
3. Enumerable
- 这个描述符控制的是属性是否会出现在对象的属性枚举中,比如说for..in 循环。如果把 enumerable 设置成 false,这个属性就不会出现在枚举中,虽然仍然可以正常访问它。相对地,设置成 true就会让它出现在枚举中。
不变性
1. 对象常量
- 结合 writable:false 和configurable:false就可以创建一个真正的常量属性(不可修改、重定义或者删除):
1 | var myObject = {}; |
2. 禁止扩展
- 如果你想禁止一个对象添加新属性并且保留已有属性,可以使用
Object.prevent Extensions(..)
:
1 | var myObject = { |
在非严格模式下,创建属性 b 会静默失败。在严格模式下,将会抛出 TypeError 错误。
3. 密封
Object.seal(..)
会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false 。
所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)。
4. 冻结
Object.freeze(..)
会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(..) 并把所有“数据访问”属性标记为 writable:false ,这样就无法修改它们的值。
这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改(不过就像我们之前说过的,这个对象引用的其他对象是不受影响的)。你可以“深度冻结”一个对象,具体方法为,首先在这个对象上调用 Object.freeze(..) ,然后遍历它引用的所有对象并在这些对象上调用 Object.freeze(..) 。但是一定要小心,因为这样做有可能会在无意中冻结其他(共享)对象。
Getter和Setter
对象默认的 [[Put]]
和 [[Get]]
操作分别可以控制属性值的设置和获取。
访问属性时,引擎实际上会调用内部的默认[[Get]]
操作
如果已经存在这个属性, [[Put]]
算法大致会检查下面这些内容。
- 属性是否是访问描述符?如果是并且存在 setter 就调用 setter。
- 属性的数据描述符中 writable 是否是 false ?如果是,在非严格模式下静默失败,在严格模式下抛出 TypeError 异常。
- 如果都不是,将该值设置为属性的值。
getter
是一个隐藏函数,会在获取属性值时调用。setter
也是一个隐藏函数,会在设置属性值时调用。
存在性
我们可以在不访问属性值的情况下判断对象中是否存在这个属性:
1 | var myObject = { |
- in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中
- hasOwnProperty(..) 只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 链。
Object.keys(..)
会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames(..)
会返回一个数组,包含所有属性,无论它们是否可枚举。
in
和hasOwnProperty(..)
的区别在于是否查找[[Prototype]]
链,然而,Object.keys(..)
和Object.getOwnPropertyNames(..)
都只会查找对象直接包含的属性。
注意
在数组上应用 for..in 循环有时会产生出人意料的结果,因为这种枚举不仅会包含所有数值索引,还会包含所有可枚举属性。最好只在对象上应用for..in 循环,如果要遍历数组就使用传统的 for 循环来遍历数值索引。
in操作符实际上检查的是某个属性名是否存在,而不是属性值是否存在
遍历
ES5 中增加了一些数组的辅助迭代器,包括 forEach(..) 、 every(..) 和 some(..) 。每种辅助迭代器都可以接受一个回调函数并把它应用到数组的每个元素上,唯一的区别就是它们对于回调函数返回值的处理方式不同。
forEach(..)
会遍历数组中的所有值并忽略回调函数的返回值。every(..)
会一直运行直到回调函数返回 false (或者“假”值)。some(..)
会一直运行直到回调函数返回 true (或者“真”值)。
every(..) 和 some(..) 中特殊的返回值和普通 for 循环中的 break 语句类似,它们会提前
终止遍历。
ES6 增加了一种用来遍历数组的 for..of
循环语法
1 | var myArray = [ 1, 2, 3 ]; |
总结
- JavaScript 中的对象有字面形式(比如 var a = { .. } )和构造形式(比如 var a = new Array(..) )。字面形式更常用,不过有时候构造形式可以提供更多选项。
- 许多人都以为“JavaScript 中万物都是对象”,这是错误的。对象是 6 个(或者是 7 个,取决于你的观点)基础类型之一。对象有包括function在内的子类型,不同子类型具有不同的行为,比如内部标签 [object Array] 表示这是对象的子类型数组。
- 对象就是键 / 值对的集合。可以通过 .propName 或者[“propName”]语法来获取属性值。访问属性时,引擎实际上会调用内部的默认
[[Get]]
操作(在设置属性值时是[[Put]]
),[[Get]]
操作会检查对象本身是否包含这个属性,如果没找到的话还会查找[[Prototype]]
链(参见第 5 章原型)。
- 属性的特性可以通过属性描述符来控制,比如
writable
和configurable
。此外,可以使用Object.preventExtensions(..)
、Object.seal(..)
和Object.freeze(..)
来设置对象(及其属性)的不可变性级别。
- 属性不一定包含值——它们可能是具备 getter/setter 的“访问描述符”。此外,属性可以是可枚举或者不可枚举的,这决定了它们是否会出现在
for..in
循环中。
- 你可以使用 ES6 的
for..of
语法来遍历数据结构(数组、对象,等等)中的值, for..of会寻找内置或者自定义的 @@iterator 对象并调用它的 next() 方法来遍历数据值。