JS基础(六)对象(下)

欢迎阅读,本篇主要介绍JS中的对象。

前言

上篇主要学习对象概念内容,本篇学习对象的属性方法、数组、对象的复制、遍历等。

上篇中提到:

1
2
3
4
5
6
var myObj = {
a: 2
}

myObj.a; // 2
myObj["a"]; // 2

对象的两种访问方式,.a被称为“属性访问”,[‘a’]被称为“键访问”,我们通常采用“属性访问”的方式。

1. 属性与方法

思考一下,如果访问的属性是一个函数,由于函数容易被认为是属于某个对象的,在其他语言中,属于某个对象(也被称为类)的函数通常被成为“方法”,但从技术角度来讲,函数永远不会“属于”一个对象。不过,有些函数具有this引用,有时候这些this确实会指向调用位置的对象引用。但这种用法从本质上来说并没有把一个函数变成一个“方法”,因为this是在运行时根据调用位置动态绑定的,所以函数和对象的关系最多算是间接关系。
无论返回值是什么类型,每次访问对象的属性就是属性访问。如果属性访问返回的是一个函数,那它也并不是一个“方法”。属性访问返回的函数和其他函数没有任何区别(除了可能发生隐式绑定this)。

1
2
3
4
5
6
7
8
9
10
11
12
function foo () {
console.log("foo");
}

var someFoo = foo; // 对foo的变量引用
var myObj = {
someFoo: foo
};

foo; // function foo() {...}
someFoo; // function foo() {...}
myObj.someFoo(); // function foo() {...}

someFoo和myObj.someFoo只是对于同一个函数的不同引用,并不能说明这个函数是特别的或者“属于”某个对象。如果foo()定义时内部有一个this引用,那这两个函数引用的唯一区别就是myObj.someFoo中的this会被隐式绑定到一个对象。无论哪种引用形式都不能称之为“方法”。

2 数组

数组也支持[]访问,不过数组有一套更加结构化的值存储机制,数组期望的下标是非负整数。数组也是对象,所以虽然每个下标都是整数,你仍可给数组添加属性:

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

虽然我们添加了命名属性,数组的length值并未发生变化。我们完全可以把数组当作一个普通键/值对象来用,并且不添加任何数值索引,但这并不是一个好主意,数组和普通对象都是根据其对应的行为和用途进行了优化的,所以最好用对象存储键/值对,用数组存储下标/值对。

需注意:

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

当你试图向数组添加一个属性,但是属性名“看起来”像一个数字,那它会变成一个数值下标,因此会修改数组的内容,而不是添加一个新属性。

3 复制对象

怎样复制一个对象呢?或许这比我们想象中的要复杂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var anotherObj = {
c: true,
}

var anotherArray = [];

function anotherFunction() {...};

var myObj = {
a: 2,
b: anotherObj, // 引用,不是复本!
c: anotherArray, // 另一个引用
d: anotherFunction,
}

anotherArray.push(anotherObj, myObj);

如何准确的表示myObj的复制呢?
首先我们需要判断它是深拷贝还是浅拷贝。对于浅拷贝来说,复制出的新对象中 a 的值会复制旧对象中 a 的值,也就是2。但是新对象中 b、c、d三个属性其实只是三个引用,它们和旧对象中引用的对象是一样的。
对于深拷贝来说,除了复制myObj以外还会复制 anotherObj 和 anotherArray,那么问题就来了,anotherArray 引用了 anotherObj 和 myObj,所以又要复制 myObj,这样就会由于循环引用导致死循环。除此之外,我们还不确定“复制”一个函数意味着什么。

那么如何解决这些棘手的问题呢?

对于JSON安全的对象(也就是说可以被序列化为一个JSON字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)来说,有一种巧妙的复制方式:
var myObj = JSON.parse(JSON.stringify(someObj)); // 这种方法需要保证对象是JSON安全的,所以只能适用于部分情况

相比深复制,浅复制更易懂,且问题要少的多,所以ES6新增Object.assign(...)方法来实现浅复制。
Object.assign(...) 第一个参数是目标对象,之后还可以跟一个或多个对象。它会遍历一个或多个源对象的所有可枚举的自有键,并把它们复制(通过 = 操作符)到目标对象,最后返回目标对象,就像这样:

