原型模式
概念
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。
UML 类图
代码演示
class CloneDemo {
name = 'clone demo';
clone(): CloneDemo {
return new CloneDemo();
}
}
JS 中并不常用原型模式,但 JS 对象本身就是基于原型的,原型和原型链是非常重要的概念。
原型和原型链
函数和显示原型 propotype
- JS 中所有函数都有一个 propotype 属性
Object.prototype;
Array.prototype;
- 自定义的函数也有
// 1. 注意第一参数 this;2.暂且用any表示,实际会用class
function Foo(this: any, name: string, age: number) {
this.name = name;
this.age = age;
}
Foo.prototype.getName = function () {
return this.name;
};
Foo.prototype.sayHi = function () {
alert('hi');
};
对象和隐式原型proto
- 引用类型 JS 所有的引用类型对象都是通过函数创建的,都有proto,指向其构造函数的 prototype
const obj = {}; // 相当于 new Object()
obj.__proto__ === Object.prototype; // true
const arr = []; // 相当于 new Array()
arr.__proto__ === Array.prototype; // true
const f1 = new Foo('张', 20);
f1.__proto__ === Foo.prototype; // true
访问对象属性或者 API 时,首先查找自身属性,然后查找它的proto
f1.name;
f1.getName;
- 值类型的 API 值类型没有proto,但它依然可访问 API。因为 JS 会将它包装为引用类型,然后触发 API
const str = 'abc';
str.slice(0, 1); // 调用 String.prototype.string
原型链
一个对象的proto指向它构造函数的 prototype,而 prototype 本身也是一个对象,也会指向它构造函数的 prototype,于是就形成了原型链。
class 是函数的语法糖
class 和函数一样,也是基于原型实现的。
class Foo {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
getName() {
return this.name;
}
sayHi() {
alert('hi');
}
}
const f1 = new Foo('张', 20);
f1.__proto__ === Foo.prototype; // ture
继承
class People {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
eat() {
alert(`${this.name} eat something`);
}
speak() {
alert(`My name is ${this.name},age ${this.age}`);
}
}
class Student extends People {
school: string;
constructor(name: string, age: number, school: string) {
super(name, age);
this.school = school;
}
study() {
alert(`${this.name} study`);
}
}
场景
最符合原型模式的应用场景就是 Object.create,它可以指定原型。
const obj1 = {};
obj1.__proto__;
const obj2 = Object.create({ x: 100 });
obj2.__proto__;
JS 对象属性描述符
用于描述对象属性的一些特性
获取属性描述符
const obj = { x: 100 };
Object.getOwnPropertyDescriptor(obj, 'x'); // {value: 100, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptors(obj); // {x: {value: 100, writable: true, enumerable: true, configurable: true}}
设置属性描述符
const obj = { x: 100 };
Object.defineProperty(obj, 'y', {
value: 200,
writable: false,
// 其他...
// PS: 还可以定义 get set
});
使用 Object.defineProperty 定义新属性,属性描述符会默认为 false{configurable:false,enumerable:false,writable:false}; 而用 {x: 100} 字面量形式定义属性,属性描述符默认为 true。
解释各个描述符
value
属性值:值类型、引用类型、函数等
const obj = { x: 100 };
Object.defineProperty(obj, 'x', { value: 101 });
如果没有 value,则打印 obj 就看不到属性
const obj = {};
const x = 100;
Object.defineProperty(obj, 'x', {
get() {
return x;
},
set(newValue) {
x = newValue;
},
});
console.log(obj);
console.log(obj.x);
configurable
- configurable===false,不可以通过 delete 删除
- configurable===false,可以修改其他属性描述符配置
- 是否可以修改 get set 方法
const obj = { x: 100 };
Object.defineProperty(obj, 'y', {
value: 200,
configurable: false,
});
Object.defineProperty(obj, 'z', {
value: 300,
configurable: true,
});
delete obj.y; // 不成功
// false:修改y报错;true修改z不报错
Object.defineProperty(obj, 'y', {
value: 201,
});
writable
属性是否可以被修改
const obj = { x: 100 };
Object.defineProperty(obj, 'x', {
writable: false,
});
obj.x = 101;
console.log(obj.x); // 还是100
Object.freeze(),冻结对象:1.现有属性不可更改;2.不可添加新属性
const obj = { a: 1, b: 2 };
Object.freeze(obj);
obj.a = 11;
console.log(obj.a); // 1, 修改不成功
Object.getOwnPropertyDescriptor(obj, 'a'); // {value: 1, writable: false, enumerable: true, configurable: false}
obj.c = 10; // 不成功,不能添加新属性
Object.isFrozen(obj); // true,是否被冻结
注意:在 vue 中,如果 data 中有比较大的对象,且不需要响应式,则可以使用 Object.freeze()冻结
对比 Object.seal() 密封对象:1.现有属性可以更改;2.不可添加新属性
const obj = { a: 1, b: 2 };
Object.seal(obj);
obj.a = 11;
console.log(obj.a); // 11, 修改成功
Object.isSealed(obj); // true
注意:Object.freeze() 和 Object.seal() 都是浅操作,不可递归下级属性。
enumerable
是否可以通过 for in 遍历到
const obj = { a: 1, b: 2 };
Object.defineProperty(obj, 'c', {
value: 3,
enumerable: false,
});
Object.defineProperty(obj, 'd', {
value: 4,
enumerable: true,
});
for (const key in obj) {
console.log(key); // a,b,c
}
console.log('c' in obj); // true --只能限制 for...in , 不能限制 in
原型的属性描述符
// 在N年之前,使用for...in 遍历对象时,需要用hasOwnProperty 剔除原型属性
const obj = { a: 1, b: 2 };
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key);
}
}
// 现在不需要了,都是通过 enumerable 来判断
Object.getOwnPropertyDescriptor(obj.__proto__, 'toString'); // {...,enumerable: false}
// 修改原型属性 enumerable,这样就可以通过 for...in 遍历出来
Object.defineProperty(obj.__proto__, 'toString', { enumerable: true });
for (const key in obj) {
console.log(key); // a,b,toString
}
obj.hasOwnProperty('toString'); // 这个依然是 false, 和 enumerable 没有关系
// 还有些地方会修改函数的prototype, 会容易忽略 constructor 的属性描述符
function Foo() {}
Foo.prototype = {
constructor: Foo, // 需要设置 {enumerable: false}, 否则for...in, 会有constructor
fn1() {},
fn2() {},
};
for (const key in Foo.prototype) {
console.log(key);
}
Symbol 类型
Object 的 Symbol 属性,即便 {enumerable: true},也无法通过 for...in 遍历
const b = Symbol('b');
const obj = { a: 1, [b]: 2 };
for (const key in obj) {
console.log(key);
}
Object.getOwnPropertyDescriptor(obj, b); // {enumerable: true}
获取 Symbol 属性,可使用 getOwnPropertySymbols 或 Reflect.ownKeys
Object.keys(obj); // ['a']
Object.getOwnPropertySymbols(obj); // [Symbol(b)]
Reflect.ownKeys(obj); // ['a', Symbol(b)]
总结
原型模式不常用,但原型和原型链是 JS 的基础