JavaScript 高级程序设计 - 第 8 章 理解对象 学习笔记
本章内容量挺大的,因此笔记的话我也会分成 3-4 个部分去写,想要理解的细一点,顺便之后回顾的时候不会看的套类。
本章主要就是了解一下和理解一下什么是对象,包括 Object 上的 properties 和 mixin。
在 JavaScript 中,所有的存在(除了 primitive value)之外,都是对象,通常意义上来说,创建对象的方式有两种:
const person = new Object();
person.name = 'John';
person.age = 28;
person.sayHi = function () {
console.log(`Hello from ${this.name}`);
};
const person2 = {
name: 'Alex',
age: 18,
sayHi: function () {
console.log(`Hello from ${this.name}`);
},
};
二者在实现上没有什么区别,只是后者使用 object literal(对象字面量)更加的方便简洁。
attributes of property
以上面的实现为例,name
, age
和 sayHi
是 person
的 数据属性(data properties),并且同样也可以使用 Object.defineProperty()
的方法去赋值,如:
const person2 = {
name: 'Alex',
age: 18,
sayHi: function () {
console.log(`Hello from ${this.name}`);
},
};
Object.defineProperty(person2, 'greet', {
value: function () {
console.log('Hello from the other side');
},
});
person2.greet();
而 defineProperty
中传的第三个值,就是该 property 的 attributes。
interface PropertyDescriptor {
configurable?: boolean;
enumerable?: boolean;
value?: any;
writable?: boolean;
get?(): any;
set?(v: any): void;
}
/**
* Marker for contextual 'this' type
*/
interface ThisType<T> { }
defineProperty<T>(o: T, p: PropertyKey, attributes: PropertyDescriptor & ThisType<any>): T;
PropertyDescriptor
中包含的,就是所有可能存在的属性,现在的规范是使用中括号括起来,如 [[Configurable]]
,不过说的都是一个东西。
-
value
value 为 property 实际的值
-
writable
person2.name = 'John'; Object.defineProperty(person2, 'name', { writable: false, }); console.log(person2.name); person2.name = 'Dick'; console.log(person2.name); Object.defineProperty(person2, 'name', { writable: true, value: 'Rewritten', }); console.log(person.name);
结果如下:
可以看到,当
writable
设置为 false 之后,重新对 property 赋值也不无法对其进行修改。就算是之后重新修改writable
也不行非严格模式下这样的执行会被忽略,严格模式下这么做会报错
-
configurable
configurable 是一个 boolean,当一个属性的 configurable 被设置成 false,意味着当前的属性无法被使用
defineProperty
被重新定义、无法被删除、无法修改configurable
,enumerable
和writable
。如果
writable
的值为true
,那么value
还是可以被修改的,否则这个对象就被彻底 freeze,再也无法修改。Object.defineProperty(person2, 'age', { configurable: false, writable: false, }); Object.defineProperty(person2, 'age', { enumerable: false, });
顺便,
Object.freeze()
等同于将configurable
和writable
设为 false。 -
enumerable
这个 attribute 代表了当前属性是否会出现在
for...in
中for (const key in person2) { console.log(key); } Object.defineProperty(person2, 'name', { enumerable: false, }); for (const key in person2) { console.log(key); }
-
get
getter 是另一个相对而言比较新的内容,它主要的目的就是为了处理一些返回值,比如:
const person2 = { _id: 1, name: 'Alex', age: 18, sayHi: function () { console.log(`Hello from ${this.name}`); }, get id() { return `id is: ${this._id}`; }, }; console.log(person2.id);
这个案例中,person2 没有直接声明
id
property,但是可以通过 getter 返回一个id
property,其原因在于,它的具体实现为:const person = new Object(); person.name = 'John'; person.age = 28; person._id = 2; person.sayHi = function () { console.log(`Hello from ${this.name}`); }; Object.defineProperty(person, 'id', { get: function () { return this._id; }, }); console.log(person.id);
使用 getter 还有另一个原因,放到下面 setter 一起说。
-
set
setter 的作用和 getter 相似,其用法如下:
const person2 = { _id: 1, name: 'Alex', age: 18, sayHi: function () { console.log(`Hello from ${this.name}`); }, get id() { return `id is: ${this._id}`; }, /** * @param {string} arg */ set realName(arg) { this.name = 'my name is: ' + arg; }, };
同样也可以用
defineProperty
去实现。JS 中出现 getter 和 setter 的另一个原因就在于,ES6 之前 JS 中是没有私有属性的。于是社区约定俗成了一个规范,就是在 property name 中添加
_
代表不应该被访问的私有属性,要去访问和修改这个私有属性,就需要通过 getter 和 setter 去实现。这样实际保存值的 property,即this.__privateValue
,就不会直接被用户所访问。
定义的方法为 defineProperty
,获取 properties 的方法有两个:Object.getOwnPropertyDescriptor()
和 Object.getOwnPropertyDescriptors()
。前者需要知道 property 的名称,后者则会直接返回当前对象上的 properties。
console.log(Object.getOwnPropertyDescriptor(person2, 'name'));
console.log(Object.getOwnPropertyDescriptors(person2));
mixin
mixin 是一个被提到很多次的 concept,如果找一下的话,会发现 vue、scss 等都会发现直接有 mixin 这一属性或是函数。
ES6 也提供了 Object.assign()
的静态方法,它会遍历接收到的参数,然后将参数中所有可枚举 (Object.propertyIsEnumerable()
为 true
的 properties) 和 自由(Object.hasOwnProperty()
为 true
的 properties) 复制到目标属性上。
这可以很方便的实现 mixin,以下面代码为例:
const writeFile = {
write: function () {
console.log('Assume I can read file.');
},
};
const scanFile = {
scan: function () {
console.log('Assume I can scan file');
},
};
const readWriter = Object.assign({}, readFile, writeFile);
console.log(readWriter);
readFile.read();
const readWriterScanner = Object.assign({}, readWriter, scanFile);
readWriterScanner.scan();
可以看到,使用 mixin 提供更加灵活的语法,并且直接地满足了 SOLID 中的 single responsibility principle 和 interface segregation principle 两条原则。
liskov substitution principle 的判定我觉得稍微有点麻烦,逻辑上来说它要满足的事子类和父类,实现上来说 Object.assign()
完成了 shallow copy……
因为 JS 没有办法提供很好的静态检查,因此当使用 mixin 还会有其他的一些问题,如属性被重写的问题,依旧用上面的分代码为例,这里添加一点新的实现:
const fileOperator = {
read: function () {
console.log('I read file randomly.');
},
scan: function () {
throw new Error('scan file fail.');
},
};
const errorFileOperator = Object.assign({}, scanFile, fileOperator);
errorFileOperator.scan();
可以看到,在这个 mixin 中,scan
被 fileOperator
重写了,所以这里的输出结果不是做一个 log,而是抛出异常:
搭配上 JS 中不存在的类型见擦,当项目规模比较大的情况下,尤其是出现 mixin 套用 mixin 的情况,property 的管理会成为一个比较麻烦的事情。
顺便,只是针对 Object.assign()
的话,它是没有回滚的概念的,也就是说,如果操作中出现一些异常,可能会出现一个部分实现复制的新对象。
总结一下 mixin 的优点和缺点。
优点:
-
提高代码的复用性
-
提高代码的灵活性
-
模块化
-
composition over inheritance
这是一个 OOP 常用的概念,组成偏向于继承,这样可以做到比较轻松的修改组成部分的代码,而不用担心影响到继承部分的功能
缺点:
-
命名冲突
假设说多个对象中都存在
id
这个属性,那么 JS 就无法 capture 这个问题 -
排序很重要
假设说多个对象中都存在
id
这个属性,那么最后一个 id 将会重写其他的 id -
直接依赖
就不太像 spring 一样下面有底层的 DI 管理,很多时候要实现 mixin 还是得手动 cv,这样就会造成直接依赖(必须要知道对象,才能够实现 assign)
-
提高代码复杂性
reference
-
Object.freeze()
-
SOLID,面向对象设计五大基本原则