希沃ENOW大前端css
公司官网:CVTE(广州视源股份)html
团队:CVTE旗下将来教育希沃软件平台中心enow团队前端
本文做者:git
设计模式的学习过程当中每每有四个境界github
本系列将会和你们一块儿从了解面向对象开始,再深刻到经常使用的设计模式,一块儿探索TypeScript配合设计模式在咱们平时开发过程当中的无限可能,设计出易维护、易扩展、易复用、灵活性好的程序。web
上一篇咱们一块儿了解了面向对象的几个基本概念typescript
类与实例、构造函数、方法重载、属性与修饰符,附上篇连接:canvas
今天咱们继续来学习面向对象的几个重要概念。浏览器
为了让你们能够更直观的了解面向对象的概念,咱们在这一篇一块儿用面向对象的思惟去实现一个能够动态添加形状到画布,而且形状能够在画布内自由拖动的效果。附上源码地址:github.com/goccult/typ…
咱们先准备从新整理一下咱们的代码,在src文件夹下新建如下文件
// index.ts
require('dist/circle.js')
require('dist/main.js')
// require.ts
const require = (path: string) => {
const script = document.createElement('script')
script.async = false
script.defer = true
script.src = path
document.body.appendChild(script)
}
// main.ts
function addCircle() {
new Circle('#canvas', {x: 0, y: 0})
}
复制代码
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="dist/require.js"></script>
<style> body { padding: 0; margin: 0; } img { -webkit-user-drag: none; } .container { width: 100vw; height: 100vh; padding: 40px; box-sizing: border-box; } #canvas { border: dashed 1px black; width: 1000px; height: 700px; position: relative; overflow: hidden; } .action-box { width: 100%; height: 60px; } </style>
</head>
<body>
<div class="container">
<div class="action-box">
<button onclick="addCircle()">添加圆形</button>
</div>
<div id="canvas"></div>
</div>
<script src="dist/index.js"></script>
</body>
</html>
复制代码
咱们从新设计一下咱们的Circle类
// circle.ts
class Circle {
private location: {x: number, y: number};
private dom: HTMLDivElement;
private canvasDom: HTMLDivElement | null; // 画布dom元素
constructor(canvasId: string, location?: {x: number, y: number}) {
this.location = location ? location : { x:0, y:0 };
this.dom = document.createElement('div')
this.canvasDom = document.querySelector(canvasId);
this.appendElement()
}
get Location() {
return this.location
}
set Location(obj: {x: number, y: number}) {
if (!obj.x || !obj.y) {
throw new Error('Invalid location')
}
this.location = obj
}
private appendElement() { // 加入appendElement方法往画布添加dom元素
this.dom.style.position = 'absolute';
this.dom.style.width = '60px';
this.dom.style.height = '60px';
this.dom.style.borderRadius = '50%';
this.dom.style.background = 'green';
this.dom.style.top = `${this.location.y}px`;
this.dom.style.left = `${this.location.x}px`;
this.dom.style.cursor = 'move';
(this.canvasDom as HTMLDivElement).appendChild(this.dom);
}
}
复制代码
Circle
接收外部传入的id用于获取画布的dom
元素,接收location
肯定插入的圆相对于画布的位置,再经过appendElement
方法肯定咱们要插入元素的样式并插入到画布。
这时候咱们的页面就已经能够添加圆形到画布里了
到这一步其实咱们就已经完成了对Circle
类的封装,它把类内部的属性和方法统一保护了起来,只保留有限的接口与外部进行联系,尽量屏蔽对象的内部实现细节,而且使用访问器属性对属性的访问和设值作了限制,防止外部随意修改内部数据。
封装有三大好处。
一、良好的封装能够减小耦合
二、类内部的实现能够自由的修改
三、类具备清晰的对外接口
复制代码
若是咱们还想画一个正方形,那你可能会说简单啦,仿造Circle
类封装一个Square
类在appendElement
方法里设置正方形的样式再添加多一个按钮就能够实现啦。
class Square {
...
constructor(canvasId: string, location?: {x: number, y: number}) {
...
}
get Location() {
...
}
set Location(obj: {x: number, y: number}) {
...
}
public appendElement() {
this.dom.style.position = 'absolute';
this.dom.style.width = '60px';
this.dom.style.height = '60px';
this.dom.style.background = 'red';
this.dom.style.top = `${this.location.y}px`;
this.dom.style.left = `${this.location.x}px`;
this.dom.style.cursor = 'move';
(this.canvasDom as HTMLDivElement).appendChild(this.dom);
}
}
复制代码
虽然咱们经过这种方式实现了功能,可是你会发现Circle
类和Square
类里面有大量重复的代码
,并且这些代码都是必须的不可去除的,要解决这个问题就须要用到面向对象的第二大特性“继承”。
咱们抛开代码层面去看圆形和正方形,其实这二者均可以归属于形状,那咱们就能够理解为圆形、正方形与形状是继承关系。
那从咱们程序设计的角度看,一个对象的继承表明了“is-a”的关系,在这里能够理解为圆形是形状,则代表圆形能够继承形状。
咱们能够新建立一个形状的类,形状类叫作父类或者基类
,圆形和正方形叫作子类或者派生类
,其中子类继承父类的全部特性,子类不但继承了父类全部的特性,还能够定义新特性。
同时在使用继承时要记住三句话:
一、子类拥有父类非private的属性和方法。
二、子类拥有本身的属性和方法,即子类能够扩展父类没有的属性和方法。
三、子类还能够本身实现父类的功能。
复制代码
有了对继承简单的认识,那咱们如今就能够对咱们的代码进行优化了。如今Square
类和Circle
类中存在大量的重复代码,咱们能够新建一个Shape
类当作父类,把重复的代码都尽可能放到Shape
类中。
// shape.ts
class Shape {
protected location: {x: number, y: number}; // 注意这里的修饰符都变成了 protected
protected dom: HTMLDivElement;
protected canvasDom: HTMLDivElement | null;
constructor(canvasId: string, location?: {x: number, y: number}) {
this.location = location ? location : { x:0, y:0 };
this.dom = document.createElement('div')
this.canvasDom = document.querySelector(canvasId);
}
get Location() {
return this.location
}
set Location(obj: {x: number, y: number}) {
if (!obj.x || !obj.y) {
throw new Error('Invalid location')
}
this.location = obj
}
protected appendElement() {} // 多态概念后面说
}
复制代码
这里要注意,咱们用上了上一篇中讲到的protected
修饰符,刚才有讲过,子类拥有父类非"private"
的属性和方法,既然咱们这里的属性都是子类须要用到的,那咱们就须要把修饰符改成protected
。
有了Shape
类,那咱们的Circle
和Square
类就能够经过继承Shape
来得到共用的属性和方法了。
// circle.ts
class Circle extends Shape{ // extends关键字指定继承的父类函数
constructor(canvasId: string, location?: {x: number, y: number}) {
super(canvasId, location) // super()方法访问父类的构造函数
this.appendElement()
}
public appendElement() {
this.dom.style.position = 'absolute';
this.dom.style.width = '60px';
this.dom.style.height = '60px';
this.dom.style.borderRadius = '50%';
this.dom.style.background = 'green';
this.dom.style.top = `${this.location.y}px`;
this.dom.style.left = `${this.location.x}px`;
this.dom.style.cursor = 'move';
(this.canvasDom as HTMLDivElement).appendChild(this.dom);
}
}
复制代码
// square.ts
class Square extends Shape{ // extends关键字指定继承的父类函数
constructor(canvasId: string, location?: {x: number, y: number}) {
super(canvasId, location) // super()方法访问父类的构造函数
this.appendElement()
}
public appendElement() {
this.dom.style.position = 'absolute';
this.dom.style.width = '60px';
this.dom.style.height = '60px';
this.dom.style.background = 'red';
this.dom.style.top = `${this.location.y}px`;
this.dom.style.left = `${this.location.x}px`;
this.dom.style.cursor = 'move';
(this.canvasDom as HTMLDivElement).appendChild(this.dom);
}
}
复制代码
而后咱们在页面中添加一个插入正方形的按钮,分别在index.ts、main.ts、index.html
加入如下代码
// index.ts
require('dist/shape.js')
require('dist/square.js')
// main.ts
function addSquare() {
new Square('#canvas', {x: 100, y: 0})
}
//index.html
<button onclick="addSquare()">添加正方形</button>
复制代码
到这一步咱们的页面就能够动态添加圆形和正方形了。
这时候前方来了需求咱们须要给每一个形状加可拖动的功能,试想一下,若是咱们如今有大于5个形状,而没有用继承的方式实现Shape
类,那么咱们添加拖动的代码就须要在多个不一样形状的类中添加剧复的代码,这工做量不只很大,并且很容易出错。咱们如今有了Shape
类,就只须要在Shape
类中改就行了。
// shape.ts
class Shape {
protected location: {x: number, y: number};
protected dom: HTMLDivElement;
protected canvasDom: HTMLDivElement | null;
protected move: boolean = false; // 判断是否处于移动状态
protected offsetY: number = 0; // 记录鼠标按下形状时鼠标在形状X轴方向的偏移量
protected offsetX: number = 0; // 记录鼠标按下形状时鼠标在形状Y轴方向的偏移量
protected canvasOffsetLeft: number = 0; // 记录画布相对于整个浏览器视口区域X轴方向的偏移量
protected canvasOffsetTop: number = 0; // 记录画布相对于整个浏览器视口区域Y轴方向的偏移量
constructor(canvasId: string, location?: {x: number, y: number}) {
this.location = location ? location : { x:0, y:0 };
this.dom = document.createElement('div')
this.canvasDom = document.querySelector(canvasId);
this.addMoveFn()
}
get Location() {
return this.location
}
set Location(obj: {x: number, y: number}) {
if (!obj.x || !obj.y) {
throw new Error('Invalid location')
}
this.location = obj
}
protected appendElement() {}
private addMoveFn() {
this.dom.addEventListener('pointerdown', (e) => {
this.move = true
this.offsetX = e.offsetX
this.offsetY = e.offsetY
const {left = 0, top = 0} = (this.canvasDom as HTMLDivElement).getBoundingClientRect()
this.canvasOffsetLeft = left
this.canvasOffsetTop = top
})
this.canvasDom?.addEventListener('pointerup', () => {
this.move = false
})
this.canvasDom?.addEventListener('pointermove', (e) => {
if (this.move) {
this.dom.style.left = `${e.clientX - this.offsetX - this.canvasOffsetLeft }px`
this.dom.style.top = `${e.clientY - this.offsetY - this.canvasOffsetTop }px`
}
})
this.canvasDom?.addEventListener('mouseleave', (e) => {
if (this.move) {
this.move = false
}
})
}
}
复制代码
具体的实现就不赘述了,就是添加一些鼠标的事件监听来实现拖动元素修改left和top属性。
总结一下继承的优点和劣势
继承可使得子类公共的部分都放在父类,使得代码获得了共享避免重复,另外继承可以使得修改或扩展而来的实现都较为容易。
继承也有它的劣势,继承把个各种的耦合性加强了,父类变子类也得跟着变,因此当两个类之间若是没有表现出"is-a"的关系,仍是要谨慎继承。
完成到这一步时咱们再回到代码上看,在建立Shape
类的时候里面的appendElement()
方法没有具体的代码实现。方法的实如今子类中经过重写该方法作了不一样的实现。
// shape.ts
protected appendElement() {}
// circle.ts
public appendElement() {
...
this.dom.style.borderRadius = '50%';
this.dom.style.background = 'green';
...
}
// square.ts
public appendElement() {
...
this.dom.style.background = 'red';
...
}
复制代码
其实这就是多态的概念:不一样的对象能够实现同名的方法,可是经过本身的实现来完成不一样的功能。
多态的优点也是比较明显的,提供了代码的扩展性,提供了代码的维护性。
咱们再次回到代码,其实咱们能够发现Shape
类的做用只是用于被子类继承,并不须要被实例化,那么这时候咱们就能够把Shape
类认为是一个抽象类。
在TypeScript
中抽象类和抽象方法用abstract
关键字定义。
由此咱们再改造一下咱们的Shape
类把它改成抽象类
// shape.ts
abstract class Shape { // 抽象类前加abstract关键字
...
protected abstract appendElement(): void // 抽象方法加abstract关键字
...
}
复制代码
抽象类有三点要求:
一、抽象类不能被实例化
二、抽象方法必须被子类重写
三、若是类中包含了抽象方法,则该类就必须被定义为抽象类
复制代码
假设这时候来了一个新人开发咱们的功能,他须要添加一个长方形到画布中,因为抽象类和抽象方法的限制,它本身新建的Rectangle
类中若是没有实现appendElement
抽象方法,那ts会给他报错提示。
形状的移动逻辑都已经封装在了父类中,那新人只须要重写appendElement
方法就能够实现添加各类想要的形状到画布中了。
class AnyShape extends Shape{
constructor(canvasId: string, location?: {x: number, y: number}) {
super(canvasId, location) // super()方法访问父类的构造函数
this.appendElement()
}
public appendElement() { // 重写该方法实现添加不一样形状或者元素
...
}
}
复制代码
本系列的基础篇就先介绍到这里啦,前两篇简单介绍了面向对象的一些基本概念,为后面学习设计模式作一些简单的铺垫。固然仅仅经过几篇文章仍是不够的,仍是须要你们在平时的开发中多思考多实践,多看一些经典书籍理解它,才能达到无剑胜有剑的境界。