(上二)3-对象

语法

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

1
2
3
4
5
6
7
8
9
var myObj = {
key: value
// ...
};

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

var myObj = Object.create(proto, [propertiesObject])

类型

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

  • 简单基本类型
    • string
    • number
    • boolean
    • null
    • undefined
  • object

当然 es6 中新增了 Symbol 简单基本类型。

注意

  • JavaScript 中万物皆是对象是不错误的。简单基本类型本身不是对象
  • typeof null 时会返回字符串 “object”,是一个 bug。
    • 原理是这样的,不同的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判 断为 object 类型,null 的二进制表示是全 0,自然前三位也是 0,所以执行 typeof 时会返回“object”

JavaScript 中有许多特殊的对象子类型,我们可以称之为复杂基本类型

  • 函数(可调用的对象)
  • 数组
  • 内置对象(String、Number、Boolean、Object、Function、Array、Date、RegExp、Error)

内置函数

我们知道,创建一个数据我们通常有两种方式实现:字面量形式和构造函数形式。但他们确实有不同。

1
2
3
4
5
6
7
8
9
var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
strPrimitive.length // 13

var strObject = new String( "I am a string" );
typeof strObject; // "object"
strObject instanceof String; // true
strObject.length // 13

如上,两种创建方式的 typeof 类型不同。字面量形式不是一个对象,但可以访问属性和使用方法。这是因为语言在必要的时候会隐式地将其转换为 String 对象。

对象就是键 / 值对的集合。可以通过 .propName 或者 ["propName"] 语法来获取属性值。访问属性时,引擎实际上会调用内部的默认 [[Get]] 操作(在设置属性值时是 [[Put]]), [[Get]] 操作会检查对象本身是否包含这个属性,如果没找到的话还会查找 [[Prototype]] 链(参见第 5 章)。
属性的特性可以通过属性描述符来控制,比如 writableconfigurable。此外,可以使用 Object.preventExtensions(..)Object.seal(..)Object.freeze(..) 来设置对象(及其属性)的不可变性级别。
属性不一定包含值——它们可能是具备 getter/setter 的“访问描述符”。此外,属性可以是可枚举或者不可枚举的,这决定了它们是否会出现在 for..in 循环中。
你可以使用 ES6 的 for..of 语法来遍历数据结构(数组、对象,等等)中的值,for..of 会寻找内置或者自定义的 @@iterator 对象并调用它的 next() 方法来遍历数据值。

内容

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

但这些值并非一定存储在对象内部。在引擎内部,这些值的存储方式是多种多样的,存储在对象容器内部的是这些属性的名称,它们就像指针(从技术角度 来说就是引用)一样,指向这些值真正的存储位置。

对象的访问

访问熟属性我们需要使用 . 操作符或者 [] 操作符。

  • .a 语法通 常被称为“属性访问”,["a"] 语法通常被称为“键访问”。实际上它们访问的是同一个位置。
  • 区别在于:.操作符要求属性名满足标识符的命名规范;[".."] 语法可以接受任意 UTF-8/Unicode 字符串作为属性名,比如"Super- Fun!"
  • 而且如果你需要通过表达式来计算属性名,就得用 myObject[..] 这种属性访问语。比如myObject[prefix + name] 法就可以派上用场了
  • 在对象中,属性名永远都是字符串。如果你使用其他值作为属性名,那它首先会被转换为一个字符串
  • 每次访问对象的属性就是属性访问。如果属性访问返回的是一个函数,那它也并不是一个“方法”。属性访问返回的函数和其他函数没有任何区别(除了可能发生的隐式绑定 this)

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

1
2
3
4
5
var prefix = "foo";
var myObject = {
[prefix + "bar"]:"hello",
[prefix + "baz"]: "world"
};

数组的访问

数组有一套更加结构化的值存储机制,数组也支持 [] 访问形式,不过数组期望的是数值下标(通常被称为索引)

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

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

复制对象

  • 对于 JSON 安全(也就是说可以被序列化为一个 JSON 字符串并且可以根据这个字符串解 析出一个结构和值完全一样的对象)

    1
    var newObj = JSON.parse( JSON.stringify( someObj ) );
  • 对于浅复制,ES6 定义了 Object.assign(..) 方法

    1
    var newObj = Object.assign( {}, myObject );

属性描述符

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

1
2
3
4
5
6
7
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
});

我们也可以读取一个属性的描述信息

