本節我們展示一種更加簡單的批量刪除方法,它的代碼量并沒有增加,但思索量卻會小很多。無論你是科班出身,還是通過其它途徑加入到計算機科學與工程這個領域的,相信一定都學習過面向對象。而且,我們當前的Angular便是完全的面向對象的思想。提到面向對象,大家腦海中想到的最多的可能就是定義幾個類、幾個接口、將方法與屬性封裝起來等。
如果面向對象僅僅是定義幾個類、幾個接口,那顯然不足以支撐它如此大的名氣。本節中,我們將以面向對象的思想重寫上節中的刪除功能。
## Student對象
在學生列表中,每行學生其實對應了一個學生對象。

顯示的學生的姓名、學號等信息則可以認為是學生的屬性,而每行這個小小的選擇框可以被點擊與用戶進行交互 ,則可以認為是學生對象的方法。
那么是否可以按面向對象的思想,為Student加一個刪除被點擊的方法呢?
## 建立方法
找到`Student`實體,添加一個刪除被點擊的方法如下:
```typescript
+++ b/first-app/src/app/entity/student.ts
@@ -42,4 +42,8 @@ export class Student {
this.email = data.email as string;
this.clazz = data.clazz as Clazz;
}
+
+ public onDeleteClick(): void {
+ console.log('delete click');
+ }
}
```
我們接下來修改學生列表組件的V層,測試一下:
```html
+++ b/first-app/src/app/student/student.component.html
@@ -20,7 +20,7 @@
</thead>
<tbody>
<tr *ngFor="let student of pageData.content; index as index">
- <td><input type="checkbox" (click)="onCheckboxClick(index)"></td>
+ <td><input type="checkbox" (click)="student.onDeleteClick()"></td>
<td>{{index + 1}}</td>
<td>{{student.name}}</td>
<td>{{student.number}}</td>
```
當點擊選擇框時,在控制臺得到了如下異常:

提示說:`onDeleteClick`不是一個方法。無論我們怎么檢查語法或是重啟`ng t`,都將還是這個錯誤。
要想徹底的弄清楚這個問題,還要深入學習下`JSON對象`與`對象`間的區別。
## 測試代碼
學習計算機最大的優勢在于其實驗成本極低。相較于土木、化工、機器、汽車等傳統行業,我們可以使用極低的成本來驗證自己的想法。這種極低的成本當然也可以應用到代碼示例上。
我們在`student.service.spec.ts`中新建一個測試方法,以代碼的方式深入學習下`JSON對象`與`對象`的區別:
首先,我們建立一個測試用例:
```typescript
fit('JSON對象與對象', () => {
});
```
然后在測試用例中新建一個`Test`類,該類有兩個屬性`id`與`name`:
```typescript
fit('JSON對象與對象', () => {
class Test {
id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
});
```
接著,為該類增一個`sayHello()`方法:
```typescript
fit('JSON對象與對象', () => {
class Test {
id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
sayHello(): void {
console.log('hello');
}
}
});
```
基礎的準備工作做完以后,我們依據該類建立一個對象,并調用對象上的`sayHello()`方法:
```typescript
+++ b/first-app/src/app/service/student.service.spec.ts
@@ -81,5 +81,11 @@ describe('StudentService', () => {
console.log('hello');
}
}
+
+ const test = new Test(1, '123');
+ console.log(test.id);
+ console.log(test.name);
+ test.sayHello();
});
});
```
單元測試通過,在`bash`中打印了對象上的屬性`id`、`name`的值,并且成功的調用了`sayHello`方法:
```bash
LOG: 1
Firefox 89.0 (Mac OS 10.15): Executed 0 of 66 SUCCESS (0 secs / 0 secs)
LOG: '123'
Firefox 89.0 (Mac OS 10.15): Executed 0 of 66 SUCCESS (0 secs / 0 secs)
LOG: 'hello'
Firefox 89.0 (Mac OS 10.15): Executed 0 of 66 SUCCESS (0 secs / 0 secs)
```
> `bash`與瀏覽器控制臺查看打印信息大同小異。
### JSON對象
接下來,我們再嘗試增加一個`JSON對象`:
```typescript
test.sayHello();
+
+ const jsonString = '{"id": 2, "name": "456"}';
+ const jsonTest = JSON.parse(jsonString) as Test;
+ console.log(jsonTest.id);
+ console.log(jsonTest.name);
+ jsonTest.sayHello();
});
```
此時控制臺在執行`sayHello()`方法時得到了一個異常:
```bash
Firefox 89.0 (Mac OS 10.15): Executed 0 of 66 SUCCESS (0 secs / 0 secs)
LOG: 2
Firefox 89.0 (Mac OS 10.15): Executed 0 of 66 SUCCESS (0 secs / 0 secs)
LOG: '456'
Firefox 89.0 (Mac OS 10.15): Executed 0 of 66 SUCCESS (0 secs / 0 secs)
Firefox 89.0 (Mac OS 10.15) StudentService JSON對象與對象 FAILED
TypeError: jsonTest.sayHello is not a function in main.js (line 2385)
```
該異常類型與我們在學生對象上調用`onDeleteClick()`方法的錯誤類型相同,異常得以重現:

