对象

# 对象

函数调用位置的不同会造成 this 绑定对象的不同

# 语法

对象可以通过两种形式定义:声明(文字)形式和构造形式。

// 声明
var myObj = {
 key: value
};

// 构造
var myObj = new Object();
myObj.key = value
1
2
3
4
5
6
7
8

声明中可以添加多个键值对,构造要一个个添加

# 类型

在 JavaScript 中一共有六种主要类型

  • string
  • number
  • boolean
  • null
  • undefined
  • object

注意,简单基本类型(string、boolean、number、null 和 undefined)本身并不是对象。

实际上,null 本身是基本类型。

typeof null 时会返回字符串 "object"

函数就是对象的一个子类型

JavaScript 中的函 数是“一等公民”,因为它们本质上和普通的对象一样 所以可以像操作 其他对象一样操作函数

内置对象

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

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

不同的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判 断为 object 类型,null 的二进制表示是全 0,自然前三位也是 0,所以执行 typeof 时会返回“object”。

在 JavaScript 中,它们实际上只是一些内置函数。这些内置函数可以当作构造函数 来使用,从而可以构造一个对应子类型的新对 象。

typeof '123'; // 'string'
'123' instanceof String; // false

Object.prototype.toString.call('123');
'[object String]'
1
2
3
4
5

获取长度、访问其中某个字符等,那需要将其 转换为 String 对象。

在必要时语言会自动把字符串字面量转换成一个 String 对象

引擎自动把字面量转换成 String 对象

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

Date 只有构造,没有 文字形式。

对于 Object、Array、Function 和 RegExp(正则表达式)来说,无论使用文字形式还是构 造形式,它们都是对象,不是字面量。

Error 对象很少在代码中显式创建,一般是在抛出异常时被自动创建。

# 内容

属性访问 键访问

. 操作符要求属性名满足标识符的命名规范,而 [".."] 语法 可以接受任意 UTF-8/Unicode 字符串作为属性名

可计算属性名

ES6 增加了可计算属性名,可以在文字形式中使用 [] 包裹一个表达式来当作属性名

this 是在运行时根据调用位置动态绑定的

ES6 增加了 super 引用,一般来说会被用在 class 中

数组

给数组添加属性 虽然添加了命名属性 数组的 length 值并未发 生变化

var a = [1,2,3]
undefined

a.length
3

a.bar = 'var'
'var'

a.length
3

a
(3) [1, 2, 3, bar: 'var']
1
2
3
4
5
6
7
8
9
10
11
12
13
14

最好 只用对象来存储键 / 值对,只用数组来存储数值下标 / 值对。

var a = [1,2,3]
undefined

a['6']=6
6

a
(7) [1, 2, 3, empty × 3, 6]
1
2
3
4
5
6
7
8

复制对象

复制方法:

var newObj = JSON.parse( JSON.stringify( someObj ) );
1

ES6 定义了 Object.assign(..) 方 法来实现浅复制。Object.assign(..) 方法的第一个参数是目标对象,之后还可以跟一个 或多个源对象。它会遍历一个或多个源对象的所有可枚举的自有键并把它们复制到目标对象,最后返回目标对象

属性描述符

ES5 开始,所有的属性都具备了属性描述符。

Object.getOwnPropertyDescriptor(myObj, 'a');

{
 value: 2,
 writable: true,
 enumerable: true,
 configurable: true
}
1
2
3
4
5
6
7
8

三个特性:writable(可写)、enumerable(可枚举)和 configurable(可配置)。

使用 Object.defineProperty(..) 来添加一个新属性或者修改一个已有属性并对特性进行设置。

var myObject = [];

Object.defineProperty(myObject, 'a', {
 value: 2,
 vritable: true,
 configurable: true,
 enumerable: true
});

myObject.a; // 2
1
2
3
4
5
6
7
8
9
10
  1. Writable writable 决定是否可以修改属性的值。
  2. Configurable 只要属性是可配置的,就可以使用 defineProperty(..) 方法来修改属性描述符
  3. Enumerable

如果你不希 望某些特殊属性出现在枚举中,那就把它设置成 enumerable:false。

不管是不是处于严格模式,尝试修改一个不可配置的属性描述符都会出错。把 configurable 修改成 false 是单向操作,无法撤销!

除了无法修改,configurable:false 还会禁止删除这个属性

有一个小小的例外

还是可以 把 writable 的状态由 true 改为 false,但是无法由 false 改为 true。