1
2
3
4
5
6
7
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// congurable: true
// }

obj.defineProperty()

  • Writable —— 决定是否可以修改属性的值。严格模式下会报错
  • Configurable —— 属性是否可配置,即是否可以使用 defineProperty(..) 方法来修改属性描述符,不可以的话,普通模式和严格模式下都会报错
  • Enumerable —— 属性是否会出现在对象的属性枚举中(比如:for in)

注意:有一个小小的例外:即便属性是 configurable:false,我们还是可以 把 writable 的状态由 true 改为 false,但是无法由 false 改为 true

对象的不变性

在某些场景下,我们可能需要属性或者对象是不可改变的。在 ES5 中我们可以有好多方法实现。

但这些方法都是浅不可变,它们只会影响目标对象和它的直接属性;如果目标对象引用了其他对象,则其他对象的内容仍是可变的。

方法 功能 实例
Object.defineProperty(...) 创建一个真正的常量属性(不可修改、 重定义或者删除) 示例1
Object.preventExtensions(..) 禁止一个对象添加新属性并且保留已有属性 示例2
Object.seal(..) 创建一个“密封”的对象,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以 修改属性的值) 示例3
Object.seal(..) 创建一个冻结对象,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性,也不能修改属性的值 示例4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var myObject = {};

// 实例1 创建常量(不可修改、 重定义或者删除)
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
});

// 实例2 禁止扩展(禁止一个对象添加新属性)
Object.preventExtensions( myObject )

// 示例3 密封对象(不能添加新属性,不能重新配置或者删除任何现有属性,但可以修改)
Object.seal( myObject )

// 示例4 冻结对象(级别最高,禁止对于对象本身及其任意 直接属性的修改)
Object.freeze( myObject )

一个深度冻结对象的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function deepFreeze(obj) {
// 取回定义在obj上的属性名
var propNames = Object.getOwnPropertyNames(obj);

// 在冻结自身之前冻结属性
propNames.forEach(function(name) {
var prop = obj[name];

// 如果prop是个对象,冻结它
if (typeof prop == 'object' && prop !== null)
deepFreeze(prop);
});

// 冻结自身(no-op if already frozen)
return Object.freeze(obj);
}

[[Get]]

属性访问在实现时有一个微妙却非常重要的细节,(在语言规范中)实际上是实现了 [[Get]] 操作(有点像函数调用:[[Get]]()):

  • 首先,对象默认的内置 [[Get]] 操作首先在对象中查找是否有名称相同的属性;
  • 第二步,遍历可能存在的 [[Prototype]] 链,也就是原型链;
  • 无论如何都没有找到名称相同的属性,那 [[Get]] 操作会返回值 undefined
1
2
3
4
var myObject = { 
a: 2
};
myObject.a; // 2

如果访问一个不存在的属性,虽然返回 undefined,实际上底层的 [[Get]] 操作进行了更复杂的处理

[[Put]]

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

如果对象中已经存在这个属性,[[Put]] 算法大致会检查下面这些内容:

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

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

访问描述符(Getter和Setter)

对象默认的 [[Put]][[Get]] 操作分别可以控制属性值的设置和获取;但在 ES5 中可以使用 gettersetter 部分改写默认操作。

当你给一个属性定义 gettersetter 或者两者都有时,这个属性会被定义为“访问描述符”:

  • 只能应用在单个属性上,无法应用在整个对象上
  • getter 是一个隐藏函数,会在获取属性值时调用
  • setter 也是一个隐藏函数,会在设置属性值时调用
  • 对于访问描述符来说,JavaScript 会忽略它们的 valuewritable 特性,取而代之的是关心 setget(还有 configurableenumerable)特性

定义访问描述符有两种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 对象语法
var myObject = {
// 给 a 定义一个 getter
get a() {
return 2;
}
};

// defineProperty 显示定义
Object.defineProperty(myObject, "b", {
// 描述符
// 给 b 设置一个 getter
get: function(){ return this.a * 2 },
// 确保 b 会出现在对象的属性列表中
enumerable: true
})

getter 会在对象中创建一个不包含值的属性,对这个属性的访问会自动调用一个隐藏函数,get 的返回值会被当作属性访问的返回值;此时即对此属性重新赋值,也将无意义。

1
2
3
4
5
6
7
var myObject = {
get a() {
return 2;
}
};
myObject.a = 3; // 赋值无意义
myObject.a; // 2

