本节我们展示一种更加简单的批量删除方法,它的代码量并没有增加,但思索量却会小很多。无论你是科班出身,还是通过其它途径加入到计算机科学与工程这个领域的,相信一定都学习过面向对象。而且,我们当前的Angular便是完全的面向对象的思想。提到面向对象,大家脑海中想到的最多的可能就是定义几个类、几个接口、将方法与属性封装起来等。 如果面向对象仅仅是定义几个类、几个接口,那显然不足以支撑它如此大的名气。本节中,我们将以面向对象的思想重写上节中的删除功能。 ## Student对象 在学生列表中,每行学生其实对应了一个学生对象。 ![image-20210608081519817](https://img.kancloud.cn/97/39/973915b2bcfb105cfa4d64209ba4c824_2514x218.png) 显示的学生的姓名、学号等信息则可以认为是学生的属性,而每行这个小小的选择框可以被点击与用户进行交互 ,则可以认为是学生对象的方法。 那么是否可以按面向对象的思想,为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> ``` 当点击选择框时,在控制台得到了如下异常: ![image-20210608082934788](https://img.kancloud.cn/ba/9c/ba9c1c8ee1b87483c7d4f7f391fff2e3_1796x434.png) 提示说:`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()`方法的错误类型相同,异常得以重现: ![image-20210608082934788](https://img.kancloud.cn/ba/9c/ba9c1c8ee1b87483c7d4f7f391fff2e3_1796x434.png) 上述异常的关键点在于在声明`jsonTest`时,我们使用了`JSON.parse(jsonString) as Test;`而这个`as`代表`看做`。潜台词是:你可能并不是这个类型,但是没有关系,我在这把你看做这个类型。这种`看做`的方式被编译器认同,所以在编译的阶段可以顺利通过。 我们刚刚使用 `JSON.parse(jsonString) as Test;`来将字符串`jsonString`转换为了一个对象,该对象使用`as`关键字并声明为`Test`。而我们把这种通过字符串转换过来的对象,称为`JSON对象`。 字符串只所以可以通过`JSON.parse(jsonString)`来转换为JSON对象,是由于该字符串符合转换为`JSON对象`的规范: ![image-20210608100049930](https://img.kancloud.cn/01/a6/01a61d44dca8a263fec497818e001c5d_1174x182.png) 如果不符合转换为`JSON对象`的规范,则会发生转换异常: ![image-20210608100208061](https://img.kancloud.cn/d6/a8/d6a8201c8e63595cd5d265ace5d9a8ec_1480x238.png) 也就是说:只有符合`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将报一个语法错误: ![image-20210608111526422](https://img.kancloud.cn/db/18/db1861278a41f4eb2115483cb4d7cc84_2228x406.png) 它在说`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数组。 ![image-20210608114419061](https://img.kancloud.cn/70/a9/70a9c4c99fab3412776781cd79957a05_916x184.png) 同时,此时原组件中的`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 类型断言 |