上述異常的關鍵點在于在聲明`jsonTest`時,我們使用了`JSON.parse(jsonString) as Test;`而這個`as`代表`看做`。潛臺詞是:你可能并不是這個類型,但是沒有關系,我在這把你看做這個類型。這種`看做`的方式被編譯器認同,所以在編譯的階段可以順利通過。
我們剛剛使用 `JSON.parse(jsonString) as Test;`來將字符串`jsonString`轉換為了一個對象,該對象使用`as`關鍵字并聲明為`Test`。而我們把這種通過字符串轉換過來的對象,稱為`JSON對象`。
字符串只所以可以通過`JSON.parse(jsonString)`來轉換為JSON對象,是由于該字符串符合轉換為`JSON對象`的規范:

如果不符合轉換為`JSON對象`的規范,則會發生轉換異常:

也就是說:只有符合`JSON對象`轉換規范的字符串才能夠使用`JSON.parse()`方法將其轉換為對象。我把這種符合轉換規范的字符串稱為`JSON字符串`,而通過`JSON字符串`轉換過來的對象稱為`JSON對象`。
最后我們注釋掉刪除剛剛觸發異常的代碼后繼續學習。
```typescript
- jsonTest.sayHello();
+ // jsonTest.sayHello();
```
### as
往往為了開發方便,我們還會根據`JSON字符串`的內容使用`as`關鍵字將其聲明為某個特定的類型。比如我們常用的`httpClient.get<T>`中的`T`便是這個作用。此處的泛型`T`僅僅是說,可以把后臺返回的數據看做是`T`,該`T`的類型本質上是個`JSON對象`,其僅具體`T`類型的屬性,但卻不具體`T`類型的方法。
由于在前臺的交互過程中,我們并沒有辦法對后臺的代碼進行約束。所以在與后臺對接時,只能是按照API的規范將返回值看做某個類型,而后臺具體返回的是不是這個類型,還需要在真實的與后臺對接時才能夠判斷出。
在這種特定的場景下,`as`是最適用不過的了。但由于`as`在類型上的靈活性,應該避免濫用,比如我們借助于`as`,完全可以這么寫:
```typescript
const a = '123' as any as number[];
a.push(123);
```
這種寫法在編譯時同樣不會出錯,但在運行時則必然出錯。
## HttpClient.get<T>()
以`HttpClient`的`get<T>()`方法為例,其實質的作用是進行Http請求,然后將請求的結果在內部使用` JSON.parse()`以及`as`關鍵字將其**看做**`T`返回。所以使用`httpClient.get<T>()`等方法得到的數據的本質上是個不具有任何方法的`JSON對象`。
所以若要使用`Student`類上的`onDeleteClick()`方法,則需要將`JSON對象`轉換為`對象`。同時由于我們早早的就在`Stduent`類中聲明了如下構造函數:
```typescript
constructor(data = {} as
{
id?: number,
name?: string,
number?: string,
phone?: string,
email?: string,
clazz?: Clazz
}) {
```
該函數中的參數`data`恰恰也是通過`as`關鍵字來**看做**一個**對象**,被看做的**對象**上只擁有屬性(id, name,...)而不具備任何方法,所以`data`完全可以看做是一個`JSON對象`。
也就是說,通過`Student`的構造函數,可以將一個`JSON對象`轉換為`對象`。
思想有了,寫代碼便成了最簡單的事情:
```typescript
+++ b/first-app/src/app/student/student.component.ts
@@ -32,7 +32,10 @@ export class StudentComponent implements OnInit {
this.studentService.pageOfCurrentTeacher({
page,
size: this.size
- }).subscribe(data => this.pageData = data);
+ }).subscribe(data => {
+ data.content = data.content.map(d => new Student(d));
+ this.pageData = data;
+ });
}
```
上述語句便使得`data.content`中的每一項都是一個Student對象,而不是看做Stduent對象的JSON對象了。
此時,當我們再次點擊某條數據前的選擇框時在控制臺得到了預期的效果:
```typescript
LOG: 'delete click'
```
## 完善功能
對象中的方法被觸發后,我們在`Student`類中再增加一個以`_`打頭的屬性,以`_`打頭代表該屬性該屬性與后臺不對接,是一個前臺的特有屬性。
```typescript
+++ b/first-app/src/app/entity/student.ts
@@ -4,6 +4,11 @@ import {Clazz} from './clazz';
* 學生.
*/
export class Student {
+ /**
+ * 是否被選中
+ */
+ _checked = false;
+
```
此時我們使用的IDE將報一個語法錯誤:

它在說`TSLInt`報了一個語法錯誤:變量的名字必須是以下三種(小駝峰、大駝峰、大寫字母和下劃線)情況之一。
## TSLint
在編程的世界時,以`lint`打頭的大多的作用都是語法檢查。所以如果你使用了是其它編程的語言,也可以使用`lint`后綴來查找相應的語法檢查器,比如`PHPLint`或是`JavaLint`。
所以`TSLint`顧名思義它是一個`typescript`的語法檢查器。
同時每個語法檢查器都會有一個相應的配置文件,而`TSLint`的配置文件則是位于項目根目錄的`tslint.json`:
```bash
panjie@panjies-iMac first-app % tree -L 1
.
├── README.md
├── angular.json
├── e2e
├── karma.conf.js
├── node_modules
├── package-lock.json
├── package.json
├── src
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
└── tslint.json ??
```
該文件中對在`rules -> variable-name -> options`上對變量名做出了限制,在此我們增加一項:允許變量名以下劃線`_`打頭:
```json
+++ b/first-app/tslint.json
@@ -108,7 +108,8 @@
"options": [
"ban-keywords",
"check-format",
- "allow-pascal-case"
+ "allow-pascal-case",
+ "allow-leading-underscore"
]
},
```
此時`Stduent`中的`_checked`字段的語法錯誤將會自動消失。
## 完善功能
有了`_checked`屬性后,便可以在`onDeleteClick()`方法來改變這個屬性:
```typescript
+++ b/first-app/src/app/entity/student.ts
@@ -49,6 +49,6 @@ export class Student {
}
public onDeleteClick(): void {
- console.log('delete click');
+ this._checked = !this._checked;
}
}
```
然后在組件中根據該屬性遍歷出要刪除的學生:
```typescript
b/first-app/src/app/student/student.component.ts
/**
* 批量刪除按鈕被點擊
*/
onBatchDeleteClick(): void {
const beDeleteIds = this.pageData.content.filter(s => s._checked).map(d => d.id); ①
if (beDeleteIds.length === 0) {
Report.warning('出錯啦', '請先選擇要刪除的學生', '返回');
} else {
Confirm.show('請確認', '該操作不可逆', '確認', '取消',
() => {
// 調用批量刪除
this.studentService.batchDelete(beDeleteIds)
.subscribe(() => {
this.loadData(this.page);
});
});
}
}
```
①中我們連續使用了`filter()`以及`map()`方法。最終得到了一個待刪除的id數組。

