【设计模式】面向对象的JavaScript(多态、封装、继承)
本文是根据书籍《JavaScript设计模式与开发实践》而写的读书笔记,因为自己经常学了就忘,因此从现在开始起写博客记录一下,大部分都只是简单介绍下。
前言
设计模式的定义是:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方法。
通俗一点来说,设计模式是在某种场合下对某个问题的一种解决方案,而设计模式就是给面向对象软件开发中的一些好的设计取个名字。
设计模式的适用性:从某些角度来说,设计模式可能会带来代码量的增加和把系统的逻辑搞得更复炸。但软件开发的成本并非全部在开发解冻,设计模式的作用就是让人们写出可复用和可维护性高的程序。
所有设计模式都遵循的原则是:找出程序中变化的地方(不稳定),并将变化封装起来。把可变的不稳定封装起来,剩下的就是不变且稳定的部分,这部分是易于复用的。
为什么我选择JavaScript作为学习设计模式的语言?
- 首先我是前端工程师,JavaScript是我的主力语言(其实也就是只会这个,哈哈哈);
- 第二,我个人认为设计模式在前端开发中是非常重要的,因为在编写页面的过程中是有很多地方都是能够通过封装组件来实现组件的复用,减少冗余代码,提高整个项目的可复用性和维护性。
面向对象的JavaScript
1.语言类型
编程语言类型可以分为:静态类型语言和动态类型语言。JavaScript属于动态类型的语言。
静态类型 | 动态类型 | |
---|---|---|
区别 | 在程序编译时确定变量类型 | 在程序运行时,待变量被赋值,才会具有某种类型 |
优点 | 在编译时即可发现错误和优化 | 代码量少、简洁 |
缺点 | 代码量大(强制编写类型声明) | 无法保证变量的类型,运行时易导致错误 |
举列 | Java、C | JavaScript |
2.多态
多态的实际含义是:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。
换句话说,给不同的对象发送同一消息的时候,这些对象会根据这个消息分别给出不同的反馈。比如,鸭和鸡都会叫(操作),但是它们的叫声(结果)却不同,用代码来表示就是:
var sound = function (animal) {
if (animal instanceof Duck) {
console.log('嘎嘎嘎');
} else if (animal instanceof Chicken) {
console.log('咯咯咯');
}
};
var Duck = function () {};
var Chicken = function () {};
sound(new Duck()); // 嘎嘎嘎
sound(new Chicken()); // 咯咯咯
虽然上面的代码体现了“多态性”,但是如果当我们需要再加一个动物,比如狗(汪汪汪),就必须得改动 sound 函数。修改代码总是危险的,修改的地方越多,程序出错的可能性就越大。
多态背后的思想是将“做什么”和“谁去做以及怎么去做”分离开来,也就是将“不变的事物”与“可能改变的事物”分离开来。这个时候我们就可以把不变的部分隔离出来,将可变的部分封装起来,这给予我们扩展程序的能力,程序看起来是可生长的,这就是符合开放-封闭原则。修改代码如下所示:
// 把不变的部分隔离出来,即所有动物都会叫(边界情况暂不考虑)
const sound = function (animal) {
animal.sound();
};
// 把可变的部分各自封装起来
const Duck = function () {};
Duck.prototype.sound = function () {
console.log('嘎嘎嘎');
};
const Chicken = function () {
Chicken.prototype.sound = function () {
console.log('咯咯咯');
};
};
// 执行操作
sound(new Duck()); // 嘎嘎嘎
sound(new Chicken()); // 咯咯咯
// 如果我们新增一个动物 狗
const Dog = function () {};
Dog.prototype.sound = function () {
console.log('汪汪汪');
};
sound(new Dog()); // 汪汪汪
多态最根本的作用就是将过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句。将行为分布在各个对象中,并让这些对象各自负责自己的行为,这正是面向对象设计的优点。
3.封装
封装的目的是将信息隐藏,封装数据、实现、类型和变化。
3.1 封装数据
在其他语言中有对应的关键字来实现封装,但JavaScript中没有,我们只能依靠JavaScript的作用域特性,外层作用域不能直接访问内层作用域变量的特性来对数据进行封装,代码如下:
const myObject = (function () {
const _name = 'wx'; // 私有(private)变量
return {
getName: function () {
return _name; // 公开(public)变量
},
};
})();
console.log(myObject._name); // undefined 访问不到
console.log(myObject.getName()); // wx 可以访问
在ECMscript6中还提供了Symbol创建私有属性来保存数据的方法,这个可以后续在JavaScript分类中再单独的写写ES6新增的一些特性。
3.2 封装实现
封装的目的是将信息隐藏,封装应该被视为“任何形式的封装”,也就是说,封装不仅仅是隐藏数据,还包括隐藏实现细节、设计细节以及隐藏对象的类型等。
个人理解就是封装内部实现过程,通过暴露API接口来和外部进行通信,只要保持API不变,内部实现可以修改。
3.3 封装类型
由于JavaScript是一门动态类型的语言,无法像传统的静态类型语言一样通过抽象类和接口来进行封装(个人也不是很了解),所有JavaScript在这方面是没有能力实现的,也是这门语言的缺陷吧,
3.4 封装变化
找到程序中的变化并对其进行封装,即“找到变化并封装之”。
23种设计模式安按照封装变化的区别可以分为三种:
- 创建型模式。封装创建对象的变化。
- 结构型模式。封装对象之间的组合关系。
- 行为型模式。封装对象之间的行为变化。
其基本思路还是围绕把系统中稳定不变和容易变化的隔离开来,提高程序的稳定性和可扩展性。
1.4 继承
1.4.1 使用克隆的原型模式
JavaScript是采用基于原型的面向对象系统,而原型模式又是一种设计模式。
原型模式创建对象时,不再关注具体的类型,而是找到一个对象,然后通过克隆的方式来创建一个一模一样的对象。
原型模式的实现关键,是语言本身是否提供了clone方法。ECMAScript 5提供了Object.create 方法,可以用来克隆对象。
var LOL_dogface = function () {
this.blood = 100;
this.color = 'red';
};
// 实例化一个小兵对象
var dogface = new LOL_dogface();
dogface.blood = 700;
dogface.color = 'blue';
// 基于Object.create方法克隆对象
var cloneDogface = Object.create(dogface);
console.log(cloneDogface.blood);
console.log(cloneDogface.color);
Object.create方法的本质实际上是创建一个新对象,并修改其原型指向:
// Object.create实现原理
Object.create = function (obj) {
var F = function () {};
F.prototype = obj; // 修改函数的原型
return new F(); // 返回新的构造函数
};
1.4.2 JavaScript中的原型继承
基于原型链的委托机制就是原型继承的本质。
在 JavaScript语言中不存在类的概念(es6中已有,但只是语法糖),对象也并非从类中创建出来的,所有的 JavaScript对象都是从某个对象上克隆而来的。 JavaScript遵守原型编程的基本规则:
- 所有的数据都是对象(所有对象都来源于Object.prototype这个根对象)
- 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它(new操作符)
- 对象会记住它的构造器的原型(通过 proto 这个属性)
- 如果对象无法响应某个请求,它会把这个请求委托给它的构造器的原型(原型链查找)
具体关于原型链的知识可以看看我写的这篇文章,虽然很简单。
JavaScript 原型与原型链
因为是第一次写这么多,还不是很会写,大部分都是参考的文章,还希望各位见谅。
参考文献
《JavaScript设计模式与开发实践——曾探》
JS设计模式与开发实践— Junli_1413