不变性

  1. 对象常量 结合 writable:false 和 configurable:false 就可以创建一个真正的常量属性(不可修改、 重定义或者删除
  2. 禁止扩展
    • 如 果 你 想 禁 止 一 个 对 象 添 加 新 属 性 并 且 保 留 已 有 属 性, 可 以 使 用 Object.preventExtensions(..) - Object.preventExtensions( myObject );
  3. 密封 - Object.seal(..) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false。
    • 密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以 修改属性的值)
  4. 冻结 Object.freeze(..) 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal(..) 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值。

[[Get]] 属性访问

实际上是实现了 [[Get]] 操作 如果没有找到名称相同的属性 遍历可能存在的 [[Prototype]] 链, 也就是原型链

如果无论如何都没有找到名称相同的属性 那 [[Get]] 操作会返回值 undefined

[[Put]]

[[Put]] 被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性

如果对象中不存在这个属性,[[Put]] 操作会更加复杂。

如果已经存在这个属性,[[Put]] 算法大致会

  1. 属性是否是访问描述符?如果是并且存在 setter 就调用 setter。
  2. 属性的数据描述符中 writable 是否是 false ?如果是,在非严格模式下静默失败,在 严格模式下抛出 TypeError 异常。
  3. 如果都不是,将该值设置为属性的值。

对象默认的 [[Put]] 和 [[Get]] 操作分别可以控制属性值的设置和获取。

Getter和Setter

访问描述符

对于访问描述符来说,JavaScript 会忽略它们的 value 和 writable 特性,取而代之的是关心 set 和 get(还有 configurable 和 enumerable)特性。

存在性

  • in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中
  • hasOwnProperty(..) 只会检查属性是否在对象中 不会检查 [[Prototype]] 链。
  • 所 有 的 普 通 对 象 都 可 以 通 过 对 于 Object.prototype 的 委 托来 访 问 hasOwnProperty(..),但是有的对象可能没有连接到 Object.prototype - 通过 Object.create(null) 来创建

Object.prototype.hasOwnProperty.call(myObject,"a")

  1. 枚举

propertyIsEnumerable(..) 会检查给定的属性名是否直接存在于对象中(而不是在原型链 上)并且满足 enumerable:true。

Object.keys(..) 会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames(..) 会返回一个数组,包含所有属性,无论它们是否可枚举。

in 和 hasOwnProperty(..) 的区别在于是否查找 [[Prototype]] 链

# 遍历

for..in 循环可以用来遍历对象的可枚举属性列表(包括 [[Prototype]] 链)

ES5 中增加了一些数组的辅助迭代器,包括 forEach(..)、every(..) 和 some(..)。

  • forEach(..) 会遍历数组中的所有值并忽略回调函数的返回值。
  • every(..) 会一直运行直到回调函数返回 false
  • some(..) 会一直运行直到回调函数返回 true

every(..) 和 some(..) 可能会会提前 终止遍历。

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

ES6 增加了一种用来遍 历数组的 for..of 循环语法

for..of 循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的 next() 方法来遍历所有返回值。

数组有内置的 @@iterator,因此 for..of 可以直接应用在数组上。

var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator](); 

it.next(); // { value:1, done:false } 
it.next(); // { value:2, done:false } 
it.next(); // { value:3, done:false } 
it.next(); // { done:true }
1
2
3
4
5
6
7

@@iterator 本身并不是一个迭代器对象,而是一个返回迭代器对象的函数

调用迭代器的 next() 方法会返回形式为 { value: .. , done: .. } 的值, value 是当前的遍历值,done 是一个布尔值,表示是否还有可以遍历的值。

你可以给任何想遍历的对象定义 @@iterator,for..of 循环每次调用 对象 迭代器对象的 next() 方法时,内部的指针都会向前移动并 返回对象属性列表的下一个值

可以定义一个“无限”迭代器,它永远不会“结束”并且总会返回一个新 值(比如随机数、递增值、唯一标识符,等等)

这个迭代器会生成“无限个”随机数,因此我们添加了一条 break 语句,防止程序被挂起。

# 小结

  • JavaScript 中的对象有字面形式和构造形式
  • 许多人都以为“JavaScript 中万物都是对象”,这是错误的。
  • 对象就是键 / 值对的集合。
  • 。访 问属性时,引擎实际上会调用内部的默认 [[Get]] 操作(在设置属性值时是 [[Put]]), [[Get]] 操作会检查对象本身是否包含这个属性,如果没找到的话还会查找 [[Prototype]] 链
  • Object.preventExtensions(..)、Object.seal(..) 和 Object.freeze(..) 来设置对象的不可变性级别。
  • ES6 的 for..of 语法来遍历数据结构(数组、对象,等等)中的值
上次更新: 2022/8/8 下午5:26:18