同時,此時原組件中的`onCheckboxClick()`等已經完成了歷史使命,可能退出歷史舞臺了:
```typescript
+++ b/first-app/src/app/student/student.component.ts
@@ -15,8 +15,6 @@ export class StudentComponent implements OnInit {
page = 0;
size = environment.size;
- beDeletedIndexes = new Array<number>();
-
constructor(private studentService: StudentService) {
}
@@ -58,18 +56,6 @@ export class StudentComponent implements OnInit {
this.loadData(this.page);
}
- /**
- * checkbox被點擊
- * @param index 索引值
- */
- onCheckboxClick(index: number): void {
- if (this.beDeletedIndexes.indexOf(index) === -1) {
- this.beDeletedIndexes.push(index);
- } else {
- this.beDeletedIndexes = this.beDeletedIndexes.filter(i => i !== index);
- }
- }
-
/**
* 批量刪除按鈕被點擊
*/
```
最后進行單元測試及集成測試,功能正常。
## 小結
本節我們深入學習了`JSON對象` 與`對象`,兩者的區別大概可以概括為:JSON對象是長的像對象的數據集合,它就像一個`1:1`的汽車模型,除了不會動以外真實汽車有的它都有;有對象就是一個真正的汽車,該汽車除了有方向盤、發動機等屬性外,還可以在公路上馳騁。
面向對象的思想在于一切皆對象,面象對象的封裝性決定了其是屬性與方法的混合體。屬性是其對象內部的各個狀態,比如一個人的年齡、身高都是屬性,而方法則是該對象具有的功能。
在計算機的世界時,實踐出真知永不過時。從來沒有一門學問是有捷徑可以走的,變道超車也僅僅停留在記者的新聞稿里。我們相信,腳踏實地就是最佳的捷徑。在這條捷徑上學而時習之就是我們最有效的學習方法。
> 學而時習之,不亦說乎:時常能夠使用學習到的知識來指導實踐,不是一件令人心生喜悅的事嗎?
## 本節資源
| 鏈接 | 名稱 |
| ------------------------------------------------------------ | ------------------- |
| [https://github.com/mengyunzhi/angular11-guild/archive/step7.5.1.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.5.1.zip) | 本節源碼 |
| [https://zhuanlan.zhihu.com/p/29119549](https://zhuanlan.zhihu.com/p/29119549) | JSON對象與對象 |
| [https://palantir.github.io/tslint/](https://palantir.github.io/tslint/) | TSLint |
| [https://eslint.org/](https://eslint.org/) | ESLint |
| [https://ts.xcatliu.com/basics/type-assertion.html](https://ts.xcatliu.com/basics/type-assertion.html) | Typescript 類型斷言 |
- 序言
- 第一章 Hello World
- 1.1 環境安裝
- 1.2 Hello Angular
- 1.3 Hello World!
- 第二章 教師管理
- 2.1 教師列表
- 2.1.1 初始化原型
- 2.1.2 組件生命周期之初始化
- 2.1.3 ngFor
- 2.1.4 ngIf、ngTemplate
- 2.1.5 引用 Bootstrap
- 2.2 請求后臺數據
- 2.2.1 HttpClient
- 2.2.2 請求數據
- 2.2.3 模塊與依賴注入
- 2.2.4 異步與回調函數
- 2.2.5 集成測試
- 2.2.6 本章小節
- 2.3 新增教師
- 2.3.1 組件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 對接后臺
- 2.3.4 路由
- 2.4 編輯教師
- 2.4.1 組件初始化
- 2.4.2 獲取路由參數
- 2.4.3 插值與模板表達式
- 2.4.4 初識泛型
- 2.4.5 更新教師
- 2.4.6 測試中的路由
- 2.5 刪除教師
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome圖標庫
- 2.6.3 firefox
- 2.7 總結
- 第三章 用戶登錄
- 3.1 初識單元測試
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 著陸組件
- 3.5 @Output
- 3.6 TypeScript 類
- 3.7 瀏覽器緩存
- 3.8 總結
- 第四章 個人中心
- 4.1 原型
- 4.2 管道
- 4.3 對接后臺
- 4.4 x-auth-token認證
- 4.5 攔截器
- 4.6 小結
- 第五章 系統菜單
- 5.1 延遲及測試
- 5.2 手動創建組件
- 5.3 隱藏測試信息
- 5.4 規劃路由
- 5.5 定義菜單
- 5.6 注銷
- 5.7 小結
- 第六章 班級管理
- 6.1 新增班級
- 6.1.1 組件初始化
- 6.1.2 MockApi 新建班級
- 6.1.3 ApiInterceptor
- 6.1.4 數據驗證
- 6.1.5 教師選擇列表
- 6.1.6 MockApi 教師列表
- 6.1.7 代碼重構
- 6.1.8 小結
- 6.2 教師列表組件
- 6.2.1 初始化
- 6.2.2 響應式表單
- 6.2.3 getTestScheduler()
- 6.2.4 應用組件
- 6.2.5 小結
- 6.3 班級列表
- 6.3.1 原型設計
- 6.3.2 初始化分頁
- 6.3.3 MockApi
- 6.3.4 靜態分頁
- 6.3.5 動態分頁
- 6.3.6 @Input()
- 6.4 編輯班級
- 6.4.1 測試模塊
- 6.4.2 響應式表單驗證
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定義FormControl
- 6.4.6 代碼重構
- 6.4.7 小結
- 6.5 刪除班級
- 6.6 集成測試
- 6.6.1 惰性加載
- 6.6.2 API攔截器
- 6.6.3 路由與跳轉
- 6.6.4 ngStyle
- 6.7 初識Service
- 6.7.1 catchError
- 6.7.2 單例服務
- 6.7.3 單元測試
- 6.8 小結
- 第七章 學生管理
- 7.1 班級列表組件
- 7.2 新增學生
- 7.2.1 exports
- 7.2.2 自定義驗證器
- 7.2.3 異步驗證器
- 7.2.4 再識DI
- 7.2.5 屬性型指令
- 7.2.6 完成功能
- 7.2.7 小結
- 7.3 單元測試進階
- 7.4 學生列表
- 7.4.1 JSON對象與對象
- 7.4.2 單元測試
- 7.4.3 分頁模塊
- 7.4.4 子組件測試
- 7.4.5 重構分頁
- 7.5 刪除學生
- 7.5.1 第三方dialog
- 7.5.2 批量刪除
- 7.5.3 面向對象
- 7.6 集成測試
- 7.7 編輯學生
- 7.7.1 初始化
- 7.7.2 自定義provider
- 7.7.3 更新學生
- 7.7.4 集成測試
- 7.7.5 可訂閱的路由參數
- 7.7.6 小結
- 7.8 總結
- 第八章 其它
- 8.1 打包構建
- 8.2 發布部署
- 第九章 總結