setter 会覆盖单个属性默认的[[Put]] (也被称为赋值)操作;通常来说 gettersetter 是成对出现的(只定义一个的话通常会产生意料之外的行为)

1
2
3
4
5
6
7
8
9
10
11
var myObject = {
get a() {
return this._a_;
},

set a(val) {
this._a_ = val * 2;
}
};
myObject.a = 2;
myObject.a; // 4

本例中,实际上我们把赋值( [[Put]] )操作中的值 2 存储到了另一个变量
_a_ 中。名称 _a_ 只是一种惯例,没有任何特殊的行为——和其他普通属性
一样

存在性

因为 myObject.a 的属性访问返回值可能是 undefined ,但是这个值有可能是属性中存储的 undefined ,也可能是因为属性不存在所以返回 undefined

1
2
3
4
5
6
var myObject = {
a: undefined
};

myObject.a // undefined
myObject.b // undefined

如何区别?

  • in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中;
  • hasOwnProperty(..) 只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]]
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var myObject = {
    a:2
    };

    // 方式 1
    ("a" in myObject); // true
    ("b" in myObject); // false

    // 方式 2
    myObject.hasOwnProperty( "a" ); // true
    myObject.hasOwnProperty( "b" ); // false

注:最好使用 Object.prototype.hasOwnProperty. call(myObject,"a");防止通过 Object.create(null) 来创建对象时原型链中断。

枚举

对象的可枚举,意即“可以出现在对象属性的遍历中”。主要是 for…in 循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
var myObject = {}

Object.defineProperty(myObject,"a",{
enumerable: false,
value: 2
});
// 可以访问
myObject.a; // 2 可以访问
(a in myObject); // true 存在

for (var k in myObject) { // 但不能便利
console.log( k, myObject[k] );
}

在数组上应用 for..in 循环有时会产生出人意料的结果,因为这种枚举不
仅会包含所有数值索引,还会包含所有可枚举属性。最好只在对象上应用
for..in 循环,如果要遍历数组就使用传统的 for 循环来遍历数值索引

我们还可以通过一些方式来直接区分属性是否可枚举:

  • propertyIsEnumerable(..)表示指定的属性名是否可枚举的Boolean;但是通过原型链继承的属性除外
  • Object.keys(..) 会返回一个数组,包含所有可枚举属性;但Object.getOwnPropertyNames(..)
    会返回一个数组,包含所有属性,无论它们是否可枚举
    1
    2
    3
    myObject.propertyIsEnumerable( "a" );   // false
    Object.keys( myObject ); // []
    Object.getOwnPropertyNames( myObject ); // ["a"]

遍历

对象的遍历:

  • 我们一般用 for..in 循环可以用来遍历对象的可枚举属性列表(包括 [[Prototype]] 链)
    • for..in 遍历的是对象中的所有可枚举属性(key),所以需要我们手动获取值(value)
    • 遍历的顺序是不确定的
  • ES6 中我们可以通过 for...of 来遍历一个定义了迭代器的对象的值(value)

数组的遍历:

  • 我们可以用 for..in 循环来遍历数组,但拿到的是数组的下标,相当于 key 值;但不推荐,不仅会包含所有数值索引,还会包含所有可枚举属性
  • 我们可以正常使用 for 循坏来遍历一个数组;
  • ES5 中增加了一些数组的辅助迭代器,包括 forEach(..)、every(..) 和 some(..)。every(..) 和 some(..) 会提前终止遍历
  • ES6 中增加了一种用来遍历数组的 for..of 循环语法(因为数组内置迭代器)
    • for..of 不会遍历手动添加到数组上的属性
1
2
3
4
5
6
7
8
9
10
let arr = [3, 5, 7];
arr.foo = 'hello';

for (let i in arr) {
console.log(i); // "0", "1", "2", "foo"
}

for (let i of arr) {
console.log(i); // "3", "5", "7"
}

小结

1. 有哪些方法可以创建一个对象?

1
2
3
4
5
6
7
8
9
10
// 1. 字面量形式
var myObj = {
key: value
// ...
};
// 2. 构造函数形式
var myObj = new Object();
myObj.key = value;
// 3. API 形式
var myObj = Object.create(proto, [propertiesObject])

字面形式更常用,不过有时候构造形式可以提供更多选项。

2. JS 中的数据类型?

所以,JavaScript 中万物皆是对象这句话显然是错误的