1
2
3
4
5
6
var newObj = Object.assign({}, myObj);

newObj.a; // 2
newObj.b = anotherObj; // true
newObj.c = anotherArray, // true
newObj.d = anotherFunction, // true

由于Object.assign就是使用 = 操作符来赋值,所以源对象属性的一些特性(如writable)不会被复制到复制对象中。

4 属性描述符

在ES5之前,JS语言本身并没有提供可以直接检测属性特性的方法,比如判断属性是否只读。从ES5开始所有属性都具备了属性描述符。

1
2
3
4
5
6
7
8
9
10
11
var myObj = {
a: 2,
}

Object.getOwnPropertyDescriptor(myObj, "a");
// {
// value: 2,
// writable: true, // 可写
// enumerable: true, // 可枚举
// configurable: true, // 可配置
// }

如你所见,这个普通的对象属性对应的属性描述符(数据描述符)除了2,还包含三个特性:writable(可写)、enumerable(可枚举)、configurable(可配置)。在创建普通属性时,属性描述符会使用默认值,可以使用Object.defineProperty(…)来添加一个新属性或者修改一个已有属性(如果它是configurable)并对特性进行设置。

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

我们使用 defineProperty(...)myObj 添加一个普通的属性并显示指定了一些特性,但一般来说不会用到这种方式,除非你想修改属性描述符。

4.1 wirtable

特性 wirtable 决定是否可以修改属性的值。如果将 wirtable 的值设为 false,我们再去修改属性值时会失败,需要注意,严格模式下,会报错:TypeError。

4.2 configurable

属性是否可配置,只要 configurable: true 就可以使用 defineProperty(...) 方法修改属性描述符,属性设置为不可配置,再去修改属性描述符不管是否在严格模式下,都会报 TypeError 错误。把 configurable 修改成 false 是单向的,无法撤销。
注意一个小小的例外:即使 configurable:false,我们还是可以把 writable 的状态由 true 变成 false,但无法从 false 变成 true
另外,除了无法修改,configurable: true 还会禁止删除这个属性。

4.3 enumerable

这个描述符控制的是属性是否会出现在对象的属性枚举中,比如 for..in 循环。如果把 enumerable 设为 false,这个属性就不会出现在枚举中,虽然仍可以正常访问它。我们一版定义的普通属性默认值都是 true,也是最常用的。

5 不变性

有时候我们希望属性或对象是不可改变的,首先我们要知道很重要的一点:所有的方法创建的都是浅不变性,既它们只会影响目标对象和它的直接属性。如果目标对象引用了其他对象(数组、对象、函数等),其他对象的内容不受影响,仍是可变的。

5.1 对象常量

结合 writable:falseconfigurable:false 就可以创建一个真正的常量属性(不可修改、重定义、删除)

5.2 禁止扩展

如果你想禁止一个对象添加新属性并且保留已有属性,可以使用 Object.preventExtensions(..):

1
2
3
4
5
6
var myObj = {
a: 2,
}
Object.preventExtensions(myObj);
myObj.b = 3;
myObj.b; // undefined
5.3 密封

Object.seal(..) 会创建一个“密封”对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions(..),并把所有属性标记 configurable: false。所以“密封”之后不仅不能添加新属性,也不能重新配置或者删除任何已有属性(虽然可以修改属性的值)。

5.4 冻结

Object.freeze(..) 会创建一个“冻结”对象,这个方法实际上在现有对象上调用 Object.seal(..) 方法,并把所有“数据访问”属性标记为 writable:false,这样就无法修改他们的值。这个方法是可以应用在对象上最高级别的不可变性,它会禁止对于对象本身及其任意直接属性的修改(不过,这个对象引用的其他对象是不受影响的)。

6 存在性

先看一个例子:

1
2
3
4
5
var myObj = {
a: undefined,
}
myObj.a; // undefined
myObj.b; // undefined

