Aelous-Blog

养一猫一狗,猫叫夜宵,狗叫宵夜

0%

JS设计模式-面向对象的JS

面向对象的JavaScript

JavaScript 没有提供传统面向对象语言中的类式继承,而是通过原型委托的方式来实现对象与对象之间的继承。JavaScript 也没有在语言层面提供对抽象类和接口的支持。正因为存在这些跟传统面向对象语言不一致的地方,我们在用设计模式编写代码的时候,要跟传统面向对象语言加以区别。所以在学习设计模式之前,我们需要对 JavaScript 在面向对象方面的认识。

动态类型语言和鸭子类型

动态类型语言

编程语言按照数据类型大体可以分为两类,一类是 静态类型语言 ,另一类式 动态类型语言

静态类型语言在编译时便已经确定变量的类型,而动态类型语言的变量类型要到程序运行的时候,待变量被赋予某个值之后,才会具有某种类型。

静态类型语言的优点:

  • 在编译的时候就能发现类型不匹配的作物,编辑器可以帮助我们提前避免程序在运行期间有可能发生的一些错误。
  • 如果在程序中明确的规定了数据类型,编译器还可以针对这些信息对程序进行一些优化工作,提高程序的执行速度。

静态类型语言的缺点:

  • 迫使程序员依照强契约来编写程序,为每个变量规定数据类型,归根结底知识辅助我们编写可靠性高的程序的一种手段,而不是编写程序的目的。毕竟大部分人写程序的目的是为了完成需求,交付工作。
  • 类型的声明也会增加更多的代码,在程序编写过程中,这些细节会让程序员的精力从思考业务逻辑上分散开来。

动态类型语言的优点:

  • 编写的代码数量更少,看起来也更加简洁,程序员可以把精力更多的放在业务逻辑上面。虽然不区分类型在某些情况会让程序变得难以理解,但整体而言,代码量越少,越专注于逻辑表达,对阅读程序越是有帮助。

动态类型语言的缺点:

  • 无法保证变量的类型,从而在程序的运行期有可能发生跟类型相关的错误。

JS 是一门动态类型语言。动态类型语言对变量的宽容给实际编码带来了很大的灵活性。由于无需进行类型检测,我们可以尝试调用任何对象的任意方法,而无需去考虑它原本是否被设计为拥有该方法。

鸭子类型

通俗说法:“如果它走起路来像鸭子,叫起来也是鸭子,那么它就是鸭子。”

鸭子类型指导我们只关注对象的行为,不关注对象本身,也就是关注 HSA-A,而不是 IS-A。

在动态类型语言的面向对象设计中,鸭子类型的概念至关重要。利用鸭子类型的思想,我们不必借助超类型的帮助,就能轻松地在动态类型语言中实现一个原则:“面向接口编程,而不是面向实现编程。”例如,一个对象又 push 和 pop 方法,并且这些方法提供了正确的实现,它就可以被当作栈来使用。一个对象如果又 length 属性,也可以依照下标来存取属性(最好还要拥有 slice 和 splice 等方法),这个对象就可以被当作数组来使用。

在静态类型语言中,要实现“面向接口编程”并不是一件容易的事情,往往要通过抽象类活着接口等将对象进行向上转型。当对象的真正类型被隐藏在它的超类型身后,这些对象才能在类型检查系统的“监视”下互相替换。只有当对象能够被相互替换使用时,才能体现出对象的多态性的价值。

“面向接口编程”是设计模式中最重要的思想,但是在 JS 语言中,“面向接口编程”的过程跟主流的静态语言不一样,因此,在 JS 中实现设计模式的过程与一些我们熟悉的语言中的实现可能大相径庭。

多态

“多态”的实际含义:同一操作作用与不同的对象上面,可以产生不同的解释和不同的执行结果。就是说,给不同的对象发送同一个消息,这些对象会根据这个消息分别给出不同的反馈。

例举一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
var makeSound = function(animal) {
if (animal instanceof Duck) {
console.log("嘎嘎嘎");
} else if (animal instanceof Chicken) {
console.log("咯咯咯");
}
};

var Duck = function() {};
var Chicken = function() {};

makeSound(new Duck()); // 嘎嘎嘎
makeSound(new Chicken()); // 咯咯咯

上面这段代码体现了“多态性”,当我们分别向鸡和鸭发出“叫”的指令,他们根据自身作出不同的反应。但是这样的多态性显然无法让人满意,比如我们要加入狗,那就需要加入“汪汪汪”。修改代码总是危险的,修改的地方越多,程序出错的可能性越大,而且如果动物非常多,makeSound 会变成一个非常大的函数。

多态背后的思想是将“做什么”和“谁去做以及怎么样做”分离开来,也就是将“不变的事物”与“可能改变的事物”分离开来。
把不变的部分隔离出来,把可变的部分封装起来,这给予了我们扩展程序的能力,程序看起来是可生长的,也是符合“开放-封闭原则”的,相对于修改代码来说,仅仅增加代码就能完成一样的功能,看起来优雅的多。

对象的多态性

改写代码,我们把不变的部分隔离出来,就是所有的动物都会发出叫声:

1
2
3
var makeSound = function(animal) {
animal.sound();
};

然后把各自可变的东西各自封装,我们刚才谈到的多态性实际上指的是对象的多态性:

1
2
3
4
5
6
7
8
9
10
11
12
var Duck = function() {};
Duck.prototype.sound = function() {
console.log("嘎嘎嘎");
};

var Chicken = function() {};
Chicken.prototype.sound = function() {
console.log("咯咯咯");
};

makeSound(new Duck()); // 嘎嘎嘎
makeSound(new Chicken()); // 咯咯咯

现在我们如果在这个世界中加入了狗,那么只要追加狗的代码就可以了,而不用改动之前的代码 makeSound 函数,如下:

1
2
3
4
5
6
var Dog = function() {};
Dog.prototype.sound = function() {
console.log("汪汪汪");
};

makeSound(new Dog()); // 汪汪汪

JS 的多态

多态的思想是把“做什么”和“谁去做”分离开来,要实现这一点,归根结底要先消除类型之间的耦合关系。如果类型之间的耦合关系没有消除,那么我们在 makeSound 方法中指定了发出叫声的对象是某个类型,它就不可能再被替换为另外一个类型。

一个 JS 对象,既可以表示 Duck 类型的对象,又可以表示 Chicken 类型的对象,这意味着 JS 对象的多态性是与生俱来的。

JavaScript 作为一门动态类型语言,它在编译时没有类型检查的过程,既没有检查创建的对象类型,有没有检查传递的参数类型。我们既可以往 makeSound 函数里传递 duck 对象当作参数,也可以传递 chicken 对象当参数。

由此可见,一个动物是否可以发声,取决于是否有 makeSound 方法,而不是取决于它是否是某种类型的对象,这里不存在任何程度上的“类型耦合”。在 JS 中,并不需要诸如向上转型之类的技术来取得多态的效果。

封装

封装的目的是将信息隐藏。一般来说,我们讨论的封装是封装数据和封装实现。这里我们还将讨论封装类型和封装变化。

封装数据

在许多语言的对象系统中,封装数据是由语法解析来实现的,这些语言也许提供了 private、public、protected 等关键字来提供不同的访问权限。

但 JS 中没有提供这些关键字,我们只能依赖变量的作用域来实现封装特性,而且只能模拟出 public 和 private 这两种封装性。

除了 ES6 中提供了 let 之外,我们一般通过函数来创建作用域:

1
2
3
4
5
6
7
8
9
10
11
12
var myObject = (function() {
var __name = "sven"; // 私有(private)变量
return {
getName: function() {
// 公开(public)变量
return __name;
}
};
})();

console.log(myObject.getName()); //输出:sven
console.log(myObject.__name); //输出:undefined

另外:在 ES6 中,还可以通过 Symbol 创建私有属性。

封装实现

以上描述的封装,指的是数据层面的封装。有时候我们喜欢把封装等同于封装数据,但是这是一种比较狭义的定义。

封装的目的是隐藏,封装应该被视为:“任何形式的封装”,也就是说,封装不仅仅隐藏数据,还包括隐藏实现细节、设计细节以及隐藏对象的类型等。

从封装实现细节来讲,封装使得对象内部的变化对其他对象而言是透明的,也就是不可见的。对象对它自己的行为负责。其他对象或者用户都不关心它内部的实现。封装使得对象之间的耦合变得松散,对象之间只通过暴露的 API 接口来通信。当我们修改一个对象时,可以随意地修改它的内部实现,只要对外的接口没有变化,就不会影响到程序的其他功能

封装类型

封装类型时静态类型语言中一种重要的封装方式。一般而言,封装类型是通过抽象类和接口来进行的。把对象的真正类型隐藏在抽象类或者接口之后,相比对象的类型,客户更关心对象的行为。在许多静态语言的设计模式中,想法设法地去隐藏对象的类型,也是促使这些模式诞生的原因之一。比如工厂方法模式、组合模式等。

当然在 JS 中,没有对抽象类和接口的支持。JS 本身也是一门类型模糊的语言。在封装类型方面,JS 没有能力也没有必要做那么多。

封装变化

从设计模式的角度出发,封装在更重要的层面体现为封装变化。

《设计模式》中曾提到:

“考虑你的设计中那些地方可能变化,这种方式与关注会导致重新设计的原因相反。它不是考虑什么时候会迫使你的设计改变,而是考虑你怎么样才能够在不重新设计的情况下进行改变。这里的关键在于封装发生变化的概念,这是许多设计模式的主题。”

《设计模式》一书总共归纳了 23 种设计模式,从意图上区分,这 23 种设计模式可以分为:创建型模式、结构型模式和行为型模式。

拿创建型模式来说,要创建一个对象,是一种抽象行为,而具体创建什么对象则是可以变化的,创建型模式的目的就是封装创建对象的变化。而结构型模式封装的是对象之间的组合关系。行为型模式封装的是对象的行为变化。

通过封装变化的方式,把系统中稳定不变的部分和容易变化的部分隔离开来,在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易。这可以最大程度地保证程序的稳定性和可扩展性。

从《设计模式》副标题“可复用面向对象软件的基础”可以知道,这本书理应教我们如何编写可复用的面向对象程序。这本书把大多数笔墨都放在如何封装变化上面,这跟编写可复用的面向对象程序是不矛盾的。当我们想办法把程序中变化的部分封装好之后,剩下的即是稳定而可复用的部分。

End~~ 撒花= ̄ω ̄=花撒
如果您读文章后有收获,可以打赏我喝咖啡哦~