3. 为什么typeof null返回的是object

注意,简单基本类型(stringbooleannumbernullundefined)本身并不是对象。 null 有时会被当作一种对象类型,但是这其实只是语言本身的一个 bug,即对 null 执行 typeof null 时会返回字符串 "object"。实际上,null 本身是基本类型,原理是这样的,不同的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判断为 object 类型,null 的二进制表示是全 0,自然前三位也是 0,所以执行 typeof 时会返回“object”

4. 基本数据类型字面量形式不是对象,为什么可以使用方法?

1
2
3
4
5
6
7
8
9
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]

原始值 "I am a string" 并不是一个对象,它只是一个字面量,并且是一个不可变的值。 如果要在这个字面量上执行一些操作,比如获取长度、访问其中某个字符等,那需要将其转换为 String 对象。在 JavaScript 中必要时语言会自动把字符串字面量转换成一个 String 对象,也就是说你并不需要显式创建一个对象。JavaScript 社区中的大多数人都认为能使用文字形式时就不要使用构造形式。

5. JS 中的对象?

对象就是键 / 值对的集合。可以通过 .propName 或者 ["propName"] 语法来获取属性值。访问属性时,引擎实际上会调用内部的默认 [[Get]] 操作(在设置属性值时是 [[Put]]), [[Get]] 操作会检查对象本身是否包含这个属性,如果没找到的话还会查找 [[Prototype]] 链。
属性的特性可以通过属性描述符来控制,比如 writableconfigurable。此外,可以使用 Object.preventExtensions(..)Object.seal(..)Object.freeze(..) 来设置对象(及其属性)的不可变性级别。
属性不一定包含值——它们可能是具备 getter/setter 的“访问描述符”。此外,属性可以是可枚举或者不可枚举的,这决定了它们是否会出现在 for..in 循环中。
你可以使用 ES6 的 for..of 语法来遍历数据结构(数组、对象,等等)中的值,for..of 会寻找内置或者自定义的 @@iterator 对象并调用它的 next() 方法来遍历数据值。

6. 对象的拷贝?

  • 对于 JSON 安全(也就是说可以被序列化为一个 JSON 字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)

    1
    var newObj = JSON.parse( JSON.stringify( someObj ) );
  • 对于浅复制,ES6 定义了 Object.assign(..) 方法

    1
    var newObj = Object.assign( {}, myObject );
  • 深拷贝一个对象,我们可以自我封装

7. 对象的属性描述符?

从 ES5 开始,所有的属性都具备了属性描述符。对象里目前存在的属性描述符有两种主要形式:数据描述符存取描述符

数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一,不能同时是两者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var myObject = {};

// 1. 数据描述符
Object.defineProperty( myObject, "a", {
value: 2,
writable: true, // 决定是否可以修改属性的值。严格模式下会报错
configurable: true, // 属性是否可配置,即是否可以使用 defineProperty(..)
enumerable: true // 属性是否会出现在对象的属性枚举中(比如:for in)
});

// 2. 存取描述符
Object.defineProperty( myObject, "a", {
get() { return _a_ },
set(newVal) { _a_ = newVal },
configurable: true,
enumerable: true
});

注意:有一个小小的例外:即便属性是 configurable:false,我们还是可以 把 writable 的状态由 true 改为 false,但是无法由 false 改为 true

8. 怎么冻结一个对象?

在某些场景下,我们可能需要属性或者对象是不可改变的。在 ES5 中我们可以有好多方法实现。

但这些方法都是浅不可变,它们只会影响目标对象和它的直接属性;如果目标对象引用了其他对象,则其他对象的内容仍是可变的。

方法 功能
Object.defineProperty(...) 创建一个真正的常量属性(不可修改、 重定义或者删除)
Object.preventExtensions(..) 禁止一个对象添加新属性并且保留已有属性
Object.seal(..) 创建一个“密封”的对象,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)
Object.seal(..) 创建一个冻结对象,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性,也不能修改属性的值

一个深度冻结对象的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function deepFreeze(obj) {
// 取回定义在obj上的属性名
var propNames = Object.getOwnPropertyNames(obj);

// 在冻结自身之前冻结属性
propNames.forEach(function(name) {
var prop = obj[name];

// 如果prop是个对象,冻结它
if (typeof prop == 'object' && prop !== null)
deepFreeze(prop);
});

// 冻结自身(no-op if already frozen)
return Object.freeze(obj);
}
0%