【07】前端常用的设计模式之原型模式

82 阅读4分钟

原型模式

概念

用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。

UML 类图

企业微信截图_53a0a5f1-a259-4157-be14-3b85767eaf2c.png

代码演示

class CloneDemo {
  name = 'clone demo';
  clone(): CloneDemo {
    return new CloneDemo();
  }
}

JS 中并不常用原型模式,但 JS 对象本身就是基于原型的,原型和原型链是非常重要的概念。

原型和原型链

企业微信截图_09f461c4-231a-4e2d-b892-6faed6ce9d98.png

函数和显示原型 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 的基础