红宝书4-第八章对象、类与面向对象编程(1)

@


1.理解对象

这里作为理解对象的开篇,介绍了两种方式来创建对象

第一种:通过基类 Object 来创建

let person = new Object(); 
person.name = "Nicholas"; 
person.age = 29; 
person.job = "Software Engineer"; 
person.sayName = function() { 
 console.log(this.name); 
};

第二种:通过字面量来创建

let person = { 
 name: "Nicholas", 
 age: 29, 
 job: "Software Engineer", 
 sayName() { 
 console.log(this.name); 
 } 
};

2. 属性的类型

属性的类型在这里是指我们定义的属性,其实内部有一些特性来来描述规范这个属性,把这些特性分成两种:数据属性访问器属性。为了区分和特定表示这些特性,我们用中括号来表示他们,比如 [[Enumerable]]

1.数据属性

数据属性有 4个特性描述它们的行为。

  • [[Configurable]] :表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true,如前面的例子所示。
  • [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。
  • [[Writable]]:表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。
  • [[Value]]:包含属性实际的值。这就是前面提到的那个读取和写入属性值的位置。这个特性的默认值为 undefined。

像咱们前面那样显式的添加属性,前三个特征都会被设置成true,最后的[[value]]会被设置成特定的值。
所以数据属性的特性包含三个判断特性,以及一个保存实际数据的储存特性。

改变数据属性特性的两种方法

第一种: Object.defineProperty()
其接受三个参数,要给其添加属性的对象、属性的名称和一个描述符对象。描述符对象上的属性就是上文中的4个特性,一一对应。

let person = {}; 
Object.defineProperty(person, "name", { 
 writable: false, 
 value: "Nicholas" 
}); 
console.log(person.name); // "Nicholas" 
person.name = "Greg"; 
console.log(person.name); // "Nicholas"

非严格模式下,给这个属性赋值会没有效果。严格模式下,则会报错。
注意:如果我们把[[configurable]]这个特性赋值为false,这样的操作是不可逆的。再次用Object.defineProperty()修改除了writable特性以外的属性都会报错。

因此,虽然可以对同一个属性多次调用 Object.defineProperty(),但在把 configurable 设置为 false 之后就会受限制了。
在调用 Object.defineProperty()时,configurable、enumerable 和 writable 的值如果不指定,则都默认为 false。这里也提到了特性默认值的问题,与上面直接给对象添加属性正好是相反的。

第二种:Object.defineProperties()

let book = {}; 
Object.defineProperties(book, { 
 year_: { 
 value: 2017 
 }, 
 edition: { 
 value: 1 
 }, 
 year: { 
 get() { 
 return this.year_; 
 set(newValue) { 
 if (newValue > 2017) { 
 this.year_ = newValue; 
 this.edition += newValue - 2017; 
 } } } });},```

#### 2. 访问器属性
它们包含一个**获取(getter)函数**和一个**设置(setter)函数**,不过这两个函数不是必需的。在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效的值。在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。访问器属性有 4 个特性描述它们的行为。

 

 - [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。 
 -  [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。 
 - [[Get]]:获取函数,在读取属性时调用。默认值为 undefined。 
 - [[Set]]:设置函数,在写入属性时调用。默认值为 undefined。

**访问器属性是不能直接定义的**,必须使用 **Object.defineProperty()**。下面是一个例子:

```javascript
// 定义一个对象,包含伪私有成员 year_和公共成员 edition 
let book = { 
 year_: 2017, 
 edition: 1
 }; 
Object.defineProperty(book, "year", { 
 get() { 
 return this.year_; 
 }, 
 set(newValue) { 
 if (newValue > 2017) { 
 this.year_ = newValue; 
 this.edition += newValue - 2017; 
 } 
 } 
}); 
book.year = 2018; 
console.log(book.edition); // 2

上面的例子就是我们定义了一个访问器属性,内部可以定义 get ,set来 自定义函数的获取值,以及设置修改属性的规则。这是访问器属性的典型使用场景,即设置一个属性值会导致一些其他变化发生。

获取函数和设置函数不一定都要定义。只定义获取函数意味着属性是只读的,尝试修改属性会被忽略。在严格模式下,尝试写入只定义了获取函数的属性会抛出错误。类似地,只有一个设置函数的属性是不能读取的,非严格模式下读取会返回 undefined,严格模式下会抛出错误。

在不支持 Object.defineProperty()的浏览器中没有办法修改[[Configurable]]或[[Enumerable]]。

3.读取属性的特性

Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符。这个方法接收两个参数:属性所在的对象和要取得其描述符的属性名。

let book = {}; 
Object.defineProperties(book, { 
 year_: { 
 value: 2017 
 }, 
 edition: { 
 value: 1 
 }, 
 year: { 
 get: function() { 
 return this.year_; 
 }, 
 set: function(newValue){ 
 if (newValue > 2017) { 
 this.year_ = newValue; 
 this.edition += newValue - 2017; 
 } 
 } 
 } 
}); 
let descriptor = Object.getOwnPropertyDescriptor(book, "year_"); 
console.log(descriptor.value); // 2017 
console.log(descriptor.configurable); // false 
console.log(typeof descriptor.get); // "undefined" 
let descriptor = Object.getOwnPropertyDescriptor(book, "year"); 
console.log(descriptor.value); // undefined 
console.log(descriptor.enumerable); // false 
console.log(typeof descriptor.get); // "function"

Object.getOwnPropertyDescriptors()静态方法,来获取所有属性的特征值。

let book = {}; 
Object.defineProperties(book, { 
 year_: { 
 value: 2017 
 }, 
 edition: { 
 value: 1 
 }, 
 year: { 
 get: function() { 
 return this.year_; 
 }, 
 set: function(newValue){ 
 if (newValue > 2017) { 
 this.year_ = newValue; 
 this.edition += newValue - 2017; 
 } 
 } 
 } 
}); 
console.log(Object.getOwnPropertyDescriptors(book)); 
// { 
// edition: { 
// configurable: false, 
// enumerable: false, 
// value: 1, 
// writable: false 
// }, 
// year: { 
// configurable: false, 
// enumerable: false, 
// get: f(), 
// set: f(newValue), 
// }, 
// year_: { 
// configurable: false, 
// enumerable: false, 
// value: 2017, 
// writable: false 
// } 
// }

4.合并对象。

Object.assign()方法。这个方法接收一个目标对象和一个或多个源对象作为参数,然后将每个源对象中可枚(Object.propertyIsEnumerable()返回 true)自有(Object.hasOwnProperty()返回 true)属性复制到目标对象。以字符串和符号为键的属性会被复制。对每个符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性的值,然后使用目标对象上的[[Set]]设置属性的值

let dest, src, result; 
/** 
 * 简单复制
 */ 
dest = {}; 
src = { id: 'src' }; 
result = Object.assign(dest, src); 
// Object.assign 修改目标对象
// 也会返回修改后的目标对象
console.log(dest === result); // true 
console.log(dest !== src); // true 
console.log(result); // { id: src } 
console.log(dest); // { id: src } 
/** 
 * 多个源对象
 */ 
dest = {}; 
result = Object.assign(dest, { a: 'foo' }, { b: 'bar' }); 
console.log(result); // { a: foo, b: bar } 
/** 
 * 获取函数与设置函数
 */ 
dest = { 
 set a(val) { 
 console.log(`Invoked dest setter with param ${val}`); 
 } 
}; 
src = { 
 get a() { 
 console.log('Invoked src getter'); 
 return 'foo'; 
 } 
}; 
Object.assign(dest, src); 
// 调用 src 的获取方法
// 调用 dest 的设置方法并传入参数"foo" 
// 因为这里的设置函数不执行赋值操作
// 所以实际上并没有把值转移过来
console.log(dest); // { set a(val) {...} }

Object.assign()实际上对每个源对象执行的是浅复制。多个对象后面的会覆盖前面的相同的属性。此外,从源对象访问属性取得的值,比如获取函数,会作为一个静态值赋给目标对象。换句话说,不能在两个对象间转移获取函数和设置函数。
就是如果我们设置了获取或者设置函数,如果需要触发他们就走我们自己设置的方法,没有设置就走默认的获取设置方法。

let dest, src, result; 
/** 
 * 覆盖属性
 */ 
dest = { id: 'dest' }; 
result = Object.assign(dest, { id: 'src1', a: 'foo' }, { id: 'src2', b: 'bar' }); 
// Object.assign 会覆盖重复的属性
console.log(result); // { id: src2, a: foo, b: bar } 
// 可以通过目标对象上的设置函数观察到覆盖的过程:
dest = { 
 set id(x) { 
 console.log(x); 
 } 
}; 
Object.assign(dest, { id: 'first' }, { id: 'second' }, { id: 'third' }); 
// first 
// second 
// third 
/** 
 * 对象引用
 */ 
dest = {}; 
src = { a: {} }; 
Object.assign(dest, src); 
// 浅复制意味着只会复制对象的引用
console.log(dest); // { a :{} } 
console.log(dest.a === src.a); // true

如果赋值期间出错,则操作会中止并退出,同时抛出错误。Object.assign()没有“回滚”之前赋值的概念,因此它是一个尽力而为、可能只会完成部分复制的方法。

let dest, src, result; 
/** 
 * 错误处理
 */ 
dest = {}; 
src = { 
 a: 'foo', 
 get b() { 
 // Object.assign()在调用这个获取函数时会抛出错误
 throw new Error(); 
 },
  c: 'bar' 
}; 
try { 
 Object.assign(dest, src); 
} catch(e) {} 
// Object.assign()没办法回滚已经完成的修改
// 因此在抛出错误之前,目标对象上已经完成的修改会继续存在:
console.log(dest); // { a: foo }
posted @ 2020-10-07 17:11  jacksonni  阅读(190)  评论(0编辑  收藏  举报