如你所见,这两种情况都返回 undefined,那我们如何区分呢?
我们可以在不访问属性值的情况下判断对象中是否存在这个属性:

1
2
3
4
5
6
7
8
var myObj = {
a: 2,
}
("a" in myObj); // true
("b" in myObj); // false

myObj.hasOwnProperty("a"); // true
myObj.hasOwnProperty("b"); // false

in 操作符会检查属性是否在对象及其 [[Protptype]] 原型链中,相比之下,hasOwnProperty(..) 只会检查属性是否存在 myObj 对象中,不会检查 [[Protptype]] 原型链。
看起来 in 操作符可以检查容器内是否有某个值,但是它实际上检查的是某个属性名是否存在。对于数组来说这个区别很重要,4 in [2, 4, 6] 的结果不是我们期待的 true,因为 [2, 4, 6] 这个数组中包含的属性名是 0、1、2,并没有 4

6.1 枚举

接下来详细介绍下枚举。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var myObj = {};
Object.defineProperty(
myObj,
"a",
// 让a像普通属性一样可枚举
{ enumerable: true, value: 2 }
);

Object.defineProperty(
myObj,
"b",
// 让b不可枚举
{ enumerable: false, value: 3 }
);

myObj.b; // 3
("b" in myObj); // true
myObj.hasOwnProperty("b"); // true

for (var k in myObj) {
console.log(k, myObj[k]);
} // "a" 2

可以看到 myObj.b 确实存在并且有访问值,但却不会出现在 for ... in 循环中(尽管可以通过 in 操作符来判断是否存在);原因是:“可枚举”就相当于“可以出现在对象属性的遍历中”。

也可以通过另一种方式来区分属性是否可枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var myObj = {};
Object.defineProperty(
myObj,
"a",
// 让a像普通属性一样可枚举
{ enumerable: true, value: 2 }
);

Object.defineProperty(
myObj,
"b",
// 让b不可枚举
{ enumerable: false, value: 3 }
);

myObj.propertyIsEnumerable("a"); // true
myObj.propertyIsEnumerable("b"); // false

Object.keys(myObj); // ["a"]
Object.getOwnPropertyNames(myObj); // ["a", "b"]

getOwnPropertyNames(..) 会检查给定的属性名是否直接存在在对象中(而不是在原型链上)并且满足 enumerable: true
Object.keys(..) 会返回一个数组,包含所有可枚举的属性,Object.getOwnPropertyNames(..) 会返回一个数组,包含所有属性,无论他们是否可枚举。
inhasOwnProperty(..) 的区别在于是否查找 [[Protptype]] 原型链,然而,Object.keysObject.getOwnPropertyNames 都只会查找对象直接包含的属性。

9 遍历

for..in 循环可以用来遍历对象的可枚举属性列表(包括 [[Protptype]] 原型链)。但是如何遍历属性的值呢?
对于数值索引的数组来说,可以使用标准化的for循环来遍历值:

1
2
3
4
var myArr = [1, 2, 3];
for (var i = 0; i < myArr.length; i++) {
console.log(myArr[i]);
} // 1 2 3

这实际上并不是在遍历值,而是遍历下标来指向值,如myArr[i]。
ES5中增加了一些数组的辅助迭代器,包括 forEach(..)every(..)some(..)。每种辅助迭代器中都可以接受一个回调函数并把它应用到数组的每个元素上,唯一的区别就是它们对于回调函数返回值的处理方式不同。
forEach(..) 会遍历数组中的所有值并忽略回调函数的返回值。every(..) 会一直运行直到回调函数返回 falsesome(..) 会一直运行直到回调函数返回 true
使用 for..in 循环遍历对象是无法获取属性值的,因为它们实际上遍历的是对象中所有可枚举属性,你需要手动获取属性值。那么如何直接遍历值,而不是数组下标或对象属性呢?ES6增加了一种用来遍历数组的 for..of 循环语法(如果对象本身定义了迭代器的话也可以遍历对象):

1
2
3
4
5
6
7
var myArr = [1, 2, 3];
for (var v of myArr) {
console.log(v);
}
// 1
// 2
// 3

下篇:混合对象 “类”