希沃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() { // 重寫該方法實現添加不一樣形狀或者元素
...
}
}
複製代碼
本系列的基礎篇就先介紹到這裏啦,前兩篇簡單介紹了面向對象的一些基本概念,爲後面學習設計模式作一些簡單的鋪墊。固然僅僅經過幾篇文章仍是不夠的,仍是須要你們在平時的開發中多思考多實踐,多看一些經典書籍理解它,才能達到無劍勝有劍的境界。