├── .gitattributes └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.md linguist-documentation=false 2 | *.md linguist-language=TypeScript -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeScript 代码整洁之道 2 | [中文](https://github.com/pipiliang/clean-code-typescript) | [English](https://github.com/labs42io/clean-code-typescript) 3 | 4 | >将 Clean Code 的概念适用到 TypeScript,灵感来自 [clean-code-javascript](https://github.com/ryanmcdermott/clean-code-javascript)。 5 | 6 | ## 目录 7 | 1. [简介](#简介) 8 | 2. [变量](#变量) 9 | 3. [函数](#函数) 10 | 4. [对象与数据结构](#对象和数据结构) 11 | 5. [类](#类) 12 | 6. [SOLID原则](#SOLID原则) 13 | 7. [测试](#测试) 14 | 8. [并发](#并发) 15 | 9. [错误处理](#错误处理) 16 | 10. [格式化](#格式化) 17 | 11. [注释](#注释) 18 | 19 | ## 简介 20 | 21 | ![Humorous image of software quality estimation as a count of how many expletives you shout when reading code](https://www.osnews.com/images/comics/wtfm.jpg) 22 | 23 | 这不是一份 TypeScript 编码风格规范,而是将 Robert C. Martin 的软件工程著作 [《Clean Code》](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882) 适用到 TypeScript,引导读者使用 TypeScript 编写[易读、复用和可扩展](https://github.com/ryanmcdermott/3rs-of-software-architecture)的软件。 24 | 25 | 实际上,并不是每一个原则都要严格遵守,能被广泛认同的原则就更少了。这看起来虽然只是一份指导原则,但却是 *Clean Code* 作者对多年编程经验的凝练。 26 | 27 | 软件工程技术已有50多年的历史了,我们仍然要学习很多的东西。当软件架构和架构本身一样古老的时候,也许我们需要遵守更严格的规则。但是现在,让这些指导原则作为评估您和您的团队代码质量的试金石。 28 | 29 | 另外,理解这些原则不会立即让您变的优秀,也不意味着不会犯错。每一段代码都是从不完美开始的,通过反复走查不断趋于完美,就像黏土制作成陶艺一样,享受这个过程吧! 30 | 31 | 32 | **[↑ 回到顶部](#目录)** 33 | 34 | ## 变量 35 | 36 | >计算机科学只存在两个难题:缓存失效和命名。—— Phil KarIton 37 | 38 | ### 使用有意义的变量名 39 | 40 | 做有意义的区分,让读者更容易理解变量的含义。 41 | 42 | **:-1: 反例:** 43 | 44 | ```ts 45 | 46 | function between(a1: T, a2: T, a3: T) { 47 | 48 | return a2 <= a1 && a1 <= a3; 49 | 50 | } 51 | 52 | ``` 53 | 54 | **:+1: 正例:** 55 | 56 | ```ts 57 | 58 | function between(value: T, left: T, right: T) { 59 | 60 | return left <= value && value <= right; 61 | 62 | } 63 | 64 | ``` 65 | 66 | **[↑ 回到顶部](#目录)** 67 | 68 | ### 可读的变量名 69 | 70 | 如果你不能正确读出它,那么你在讨论它时听起来就会像个白痴。 71 | 72 | **:-1: 反例:** 73 | 74 | ```ts 75 | 76 | class DtaRcrd102 { 77 | 78 | private genymdhms: Date; # // 你能读出这个变量名么? 79 | 80 | private modymdhms: Date; 81 | 82 | private pszqint = '102'; 83 | 84 | } 85 | 86 | ``` 87 | 88 | **:+1: 正例:** 89 | 90 | ```ts 91 | 92 | class Customer { 93 | 94 | private generationTimestamp: Date; 95 | 96 | private modificationTimestamp: Date; 97 | 98 | private recordId = '102'; 99 | 100 | } 101 | 102 | ``` 103 | 104 | **[↑ 回到顶部](#目录)** 105 | 106 | ### 合并功能一致的变量 107 | 108 | **:-1: 反例:** 109 | 110 | ```ts 111 | 112 | function getUserInfo(): User; 113 | 114 | function getUserDetails(): User; 115 | 116 | function getUserData(): User; 117 | 118 | ``` 119 | 120 | **:+1: 正例:** 121 | 122 | ```ts 123 | 124 | function getUser(): User; 125 | 126 | ``` 127 | 128 | **[↑ 回到顶部](#目录)** 129 | 130 | ### 便于搜索的名字 131 | 132 | 往往我们读代码要比写的多,所以易读性和可搜索非常重要。如果不抽取并命名有意义的变量名,那就坑了读代码的人。代码一定要便于搜索,[TSLint](https://palantir.github.io/tslint/rules/no-magic-numbers/) 就可以帮助识别未命名的常量。 133 | 134 | **:-1: 反例:** 135 | 136 | ```ts 137 | 138 | //86400000 代表什么? 139 | 140 | setTimeout(restart, 86400000); 141 | 142 | ``` 143 | 144 | **:+1: 正例:** 145 | 146 | ```ts 147 | 148 | // 声明为常量,要大写且有明确含义。 149 | 150 | const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000; 151 | 152 | setTimeout(restart, MILLISECONDS_IN_A_DAY); 153 | 154 | ``` 155 | 156 | **[↑ 回到顶部](#目录)** 157 | 158 | ### 使用自解释的变量名 159 | 160 | **:-1: 反例:** 161 | 162 | ```ts 163 | 164 | declare const users:Map; 165 | 166 | for (const keyValue of users) { 167 | // ... 168 | } 169 | 170 | ``` 171 | 172 | **:+1: 正例:** 173 | 174 | ```ts 175 | 176 | declare const users:Map; 177 | 178 | for (const [id, user] of users) { 179 | // ... 180 | } 181 | 182 | ``` 183 | 184 | **[↑ 回到顶部](#目录)** 185 | 186 | ### 避免思维映射 187 | 188 | 不要让人去猜测或想象变量的含义,*明确是王道。* 189 | 190 | **:-1: 反例:** 191 | 192 | ```ts 193 | 194 | const u = getUser(); 195 | 196 | const s = getSubscription(); 197 | 198 | const t = charge(u, s); 199 | 200 | ``` 201 | 202 | **:+1: 正例:** 203 | 204 | ```ts 205 | 206 | const user = getUser(); 207 | 208 | const subscription = getSubscription(); 209 | 210 | const transaction = charge(user, subscription); 211 | 212 | ``` 213 | 214 | **[↑ 回到顶部](#目录)** 215 | 216 | ### 不添加无用的上下文 217 | 218 | 如果类名或对象名已经表达了某些信息,在内部变量名中不要再重复表达。 219 | 220 | **:-1: 反例:** 221 | 222 | ```ts 223 | 224 | type Car = { 225 | 226 | carMake: string; 227 | 228 | carModel: string; 229 | 230 | carColor: string; 231 | 232 | } 233 | 234 | function print(car: Car): void { 235 | 236 | console.log(`${this.carMake} ${this.carModel} (${this.carColor})`); 237 | 238 | } 239 | 240 | ``` 241 | 242 | **:+1: 正例:** 243 | 244 | ```ts 245 | 246 | type Car = { 247 | 248 | make: string; 249 | 250 | model: string; 251 | 252 | color: string; 253 | 254 | } 255 | 256 | function print(car: Car): void { 257 | 258 | console.log(`${this.make} ${this.model} (${this.color})`); 259 | 260 | } 261 | 262 | ``` 263 | 264 | **[↑ 回到顶部](#目录)** 265 | 266 | ### 使用默认参数,而非短路或条件判断 267 | 268 | 通常,默认参数比短路更整洁。 269 | 270 | **:-1: 反例:** 271 | 272 | ```ts 273 | 274 | function loadPages(count: number) { 275 | 276 | const loadCount = count !== undefined ? count : 10; 277 | 278 | // ... 279 | 280 | } 281 | 282 | ``` 283 | 284 | **:+1: 正例:** 285 | 286 | ```ts 287 | 288 | function loadPages(count: number = 10) { 289 | 290 | // ... 291 | 292 | } 293 | 294 | ``` 295 | 296 | **[↑ 回到顶部](#目录)** 297 | 298 | ## 函数 299 | 300 | ### 参数越少越好 (理想情况不超过2个) 301 | 302 | 限制参数个数,这样函数测试会更容易。超过三个参数会导致测试复杂度激增,需要测试众多不同参数的组合场景。 303 | 理想情况,只有一两个参数。如果有两个以上的参数,那么您的函数可能就太过复杂了。 304 | 305 | 如果需要很多参数,请您考虑使用对象。为了使函数的属性更清晰,可以使用[解构](https://basarat.gitbooks.io/typescript/docs/destructuring.html),它有以下优点: 306 | 307 | 1. 当有人查看函数签名时,会立即清楚使用了哪些属性。 308 | 2. 解构对传递给函数的参数对象做深拷贝,这可预防副作用。(注意:**不会克隆**从参数对象中解构的对象和数组) 309 | 3. TypeScript 会对未使用的属性显示警告。 310 | 311 | **:-1: 反例:** 312 | 313 | ```ts 314 | 315 | function createMenu(title: string, body: string, buttonText: string, cancellable: boolean) { 316 | 317 | // ... 318 | 319 | } 320 | 321 | createMenu('Foo', 'Bar', 'Baz', true); 322 | 323 | ``` 324 | 325 | **:+1: 正例:** 326 | 327 | ```ts 328 | 329 | function createMenu(options: {title: string, body: string, buttonText: string, cancellable: boolean}) { 330 | 331 | // ... 332 | 333 | } 334 | 335 | createMenu( 336 | { 337 | title: 'Foo', 338 | body: 'Bar', 339 | buttonText: 'Baz', 340 | cancellable: true 341 | } 342 | ); 343 | 344 | ``` 345 | 通过 TypeScript 的[类型别名](https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-aliases),可以进一步提高可读性。 346 | 347 | ```ts 348 | 349 | type MenuOptions = {title: string, body: string, buttonText: string, cancellable: boolean}; 350 | 351 | function createMenu(options: MenuOptions) { 352 | 353 | // ... 354 | 355 | } 356 | 357 | createMenu( 358 | { 359 | title: 'Foo', 360 | body: 'Bar', 361 | buttonText: 'Baz', 362 | cancellable: true 363 | } 364 | ); 365 | 366 | ``` 367 | 368 | **[↑ 回到顶部](#目录)** 369 | 370 | ### 只做一件事 371 | 372 | 这是目前软件工程中最重要的规则。如果函数做不止一件事,它就更难组合、测试以及理解。反之,函数只有一个行为,它就更易于重构、代码就更清晰。如果能做好这一点,你一定很优秀! 373 | 374 | **:-1: 反例:** 375 | 376 | ```ts 377 | 378 | function emailClients(clients: Client[]) { 379 | 380 | clients.forEach((client) => { 381 | 382 | const clientRecord = database.lookup(client); 383 | 384 | if (clientRecord.isActive()) { 385 | 386 | email(client); 387 | 388 | } 389 | 390 | }); 391 | 392 | } 393 | 394 | ``` 395 | 396 | **:+1: 正例:** 397 | 398 | ```ts 399 | 400 | function emailClients(clients: Client[]) { 401 | 402 | clients.filter(isActiveClient).forEach(email); 403 | 404 | } 405 | 406 | function isActiveClient(client: Client) { 407 | 408 | const clientRecord = database.lookup(client); 409 | 410 | return clientRecord.isActive(); 411 | 412 | } 413 | 414 | ``` 415 | 416 | **[↑ 回到顶部](#目录)** 417 | 418 | ### 名副其实 419 | 420 | 通过函数名就可以看得出函数实现的功能。 421 | 422 | **:-1: 反例:** 423 | 424 | ```ts 425 | 426 | function addToDate(date: Date, month: number): Date { 427 | // ... 428 | } 429 | 430 | const date = new Date(); 431 | 432 | // 从函数名很难看的出需要加什么? 433 | addToDate(date, 1); 434 | 435 | ``` 436 | 437 | **:+1: 正例:** 438 | 439 | ```ts 440 | 441 | function addMonthToDate(date: Date, month: number): Date { 442 | // ... 443 | } 444 | 445 | const date = new Date(); 446 | 447 | addMonthToDate(date, 1); 448 | 449 | ``` 450 | 451 | **[↑ 回到顶部](#目录)** 452 | 453 | ### 每个函数只包含同一个层级的抽象 454 | 455 | 当有多个抽象级别时,函数应该是做太多事了。拆分函数以便可复用,也让测试更容易。 456 | 457 | **:-1: 反例:** 458 | 459 | ```ts 460 | 461 | function parseCode(code:string) { 462 | 463 | const REGEXES = [ /* ... */ ]; 464 | const statements = code.split(' '); 465 | const tokens = []; 466 | 467 | REGEXES.forEach((regex) => { 468 | 469 | statements.forEach((statement) => { 470 | // ... 471 | }); 472 | 473 | }); 474 | 475 | const ast = []; 476 | 477 | tokens.forEach((token) => { 478 | // lex... 479 | }); 480 | 481 | ast.forEach((node) => { 482 | // 解析 ... 483 | }); 484 | 485 | } 486 | 487 | ``` 488 | 489 | **:+1: 正例:** 490 | 491 | ```ts 492 | 493 | const REGEXES = [ /* ... */ ]; 494 | 495 | function parseCode(code:string) { 496 | 497 | const tokens = tokenize(code); 498 | 499 | const syntaxTree = parse(tokens); 500 | 501 | syntaxTree.forEach((node) => { 502 | 503 | // parse... 504 | 505 | }); 506 | 507 | } 508 | 509 | function tokenize(code: string):Token[] { 510 | 511 | const statements = code.split(' '); 512 | 513 | const tokens:Token[] = []; 514 | 515 | REGEXES.forEach((regex) => { 516 | 517 | statements.forEach((statement) => { 518 | 519 | tokens.push( /* ... */ ); 520 | 521 | }); 522 | 523 | }); 524 | 525 | return tokens; 526 | 527 | } 528 | 529 | function parse(tokens: Token[]): SyntaxTree { 530 | 531 | const syntaxTree:SyntaxTree[] = []; 532 | 533 | tokens.forEach((token) => { 534 | 535 | syntaxTree.push( /* ... */ ); 536 | 537 | }); 538 | 539 | return syntaxTree; 540 | 541 | } 542 | 543 | ``` 544 | 545 | **[↑ 回到顶部](#目录)** 546 | 547 | ### 删除重复代码 548 | 549 | 重复乃万恶之源!重复意味着如果要修改某个逻辑,需要修改多处代码:cry:。 550 | 想象一下,如果你经营一家餐厅,要记录你的库存:所有的西红柿、洋葱、大蒜、香料等等。如果要维护多个库存列表,那是多么痛苦的事! 551 | 552 | 存在重复代码,是因为有两个或两个以上很近似的功能,只有一点不同,但是这点不同迫使你用多个独立的函数来做很多几乎相同的事情。删除重复代码,则意味着创建一个抽象,该抽象仅用一个函数/模块/类就可以处理这组不同的东西。 553 | 554 | 合理的抽象至关重要,这就是为什么您应该遵循[SOLID原则](#SOLID原则)。糟糕的抽象可能还不如重复代码,所以要小心!话虽如此,还是要做好抽象!尽量不要重复。 555 | 556 | **:-1: 反例:** 557 | 558 | ```ts 559 | 560 | function showDeveloperList(developers: Developer[]) { 561 | 562 | developers.forEach((developer) => { 563 | 564 | const expectedSalary = developer.calculateExpectedSalary(); 565 | 566 | const experience = developer.getExperience(); 567 | 568 | const githubLink = developer.getGithubLink(); 569 | 570 | const data = { 571 | 572 | expectedSalary, 573 | 574 | experience, 575 | 576 | githubLink 577 | 578 | }; 579 | 580 | render(data); 581 | 582 | }); 583 | 584 | } 585 | 586 | function showManagerList(managers: Manager[]) { 587 | 588 | managers.forEach((manager) => { 589 | 590 | const expectedSalary = manager.calculateExpectedSalary(); 591 | 592 | const experience = manager.getExperience(); 593 | 594 | const portfolio = manager.getMBAProjects(); 595 | 596 | const data = { 597 | 598 | expectedSalary, 599 | 600 | experience, 601 | 602 | portfolio 603 | 604 | }; 605 | 606 | render(data); 607 | 608 | }); 609 | 610 | } 611 | 612 | ``` 613 | 614 | **:+1: 正例:** 615 | 616 | ```ts 617 | 618 | class Developer { 619 | 620 | // ... 621 | 622 | getExtraDetails() { 623 | 624 | return { 625 | 626 | githubLink: this.githubLink, 627 | 628 | } 629 | 630 | } 631 | 632 | } 633 | 634 | class Manager { 635 | 636 | // ... 637 | 638 | getExtraDetails() { 639 | 640 | return { 641 | 642 | portfolio: this.portfolio, 643 | 644 | } 645 | 646 | } 647 | 648 | } 649 | 650 | function showEmployeeList(employee: Developer | Manager) { 651 | 652 | employee.forEach((employee) => { 653 | 654 | const expectedSalary = developer.calculateExpectedSalary(); 655 | 656 | const experience = developer.getExperience(); 657 | 658 | const extra = employee.getExtraDetails(); 659 | 660 | const data = { 661 | 662 | expectedSalary, 663 | 664 | experience, 665 | 666 | extra, 667 | 668 | }; 669 | 670 | render(data); 671 | 672 | }); 673 | 674 | } 675 | 676 | ``` 677 | 678 | 有时,在重复代码和引入不必要的抽象而增加的复杂性之间,需要做权衡。当来自不同领域的两个不同模块,它们的实现看起来相似,复制也是可以接受的,并且比抽取公共代码要好一点。因为抽取公共代码会导致两个模块产生间接的依赖关系。 679 | 680 | 681 | **[↑ 回到顶部](#目录)** 682 | 683 | ### 使用`Object.assign`或`解构`来设置默认对象 684 | 685 | **:-1: 反例:** 686 | 687 | ```ts 688 | 689 | type MenuConfig = {title?: string, body?: string, buttonText?: string, cancellable?: boolean}; 690 | 691 | function createMenu(config: MenuConfig) { 692 | 693 | config.title = config.title || 'Foo'; 694 | 695 | config.body = config.body || 'Bar'; 696 | 697 | config.buttonText = config.buttonText || 'Baz'; 698 | 699 | config.cancellable = config.cancellable !== undefined ? config.cancellable : true; 700 | 701 | } 702 | 703 | const menuConfig = { 704 | 705 | title: null, 706 | 707 | body: 'Bar', 708 | 709 | buttonText: null, 710 | 711 | cancellable: true 712 | 713 | }; 714 | 715 | createMenu(menuConfig); 716 | 717 | ``` 718 | 719 | **:+1: 正例:** 720 | 721 | ```ts 722 | 723 | type MenuConfig = {title?: string, body?: string, buttonText?: string, cancellable?: boolean}; 724 | 725 | function createMenu(config: MenuConfig) { 726 | 727 | const menuConfig = Object.assign({ 728 | 729 | title: 'Foo', 730 | 731 | body: 'Bar', 732 | 733 | buttonText: 'Baz', 734 | 735 | cancellable: true 736 | 737 | }, config); 738 | 739 | } 740 | 741 | createMenu({ body: 'Bar' }); 742 | 743 | ``` 744 | 745 | 或者,您可以使用默认值的解构: 746 | 747 | ```ts 748 | 749 | type MenuConfig = {title?: string, body?: string, buttonText?: string, cancellable?: boolean}; 750 | 751 | function createMenu({title = 'Foo', body = 'Bar', buttonText = 'Baz', cancellable = true}: MenuConfig) { 752 | 753 | // ... 754 | 755 | } 756 | 757 | createMenu({ body: 'Bar' }); 758 | 759 | ``` 760 | 761 | 为了避免副作用,不允许显式传递`undefined`或`null`值。参见 TypeScript 编译器的`--strictnullcheck`选项。 762 | 763 | **[↑ 回到顶部](#目录)** 764 | 765 | ### 不要使用Flag参数 766 | 767 | Flag参数告诉用户这个函数做了不止一件事。如果函数使用布尔值实现不同的代码逻辑路径,则考虑将其拆分。 768 | 769 | **:-1: 反例:** 770 | 771 | ```ts 772 | 773 | function createFile(name:string, temp:boolean) { 774 | 775 | if (temp) { 776 | 777 | fs.create(`./temp/${name}`); 778 | 779 | } else { 780 | 781 | fs.create(name); 782 | 783 | } 784 | 785 | } 786 | 787 | ``` 788 | 789 | **:+1: 正例:** 790 | 791 | ```ts 792 | 793 | function createFile(name:string) { 794 | 795 | fs.create(name); 796 | 797 | } 798 | 799 | function createTempFile(name:string) { 800 | 801 | fs.create(`./temp/${name}`); 802 | 803 | } 804 | 805 | ``` 806 | 807 | **[↑ 回到顶部](#目录)** 808 | 809 | ### 避免副作用 (part1) 810 | 811 | 当函数产生除了“一个输入一个输出”之外的行为时,称该函数产生了副作用。比如写文件、修改全局变量或将你的钱全转给了一个陌生人等。 812 | 813 | 在某些情况下,程序需要一些副作用。如先前例子中的写文件,这时应该将这些功能集中在一起,不要用多个函数/类修改某个文件。用且只用一个 service 完成这一需求。 814 | 815 | 重点是要规避常见陷阱,比如,在无结构对象之间共享状态、使用可变数据类型,以及不确定副作用发生的位置。如果你能做到这点,你才可能笑到最后! 816 | 817 | **:-1: 反例:** 818 | 819 | ```ts 820 | 821 | // Global variable referenced by following function. 822 | 823 | // If we had another function that used this name, now it'd be an array and it could break it. 824 | 825 | let name = 'Robert C. Martin'; 826 | 827 | function toBase64() { 828 | 829 | name = btoa(name); 830 | 831 | } 832 | 833 | toBase64(); // produces side effects to `name` variable 834 | 835 | console.log(name); // expected to print 'Robert C. Martin' but instead 'Um9iZXJ0IEMuIE1hcnRpbg==' 836 | 837 | ``` 838 | 839 | **:+1: 正例:** 840 | 841 | ```ts 842 | 843 | // Global variable referenced by following function. 844 | 845 | // If we had another function that used this name, now it'd be an array and it could break it. 846 | 847 | const name = 'Robert C. Martin'; 848 | 849 | function toBase64(text:string):string { 850 | 851 | return btoa(text); 852 | 853 | } 854 | 855 | const encodedName = toBase64(name); 856 | 857 | console.log(name); 858 | 859 | ``` 860 | 861 | **[↑ 回到顶部](#目录)** 862 | 863 | ### 避免副作用 (part2) 864 | 865 | 在 JavaScript 中,原类型是值传递,对象、数组是引用传递。 866 | 867 | 有这样一种情况,如果您的函数修改了购物车数组,用来添加购买的商品,那么其他使用该`cart`数组的函数都将受此添加操作的影响。想象一个糟糕的情况: 868 | 869 | 用户点击“购买”按钮,该按钮调用`purchase`函数,函数请求网络并将`cart`数组发送到服务器。由于网络连接不好,购买功能必须不断重试请求。恰巧在网络请求开始前,用户不小心点击了某个不想要的项目上的“Add to Cart”按钮,该怎么办?而此时网络请求开始,那么`purchase`函数将发送意外添加的项,因为它引用了一个购物车数组,`addItemToCart`函数修改了该数组,添加了不需要的项。 870 | 871 | 一个很好的解决方案是`addItemToCart`总是克隆`cart`,编辑它,并返回克隆。这确保引用购物车的其他函数不会受到任何更改的影响。 872 | 873 | 注意两点: 874 | 875 | 1. 在某些情况下,可能确实想要修改输入对象,这种情况非常少见。且大多数可以重构,确保没副作用!(见[纯函数](https://en.wikipedia.org/wiki/Pure_function)) 876 | 877 | 2. 性能方面,克隆大对象代价确实比较大。还好有一些很好的库,它提供了一些高效快速的方法,且不像手动克隆对象和数组那样占用大量内存。 878 | 879 | 880 | **:-1: 反例:** 881 | 882 | ```ts 883 | 884 | function addItemToCart(cart: CartItem[], item:Item):void { 885 | 886 | cart.push({ item, date: Date.now() }); 887 | 888 | }; 889 | 890 | ``` 891 | 892 | **:+1: 正例:** 893 | 894 | ```ts 895 | 896 | function addItemToCart(cart: CartItem[], item:Item):CartItem[] { 897 | 898 | return [...cart, { item, date: Date.now() }]; 899 | 900 | }; 901 | 902 | ``` 903 | 904 | **[↑ 回到顶部](#目录)** 905 | 906 | ### 不要写全局函数 907 | 908 | 在 JavaScript 中污染全局的做法非常糟糕,这可能导致和其他库冲突,而调用你的 API 的用户在实际环境中得到一个 exception 前对这一情况是一无所知的。 909 | 910 | 考虑这样一个例子:如果想要扩展 JavaScript 的 `Array`,使其拥有一个可以显示两个数组之间差异的 `diff`方法,该怎么做呢?可以将新函数写入`Array.prototype` ,但它可能与另一个尝试做同样事情的库冲突。如果另一个库只是使用`diff`来查找数组的第一个元素和最后一个元素之间的区别呢? 911 | 912 | 更好的做法是扩展`Array`,实现对应的函数功能。 913 | 914 | **:-1: 反例:** 915 | 916 | ```ts 917 | 918 | declare global { 919 | 920 | interface Array { 921 | 922 | diff(other: T[]): Array; 923 | 924 | } 925 | 926 | } 927 | 928 | if (!Array.prototype.diff){ 929 | 930 | Array.prototype.diff = function (other: T[]): T[] { 931 | 932 | const hash = new Set(other); 933 | 934 | return this.filter(elem => !hash.has(elem)); 935 | 936 | }; 937 | 938 | } 939 | 940 | ``` 941 | 942 | **:+1: 正例:** 943 | 944 | ```ts 945 | 946 | class MyArray extends Array { 947 | 948 | diff(other: T[]): T[] { 949 | 950 | const hash = new Set(other); 951 | 952 | return this.filter(elem => !hash.has(elem)); 953 | 954 | }; 955 | 956 | } 957 | 958 | ``` 959 | 960 | **[↑ 回到顶部](#目录)** 961 | 962 | ### 函数式编程优于命令式编程 963 | 964 | 尽量使用函数式编程! 965 | 966 | **:-1: 反例:** 967 | 968 | ```ts 969 | 970 | const contributions = [ 971 | 972 | { 973 | 974 | name: 'Uncle Bobby', 975 | 976 | linesOfCode: 500 977 | 978 | }, { 979 | 980 | name: 'Suzie Q', 981 | 982 | linesOfCode: 1500 983 | 984 | }, { 985 | 986 | name: 'Jimmy Gosling', 987 | 988 | linesOfCode: 150 989 | 990 | }, { 991 | 992 | name: 'Gracie Hopper', 993 | 994 | linesOfCode: 1000 995 | 996 | } 997 | 998 | ]; 999 | 1000 | let totalOutput = 0; 1001 | 1002 | for (let i = 0; i < contributions.length; i++) { 1003 | 1004 | totalOutput += contributions[i].linesOfCode; 1005 | 1006 | } 1007 | 1008 | ``` 1009 | 1010 | **:+1: 正例:** 1011 | 1012 | ```ts 1013 | 1014 | const contributions = [ 1015 | 1016 | { 1017 | 1018 | name: 'Uncle Bobby', 1019 | 1020 | linesOfCode: 500 1021 | 1022 | }, { 1023 | 1024 | name: 'Suzie Q', 1025 | 1026 | linesOfCode: 1500 1027 | 1028 | }, { 1029 | 1030 | name: 'Jimmy Gosling', 1031 | 1032 | linesOfCode: 150 1033 | 1034 | }, { 1035 | 1036 | name: 'Gracie Hopper', 1037 | 1038 | linesOfCode: 1000 1039 | 1040 | } 1041 | 1042 | ]; 1043 | 1044 | const totalOutput = contributions 1045 | 1046 | .reduce((totalLines, output) => totalLines + output.linesOfCode, 0) 1047 | 1048 | ``` 1049 | 1050 | **[↑ 回到顶部](#目录)** 1051 | 1052 | ### 封装判断条件 1053 | 1054 | **:-1: 反例:** 1055 | 1056 | ```ts 1057 | 1058 | if (subscription.isTrial || account.balance > 0) { 1059 | 1060 | // ... 1061 | 1062 | } 1063 | 1064 | ``` 1065 | 1066 | **:+1: 正例:** 1067 | 1068 | ```ts 1069 | 1070 | function canActivateService(subscription: Subscription, account: Account) { 1071 | 1072 | return subscription.isTrial || account.balance > 0 1073 | 1074 | } 1075 | 1076 | if (canActivateService(subscription, account)) { 1077 | 1078 | // ... 1079 | 1080 | } 1081 | 1082 | ``` 1083 | 1084 | **[↑ 回到顶部](#目录)** 1085 | 1086 | ### 避免“否定”的判断 1087 | 1088 | **:-1: 反例:** 1089 | 1090 | ```ts 1091 | 1092 | function isEmailNotUsed(email: string) { 1093 | 1094 | // ... 1095 | 1096 | } 1097 | 1098 | if (isEmailNotUsed(email)) { 1099 | 1100 | // ... 1101 | 1102 | } 1103 | 1104 | ``` 1105 | 1106 | **:+1: 正例:** 1107 | 1108 | ```ts 1109 | 1110 | function isEmailUsed(email) { 1111 | 1112 | // ... 1113 | 1114 | } 1115 | 1116 | if (!isEmailUsed(node)) { 1117 | 1118 | // ... 1119 | 1120 | } 1121 | 1122 | ``` 1123 | 1124 | **[↑ 回到顶部](#目录)** 1125 | 1126 | ### 避免判断条件 1127 | 1128 | 这看起来似乎不太可能完成啊。大多数人听到后第一反应是,“没有if语句怎么实现功能呢?” 在多数情况下,可以使用多态性来实现相同的功能。接下来的问题是 “为什么要这么做?” 原因就是之前提到的:函数只做一件事。 1129 | 1130 | **:-1: 反例:** 1131 | 1132 | ```ts 1133 | 1134 | class Airplane { 1135 | 1136 | private type: string; 1137 | 1138 | // ... 1139 | 1140 | getCruisingAltitude() { 1141 | 1142 | switch (this.type) { 1143 | 1144 | case '777': 1145 | 1146 | return this.getMaxAltitude() - this.getPassengerCount(); 1147 | 1148 | case 'Air Force One': 1149 | 1150 | return this.getMaxAltitude(); 1151 | 1152 | case 'Cessna': 1153 | 1154 | return this.getMaxAltitude() - this.getFuelExpenditure(); 1155 | 1156 | default: 1157 | 1158 | throw new Error('Unknown airplane type.'); 1159 | 1160 | } 1161 | 1162 | } 1163 | 1164 | } 1165 | 1166 | ``` 1167 | 1168 | **:+1: 正例:** 1169 | 1170 | ```ts 1171 | 1172 | class Airplane { 1173 | 1174 | // ... 1175 | 1176 | } 1177 | 1178 | class Boeing777 extends Airplane { 1179 | 1180 | // ... 1181 | 1182 | getCruisingAltitude() { 1183 | 1184 | return this.getMaxAltitude() - this.getPassengerCount(); 1185 | 1186 | } 1187 | 1188 | } 1189 | 1190 | class AirForceOne extends Airplane { 1191 | 1192 | // ... 1193 | 1194 | getCruisingAltitude() { 1195 | 1196 | return this.getMaxAltitude(); 1197 | 1198 | } 1199 | 1200 | } 1201 | 1202 | class Cessna extends Airplane { 1203 | 1204 | // ... 1205 | 1206 | getCruisingAltitude() { 1207 | 1208 | return this.getMaxAltitude() - this.getFuelExpenditure(); 1209 | 1210 | } 1211 | 1212 | } 1213 | 1214 | ``` 1215 | 1216 | **[↑ 回到顶部](#目录)** 1217 | 1218 | ### 避免类型检查 1219 | 1220 | TypeScript 是 JavaScript 的一个严格的语法超集,具有静态类型检查的特性。所以指定变量、参数和返回值的类型,以充分利用此特性,能让重构更容易。 1221 | 1222 | **:-1: 反例:** 1223 | 1224 | ```ts 1225 | 1226 | function travelToTexas(vehicle: Bicycle | Car) { 1227 | 1228 | if (vehicle instanceof Bicycle) { 1229 | 1230 | vehicle.pedal(this.currentLocation, new Location('texas')); 1231 | 1232 | } else if (vehicle instanceof Car) { 1233 | 1234 | vehicle.drive(this.currentLocation, new Location('texas')); 1235 | 1236 | } 1237 | 1238 | } 1239 | 1240 | ``` 1241 | 1242 | **:+1: 正例:** 1243 | 1244 | ```ts 1245 | 1246 | type Vehicle = Bicycle | Car; 1247 | 1248 | function travelToTexas(vehicle: Vehicle) { 1249 | 1250 | vehicle.move(this.currentLocation, new Location('texas')); 1251 | 1252 | } 1253 | 1254 | ``` 1255 | 1256 | **[↑ 回到顶部](#目录)** 1257 | 1258 | ### 不要过度优化 1259 | 1260 | 现代浏览器在运行时进行大量的底层优化。很多时候,你做优化只是在浪费时间。有些优秀[资源](https://github.com/petkaantonov/bluebird/wiki/Optimization-killers)可以帮助定位哪里需要优化,找到并修复它。 1261 | 1262 | **:-1: 反例:** 1263 | 1264 | ```ts 1265 | 1266 | // On old browsers, each iteration with uncached `list.length` would be costly 1267 | 1268 | // because of `list.length` recomputation. In modern browsers, this is optimized. 1269 | 1270 | for (let i = 0, len = list.length; i < len; i++) { 1271 | 1272 | // ... 1273 | 1274 | } 1275 | 1276 | ``` 1277 | 1278 | **:+1: 正例:** 1279 | 1280 | ```ts 1281 | 1282 | for (let i = 0; i < list.length; i++) { 1283 | 1284 | // ... 1285 | 1286 | } 1287 | 1288 | ``` 1289 | 1290 | **[↑ 回到顶部](#目录)** 1291 | 1292 | ### 删除无用代码 1293 | 1294 | 无用代码和重复代码一样无需保留。如果没有地方调用它,请删除!如果仍然需要它,可以查看版本历史。 1295 | 1296 | **:-1: 反例:** 1297 | 1298 | ```ts 1299 | 1300 | function oldRequestModule(url: string) { 1301 | 1302 | // ... 1303 | 1304 | } 1305 | 1306 | function requestModule(url: string) { 1307 | 1308 | // ... 1309 | 1310 | } 1311 | 1312 | const req = requestModule; 1313 | 1314 | inventoryTracker('apples', req, 'www.inventory-awesome.io'); 1315 | 1316 | ``` 1317 | 1318 | **:+1: 正例:** 1319 | 1320 | ```ts 1321 | 1322 | function requestModule(url: string) { 1323 | 1324 | // ... 1325 | 1326 | } 1327 | 1328 | const req = requestModule; 1329 | 1330 | inventoryTracker('apples', req, 'www.inventory-awesome.io'); 1331 | 1332 | ``` 1333 | 1334 | **[↑ 回到顶部](#目录)** 1335 | 1336 | ### 使用迭代器和生成器 1337 | 1338 | 像使用流一样处理数据集合时,请使用生成器和迭代器。 1339 | 1340 | 理由如下: 1341 | - 将调用者与生成器实现解耦,在某种意义上,调用者决定要访问多少项。 1342 | - 延迟执行,按需使用。 1343 | - 内置支持使用`for-of`语法进行迭代 1344 | - 允许实现优化的迭代器模式 1345 | 1346 | **:-1: 反例:** 1347 | 1348 | ```ts 1349 | function fibonacci(n: number): number[] { 1350 | if (n === 1) return [0]; 1351 | if (n === 2) return [0, 1]; 1352 | 1353 | const items: number[] = [0, 1]; 1354 | while (items.length < n) { 1355 | items.push(items[items.length - 2] + items[items.length - 1]); 1356 | } 1357 | 1358 | return items; 1359 | } 1360 | 1361 | function print(n: number) { 1362 | fibonacci(n).forEach(fib => console.log(fib)); 1363 | } 1364 | 1365 | // Print first 10 Fibonacci numbers. 1366 | print(10); 1367 | ``` 1368 | 1369 | **:+1: 正例:** 1370 | 1371 | ```ts 1372 | // Generates an infinite stream of Fibonacci numbers. 1373 | // The generator doesn't keep the array of all numbers. 1374 | function* fibonacci(): IterableIterator { 1375 | let [a, b] = [0, 1]; 1376 | 1377 | while (true) { 1378 | yield a; 1379 | [a, b] = [b, a + b]; 1380 | } 1381 | } 1382 | 1383 | function print(n: number) { 1384 | let i = 0; 1385 | for (const fib of fibonacci()) { 1386 | if (i++ === n) break; 1387 | console.log(fib); 1388 | } 1389 | } 1390 | 1391 | // Print first 10 Fibonacci numbers. 1392 | print(10); 1393 | ``` 1394 | 1395 | 有些库通过链接“map”、“slice”、“forEach”等方法,达到与原生数组类似的方式处理迭代。参见 [itiriri](https://www.npmjs.com/package/itiriri) 里面有一些使用迭代器的高级操作示例(或异步迭代的操作 [itiriri-async](https://www.npmjs.com/package/itiriri-async))。 1396 | 1397 | ```ts 1398 | import itiriri from 'itiriri'; 1399 | 1400 | function* fibonacci(): IterableIterator { 1401 | let [a, b] = [0, 1]; 1402 | 1403 | while (true) { 1404 | yield a; 1405 | [a, b] = [b, a + b]; 1406 | } 1407 | } 1408 | 1409 | itiriri(fibonacci()) 1410 | .take(10) 1411 | .forEach(fib => console.log(fib)); 1412 | ``` 1413 | 1414 | **[↑ 回到顶部](#目录)** 1415 | 1416 | ## 对象和数据结构 1417 | 1418 | ### 使用`getters`和`setters` 1419 | 1420 | TypeScript 支持 getter/setter 语法。使用 getter 和 setter 从对象中访问数据比简单地在对象上查找属性要好。原因如下: 1421 | 1422 | * 当需要在获取对象属性之前做一些事情时,不必在代码中查找并修改每个访问器。 1423 | * 执行`set`时添加验证更简单。 1424 | * 封装内部表示。 1425 | * 更容易添加日志和错误处理。 1426 | * 可以延迟加载对象的属性,比如从服务器获取它。 1427 | 1428 | **:-1: 反例:** 1429 | 1430 | ```ts 1431 | 1432 | class BankAccount { 1433 | 1434 | balance: number = 0; 1435 | 1436 | // ... 1437 | 1438 | } 1439 | 1440 | const value = 100; 1441 | 1442 | const account = new BankAccount(); 1443 | 1444 | if (value < 0) { 1445 | 1446 | throw new Error('Cannot set negative balance.'); 1447 | 1448 | } 1449 | 1450 | account.balance = value; 1451 | 1452 | ``` 1453 | 1454 | **:+1: 正例:** 1455 | 1456 | ```ts 1457 | 1458 | class BankAccount { 1459 | 1460 | private accountBalance: number = 0; 1461 | 1462 | get balance(): number { 1463 | 1464 | return this.accountBalance; 1465 | 1466 | } 1467 | 1468 | set balance(value: number) { 1469 | 1470 | if (value < 0) { 1471 | 1472 | throw new Error('Cannot set negative balance.'); 1473 | 1474 | } 1475 | 1476 | this.accountBalance = value; 1477 | 1478 | } 1479 | 1480 | // ... 1481 | 1482 | } 1483 | 1484 | const account = new BankAccount(); 1485 | 1486 | account.balance = 100; 1487 | 1488 | ``` 1489 | 1490 | **[↑ 回到顶部](#目录)** 1491 | 1492 | ### 让对象拥有 private/protected 成员 1493 | 1494 | TypeScript 类成员支持 `public`*(默认)*、`protected` 以及 `private`的访问限制。 1495 | 1496 | **:-1: 反例:** 1497 | 1498 | ```ts 1499 | 1500 | class Circle { 1501 | 1502 | radius: number; 1503 | 1504 | 1505 | 1506 | constructor(radius: number) { 1507 | 1508 | this.radius = radius; 1509 | 1510 | } 1511 | 1512 | perimeter(){ 1513 | 1514 | return 2 * Math.PI * this.radius; 1515 | 1516 | } 1517 | 1518 | surface(){ 1519 | 1520 | return Math.PI * this.radius * this.radius; 1521 | 1522 | } 1523 | 1524 | } 1525 | 1526 | ``` 1527 | 1528 | **:+1: 正例:** 1529 | 1530 | ```ts 1531 | 1532 | class Circle { 1533 | 1534 | constructor(private readonly radius: number) { 1535 | 1536 | } 1537 | 1538 | perimeter(){ 1539 | 1540 | return 2 * Math.PI * this.radius; 1541 | 1542 | } 1543 | 1544 | surface(){ 1545 | 1546 | return Math.PI * this.radius * this.radius; 1547 | 1548 | } 1549 | 1550 | } 1551 | 1552 | ``` 1553 | 1554 | **[↑ 回到顶部](#目录)** 1555 | 1556 | ### 不变性 1557 | 1558 | TypeScript 类型系统允许将接口、类上的单个属性设置为只读,能以函数的方式运行。 1559 | 1560 | 还有个高级场景,可以使用内置类型`Readonly`,它接受类型 T 并使用[映射类型](https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types)将其所有属性标记为只读。 1561 | 1562 | **:-1: 反例:** 1563 | 1564 | ```ts 1565 | 1566 | interface Config { 1567 | 1568 | host: string; 1569 | 1570 | port: string; 1571 | 1572 | db: string; 1573 | 1574 | } 1575 | 1576 | ``` 1577 | 1578 | **:+1: 正例:** 1579 | 1580 | ```ts 1581 | 1582 | interface Config { 1583 | 1584 | readonly host: string; 1585 | 1586 | readonly port: string; 1587 | 1588 | readonly db: string; 1589 | 1590 | } 1591 | 1592 | ``` 1593 | 1594 | **[↑ 回到顶部](#目录)** 1595 | 1596 | ### 类型 vs 接口 1597 | 1598 | 当可能需要联合或交集时,请使用类型。如果需要`扩展`或`实现`,请使用接口。然而,没有严格的规则,只有适合的规则。 1599 | 1600 | 详细解释参考关于 Typescript 中`type`和`interface`区别的[解答](https://stackoverflow.com/questions/37233735/typescript-interfaces-vs-types/54101543#54101543) 。 1601 | 1602 | **:-1: 反例:** 1603 | 1604 | ```ts 1605 | 1606 | interface EmailConfig { 1607 | 1608 | // ... 1609 | 1610 | } 1611 | 1612 | interface DbConfig { 1613 | 1614 | // ... 1615 | 1616 | } 1617 | 1618 | interface Config { 1619 | 1620 | // ... 1621 | 1622 | } 1623 | 1624 | //... 1625 | 1626 | type Shape { 1627 | 1628 | // ... 1629 | 1630 | } 1631 | 1632 | ``` 1633 | 1634 | **:+1: 正例:** 1635 | 1636 | ```ts 1637 | 1638 | type EmailConfig { 1639 | 1640 | // ... 1641 | 1642 | } 1643 | 1644 | type DbConfig { 1645 | 1646 | // ... 1647 | 1648 | } 1649 | 1650 | type Config = EmailConfig | DbConfig; 1651 | 1652 | // ... 1653 | 1654 | interface Shape { 1655 | 1656 | } 1657 | 1658 | class Circle implements Shape { 1659 | 1660 | // ... 1661 | 1662 | } 1663 | 1664 | class Square implements Shape { 1665 | 1666 | // ... 1667 | 1668 | } 1669 | 1670 | ``` 1671 | 1672 | **[↑ 回到顶部](#目录)** 1673 | 1674 | 1675 | ## 类 1676 | 1677 | ### 小、小、小!要事情说三遍 1678 | 1679 | 类的大小是由它的职责来度量的。按照*单一职责原则*,类要小。 1680 | 1681 | **:-1: 反例:** 1682 | 1683 | ```ts 1684 | 1685 | class Dashboard { 1686 | 1687 | getLanguage(): string { /* ... */ } 1688 | 1689 | setLanguage(language: string): void { /* ... */ } 1690 | 1691 | showProgress(): void { /* ... */ } 1692 | 1693 | hideProgress(): void { /* ... */ } 1694 | 1695 | isDirty(): boolean { /* ... */ } 1696 | 1697 | disable(): void { /* ... */ } 1698 | 1699 | enable(): void { /* ... */ } 1700 | 1701 | addSubscription(subscription: Subscription): void { /* ... */ } 1702 | 1703 | removeSubscription(subscription: Subscription): void { /* ... */ } 1704 | 1705 | addUser(user: User): void { /* ... */ } 1706 | 1707 | removeUser(user: User): void { /* ... */ } 1708 | 1709 | goToHomePage(): void { /* ... */ } 1710 | 1711 | updateProfile(details: UserDetails): void { /* ... */ } 1712 | 1713 | getVersion(): string { /* ... */ } 1714 | 1715 | // ... 1716 | 1717 | } 1718 | 1719 | ``` 1720 | 1721 | **:+1: 正例:** 1722 | 1723 | ```ts 1724 | 1725 | class Dashboard { 1726 | 1727 | disable(): void { /* ... */ } 1728 | 1729 | enable(): void { /* ... */ } 1730 | 1731 | getVersion(): string { /* ... */ } 1732 | 1733 | } 1734 | 1735 | // split the responsibilities by moving the remaining methods to other classes 1736 | 1737 | // ... 1738 | 1739 | ``` 1740 | 1741 | **[↑ 回到顶部](#目录)** 1742 | 1743 | ### 高内聚低耦合 1744 | 1745 | 内聚:定义了类成员之间相互关联的程度。理想情况下,高内聚类的每个方法都应该使用类中的所有字段,实际上这不可能也不可取。但我们依然提倡高内聚。 1746 | 1747 | 耦合:指的是两个类之间的关联程度。如果其中一个类的更改不影响另一个类,则称为低耦合类。 1748 | 1749 | 好的软件设计具有**高内聚性**和**低耦合性**。 1750 | 1751 | **:-1: 反例:** 1752 | 1753 | ```ts 1754 | 1755 | class UserManager { 1756 | 1757 | // Bad: each private variable is used by one or another group of methods. 1758 | 1759 | // It makes clear evidence that the class is holding more than a single responsibility. 1760 | 1761 | // If I need only to create the service to get the transactions for a user, 1762 | 1763 | // I'm still forced to pass and instance of emailSender. 1764 | 1765 | constructor( 1766 | 1767 | private readonly db: Database, 1768 | 1769 | private readonly emailSender: EmailSender) { 1770 | 1771 | } 1772 | 1773 | async getUser(id: number): Promise { 1774 | 1775 | return await db.users.findOne({ id }) 1776 | 1777 | } 1778 | 1779 | async getTransactions(userId: number): Promise { 1780 | 1781 | return await db.transactions.find({ userId }) 1782 | 1783 | } 1784 | 1785 | async sendGreeting(): Promise { 1786 | 1787 | await emailSender.send('Welcome!'); 1788 | 1789 | } 1790 | 1791 | async sendNotification(text: string): Promise { 1792 | 1793 | await emailSender.send(text); 1794 | 1795 | } 1796 | 1797 | async sendNewsletter(): Promise { 1798 | 1799 | // ... 1800 | 1801 | } 1802 | 1803 | } 1804 | 1805 | ``` 1806 | 1807 | **:+1: 正例:** 1808 | 1809 | ```ts 1810 | 1811 | class UserService { 1812 | 1813 | constructor(private readonly db: Database) { 1814 | 1815 | } 1816 | 1817 | async getUser(id: number): Promise { 1818 | 1819 | return await db.users.findOne({ id }) 1820 | 1821 | } 1822 | 1823 | async getTransactions(userId: number): Promise { 1824 | 1825 | return await db.transactions.find({ userId }) 1826 | 1827 | } 1828 | 1829 | } 1830 | 1831 | class UserNotifier { 1832 | 1833 | constructor(private readonly emailSender: EmailSender) { 1834 | 1835 | } 1836 | 1837 | async sendGreeting(): Promise { 1838 | 1839 | await emailSender.send('Welcome!'); 1840 | 1841 | } 1842 | 1843 | async sendNotification(text: string): Promise { 1844 | 1845 | await emailSender.send(text); 1846 | 1847 | } 1848 | 1849 | async sendNewsletter(): Promise { 1850 | 1851 | // ... 1852 | 1853 | } 1854 | 1855 | } 1856 | 1857 | ``` 1858 | 1859 | **[↑ 回到顶部](#目录)** 1860 | 1861 | ### 组合大于继承 1862 | 1863 | 正如“四人帮”在[设计模式](https://en.wikipedia.org/wiki/Design_Patterns)中所指出的那样,您尽可能使用组合而不是继承。组合和继承各有优劣。这个准则的主要观点是,如果你潜意识地倾向于继承,试着想想组合是否能更好地给你的问题建模,在某些情况下可以。 1864 | 1865 | 什么时候应该使用继承?这取决于你面临的问题。以下场景使用继承更好: 1866 | 1867 | 1. 继承代表的是“is-a”关系,而不是“has-a”关系 (人 -> 动物 vs. 用户 -> 用户详情)。 1868 | 2. 可复用基类的代码 (人类可以像所有动物一样移动)。 1869 | 3. 希望通过更改基类对派生类进行全局更改(改变所有动物在运动时的热量消耗)。 1870 | 1871 | **:-1: 反例:** 1872 | 1873 | ```ts 1874 | 1875 | class Employee { 1876 | 1877 | constructor( 1878 | 1879 | private readonly name: string, 1880 | 1881 | private readonly email:string) { 1882 | 1883 | } 1884 | 1885 | // ... 1886 | 1887 | } 1888 | 1889 | // Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee 1890 | 1891 | class EmployeeTaxData extends Employee { 1892 | 1893 | constructor( 1894 | 1895 | name: string, 1896 | 1897 | email:string, 1898 | 1899 | private readonly ssn: string, 1900 | 1901 | private readonly salary: number) { 1902 | 1903 | super(name, email); 1904 | 1905 | } 1906 | 1907 | // ... 1908 | 1909 | } 1910 | 1911 | ``` 1912 | 1913 | **:+1: 正例:** 1914 | 1915 | ```ts 1916 | 1917 | class Employee { 1918 | 1919 | private taxData: EmployeeTaxData; 1920 | 1921 | constructor( 1922 | 1923 | private readonly name: string, 1924 | 1925 | private readonly email:string) { 1926 | 1927 | } 1928 | 1929 | setTaxData(ssn: string, salary: number): Employee { 1930 | 1931 | this.taxData = new EmployeeTaxData(ssn, salary); 1932 | 1933 | return this; 1934 | 1935 | } 1936 | 1937 | // ... 1938 | 1939 | } 1940 | 1941 | class EmployeeTaxData { 1942 | 1943 | constructor( 1944 | 1945 | public readonly ssn: string, 1946 | 1947 | public readonly salary: number) { 1948 | 1949 | } 1950 | 1951 | // ... 1952 | 1953 | } 1954 | 1955 | ``` 1956 | 1957 | **[↑ 回到顶部](#目录)** 1958 | 1959 | ### 使用方法链 1960 | 1961 | 非常有用的模式,在许多库中都可以看到。它让代码表达力更好,也更简洁。 1962 | 1963 | **:-1: 反例:** 1964 | 1965 | ```ts 1966 | 1967 | class QueryBuilder { 1968 | 1969 | private collection: string; 1970 | 1971 | private pageNumber: number = 1; 1972 | 1973 | private itemsPerPage: number = 100; 1974 | 1975 | private orderByFields: string[] = []; 1976 | 1977 | from(collection: string): void { 1978 | 1979 | this.collection = collection; 1980 | 1981 | } 1982 | 1983 | page(number: number, itemsPerPage: number = 100): void { 1984 | 1985 | this.pageNumber = number; 1986 | 1987 | this.itemsPerPage = itemsPerPage; 1988 | 1989 | } 1990 | 1991 | orderBy(...fields: string[]): void { 1992 | 1993 | this.orderByFields = fields; 1994 | 1995 | } 1996 | 1997 | build(): Query { 1998 | 1999 | // ... 2000 | 2001 | } 2002 | 2003 | } 2004 | 2005 | // ... 2006 | 2007 | const query = new QueryBuilder(); 2008 | 2009 | query.from('users'); 2010 | 2011 | query.page(1, 100); 2012 | 2013 | query.orderBy('firstName', 'lastName'); 2014 | 2015 | const query = queryBuilder.build(); 2016 | 2017 | ``` 2018 | 2019 | **:+1: 正例:** 2020 | 2021 | ```ts 2022 | 2023 | class QueryBuilder { 2024 | 2025 | private collection: string; 2026 | 2027 | private pageNumber: number = 1; 2028 | 2029 | private itemsPerPage: number = 100; 2030 | 2031 | private orderByFields: string[] = []; 2032 | 2033 | from(collection: string): this { 2034 | 2035 | this.collection = collection; 2036 | 2037 | return this; 2038 | 2039 | } 2040 | 2041 | page(number: number, itemsPerPage: number = 100): this { 2042 | 2043 | this.pageNumber = number; 2044 | 2045 | this.itemsPerPage = itemsPerPage; 2046 | 2047 | return this; 2048 | 2049 | } 2050 | 2051 | orderBy(...fields: string[]): this { 2052 | 2053 | this.orderByFields = fields; 2054 | 2055 | return this; 2056 | 2057 | } 2058 | 2059 | build(): Query { 2060 | 2061 | // ... 2062 | 2063 | } 2064 | 2065 | } 2066 | 2067 | // ... 2068 | 2069 | const query = new QueryBuilder() 2070 | 2071 | .from('users') 2072 | 2073 | .page(1, 100) 2074 | 2075 | .orderBy('firstName', 'lastName') 2076 | 2077 | .build(); 2078 | 2079 | ``` 2080 | 2081 | **[↑ 回到顶部](#目录)** 2082 | 2083 | ## SOLID原则 2084 | 2085 | ### 单一职责原则 (SRP) 2086 | 2087 | 正如 Clean Code 中所述,“类更改的原因不应该超过一个”。将很多功能打包在一个类看起来很诱人,就像在航班上您只能带一个手提箱。这样带来的问题是,在概念上类不具有内聚性,且有很多原因去修改类。而我们应该尽量减少修改类的次数。如果一个类功能太多,修改了其中一处很难确定对代码库中其他依赖模块的影响。 2088 | 2089 | **:-1: 反例:** 2090 | 2091 | ```ts 2092 | 2093 | class UserSettings { 2094 | 2095 | constructor(private readonly user: User) { 2096 | 2097 | } 2098 | 2099 | changeSettings(settings: UserSettings) { 2100 | 2101 | if (this.verifyCredentials()) { 2102 | 2103 | // ... 2104 | 2105 | } 2106 | 2107 | } 2108 | 2109 | verifyCredentials() { 2110 | 2111 | // ... 2112 | 2113 | } 2114 | 2115 | } 2116 | 2117 | ``` 2118 | 2119 | **:+1: 正例:** 2120 | 2121 | ```ts 2122 | 2123 | class UserAuth { 2124 | 2125 | constructor(private readonly user: User) { 2126 | 2127 | } 2128 | 2129 | verifyCredentials() { 2130 | 2131 | // ... 2132 | 2133 | } 2134 | 2135 | } 2136 | 2137 | class UserSettings { 2138 | 2139 | private readonly auth: UserAuth; 2140 | 2141 | constructor(private readonly user: User) { 2142 | 2143 | this.auth = new UserAuth(user); 2144 | 2145 | } 2146 | 2147 | changeSettings(settings: UserSettings) { 2148 | 2149 | if (this.auth.verifyCredentials()) { 2150 | 2151 | // ... 2152 | 2153 | } 2154 | 2155 | } 2156 | 2157 | } 2158 | 2159 | ``` 2160 | 2161 | **[↑ 回到顶部](#目录)** 2162 | 2163 | ### 开闭原则 (OCP) 2164 | 2165 | 正如 Bertrand Meyer 所说,“软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。” 换句话说,就是允许在不更改现有代码的情况下添加新功能。 2166 | 2167 | **:-1: 反例:** 2168 | 2169 | ```ts 2170 | 2171 | class AjaxAdapter extends Adapter { 2172 | 2173 | constructor() { 2174 | 2175 | super(); 2176 | 2177 | } 2178 | 2179 | // ... 2180 | 2181 | } 2182 | 2183 | class NodeAdapter extends Adapter { 2184 | 2185 | constructor() { 2186 | 2187 | super(); 2188 | 2189 | } 2190 | 2191 | // ... 2192 | 2193 | } 2194 | 2195 | class HttpRequester { 2196 | 2197 | constructor(private readonly adapter: Adapter) { 2198 | 2199 | } 2200 | 2201 | async fetch(url: string): Promise { 2202 | 2203 | if (this.adapter instanceof AjaxAdapter) { 2204 | 2205 | const response = await makeAjaxCall(url); 2206 | 2207 | // transform response and return 2208 | 2209 | } else if (this.adapter instanceof NodeAdapter) { 2210 | 2211 | const response = await makeHttpCall(url); 2212 | 2213 | // transform response and return 2214 | 2215 | } 2216 | 2217 | } 2218 | 2219 | } 2220 | 2221 | function makeAjaxCall(url: string): Promise { 2222 | 2223 | // request and return promise 2224 | 2225 | } 2226 | 2227 | function makeHttpCall(url: string): Promise { 2228 | 2229 | // request and return promise 2230 | 2231 | } 2232 | 2233 | ``` 2234 | 2235 | **:+1: 正例:** 2236 | 2237 | ```ts 2238 | 2239 | abstract class Adapter { 2240 | 2241 | abstract async request(url: string): Promise; 2242 | 2243 | } 2244 | 2245 | class AjaxAdapter extends Adapter { 2246 | 2247 | constructor() { 2248 | 2249 | super(); 2250 | 2251 | } 2252 | 2253 | async request(url: string): Promise{ 2254 | 2255 | // request and return promise 2256 | 2257 | } 2258 | 2259 | // ... 2260 | 2261 | } 2262 | 2263 | class NodeAdapter extends Adapter { 2264 | 2265 | constructor() { 2266 | 2267 | super(); 2268 | 2269 | } 2270 | 2271 | async request(url: string): Promise{ 2272 | 2273 | // request and return promise 2274 | 2275 | } 2276 | 2277 | // ... 2278 | 2279 | } 2280 | 2281 | class HttpRequester { 2282 | 2283 | constructor(private readonly adapter: Adapter) { 2284 | 2285 | } 2286 | 2287 | async fetch(url: string): Promise { 2288 | 2289 | const response = await this.adapter.request(url); 2290 | 2291 | // transform response and return 2292 | 2293 | } 2294 | 2295 | } 2296 | 2297 | ``` 2298 | 2299 | **[↑ 回到顶部](#目录)** 2300 | 2301 | ### 里氏替换原则 (LSP) 2302 | 2303 | 对一个非常简单的概念来说,这是个可怕的术语。 2304 | 2305 | 它的正式定义是:“如果 S 是 T 的一个子类型,那么类型 T 的对象可以被替换为类型 S 的对象,而不会改变程序任何期望的属性(正确性、执行的任务等)“。这是一个更可怕的定义。 2306 | 2307 | 更好的解释是,如果您有一个父类和一个子类,那么父类和子类可以互换使用,而不会出现问题。这可能仍然令人困惑,所以让我们看一看经典的正方形矩形的例子。从数学上讲,正方形是矩形,但是如果您通过继承使用 “is-a” 关系对其建模,您很快就会遇到麻烦。 2308 | 2309 | **:-1: 反例:** 2310 | 2311 | ```ts 2312 | 2313 | class Rectangle { 2314 | 2315 | constructor( 2316 | 2317 | protected width: number = 0, 2318 | 2319 | protected height: number = 0) { 2320 | 2321 | } 2322 | 2323 | setColor(color: string) { 2324 | 2325 | // ... 2326 | 2327 | } 2328 | 2329 | render(area: number) { 2330 | 2331 | // ... 2332 | 2333 | } 2334 | 2335 | setWidth(width: number) { 2336 | 2337 | this.width = width; 2338 | 2339 | } 2340 | 2341 | setHeight(height: number) { 2342 | 2343 | this.height = height; 2344 | 2345 | } 2346 | 2347 | getArea(): number { 2348 | 2349 | return this.width * this.height; 2350 | 2351 | } 2352 | 2353 | } 2354 | 2355 | class Square extends Rectangle { 2356 | 2357 | setWidth(width: number) { 2358 | 2359 | this.width = width; 2360 | 2361 | this.height = width; 2362 | 2363 | } 2364 | 2365 | setHeight(height: number) { 2366 | 2367 | this.width = height; 2368 | 2369 | this.height = height; 2370 | 2371 | } 2372 | 2373 | } 2374 | 2375 | function renderLargeRectangles(rectangles: Rectangle[]) { 2376 | 2377 | rectangles.forEach((rectangle) => { 2378 | 2379 | rectangle.setWidth(4); 2380 | 2381 | rectangle.setHeight(5); 2382 | 2383 | const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20. 2384 | 2385 | rectangle.render(area); 2386 | 2387 | }); 2388 | 2389 | } 2390 | 2391 | const rectangles = [new Rectangle(), new Rectangle(), new Square()]; 2392 | 2393 | renderLargeRectangles(rectangles); 2394 | 2395 | ``` 2396 | 2397 | **:+1: 正例:** 2398 | 2399 | ```ts 2400 | 2401 | abstract class Shape { 2402 | 2403 | setColor(color: string) { 2404 | 2405 | // ... 2406 | 2407 | } 2408 | 2409 | render(area: number) { 2410 | 2411 | // ... 2412 | 2413 | } 2414 | 2415 | abstract getArea(): number; 2416 | 2417 | } 2418 | 2419 | class Rectangle extends Shape { 2420 | 2421 | constructor( 2422 | 2423 | private readonly width = 0, 2424 | 2425 | private readonly height = 0) { 2426 | 2427 | super(); 2428 | 2429 | } 2430 | 2431 | getArea(): number { 2432 | 2433 | return this.width * this.height; 2434 | 2435 | } 2436 | 2437 | } 2438 | 2439 | class Square extends Shape { 2440 | 2441 | constructor(private readonly length: number) { 2442 | 2443 | super(); 2444 | 2445 | } 2446 | 2447 | getArea(): number { 2448 | 2449 | return this.length * this.length; 2450 | 2451 | } 2452 | 2453 | } 2454 | 2455 | function renderLargeShapes(shapes: Shape[]) { 2456 | 2457 | shapes.forEach((shape) => { 2458 | 2459 | const area = shape.getArea(); 2460 | 2461 | shape.render(area); 2462 | 2463 | }); 2464 | 2465 | } 2466 | 2467 | const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)]; 2468 | 2469 | renderLargeShapes(shapes); 2470 | 2471 | ``` 2472 | 2473 | **[↑ 回到顶部](#目录)** 2474 | 2475 | ### 接口隔离原则 (ISP) 2476 | 2477 | “客户不应该被迫依赖于他们不使用的接口。” 这一原则与单一责任原则密切相关。这意味着不应该设计一个大而全的抽象,否则会增加客户的负担,因为他们需要实现一些不需要的方法。 2478 | 2479 | **:-1: 反例:** 2480 | 2481 | ```ts 2482 | 2483 | interface ISmartPrinter { 2484 | 2485 | print(); 2486 | 2487 | fax(); 2488 | 2489 | scan(); 2490 | 2491 | } 2492 | 2493 | class AllInOnePrinter implements ISmartPrinter { 2494 | 2495 | print() { 2496 | 2497 | // ... 2498 | 2499 | } 2500 | 2501 | 2502 | 2503 | fax() { 2504 | 2505 | // ... 2506 | 2507 | } 2508 | 2509 | scan() { 2510 | 2511 | // ... 2512 | 2513 | } 2514 | 2515 | } 2516 | 2517 | class EconomicPrinter implements ISmartPrinter { 2518 | 2519 | print() { 2520 | 2521 | // ... 2522 | 2523 | } 2524 | 2525 | 2526 | 2527 | fax() { 2528 | 2529 | throw new Error('Fax not supported.'); 2530 | 2531 | } 2532 | 2533 | scan() { 2534 | 2535 | throw new Error('Scan not supported.'); 2536 | 2537 | } 2538 | 2539 | } 2540 | 2541 | ``` 2542 | 2543 | **:+1: 正例:** 2544 | 2545 | ```ts 2546 | 2547 | interface IPrinter { 2548 | 2549 | print(); 2550 | 2551 | } 2552 | 2553 | interface IFax { 2554 | 2555 | fax(); 2556 | 2557 | } 2558 | 2559 | interface IScanner { 2560 | 2561 | scan(); 2562 | 2563 | } 2564 | 2565 | class AllInOnePrinter implements IPrinter, IFax, IScanner { 2566 | 2567 | print() { 2568 | 2569 | // ... 2570 | 2571 | } 2572 | 2573 | 2574 | 2575 | fax() { 2576 | 2577 | // ... 2578 | 2579 | } 2580 | 2581 | scan() { 2582 | 2583 | // ... 2584 | 2585 | } 2586 | 2587 | } 2588 | 2589 | class EconomicPrinter implements IPrinter { 2590 | 2591 | print() { 2592 | 2593 | // ... 2594 | 2595 | } 2596 | 2597 | } 2598 | 2599 | ``` 2600 | 2601 | **[↑ 回到顶部](#目录)** 2602 | 2603 | ### 依赖反转原则(Dependency Inversion Principle) 2604 | 2605 | 这个原则有两个要点: 2606 | 1. 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。 2607 | 2. 抽象不依赖实现,实现应依赖抽象。 2608 | 2609 | 一开始这难以理解,但是如果你使用过 Angular,你就会看到以依赖注入(DI)的方式实现了这一原则。虽然概念不同,但是 DIP 阻止高级模块了解其低级模块的细节并进行设置。它可以通过 DI 实现这一点。这样做的一个巨大好处是减少了模块之间的耦合。耦合非常糟糕,它让代码难以重构。 2610 | 2611 | DIP 通常是通过使用控制反转(IoC)容器来实现的。比如:TypeScript 的 IoC 容器 [InversifyJs](https://www.npmjs.com/package/inversify) 2612 | 2613 | **:-1: 反例:** 2614 | 2615 | ```ts 2616 | 2617 | import { readFile as readFileCb } from 'fs'; 2618 | 2619 | import { promisify } from 'util'; 2620 | 2621 | const readFile = promisify(readFileCb); 2622 | 2623 | type ReportData = { 2624 | 2625 | // .. 2626 | 2627 | } 2628 | 2629 | class XmlFormatter { 2630 | 2631 | parse(content: string): T { 2632 | 2633 | // Converts an XML string to an object T 2634 | 2635 | } 2636 | 2637 | } 2638 | 2639 | class ReportReader { 2640 | 2641 | // BAD: We have created a dependency on a specific request implementation. 2642 | 2643 | // We should just have ReportReader depend on a parse method: `parse` 2644 | 2645 | private readonly formatter = new XmlFormatter(); 2646 | 2647 | async read(path: string): Promise { 2648 | 2649 | const text = await readFile(path, 'UTF8'); 2650 | 2651 | return this.formatter.parse(text); 2652 | 2653 | } 2654 | 2655 | } 2656 | 2657 | // ... 2658 | 2659 | const reader = new ReportReader(); 2660 | 2661 | await report = await reader.read('report.xml'); 2662 | 2663 | ``` 2664 | 2665 | **:+1: 正例:** 2666 | 2667 | ```ts 2668 | 2669 | import { readFile as readFileCb } from 'fs'; 2670 | 2671 | import { promisify } from 'util'; 2672 | 2673 | const readFile = promisify(readFileCb); 2674 | 2675 | type ReportData = { 2676 | 2677 | // .. 2678 | 2679 | } 2680 | 2681 | interface Formatter { 2682 | 2683 | parse(content: string): T; 2684 | 2685 | } 2686 | 2687 | class XmlFormatter implements Formatter { 2688 | 2689 | parse(content: string): T { 2690 | 2691 | // Converts an XML string to an object T 2692 | 2693 | } 2694 | 2695 | } 2696 | 2697 | class JsonFormatter implements Formatter { 2698 | 2699 | parse(content: string): T { 2700 | 2701 | // Converts a JSON string to an object T 2702 | 2703 | } 2704 | 2705 | } 2706 | 2707 | class ReportReader { 2708 | 2709 | constructor(private readonly formatter: Formatter){ 2710 | 2711 | } 2712 | 2713 | async read(path: string): Promise { 2714 | 2715 | const text = await readFile(path, 'UTF8'); 2716 | 2717 | return this.formatter.parse(text); 2718 | 2719 | } 2720 | 2721 | } 2722 | 2723 | // ... 2724 | 2725 | const reader = new ReportReader(new XmlFormatter()); 2726 | 2727 | await report = await reader.read('report.xml'); 2728 | 2729 | // or if we had to read a json report: 2730 | 2731 | const reader = new ReportReader(new JsonFormatter()); 2732 | 2733 | await report = await reader.read('report.json'); 2734 | 2735 | ``` 2736 | 2737 | **[↑ 回到顶部](#目录)** 2738 | 2739 | ## 测试 2740 | 2741 | 测试比发布更重要。如果没有测试或测试不充分,那么每次发布代码时都无法确保不引入问题。怎样才算是足够的测试?这取决于团队,但是拥有100%的覆盖率(所有语句和分支)会让团队更有信心。这一切都要基于好的测试框架以及[覆盖率工具](https://github.com/gotwarlost/istanbul)。 2742 | 2743 | 没有任何理由不编写测试。有[很多优秀的 JS 测试框架](http://jstherightway.org/#testing-tools)都支持 TypeScript,找个团队喜欢的。然后为每个新特性/模块编写测试。如果您喜欢测试驱动开发(TDD),那就太好了,重点是确保在开发任何特性或重构现有特性之前,代码覆盖率已经达到要求。 2744 | 2745 | ### TDD(测试驱动开发)三定律 2746 | 2747 | 1. 在编写不能通过的单元测试前,不可编写生产代码。 2748 | 2. 只可编写刚好无法通过的单元测试,不能编译也算不过。 2749 | 3. 只可编写刚好足以通过当前失败测试的生产代码。 2750 | 2751 | **[↑ 回到顶部](#目录)** 2752 | 2753 | ### F.I.R.S.T.准则 2754 | 整洁的测试应遵循以下准则: 2755 | - **快速**(Fast),测试应该快(及时反馈出业务代码的问题)。 2756 | - **独立**(Independent),每个测试流程应该独立。 2757 | - **可重复**(Repeatable),测试应该在任何环境上都能重复通过。 2758 | - **自我验证**(Self-Validating),测试结果应该明确*通过*或者*失败*。 2759 | - **及时**(Timely),测试代码应该在产品代码之前编写。 2760 | 2761 | **[↑ 回到顶部](#目录)** 2762 | 2763 | ### 单一的测试每个概念 2764 | 2765 | 测试也应该遵循*单一职责原则*,每个单元测试只做一个断言。 2766 | 2767 | **:-1: 反例:** 2768 | 2769 | ```ts 2770 | 2771 | import { assert } from 'chai'; 2772 | 2773 | describe('AwesomeDate', () => { 2774 | 2775 | it('handles date boundaries', () => { 2776 | 2777 | let date: AwesomeDate; 2778 | 2779 | date = new AwesomeDate('1/1/2015'); 2780 | 2781 | date.addDays(30); 2782 | 2783 | assert.equal('1/31/2015', date); 2784 | 2785 | date = new AwesomeDate('2/1/2016'); 2786 | 2787 | date.addDays(28); 2788 | 2789 | assert.equal('02/29/2016', date); 2790 | 2791 | date = new AwesomeDate('2/1/2015'); 2792 | 2793 | date.addDays(28); 2794 | 2795 | assert.equal('03/01/2015', date); 2796 | 2797 | }); 2798 | 2799 | }); 2800 | 2801 | ``` 2802 | 2803 | **:+1: 正例:** 2804 | 2805 | ```ts 2806 | 2807 | import { assert } from 'chai'; 2808 | 2809 | describe('AwesomeDate', () => { 2810 | 2811 | it('handles 30-day months', () => { 2812 | 2813 | const date = new AwesomeDate('1/1/2015'); 2814 | 2815 | date.addDays(30); 2816 | 2817 | assert.equal('1/31/2015', date); 2818 | 2819 | }); 2820 | 2821 | it('handles leap year', () => { 2822 | 2823 | const date = new AwesomeDate('2/1/2016'); 2824 | 2825 | date.addDays(28); 2826 | 2827 | assert.equal('02/29/2016', date); 2828 | 2829 | }); 2830 | 2831 | it('handles non-leap year', () => { 2832 | 2833 | const date = new AwesomeDate('2/1/2015'); 2834 | 2835 | date.addDays(28); 2836 | 2837 | assert.equal('03/01/2015', date); 2838 | 2839 | }); 2840 | 2841 | }); 2842 | 2843 | ``` 2844 | 2845 | **[↑ 回到顶部](#目录)** 2846 | 2847 | ### 测试用例名称应该显示它的意图 2848 | 2849 | 当测试失败时,出错的第一个迹象可能就是它的名字。 2850 | 2851 | **:-1: 反例:** 2852 | 2853 | ```ts 2854 | 2855 | describe('Calendar', () => { 2856 | 2857 | it('2/29/2020', () => { 2858 | 2859 | // ... 2860 | 2861 | }); 2862 | 2863 | it('throws', () => { 2864 | 2865 | // ... 2866 | 2867 | }); 2868 | 2869 | }); 2870 | 2871 | ``` 2872 | 2873 | **:+1: 正例:** 2874 | 2875 | ```ts 2876 | 2877 | describe('Calendar', () => { 2878 | 2879 | it('should handle leap year', () => { 2880 | 2881 | // ... 2882 | 2883 | }); 2884 | 2885 | it('should throw when format is invalid', () => { 2886 | 2887 | // ... 2888 | 2889 | }); 2890 | 2891 | }); 2892 | 2893 | ``` 2894 | 2895 | **[↑ 回到顶部](#目录)** 2896 | 2897 | ## 并发 2898 | 2899 | ### 用 Promises 替代回调 2900 | 2901 | 回调不够整洁而且会导致过多的嵌套*(回调地狱)*。 2902 | 2903 | 有些工具使用回调的方式将现有函数转换为 promise 对象: 2904 | - Node.js 参见[`util.promisify`](https://nodejs.org/dist/latest-v8.x/docs/api/util.html#util_util_promisify_original) 2905 | - 通用参见 [pify](https://www.npmjs.com/package/pify), [es6-promisify](https://www.npmjs.com/package/es6-promisify) 2906 | 2907 | **:-1: 反例:** 2908 | 2909 | ```ts 2910 | 2911 | import { get } from 'request'; 2912 | 2913 | import { writeFile } from 'fs'; 2914 | 2915 | function downloadPage(url: string, saveTo: string, callback: (error: Error, content?: string) => void){ 2916 | 2917 | get(url, (error, response) => { 2918 | 2919 | if (error) { 2920 | 2921 | callback(error); 2922 | 2923 | } else { 2924 | 2925 | writeFile(saveTo, response.body, (error) => { 2926 | 2927 | if (error) { 2928 | 2929 | callback(error); 2930 | 2931 | } else { 2932 | 2933 | callback(null, response.body); 2934 | 2935 | } 2936 | 2937 | }); 2938 | 2939 | } 2940 | 2941 | }) 2942 | 2943 | } 2944 | 2945 | downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html', (error, content) => { 2946 | 2947 | if (error) { 2948 | 2949 | console.error(error); 2950 | 2951 | } else { 2952 | 2953 | console.log(content); 2954 | 2955 | } 2956 | 2957 | }); 2958 | 2959 | ``` 2960 | 2961 | **:+1: 正例:** 2962 | 2963 | ```ts 2964 | 2965 | import { get } from 'request'; 2966 | 2967 | import { writeFile } from 'fs'; 2968 | 2969 | import { promisify } from 'util'; 2970 | 2971 | const write = promisify(writeFile); 2972 | 2973 | function downloadPage(url: string, saveTo: string): Promise { 2974 | 2975 | return get(url) 2976 | 2977 | .then(response => write(saveTo, response)) 2978 | 2979 | } 2980 | 2981 | downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html') 2982 | 2983 | .then(content => console.log(content)) 2984 | 2985 | .catch(error => console.error(error)); 2986 | 2987 | ``` 2988 | 2989 | Promise 提供了一些辅助方法,能让代码更简洁: 2990 | 2991 | | 方法 | 描述 | 2992 | | ------------------------ | ----------------------------------------- | 2993 | | `Promise.resolve(value)` | 返回一个传入值解析后的 promise 。 | 2994 | | `Promise.reject(error)` | 返回一个带有拒绝原因的 promise 。 | 2995 | | `Promise.all(promises)` | 返回一个新的 promise,传入数组中的**每个** promise 都执行完成后返回的 promise 才算完成,或第一个 promise 拒绝而拒绝。| 2996 | | `Promise.race(promises)` | 返回一个新的 promise,传入数组中的**某个** promise 解决或拒绝,返回的 promise 就会解决或拒绝。| 2997 | 2998 | 2999 | `Promise.all`在并行运行任务时尤其有用,`Promise.race`让为 Promise 更容易实现超时。 3000 | 3001 | **[↑ 回到顶部](#目录)** 3002 | 3003 | ### `Async/Await` 比 `Promises` 更好 3004 | 3005 | 使用`async`/`await`语法,可以编写更简洁、更易理解的链式 promise 的代码。一个函数使用`async`关键字作为前缀,JavaScript 运行时会暂停`await`关键字上的代码执行(当使用 promise 时)。 3006 | 3007 | **:-1: 反例:** 3008 | 3009 | ```ts 3010 | 3011 | import { get } from 'request'; 3012 | 3013 | import { writeFile } from 'fs'; 3014 | 3015 | import { promisify } from 'util'; 3016 | 3017 | const write = util.promisify(writeFile); 3018 | 3019 | function downloadPage(url: string, saveTo: string): Promise { 3020 | 3021 | return get(url).then(response => write(saveTo, response)) 3022 | 3023 | } 3024 | 3025 | downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html') 3026 | 3027 | .then(content => console.log(content)) 3028 | 3029 | .catch(error => console.error(error)); 3030 | 3031 | ``` 3032 | 3033 | **:+1: 正例:** 3034 | 3035 | ```ts 3036 | 3037 | import { get } from 'request'; 3038 | 3039 | import { writeFile } from 'fs'; 3040 | 3041 | import { promisify } from 'util'; 3042 | 3043 | const write = promisify(writeFile); 3044 | 3045 | async function downloadPage(url: string, saveTo: string): Promise { 3046 | 3047 | const response = await get(url); 3048 | 3049 | await write(saveTo, response); 3050 | 3051 | return response; 3052 | 3053 | } 3054 | 3055 | // somewhere in an async function 3056 | 3057 | try { 3058 | 3059 | const content = await downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html'); 3060 | 3061 | console.log(content); 3062 | 3063 | } catch (error) { 3064 | 3065 | console.error(error); 3066 | 3067 | } 3068 | 3069 | ``` 3070 | 3071 | **[↑ 回到顶部](#目录)** 3072 | 3073 | ## 错误处理 3074 | 3075 | 抛出错误是件好事!它表示着运行时已经成功识别出程序中的错误,通过停止当前堆栈上的函数执行,终止进程(在Node.js),以及在控制台中打印堆栈信息来让你知晓。 3076 | 3077 | ### 抛出`Error`或 使用`reject` 3078 | 3079 | JavaScript 和 TypeScript 允许你 `throw` 任何对象。Promise 也可以用任何理由对象拒绝。 3080 | 3081 | 建议使用 `Error` 类型的 `throw` 语法。因为你的错误可能在写有 `catch`语法的高级代码中被捕获。在那里捕获字符串消息显得非常混乱,并且会使[调试更加痛苦](https://basarat.gitbooks.io/typescript/docs/types/exceptions.html#always-use-error)。出于同样的原因,也应该在拒绝 promise 时使用 `Error `类型。 3082 | 3083 | **:-1: 反例:** 3084 | 3085 | ```ts 3086 | 3087 | function calculateTotal(items: Item[]): number { 3088 | 3089 | throw 'Not implemented.'; 3090 | 3091 | } 3092 | 3093 | function get(): Promise { 3094 | 3095 | return Promise.reject('Not implemented.'); 3096 | 3097 | } 3098 | 3099 | ``` 3100 | 3101 | **:+1: 正例:** 3102 | 3103 | ```ts 3104 | 3105 | function calculateTotal(items: Item[]): number { 3106 | 3107 | throw new Error('Not implemented.'); 3108 | 3109 | } 3110 | 3111 | function get(): Promise { 3112 | 3113 | return Promise.reject(new Error('Not implemented.')); 3114 | 3115 | } 3116 | 3117 | // or equivalent to: 3118 | 3119 | async function get(): Promise { 3120 | 3121 | throw new Error('Not implemented.'); 3122 | 3123 | } 3124 | 3125 | ``` 3126 | 3127 | 使用 `Error` 类型的好处是 `try/catch/finally` 语法支持它,并且隐式地所有错误都具有 `stack` 属性,该属性对于调试非常有用。 3128 | 3129 | 另外,即使不用 `throw` 语法而是返回自定义错误对象,TypeScript在这块更容易。考虑下面的例子: 3130 | 3131 | ```ts 3132 | 3133 | type Failable = { 3134 | 3135 | isError: true; 3136 | 3137 | error: E; 3138 | 3139 | } | { 3140 | 3141 | isError: false; 3142 | 3143 | value: R; 3144 | 3145 | } 3146 | 3147 | function calculateTotal(items: Item[]): Failable { 3148 | 3149 | if (items.length === 0) { 3150 | 3151 | return { isError: true, error: 'empty' }; 3152 | 3153 | } 3154 | 3155 | // ... 3156 | 3157 | return { isError: false, value: 42 }; 3158 | 3159 | } 3160 | 3161 | ``` 3162 | 3163 | 详细解释请参考[原文](https://medium.com/@dhruvrajvanshi/making-exceptions-type-safe-in-typescript-c4d200ee78e9)。 3164 | 3165 | **[↑ 回到顶部](#目录)** 3166 | 3167 | ### 别忘了捕获错误 3168 | 3169 | 捕获错误而不处理实际上也是没有修复错误,将错误记录到控制台(console.log)也好不到哪里去,因为它常常丢失在控制台大量的日志之中。如果将代码写在`try/catch` 中,说明那里可能会发生错误,因此应该考虑在错误发生时做一些处理。 3170 | 3171 | **:-1: 反例:** 3172 | 3173 | ```ts 3174 | 3175 | try { 3176 | 3177 | functionThatMightThrow(); 3178 | 3179 | } catch (error) { 3180 | 3181 | console.log(error); 3182 | 3183 | } 3184 | 3185 | // or even worse 3186 | 3187 | try { 3188 | 3189 | functionThatMightThrow(); 3190 | 3191 | } catch (error) { 3192 | 3193 | // ignore error 3194 | 3195 | } 3196 | 3197 | ``` 3198 | 3199 | **:+1: 正例:** 3200 | 3201 | ```ts 3202 | 3203 | import { logger } from './logging' 3204 | 3205 | try { 3206 | 3207 | functionThatMightThrow(); 3208 | 3209 | } catch (error) { 3210 | 3211 | logger.log(error); 3212 | 3213 | } 3214 | 3215 | ``` 3216 | 3217 | **[↑ 回到顶部](#目录)** 3218 | 3219 | ### 不要忽略被拒绝的 promises 3220 | 3221 | 理由和不能在`try/catch`中忽略`Error`一样。 3222 | 3223 | **:-1: 反例:** 3224 | 3225 | ```ts 3226 | 3227 | getUser() 3228 | 3229 | .then((user: User) => { 3230 | 3231 | return sendEmail(user.email, 'Welcome!'); 3232 | 3233 | }) 3234 | 3235 | .catch((error) => { 3236 | 3237 | console.log(error); 3238 | 3239 | }); 3240 | 3241 | ``` 3242 | 3243 | **:+1: 正例:** 3244 | 3245 | ```ts 3246 | 3247 | import { logger } from './logging' 3248 | 3249 | getUser() 3250 | 3251 | .then((user: User) => { 3252 | 3253 | return sendEmail(user.email, 'Welcome!'); 3254 | 3255 | }) 3256 | 3257 | .catch((error) => { 3258 | 3259 | logger.log(error); 3260 | 3261 | }); 3262 | 3263 | // or using the async/await syntax: 3264 | 3265 | try { 3266 | 3267 | const user = await getUser(); 3268 | 3269 | await sendEmail(user.email, 'Welcome!'); 3270 | 3271 | } catch (error) { 3272 | 3273 | logger.log(error); 3274 | 3275 | } 3276 | 3277 | ``` 3278 | 3279 | **[↑ 回到顶部](#目录)** 3280 | 3281 | ## 格式化 3282 | 3283 | 就像这里的许多规则一样,没有什么是硬性规定,格式化也是。重点是**不要争论**格式,使用自动化工具实现格式化。对于工程师来说,争论格式就是浪费时间和金钱。通用的原则是*保持一致的格式规则*。 3284 | 3285 | 对于 TypeScript ,有一个强大的工具叫做 TSLint。它是一个静态分析工具,可以帮助您显著提高代码的可读性和可维护性。项目中使用可以参考以下 TSLint 配置: 3286 | 3287 | * [TSLint Config Standard](https://www.npmjs.com/package/tslint-config-standard) - 标准格式规则 3288 | 3289 | * [TSLint Config Airbnb](https://www.npmjs.com/package/tslint-config-airbnb) - Airbnb 格式规则 3290 | 3291 | * [TSLint Clean Code](https://www.npmjs.com/package/tslint-clean-code) - 灵感来自于[Clean Code: A Handbook of Agile Software Craftsmanship](https://www.amazon.ca/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882) 的 TSLint 规则。 3292 | 3293 | * [TSLint react](https://www.npmjs.com/package/tslint-react) - React 相关的Lint规则 3294 | 3295 | * [TSLint + Prettier](https://www.npmjs.com/package/tslint-config-prettier) - [Prettier](https://github.com/prettier/prettier) 代码格式化相关的 lint 规则 3296 | 3297 | * [ESLint rules for TSLint](https://www.npmjs.com/package/tslint-eslint-rules) - TypeScript 的 ESLint 3298 | 3299 | * [Immutable](https://www.npmjs.com/package/tslint-immutable) - 在 TypeScript 中禁用 mutation 的规则 3300 | 3301 | 还可以参考[TypeScript 风格指南和编码约定](https://basarat.gitbooks.io/typescript/docs/styleguide/styleguide.html)的源代码。 3302 | 3303 | ### 大小写一致 3304 | 3305 | 大写可以告诉你很多关于变量、函数等的信息。这些都是主观规则,由你的团队做选择。关键是无论怎么选,都要*一致*。 3306 | 3307 | **:-1: 反例:** 3308 | 3309 | ```ts 3310 | 3311 | const DAYS_IN_WEEK = 7; 3312 | 3313 | const daysInMonth = 30; 3314 | 3315 | const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude']; 3316 | 3317 | const Artists = ['ACDC', 'Led Zeppelin', 'The Beatles']; 3318 | 3319 | function eraseDatabase() {} 3320 | 3321 | function restore_database() {} 3322 | 3323 | class animal {} 3324 | 3325 | class Container {} 3326 | 3327 | ``` 3328 | 3329 | **:+1: 正例:** 3330 | 3331 | ```ts 3332 | 3333 | const DAYS_IN_WEEK = 7; 3334 | 3335 | const DAYS_IN_MONTH = 30; 3336 | 3337 | const SONGS = ['Back In Black', 'Stairway to Heaven', 'Hey Jude']; 3338 | 3339 | const ARTISTS = ['ACDC', 'Led Zeppelin', 'The Beatles']; 3340 | 3341 | function eraseDatabase() {} 3342 | 3343 | function restoreDatabase() {} 3344 | 3345 | class Animal {} 3346 | 3347 | class Container {} 3348 | 3349 | ``` 3350 | 3351 | 类名、接口名、类型名和命名空间名最好使用“帕斯卡命名”。 3352 | 3353 | 变量、函数和类成员使用“驼峰式命名”。 3354 | 3355 | **[↑ 回到顶部](#目录)** 3356 | 3357 | ### 调用函数的函数和被调函数应靠近放置 3358 | 3359 | 当函数间存在相互调用的情况时,应将两者靠近放置。最好是应将调用者写在被调者的上方。这就像读报纸一样,我们都是从上往下读,那么读代码也是。 3360 | 3361 | **:-1: 反例:** 3362 | 3363 | ```ts 3364 | 3365 | class PerformanceReview { 3366 | 3367 | constructor(private readonly employee: Employee) { 3368 | 3369 | } 3370 | 3371 | private lookupPeers() { 3372 | 3373 | return db.lookup(this.employee.id, 'peers'); 3374 | 3375 | } 3376 | 3377 | private lookupManager() { 3378 | 3379 | return db.lookup(this.employee, 'manager'); 3380 | 3381 | } 3382 | 3383 | private getPeerReviews() { 3384 | 3385 | const peers = this.lookupPeers(); 3386 | 3387 | // ... 3388 | 3389 | } 3390 | 3391 | review() { 3392 | 3393 | this.getPeerReviews(); 3394 | 3395 | this.getManagerReview(); 3396 | 3397 | this.getSelfReview(); 3398 | 3399 | // ... 3400 | 3401 | } 3402 | 3403 | private getManagerReview() { 3404 | 3405 | const manager = this.lookupManager(); 3406 | 3407 | } 3408 | 3409 | private getSelfReview() { 3410 | 3411 | // ... 3412 | 3413 | } 3414 | 3415 | } 3416 | 3417 | const review = new PerformanceReview(employee); 3418 | 3419 | review.review(); 3420 | 3421 | ``` 3422 | 3423 | **:+1: 正例:** 3424 | 3425 | ```ts 3426 | 3427 | class PerformanceReview { 3428 | 3429 | constructor(private readonly employee: Employee) { 3430 | 3431 | } 3432 | 3433 | review() { 3434 | 3435 | this.getPeerReviews(); 3436 | 3437 | this.getManagerReview(); 3438 | 3439 | this.getSelfReview(); 3440 | 3441 | // ... 3442 | 3443 | } 3444 | 3445 | private getPeerReviews() { 3446 | 3447 | const peers = this.lookupPeers(); 3448 | 3449 | // ... 3450 | 3451 | } 3452 | 3453 | private lookupPeers() { 3454 | 3455 | return db.lookup(this.employee.id, 'peers'); 3456 | 3457 | } 3458 | 3459 | private getManagerReview() { 3460 | 3461 | const manager = this.lookupManager(); 3462 | 3463 | } 3464 | 3465 | private lookupManager() { 3466 | 3467 | return db.lookup(this.employee, 'manager'); 3468 | 3469 | } 3470 | 3471 | private getSelfReview() { 3472 | 3473 | // ... 3474 | 3475 | } 3476 | 3477 | } 3478 | 3479 | const review = new PerformanceReview(employee); 3480 | 3481 | review.review(); 3482 | 3483 | ``` 3484 | 3485 | **[↑ 回到顶部](#目录)** 3486 | 3487 | ### 组织导入 3488 | 3489 | 使用整洁且易于阅读的`import`语句,您可以快速查看当前代码的依赖关系。导入语句应遵循以下做法: 3490 | 3491 | - `Import`语句应该按字母顺序排列和分组。 3492 | - 应该删除未使用的导入语句。 3493 | - 命名导入必须按字母顺序(例如:`import {A, B, C} from 'foo';`)。 3494 | - 导入源必须在组中按字母顺序排列。 例如: `import * as foo from 'a'; import * as bar from 'b';` 3495 | - 导入组用空行隔开。 3496 | - 组内按照如下排序: 3497 | - Polyfills (例如: `import 'reflect-metadata';`) 3498 | - Node 内置模块 (例如: `import fs from 'fs';`) 3499 | - 外部模块 (例如: `import { query } from 'itiriri';`) 3500 | - 内部模块 (例如: `import { UserService } from 'src/services/userService';`) 3501 | - 父目录中的模块 (例如: `import foo from '../foo'; import qux from '../../foo/qux';`) 3502 | - 来自相同或兄弟目录的模块 (例如: `import bar from './bar'; import baz from './bar/baz';`) 3503 | 3504 | **:-1: 反例:** 3505 | 3506 | ```ts 3507 | import { TypeDefinition } from '../types/typeDefinition'; 3508 | import { AttributeTypes } from '../model/attribute'; 3509 | import { ApiCredentials, Adapters } from './common/api/authorization'; 3510 | import fs from 'fs'; 3511 | import { ConfigPlugin } from './plugins/config/configPlugin'; 3512 | import { BindingScopeEnum, Container } from 'inversify'; 3513 | import 'reflect-metadata'; 3514 | ``` 3515 | 3516 | **:+1: 正例:** 3517 | 3518 | ```ts 3519 | import 'reflect-metadata'; 3520 | 3521 | import fs from 'fs'; 3522 | import { BindingScopeEnum, Container } from 'inversify'; 3523 | 3524 | import { AttributeTypes } from '../model/attribute'; 3525 | import { TypeDefinition } from '../types/typeDefinition'; 3526 | 3527 | import { ApiCredentials, Adapters } from './common/api/authorization'; 3528 | import { ConfigPlugin } from './plugins/config/configPlugin'; 3529 | ``` 3530 | 3531 | **[↑ 回到顶部](#目录)** 3532 | 3533 | ### 使用 typescript 别名 3534 | 3535 | 为了创建整洁漂亮的导入语句,可以在`tsconfig.json`中设置编译器选项的`paths`和`baseUrl`属性。 3536 | 3537 | 这样可以避免导入时使用较长的相对路径。 3538 | 3539 | **:-1: 反例:** 3540 | 3541 | ```ts 3542 | import { UserService } from '../../../services/UserService'; 3543 | ``` 3544 | 3545 | **:+1: 正例:** 3546 | 3547 | ```ts 3548 | import { UserService } from '@services/UserService'; 3549 | ``` 3550 | 3551 | ```js 3552 | // tsconfig.json 3553 | ... 3554 | "compilerOptions": { 3555 | ... 3556 | "baseUrl": "src", 3557 | "paths": { 3558 | "@services": ["services/*"] 3559 | } 3560 | ... 3561 | } 3562 | ... 3563 | ``` 3564 | 3565 | **[↑ 回到顶部](#目录)** 3566 | 3567 | 3568 | ## 注释 3569 | 3570 | 写注释意味着没有注释就无法表达清楚,而最好用代码去表达。 3571 | 3572 | > 不要注释坏代码,重写吧!— *Brian W. Kernighan and P. J. Plaugher* 3573 | 3574 | ### 代码自解释而不是用注释 3575 | 3576 | 代码即文档。 3577 | 3578 | **:-1: 反例:** 3579 | 3580 | ```ts 3581 | 3582 | // Check if subscription is active. 3583 | 3584 | if (subscription.endDate > Date.now) { } 3585 | 3586 | ``` 3587 | 3588 | **:+1: 正例:** 3589 | 3590 | ```ts 3591 | 3592 | const isSubscriptionActive = subscription.endDate > Date.now; 3593 | 3594 | if (isSubscriptionActive) { /* ... */ } 3595 | 3596 | ``` 3597 | 3598 | **[↑ 回到顶部](#目录)** 3599 | 3600 | ### 不要将注释掉的代码留在代码库中 3601 | 3602 | 版本控制存在的一个理由,就是让旧代码成为历史。 3603 | 3604 | **:-1: 反例:** 3605 | 3606 | ```ts 3607 | 3608 | class User { 3609 | 3610 | name: string; 3611 | 3612 | email: string; 3613 | 3614 | // age: number; 3615 | 3616 | // jobPosition: string; 3617 | 3618 | } 3619 | 3620 | ``` 3621 | 3622 | **:+1: 正例:** 3623 | 3624 | ```ts 3625 | 3626 | class User { 3627 | 3628 | name: string; 3629 | 3630 | email: string; 3631 | 3632 | } 3633 | 3634 | ``` 3635 | 3636 | **[↑ 回到顶部](#目录)** 3637 | 3638 | ### 不要像写日记一样写注释 3639 | 3640 | 记住,使用版本控制!不需要保留无用代码、注释掉的代码,尤其像日记一样的注释。使用`git log`来获取历史。 3641 | 3642 | **:-1: 反例:** 3643 | 3644 | ```ts 3645 | 3646 | /** 3647 | 3648 | * 2016-12-20: Removed monads, didn't understand them (RM) 3649 | 3650 | * 2016-10-01: Improved using special monads (JP) 3651 | 3652 | * 2016-02-03: Added type-checking (LI) 3653 | 3654 | * 2015-03-14: Implemented combine (JR) 3655 | 3656 | */ 3657 | 3658 | function combine(a:number, b:number): number { 3659 | 3660 | return a + b; 3661 | 3662 | } 3663 | 3664 | ``` 3665 | 3666 | **:+1: 正例:** 3667 | 3668 | ```ts 3669 | 3670 | function combine(a:number, b:number): number { 3671 | 3672 | return a + b; 3673 | 3674 | } 3675 | 3676 | ``` 3677 | 3678 | **[↑ 回到顶部](#目录)** 3679 | 3680 | ### 避免使用注释标记位置 3681 | 3682 | 它们常常扰乱代码。要让代码结构化,函数和变量要有合适的缩进和格式。 3683 | 3684 | 另外,你可以使用支持代码折叠的IDE (看下 Visual Studio Code [代码折叠](https://code.visualstudio.com/updates/v1_17#_folding-regions)). 3685 | 3686 | **:-1: 反例:** 3687 | 3688 | ```ts 3689 | 3690 | //////////////////////////////////////////////////////////////////////////////// 3691 | 3692 | // Client class 3693 | 3694 | //////////////////////////////////////////////////////////////////////////////// 3695 | 3696 | class Client { 3697 | 3698 | id: number; 3699 | 3700 | name: string; 3701 | 3702 | address: Address; 3703 | 3704 | contact: Contact; 3705 | 3706 | //////////////////////////////////////////////////////////////////////////////// 3707 | 3708 | // public methods 3709 | 3710 | //////////////////////////////////////////////////////////////////////////////// 3711 | 3712 | public describe(): string { 3713 | 3714 | // ... 3715 | 3716 | } 3717 | 3718 | //////////////////////////////////////////////////////////////////////////////// 3719 | 3720 | // private methods 3721 | 3722 | //////////////////////////////////////////////////////////////////////////////// 3723 | 3724 | private describeAddress(): string { 3725 | 3726 | // ... 3727 | 3728 | } 3729 | 3730 | private describeContact(): string { 3731 | 3732 | // ... 3733 | 3734 | } 3735 | 3736 | }; 3737 | 3738 | ``` 3739 | 3740 | **:+1: 正例:** 3741 | 3742 | ```ts 3743 | 3744 | class Client { 3745 | 3746 | id: number; 3747 | 3748 | name: string; 3749 | 3750 | address: Address; 3751 | 3752 | contact: Contact; 3753 | 3754 | public describe(): string { 3755 | 3756 | // ... 3757 | 3758 | } 3759 | 3760 | private describeAddress(): string { 3761 | 3762 | // ... 3763 | 3764 | } 3765 | 3766 | private describeContact(): string { 3767 | 3768 | // ... 3769 | 3770 | } 3771 | 3772 | }; 3773 | 3774 | ``` 3775 | 3776 | 3777 | **[↑ 回到顶部](#目录)** 3778 | 3779 | ### TODO 注释 3780 | 3781 | 当发现自己需要在代码中留下注释,以提醒后续改进时,使用`// TODO`注释。大多数IDE都对这类注释提供了特殊的支持,你可以快速浏览整个`TODO`列表。 3782 | 3783 | 但是,请记住**TODO**注释并不是坏代码的借口。 3784 | 3785 | **:-1: 反例:** 3786 | 3787 | ```ts 3788 | function getActiveSubscriptions(): Promise { 3789 | // ensure `dueDate` is indexed. 3790 | return db.subscriptions.find({ dueDate: { $lte: new Date() } }); 3791 | } 3792 | ``` 3793 | 3794 | **:+1: 正例:** 3795 | 3796 | ```ts 3797 | function getActiveSubscriptions(): Promise { 3798 | // TODO: ensure `dueDate` is indexed. 3799 | return db.subscriptions.find({ dueDate: { $lte: new Date() } }); 3800 | } 3801 | ``` 3802 | 3803 | **[↑ 回到顶部](#目录)** 3804 | --------------------------------------------------------------------------------