最近复刻了一个《 javascript 的设计模式》。也再一次温习了 js 的一些看似不怎么用的知识点,但是在设计模式中又是非常重要的。对于这种容易遗忘的细节但却重要的东西,我觉得来记录这些知识点,以便需要的时候可以看看。
首先我们知道 JS 是基于设计模式中的原型模式设计原型链,那么类一定是有共通的原型在里面。那么在子类实例化之后,在基类的 this 也就变成了子类的 this,那么只需要通过新建一个对象,并把当前子类的 prototype 即实例化类的__proto__重现给予到新对象中,由于 this.__proto__是子类的 prototype,那么绑定新对象调用子类的 prototype 的 constructor 即可实现,这也是 new 的过程。
但是在 es6 中有一些变更就是如果使用了 class 关键字,是不可以被 call 调用的,但是还是可以通过 ES6 推出的 Reflect.construct 来达到相同效果,代码如下,下面也有详细说明。
// 这是基类
class Shape {
// 代码省略...
clone() {
/**
* 如果子类要改成 class 形式,这个方法要改写成下面形式
* 因为主要是通过 JS 原型链帮助理解原型模式,所以子类不使用 class 形式
* class 和 function 构造函数的区别是 class 的构造函数增加了只能作为构造函数使用的校验,比如 new
* return Reflect.construct(
* this.__proto__.constructor,
* [],
* this.__proto__.constructor
* )
*/
let clone = {};
// 注意如果此类被继承,this 会变成子类的方法
// 同时这里使用的是原型的指针,所以比直接创建对象性能损耗更低
clone.__proto__ = this.__proto__;
this.__proto__.constructor.call(clone);
return clone;
}
}
// 这是子类
function Rectangle() {
this.type = "Rectangle";
}
Rectangle.prototype.__proto__ = new Shape();
Rectangle.prototype.draw = function() {
console.log("I'm a rectangle")
}
设计模式中有很多抽象方法,但是抽象方法是不能被初始化,只能被继承,那么抽象类要如何实现呢?
其实有两种方法,一种是通过判断 this 是否 instanceof 这个基类,二是用 ES6 的方法使用 new.target,如下:
class AbstractLogger {
constructor() {
if(new.target == AbstractLogger) {
throw new Error('this class must be extends.')
}
}
// 代码省略...
}
实现私有变量有很多方法,比如 Symbol,但 Symbol 的实现需要通过作用域隔离,其次当访问私有属性的关键字只能返回 undefined,没有错误信息抛出,这是一种非常不好的设计或实践.
那么私有属性还没有推出,如何来更好的实现呢?可以通过数据定义的方式来做这件事情,即 defineProperty。
那么问题又来了,因为私有属性只允许自己调用,子类不能调用,那么如何保证是自己而不是子类或者其它类型呢?那么可以根据当前 this 的__proto__来判断,如果__proto__引用等于自己的 prototype 则为自己。因为如果是子类继承,那么 this 的__proto__等于继承者的 prototype。那么根据这一点,我们可以这样做,如下:
class Meal {
constructor () {
const items = [];
/**
* 为什么不用 Proxy 而使用 defineProperty
* 因为 Proxy 虽然实现和 defineProperty 类似的功能
* 但是在这个场景下,语意上是定义属性,而不是需要代理
*/
Reflect.defineProperty(this, 'items', {
get:()=>{
if(this.__proto__ != Meal.prototype) {
throw new Error('items is private!');
}
return items;
}
})
}
// 省略代码...
}
在设计模式中,很多方法要实现终态化,即基类的方法不能被子类覆盖。
如何做到不被子类方法覆盖父类,貌似在 JS 中是个难题,但真的是难题吗?
并不是,因为当子类实例化的时候需要调用父类的构造函数,但此时父类的构造函数的 this 就是子类的方法,而 JS 对象构造又是基于原型的,那么如果子类自己实现了方法,那么子类实现的方法必然不等于父类原型中的方法,通过这个方法来实现父类方法的终态化。如下:
class Game {
constructor() {
if(this.play!= Game.prototype.play) {
throw new Error("play mothed is final,can't be modify!");
}
}
// 代码省略...
play(){
// 代码省略...
}
}
通过这些有意思的方法实现通过基类克隆一个子类对象,不可被初始化,私有化变量,终态方法的实现。即感叹 JS 的灵活性,但又对各种行为保有余地,这非常棒。同时在写《 javascript 的设计模式》的时候,发现 JS 本身就是一个设计模式的教科书,是很值得我们学习的。
以上例子出自于《 javascript 的设计模式》。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.