├── .gitignore ├── CNAME ├── Gemfile ├── README.md ├── _config.yml ├── _includes ├── HEADER_LINKS.html └── SUMMARY.html ├── fis-conf.js ├── ideology └── web-fe-up.md ├── jest.config.js ├── package.json └── type ├── ts-and-js.md ├── ts-go-generics.dos.ts ├── ts-go-generics.md ├── zero_value_typescript.doc.ts └── zero_value_typescript.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | fe.nimo.run -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gem 'github-pages', group: :jekyll_plugins 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 前端理论与实践 2 | 3 | https://fe.nimo.run 4 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | title: 前端理论与实践 2 | remote_theme: 2type/gitbook 3 | -------------------------------------------------------------------------------- /_includes/HEADER_LINKS.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | nimo.run 4 | 5 | 6 | 7 | 哔哩哔哩 8 | 9 | 10 | 11 | 知乎 12 | 13 | -------------------------------------------------------------------------------- /_includes/SUMMARY.html: -------------------------------------------------------------------------------- 1 | 4 |

思考

5 | 8 |

类型系统

9 | 14 |
15 | 18 | -------------------------------------------------------------------------------- /fis-conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var fs = require('fs') 3 | function tsdocHTML(content) { 4 | return tsdoc(content) 5 | } 6 | function tsdoc (content) { 7 | let md = content.trim().replace(/\;\`([\s\S]*?)^\`/gm, function(source, $1){ 8 | return '```\r\n' + $1.replace(/\\\`/g,'`') + '```ts' 9 | }) 10 | md = md.trim().replace(/^\`\`\`/,'') 11 | md +="\r\n```" 12 | md = md.trim() 13 | md = md.replace(/"\.\.\/lib\"/g,"\"percent-demo\"") 14 | 15 | /* 16 | 支持语法: 17 | // @tsrun:hidden begin 18 | 19 | test('part 0 total 10 return 0', () = { 20 | expect(percent(0,10)).toBe(0)> 21 | }) 22 | 23 | // @tsrun:hidden end 24 | */ 25 | md = md.replace(/\/\/ \@tsrun:hidden begin[\s\S]*\/\/ \@tsrun:hidden end/g, '') 26 | 27 | md = md.replace(new RegExp("// \@ts-ignore.*", "g"), "") 28 | md = md.replace(/\`\`\`ts([\s\S]*)\`\`\`/gm, function (source, $1) { 29 | return '```ts\r\n' + $1.trim() + '\r\n```' 30 | }) 31 | md = md.replace(/```ts;/, "```ts") 32 | md = md.replace(/\^\^\^/g, "```") 33 | md = md.replace(/\^/g, "`") 34 | md = md.trim() 35 | return md 36 | } 37 | 38 | fis.match("**", { 39 | release: false, 40 | },1) 41 | fis.match("(**).source.md", { 42 | parser: [ 43 | function (content, file) { 44 | return content.replace(/\[(.*?)\|embed\]\((.*)\)/g, function (source, name, ref) { 45 | const extname = path.extname(ref).replace(".", "") 46 | const code = fs.readFileSync(path.join(__dirname,file.subdirname, ref)).toString() 47 | return `[${name}](${ref})` + 48 | '\r\n' + 49 | '```' + extname + 50 | '\r\n' + 51 | code + 52 | '\r\n' + 53 | '```' 54 | }) 55 | } 56 | ], 57 | release: "/$1.md", 58 | }, 999) 59 | 60 | fis.match('(**).doc.ts', { 61 | parser: [ 62 | function (content, file) { 63 | return tsdoc(content) 64 | } 65 | ], 66 | release: "/$1.md", 67 | isHtmlLike: true, 68 | rExt: "md", 69 | }, 999) 70 | -------------------------------------------------------------------------------- /ideology/web-fe-up.md: -------------------------------------------------------------------------------- 1 | # Web 前端的困局与突破 2 | 3 | > 每个Web前端都会不时思考自身对于团队的价值和在团队中的话语权,这类思考背后存在困局和突破 4 | 5 | ## 困局 6 | 7 | 困局必然是负能量,耐下心来了解困局后再看突破. 8 | 9 | ### 价值 10 | 11 | 在团队中前端的职责是实现原型和设计工作中的客户端部分,是**实现者**.很少参与功能/程序设计.极少数业务场景下是重前端的.需要前端实现可视化编辑组件(可视化创建页面/Web编辑器).这样的团队少之又少.大部分情况下前端的职责就只是实现产品设计. 12 | 13 | 前端巧妙的程序设计,优秀的页面大部分情况下与商业逻辑是无关的.技术是依附于商业生存的.在客观的管理者视角看,前端是有价值的,但在技术团队中前端的价值不重要.只要把活干好了,别出错就可以了.也不需要维护管理公司命脉数据库. 14 | 15 | > 价值的意义:收入,市场竞争力都是基于价值的 16 | 17 | 18 | 19 | ### 话语权 20 | 21 | 从项目管理的角度看: 22 | 23 | | 岗位 | 职责 | 分工 | 24 | | -------- | ------------------------------------ | ------------------ | 25 | | 产品经理 | 基于市场需求表达产品界面和功能逻辑 | 需求与业务逻辑设计 | 26 | | UI设计 | 基于产品表达的界面进行界面设计 | 基于原型UI交互设计 | 27 | | 前端 | 基于产品原型与UI设计完成客户端的实现 | 实现客户端 | 28 | | 后端 | 数据结构与程序的设计和实现 | 程序设计与实现 | 29 | 30 | 31 | 32 | 产品同事对项目有绝对的话语权,设计同事是产品同事的下游,是基于产品同事的要求完成界面设计工作. 33 | 34 | 后端同事看似也是实现者,实则是**后端是程序设计者** 35 | 36 | 当前端遇到上下游意见相左时,因为前端的分工是实现者,所以往往前端是没有话语权的. 37 | 38 | 有些人常说的用户体验,实际上这个活是UI设计干的活,前端只需要做到快速的基于设计完成客户端开发.并保证客户端的加载性能. 39 | 40 | 高流量多信息聚合的页面需要基于前端的专业意见去协作开发,不过这种情况太少了 41 | 42 | > 话语权的意义: 决定项目中与前端相关的问题能用你的专业想法解决,而不是由"外行"解决. 43 | 44 | 45 | 46 | ### 恶劣的的工作环境 47 | 48 | 1. 没有产品原型 49 | 2. 没有设计稿 50 | 3. 没有后端接口文档 51 | 4. 联调阶段扯皮 52 | 53 | 以上问题在很多小团队出现的特别频繁,严重影响了工作进度. 54 | 55 | 前端是实现者,要基于设计去实现,基于实现去"消费"接口. 56 | 57 | 3 4 问题是出现的最频繁的,扯皮无止境,不停的内耗. 58 | 59 | 60 | 61 | ### 天花板 62 | 63 | 随着三大剑客(react vue ng)的流行,基于成熟的组件库能极大的提高前端开发效率.大部分团队不需要再造适合公司业务场景的组件轮子.从业两三年就可以达到日常90%的页面开发工作是"**机械化**"的.大量的时间反而是解决上节说到的恶劣的工作环境 64 | 65 | 前端工程管理,前端开发流程的制定,前端架构设计这些都是由前端话事人去设计和执行的.在技能水平+经验+对团队的了解程度+话语权上都应该由前端话事人去解决.大部分前端只需要遵循团队流程规范即可. 66 | 67 | 68 | 69 | ### JavaScript与Node 70 | 71 | > JavaScript 是困局这一点是很多人没有意识到,也不认同的. 72 | 73 | JavaScript 过于灵活宽松让代码不易于维护.动态语言在大型项目会降低可维护性. TypeScript 的类型系统需要对JavaScript 妥协.始终让前端在语言层面无法使用类型系统方便的写出分层明确和易于维护的代码. 74 | 75 | 这点不深入讨论,了解过其他强类型系统语言再来使用 TypeScript 写非常严谨容错率低的代码自然就能明白. 76 | 77 | 78 | 很多团队希望使用 Node 去提升自己的话语权,殊不知必须 Node 与公司所遇到的场景和问题契合才能解决问题. 79 | 80 | 不要把自己限死在 JavaScript ,前端掌握当前团队的后端语言不是啥坏事.不是每个项目都需要要SSR.都需要使用Node进行前后端分离. 81 | 82 | **容我吐槽一番** 83 | 84 | Web前端难以100%利用机器,这里的"利用"指的并不是编写高性能代码.而是无法控制代码运行环境. 85 | 86 | App前端相对于Web前端,能利用的机器资源更多一点.可以在App开发大型游戏.这能扩展程序员的边界. 87 | 88 | 后端相对于Web前端,对机器的利用能达到主宰的程度,语言不顺手直接换个编程语言.数据库不适合目前业务场景直接换一种类型的数据库.某个库或服务器有Bug直接升级解决. 89 | 90 | 而 Web 前端很容易搞来搞去都在 JavaScript 的世界,要内卷到熟读ECMA 规范.在运行环境层面要被小程序动不动的运营规则改版折磨的发版前一晚改代码, 91 | 92 | ## 突破 93 | 94 | 困局有: 95 | 96 | 1. 价值 97 | 2. 话语权 98 | 3. 恶劣的工作环境 99 | 4. 天花板 100 | 5. JavaScript 101 | 102 | 103 | 104 | 一句话就能说明白如何突破: 105 | 106 | **不要把自己限制在客户端的实现者,让自己参与产品程序设计,精前端,懂业务,懂后端.** 107 | 108 | ### 恶劣的工作环境 109 | 110 | 当**没有产品原型和设计**的情况下,正确更多的时间,去了解产品需求,使用前端界面作为"原型".完成部分产品没时间或者认为不重要的产品设计工作.没有设计的情况下基于成熟的UI组件,antd element 完成表单类的开发,一些交互设计自己做主设计交互细节并实现.没有设计的情况下大概率你能做主. 111 | 112 | 当后端**给不到接口**时,在前端角度维护接口文档,通过接口更深入的理解程序设计.有理有据的要求后端同时配合你给到接口让你使用,因为客户端是接口消费方.如果后端给不到接口就自行模拟接口,再联调的时候再修改也无妨,这样单前端定义接口的过程就是在进行程序设计.基于客观接口进行联调,避免主观争论. 113 | 114 | 善于**使用错误追踪系统**记录错误,例如 Sentry . 有理有据的去排查问题,定位问题,给出问题修复报告.防止莫名其妙背锅. 115 | 116 | > 愿你不遇到恶劣的工作环境,遇到相互配合的好同事 117 | 118 | 119 | 120 | ### 天花板与JavaScript 121 | 122 | 在前端角度不要局限于 Web 前端,在吃透当前工作环境时去了解其他客户端技术,比如各类小程序, IOS Android. 123 | 124 | Swift Kotlin Java 都是不错的语言,了解他们后再回头写 TypeScript 会对编程有新的认识和理解. 125 | 126 | 不要局限于前端,在项目中了解目前团队的后端语言和后端知识.参与后端提供的接口中数据类型的定义,(类似 TypeScript 中的 interface).了解后端语言(PHP就算了),对项目后端的程序设计有足够了解.必要时尝试参与后端开发,先完成一些简单不重要的后端工作. 127 | 128 | 大部分情况下足够努力的前端做个三五年很容易在小团队做到天花板.不考虑换工作的情况下,去向全栈发展是个很好的选择.只有掌握了程序设计才能掌握话语权. 129 | 130 | 现在很多人错误的理解了全栈,认为会 node 写个编译工具就是全栈,不要局限于掌握的后端语言是 Node . 使用 Node 能免去学习一门新语言的好处没有那么重要,反而会将你局限在 JavaScript. 131 | 132 | **了解一门强类型语言,了解各种后端技术.了解其他前端领域.** 133 | 134 | 135 | 136 | ### 价值与话语权 137 | 138 | 吃透目前工作环境所需要使用的前端技术后,由**实现者变为程序设计者**不要将自己局限于"页面仔".去理解了解业务才能提升自身价值. 139 | 140 | ## 总结 141 | 142 | 始于前端,不局限于前端.**不要把自己限制在客户端的实现者,让自己参与产品程序设计,精前端,懂业务,懂后端. 143 | 144 | 在Github发表评论: https://github.com/nimoc/fe/discussions/40 145 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.tsx?$': 'ts-jest', 4 | }, 5 | testRegex: '(.*\.(test|spec|doc))\\.(jsx?|tsx?)$', 6 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "fis3 release -w -d ./" 4 | }, 5 | "hexo": { 6 | "version": "5.4.0" 7 | }, 8 | "devDependencies": { 9 | "@types/jest": "^24.0.18", 10 | "jest": "^24.9.0", 11 | "now": "^21.0.1", 12 | "ts-jest": "^24.0.2", 13 | "typescript": "^3.6.2" 14 | }, 15 | "dependencies": { 16 | "markrun": "^0.23.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /type/ts-and-js.md: -------------------------------------------------------------------------------- 1 | # 客观看待 TypeScript 与 JavaScript 2 | 3 | ## TypeScript 只是个类型注解/静态分析 4 | 5 | TypeScript 赋予了 JavaScript 静态类型的能力但是由于 JavaScript 的语法向前兼容的历史包袱和 TypeScript 不想限制 JavaScript 本身的灵活性.导致 TypeScript 只是个类型注解,并不是 强类型语言.又因为浏览器运行环境导致在运行时还是以 JavaScript 运行,会出现命名定义了类型是 number 结果运行时候还是可能会是 undefined 6 | 7 | ## 使用 TypeScript 需要转换思维 8 | 9 | 母语是 JavaScript 的开发人员去学习 TypeScript 比学习一门新的静态类型语言还要难.因为始终会以 JavaScript 动态灵活的思维去写 TypeScript 代码,而 TypeScript 为了满足 JavaScript 的灵活性,有大量的高级类型语法.有些已经高级到复杂的程度.并且因为复杂度高经常出现新手看不懂的 TypeScript 编译报错. 10 | TypeScript 很优秀.但是历史包袱和依附 JavaScript 本身会导致 TypeScript 很难写. 思维逻辑要转换,要 "自废武功" 去将一些以前 JavaScript 写起来非常简单的代码,用 TypeScript 使用很"啰嗦的"语法去实现.这种"啰嗦"真是静态语言严谨的代码风格. 所以使用 TypeScript 需要转换思维.最好学习一门跟 JavaScript 很像的强静态类型语言,比如我推荐 golang. 11 | 12 | TypeScript 历史包袱和依附 JavaScript 本身会导致 TypeScript 很难写. 思维逻辑要转换,要 "自废武功" 去将一些以前 JavaScript 写起来非常简单的代码,用 TypeScript 使用很"啰嗦的"语法去实现.这种"啰嗦"真是静态语言严谨的代码风格. 所以使用 TypeScript 需要转换思维.最好学习一门出生就支持类型系统的语言,比如 java/golang/swift. 13 | 14 | 15 | ## node 中一定要使用 TypeScript 16 | 17 | 在 node 中一定要使用 TypeScript ,因为后端的各个函数各个数据之间的调用是关联性很强的.相比前端而言,我认为后端是要解决整个面的复杂度,前端是要解决单个点的复杂度,点与点之间的复杂度不是特别高.难度不分高低,根据场景决定. 18 | 19 | ## 在前端代码中不一定要使用 TypeScript 20 | 21 | **在前端代码中不一定非要使用 TypeScript**, 在开发组件和开发核心高复用的模块时一定要用 TypeScript ,因为要确保稳定型和可维护性.但有些页面逻辑代码,将响应数据转换为渲染数据,并根据事件修改渲染数据的这些简单繁琐的逻辑是可以不用 TypeScript 的因为有时候时间不等人,项目 dealline 会逼得你 any 满天飞. 22 | 23 | ## 面对现实做出明确定义 24 | 25 | 我们要实事求是,如果你时间允许的情况下应该全部 TypeScript 加上类型,如果在页面琐碎的ui逻辑中时间来不及,是允许使用 any 的.因为现实会让你还是写 any.不如我们**面对现实做出明确定义**,而不是一刀切的不允许用 any 或者一刀切不用 TypeScript. 26 | 27 | ## 不要让编程语言限制住了自己 28 | 29 | JavaScript 的类型系统并不只是只有 TypeScript ,但在前端领域目前类型系统只能用 TypeScript,因为其他类型系统的前端生态不成熟. 30 | 31 | 在后端领域,如果团队有足够的精力并且明显感觉到了本文说到的js历史包袱和ts后端生态参差不齐,可以考虑使用 [rescript](https://rescript-lang.org.cn/) 开发一套完整的后端工具链. rescript的类型系统更加严格,可减少类型体操的出现,减少类型代码复杂度. 32 | 33 | > 后端也不必非要限制在 Node, 了解了解其他母胎就自带强类型系统的语言也是可以的. 34 | 35 | 在Github发表评论: https://github.com/nimoc/fe/discussions/55 36 | -------------------------------------------------------------------------------- /type/ts-go-generics.dos.ts: -------------------------------------------------------------------------------- 1 | ;` 2 | # 将 TypeScript 中 松散的类型当做药品 3 | 4 | 5 | > 本文代码有大量的 test 和 expect 函数,目的是替代注释,用 expect 说明变量和函数的返回值 6 | 7 | 8 | ## 动态语言不需要泛型 9 | 10 | 基于显而易见的原因如果你使用的是动态语言没有类型系统意味着一切都是泛型. 11 | 12 | 我通过列举一个 ^filterZeroValue^ 的例子来说明情况: 13 | 14 | > 为了把重点放在类型系统上所以使用 ^filterZeroValue^ 这个简单的函数,实际情况中不大可能封装 ^filterZeroValue^ 而是直接写 ^list.filter^. 15 | 16 | 比如在 JavaScript 中: 17 | 18 | ` 19 | /* 20 | * 排除数组中的空值' 21 | * @param list 22 | * @return notZeroValueList 23 | * */ 24 | function jsFilterZeroValue(list) { 25 | return list.filter(function (item) { 26 | switch (typeof item) { 27 | case "string": 28 | return item != "" 29 | break 30 | case "number": 31 | return item != 0 32 | break 33 | default: 34 | throw new Error("filterZeroValue: list[] item must be string or number" ) 35 | } 36 | }) 37 | } 38 | 39 | test("jsFilterZeroValue", function () { 40 | expect(jsFilterZeroValue(["a","","c"])).toStrictEqual(["a","c"]) 41 | expect(jsFilterZeroValue([1,0,3])).toStrictEqual([1,3]) 42 | }) 43 | 44 | 45 | ;` 46 | 47 | 你甚至可以3行代码搞定 48 | 49 | ^^^js 50 | function jsFilterZeroValue() { 51 | return list.filter((item)=> {return !!item}) 52 | } 53 | ^^^ 54 | 55 | 56 | 不这么做是因为需要在参数是 string number 之外的的类型时进行错误提示,和减少隐式类型转换. 57 | 58 | 59 | ## TypeScript 实现泛型 60 | 61 | > 注意不要只看下面的代码后就结束,看完文章会发现下面的代码是不好的 62 | 63 | ` 64 | 65 | function tsFilterZeroValue(list: T[]): T[] { 66 | return list.filter(function (item) { 67 | switch (typeof item) { 68 | case "string": 69 | return item != "" 70 | break 71 | case "number": 72 | return item != 0 73 | break 74 | default: 75 | throw new Error("filterZeroValue: list[] item must be string or number") 76 | } 77 | }) 78 | } 79 | 80 | 81 | test("tsFilterZeroValue", function () { 82 | expect(tsFilterZeroValue(["a","","c"])).toStrictEqual(["a","c"]) 83 | expect(tsFilterZeroValue([1,0,3])).toStrictEqual([1,3]) 84 | }) 85 | 86 | ;` 87 | 虽然通过 ^(list: T[]): T[]^ 约束了必须是个数组,并且输出的类型和输入的类型一致.但是还是不能明确只允许 ^number[]^ ^string[]^ . 88 | 89 | 90 | ### 联合类型 91 | 92 | 上面的列子可以用联合类型来解决,但是联合类型也不够好哦. 93 | 94 | > 联合类型和泛型其实是一类方法,在现在的这个场景的目的就是偷懒. 95 | 96 | ` 97 | 98 | function unionTypeFilterZeroValue(list: string[] | number[]) :string[] | number[] { 99 | let output = [] 100 | for (let i= 0;i 不要带入动态类型快猛糙的思维去写 TypeScript 161 | 162 | 该多写点"重复"的代码,这样反而实现会更简单,更易于阅读. 163 | 164 | 最重要的是有些情况下使用了泛型或联合类型加上编码时疏忽了会造成想不到的bug: 165 | 166 | `; 167 | 168 | function updateSQL(id: string, names: string[]) :{sql:string, values:any[]} { 169 | const updateValue = stringListFilterZeroValue(names) 170 | // 如果 updateSQL 的函数参数 names 改成了 ages int[] 171 | // stringListFilterZeroValue 将会在编译期报错 172 | // 如果使用的是 unionTypeFilterZeroValue 则不会 173 | 174 | // names 修改后 要让此处编译期报错的目的是要 175 | // 提醒自己,在没有修改前的代码逻辑中期望 updateValue 是一个 string[] 176 | // 如果使用 unionTypeFilterZeroValue 则没有了这一层提醒 177 | // 而 JSON.stringify(string[]) 和 JSON.stringify(number[]) 的结果是不一样的 178 | // 而这个不一样类型系统是无法检查到的,因为返回值 的 values 属性因为 sql 的场景导致就是 any[] 179 | return { 180 | sql: `UPDATE tableName SET names = ? WHERE id = ?`, 181 | values: [JSON.stringify(updateValue), id], 182 | } 183 | } 184 | 185 | 186 | 187 | test("updateSQL", function (){ 188 | expect(updateSQL("1", ["nimo", "nico"])).toStrictEqual( 189 | { 190 | sql: "UPDATE tableName SET names = ? WHERE id = ?", 191 | values: [ 192 | '["nimo","nico"]', 193 | "1", 194 | ], 195 | } 196 | ) 197 | }) 198 | 199 | 200 | ;` 201 | 202 | 上面的例子不够完美,本文想表达的主要的观点是: 203 | 204 | **控制参数数量和类型不可变** 205 | 206 | 在代码中明确函数参数固定且每个参数只能有一个类型能让代码更易于维护 207 | 208 | **尽可能多的在编译期做类型检查发现问题** 209 | 210 | 即使单元测试和细心编码能检查出这种小概率的错误,但是编码要做悲观设计.不能总期望写代码的人状态在线 211 | 212 | **将松散的类型当做药品使用** 213 | 214 | 泛型,联合类型这种应当当做药品去使用,不到万不得已不要使用.比如 Go 语言中就不支持 TypeScript 这种泛型,也照样构建了那么稳定的项目, 215 | 只要不是觉得业务代码中出现大量重复代码太麻烦,就要避免使用松散的类型.非业务逻辑的第三方封装代码,就必须让参数类型只能有一个. 216 | 除非你实现是 JSON.parse 这种必须用 any 的库. 217 | 218 | 219 | > 有些人对于效率和质量的认知可能与作者有偏差,作者是绝对侧重质量,在要效率非常低下的情况下才通过深思熟虑的才写一些"偷懒的代码". 220 | > 读者可以有自己的判断,但请注意: 如果因为类型不严谨导致项目中出现一个 bug,如果能后悔你会愿意花十倍的时间去弥补写出更多类型严谨的代码. 221 | 222 | 223 | 如果你觉得本文观点不错,请将本文推荐给你的朋友或同事 224 | 225 | 在Github发表评论: https://github.com/nimoc/fe/discussions/57 226 | 227 | `; 228 | -------------------------------------------------------------------------------- /type/ts-go-generics.md: -------------------------------------------------------------------------------- 1 | # 将 TypeScript 中 松散的类型当做药品 2 | 3 | 4 | > 本文代码有大量的 test 和 expect 函数,目的是替代注释,用 expect 说明变量和函数的返回值 5 | 6 | 7 | ## 动态语言不需要泛型 8 | 9 | 基于显而易见的原因如果你使用的是动态语言没有类型系统意味着一切都是泛型. 10 | 11 | 我通过列举一个 `filterZeroValue` 的例子来说明情况: 12 | 13 | > 为了把重点放在类型系统上所以使用 `filterZeroValue` 这个简单的函数,实际情况中不大可能封装 `filterZeroValue` 而是直接写 `list.filter`. 14 | 15 | 比如在 JavaScript 中: 16 | 17 | ```ts 18 | /* 19 | * 排除数组中的空值' 20 | * @param list 21 | * @return notZeroValueList 22 | * */ 23 | function jsFilterZeroValue(list) { 24 | return list.filter(function (item) { 25 | switch (typeof item) { 26 | case "string": 27 | return item != "" 28 | break 29 | case "number": 30 | return item != 0 31 | break 32 | default: 33 | throw new Error("filterZeroValue: list[] item must be string or number" ) 34 | } 35 | }) 36 | } 37 | 38 | test("jsFilterZeroValue", function () { 39 | expect(jsFilterZeroValue(["a","","c"])).toStrictEqual(["a","c"]) 40 | expect(jsFilterZeroValue([1,0,3])).toStrictEqual([1,3]) 41 | }) 42 | 43 | 44 | ``` 45 | 46 | 47 | 你甚至可以3行代码搞定 48 | 49 | ```js 50 | function jsFilterZeroValue() { 51 | return list.filter((item)=> {return !!item}) 52 | } 53 | ``` 54 | 55 | 56 | 不这么做是因为需要在参数是 string number 之外的的类型时进行错误提示,和减少隐式类型转换. 57 | 58 | 59 | ## TypeScript 实现泛型 60 | 61 | > 注意不要只看下面的代码后就结束,看完文章会发现下面的代码是不好的 62 | 63 | ```ts 64 | 65 | function tsFilterZeroValue(list: T[]): T[] { 66 | return list.filter(function (item) { 67 | switch (typeof item) { 68 | case "string": 69 | return item != "" 70 | break 71 | case "number": 72 | return item != 0 73 | break 74 | default: 75 | throw new Error("filterZeroValue: list[] item must be string or number") 76 | } 77 | }) 78 | } 79 | 80 | 81 | test("tsFilterZeroValue", function () { 82 | expect(tsFilterZeroValue(["a","","c"])).toStrictEqual(["a","c"]) 83 | expect(tsFilterZeroValue([1,0,3])).toStrictEqual([1,3]) 84 | }) 85 | 86 | ``` 87 | 88 | 虽然通过 `(list: T[]): T[]` 约束了必须是个数组,并且输出的类型和输入的类型一致.但是还是不能明确只允许 `number[]` `string[]` . 89 | 90 | 91 | ### 联合类型 92 | 93 | 上面的列子可以用联合类型来解决,但是联合类型也不够好哦. 94 | 95 | > 联合类型和泛型其实是一类方法,在现在的这个场景的目的就是偷懒. 96 | 97 | ```ts 98 | 99 | function unionTypeFilterZeroValue(list: string[] | number[]) :string[] | number[] { 100 | let output = [] 101 | for (let i= 0;i 不要带入动态类型快猛糙的思维去写 TypeScript 164 | 165 | 该多写点"重复"的代码,这样反而实现会更简单,更易于阅读. 166 | 167 | 最重要的是有些情况下使用了泛型或联合类型加上编码时疏忽了会造成想不到的bug: 168 | 169 | ```ts 170 | 171 | function updateSQL(id: string, names: string[]) :{sql:string, values:any[]} { 172 | const updateValue = stringListFilterZeroValue(names) 173 | // 如果 updateSQL 的函数参数 names 改成了 ages int[] 174 | // stringListFilterZeroValue 将会在编译期报错 175 | // 如果使用的是 unionTypeFilterZeroValue 则不会 176 | 177 | // names 修改后 要让此处编译期报错的目的是要 178 | // 提醒自己,在没有修改前的代码逻辑中期望 updateValue 是一个 string[] 179 | // 如果使用 unionTypeFilterZeroValue 则没有了这一层提醒 180 | // 而 JSON.stringify(string[]) 和 JSON.stringify(number[]) 的结果是不一样的 181 | // 而这个不一样类型系统是无法检查到的,因为返回值 的 values 属性因为 sql 的场景导致就是 any[] 182 | return { 183 | sql: `UPDATE tableName SET names = ? WHERE id = ?`, 184 | values: [JSON.stringify(updateValue), id], 185 | } 186 | } 187 | 188 | 189 | 190 | test("updateSQL", function (){ 191 | expect(updateSQL("1", ["nimo", "nico"])).toStrictEqual( 192 | { 193 | sql: "UPDATE tableName SET names = ? WHERE id = ?", 194 | values: [ 195 | '["nimo","nico"]', 196 | "1", 197 | ], 198 | } 199 | ) 200 | }) 201 | 202 | 203 | ``` 204 | 205 | 206 | 上面的例子不够完美,本文想表达的主要的观点是: 207 | 208 | **控制参数数量和类型不可变** 209 | 210 | 在代码中明确函数参数固定且每个参数只能有一个类型能让代码更易于维护 211 | 212 | **尽可能多的在编译期做类型检查发现问题** 213 | 214 | 即使单元测试和细心编码能检查出这种小概率的错误,但是编码要做悲观设计.不能总期望写代码的人状态在线 215 | 216 | **将松散的类型当做药品使用** 217 | 218 | 泛型,联合类型这种应当当做药品去使用,不到万不得已不要使用.比如 Go 语言中就不支持 TypeScript 这种泛型,也照样构建了那么稳定的项目, 219 | 只要不是觉得业务代码中出现大量重复代码太麻烦,就要避免使用松散的类型.非业务逻辑的第三方封装代码,就必须让参数类型只能有一个. 220 | 除非你实现是 JSON.parse 这种必须用 any 的库. 221 | 222 | 223 | > 有些人对于效率和质量的认知可能与作者有偏差,作者是绝对侧重质量,在要效率非常低下的情况下才通过深思熟虑的才写一些"偷懒的代码". 224 | > 读者可以有自己的判断,但请注意: 如果因为类型不严谨导致项目中出现一个 bug,如果能后悔你会愿意花十倍的时间去弥补写出更多类型严谨的代码. 225 | 226 | 227 | 如果你觉得本文观点不错,请将本文推荐给你的朋友或同事 228 | 229 | 230 | 在Github发表评论: https://github.com/nimoc/fe/discussions/57 231 | 232 | ```ts; 233 | ``` 234 | -------------------------------------------------------------------------------- /type/zero_value_typescript.doc.ts: -------------------------------------------------------------------------------- 1 | ;` 2 | # 利用空值设计让 TypeScript 更稳定和易于维护 3 | 4 | 5 | > 本文代码有大量的 test 和 expect 函数,目的是替代注释,用 expect 说明变量和函数的返回值 6 | 7 | ## 初始化缺省数据 8 | 9 | 举一个场景的例子, 前端ajax接收后端响应数据: 10 | ` 11 | 12 | interface iUser { 13 | name:string 14 | age:number 15 | } 16 | test("响应数据", function () { 17 | let responseJSON :string = `{"name": "nimo"}` 18 | let res:iUser = JSON.parse(responseJSON) 19 | expect(res.name).toBe("nimo") 20 | expect(res.age).toBe(undefined); 21 | // iUser 中 age 不是 age?:number 但是结果值是 undefined 22 | // 具体为什么会是 undefined,和为什么后端响应的数据没有 age 我们就不深入讨论了 23 | // 但是这种情况会导致出现一些bug,比如: 24 | expect(res.age + 1).toBe(NaN); 25 | // 明明使用了ts,结果居然将 number 加上 number 得到了 NaN 26 | // 因为此时 res.age 是 undefined 27 | }) 28 | 29 | ;` 30 | 31 | ## 数据接口新增属性 32 | 33 | 接着来看对象数据的场景: 34 | 35 | 最初定义了一个数据结构,只有 url 属性 36 | `; 37 | interface iPost { 38 | url:string 39 | } 40 | 41 | test("然后有很多地方使用了 iData", function () { 42 | let a :iPost = { 43 | url: "https://github.com/nimoc/blog/issues/33" 44 | } 45 | console.log(a) 46 | let b :iPost = { 47 | url: "https://github.com/nimoc" 48 | } 49 | console.log(b) 50 | }) 51 | 52 | ;` 53 | 如果此时 iPost 新增了属性 title,则会导致 a b 声明时编译期报错 54 | 55 | ^^^ts 56 | interface iPost { 57 | url:string 58 | title: string 59 | } 60 | 61 | test("然后有很多地方使用了 iData", function () { 62 | // TS2741: Property 'title' is missing in type '{ url: string; }' but required in type 'iPost'. 63 | let a :iPost = { 64 | url: "https://github.com/nimoc/blog/issues/33" 65 | } 66 | console.log(a) 67 | // TS2741: Property 'title' is missing in type '{ url: string; }' but required in type 'iPost'. 68 | let b :iPost = { 69 | url: "https://github.com/nimoc" 70 | } 71 | console.log(b) 72 | }) 73 | ^^^ 74 | 75 | 想要解决这个问题就需要在 a b 两处声明的地方加上 title 属性,如果不只是 a b 两次,而是由几十处就会变得非常麻烦. 76 | 77 | 虽然你可以认为类型系统就应该这样严格,但是在这个场景下我更希望不需要改几十处代码 78 | 79 | 如果不想改几十处可能会导致我们写出不好的代码,例如修改 iPost 为 80 | 81 | ^^^ts 82 | interface iPost { 83 | url:string 84 | title?: string 85 | } 86 | ^^^ 87 | 88 | 这种做法虽然不需要些几十处了,但是 [nimo](https://github.com/nimoc) 认为这种方式会引入不必要的 undefined . 89 | 导致明明用了类型系统,结果还要处理最繁琐的 undefined 问题. 90 | 91 | ## 空值设计 (zero values) 92 | 93 | 我们借鉴 [golang](http://golang.org/) 中zero values 的设计,来解决上述2个问题. 94 | 95 | 96 | 请看代码: 97 | ` 98 | 99 | interface iPerson { 100 | name:string 101 | age:number 102 | } 103 | interface iMakePerson { 104 | name?:string 105 | age?:number 106 | } 107 | 108 | function Person(v: iMakePerson) :iPerson { 109 | return { 110 | name: v.name || "", 111 | age: v.age || 0 112 | } 113 | } 114 | test("响应数据空值填充", function () { 115 | let response = Person(JSON.parse(`{"name":"nimo"}`)) 116 | // 不会出现 response.age 是 undefined 导致的 NaN 的情况 117 | expect(response.age + 1).toBe(1) 118 | }) 119 | 120 | test("多处使用 Person ", function () { 121 | let a = Person({ 122 | name: "nimo", 123 | }) 124 | expect(a).toStrictEqual({name:"nimo",age:0}) 125 | let b = Person({ 126 | age: 18, 127 | }); 128 | expect(b).toStrictEqual({name:"",age:18}) 129 | }) 130 | 131 | ;` 132 | 如果要新增属性则只需在 iPerson 和 iPerson中分别增加新属性 133 | 134 | 比如新增了 nikename 135 | 136 | ^^^ts 137 | interface iPerson { 138 | name:string 139 | age:number 140 | nikename:string 141 | } 142 | interface iMakePerson { 143 | name?:string 144 | age?:number 145 | nikename?:string 146 | } 147 | function Person(v: iMakePerson) :iPerson { 148 | return { 149 | name: v.name || "", 150 | age: v.age || 0, 151 | nikename: v.nikename || "", 152 | } 153 | } 154 | ^^^ 155 | 156 | 使用所有 Person不会报错,因为接口定义了 nikename?:string 157 | 158 | ^^^ts 159 | let a = Person({ 160 | name: "nimo", 161 | }) 162 | expect(a).toStrictEqual({name:"nimo",age:0,nikename:""}) 163 | ^^^ 164 | 165 | 166 | 如果新增了 gender ,并且要求 gender 是必填的那么可以这样修改 iPerson 167 | 168 | ^^^ts 169 | interface iPerson { 170 | name:string 171 | age:number 172 | nikename:string 173 | gender:string 174 | } 175 | interface iMakePerson { 176 | name?:string 177 | age?:number 178 | nikename?:string 179 | gender:string 180 | } 181 | function Person(v: iMakePerson) :iPerson { 182 | return { 183 | name: v.name || "", 184 | age: v.age || 0, 185 | nikename: v.nikename || "", 186 | gender: v.gender, 187 | } 188 | } 189 | ^^^ 190 | 191 | 注意此时在 iPerson 中 ^gender^ 不是 ^gender?^,没有通过 ? 定义可以为undefined. 这样在所有调用 Person 的地方都需要定义 gender 192 | 193 | ^^^ts 194 | // 编译期报错 195 | // TS2345: Argument of type '{ name: string; }' is not assignable to parameter of type 'iPerson'. 196 | Person({ 197 | name: "nimo", 198 | }) 199 | 200 | // 不报错 201 | Person({ 202 | name: "nimo", 203 | gender: "male", 204 | }) 205 | ^^^ 206 | 207 | 208 | **通过空值设计可以消除代码中的 undefined , 提高开发效率,增加项目稳定性** 209 | 210 | 基于空值make函数你可以略过部分属性的声明,不必要写大量的重复代码,但请切记一点在 make 函数中空值只能有 211 | 212 | ^^^js 213 | "" 214 | 0 215 | false 216 | [] 217 | 另外一个 make 函数 218 | ^^^ 219 | 220 | 这是因为如果你在 make 中定义了以上其他的值,会让调用 make 函数的人不明白到底make后属性默认值是什么. 221 | 222 | > 不能用 {} 是因为 另外一个make函数替代了空值对象. 223 | 224 | 另外一个make 函数请看下面的例子 225 | 226 | ` 227 | interface iSon { 228 | name:string 229 | } 230 | interface iMakeSon { 231 | name?:string 232 | } 233 | function Son (v :iMakeSon):iSon { 234 | return { 235 | name: v.name || "" 236 | } 237 | } 238 | interface iFamily { 239 | unity:boolean 240 | son: iSon 241 | } 242 | interface iMakeFamily { 243 | unity?: boolean 244 | son?: iSon 245 | } 246 | function Family(v :iMakeFamily):iFamily { 247 | return { 248 | unity: v.unity || false, 249 | son: v.son || Son({}) 250 | } 251 | } 252 | 253 | test("多层mark",function () { 254 | let data = Family({ 255 | unity: true, 256 | // son: Son({}) // 此行可有可无,根据实际场景决定 257 | }) 258 | expect(data).toStrictEqual({ 259 | "son": { 260 | "name": "" 261 | }, 262 | "unity": true 263 | }) 264 | }) 265 | 266 | ;` 267 | 268 | ---- 269 | 270 | 多说一句,在 ts 中还可以将 son 直接包括在 family 中 271 | ` 272 | interface iSome { 273 | unity:boolean 274 | son: { 275 | name:string 276 | } 277 | } 278 | ;` 279 | --- 280 | 281 | 使用 ts 要带入静态类型的思维,虽然会写出很多在js角度看起来很麻烦的类型代码,但是这些代码会让你的项目稳定性更高. 282 | 而空值设计可以让编写 ts 更加轻松稳定. 283 | 284 | > 如果你学习 typescript 发现怎么用都不顺手,我建议先学习一门纯粹的强静态类型语言.用于掌握强静态类型语言编程思维. 285 | > 因为 TypeScript 是 对 JavaScript进行 类型批注,而不是真正意义上的静态类型语言. 286 | 287 | 288 | 如果你觉得空值函数的设计不错,请将本文推荐给你的朋友或同事 289 | 290 | 在Github发表评论: https://github.com/nimoc/fe/discussions/59 291 | 292 | `; 293 | -------------------------------------------------------------------------------- /type/zero_value_typescript.md: -------------------------------------------------------------------------------- 1 | # 利用空值设计让 TypeScript 更稳定和易于维护 2 | 3 | > 本文代码有大量的 test 和 expect 函数,目的是替代注释,用 expect 说明变量和函数的返回值 4 | 5 | ## 初始化缺省数据 6 | 7 | 举一个场景的例子, 前端ajax接收后端响应数据: 8 | ```ts 9 | interface iUser { 10 | name:string 11 | age:number 12 | } 13 | test("响应数据", function () { 14 | let responseJSON :string = `{"name": "nimo"}` 15 | let res:iUser = JSON.parse(responseJSON) 16 | expect(res.name).toBe("nimo") 17 | expect(res.age).toBe(undefined); 18 | // iUser 中 age 不是 age?:number 但是结果值是 undefined 19 | // 具体为什么会是 undefined,和为什么后端响应的数据没有 age 我们就不深入讨论了 20 | // 但是这种情况会导致出现一些bug,比如: 21 | expect(res.age + 1).toBe(NaN); 22 | // 明明使用了ts,结果居然将 number 加上 number 得到了 NaN 23 | // 因为此时 res.age 是 undefined 24 | }) 25 | 26 | ``` 27 | 28 | 29 | ## 数据接口新增属性 30 | 31 | 接着来看对象数据的场景: 32 | 33 | 最初定义了一个数据结构,只有 url 属性 34 | ```ts 35 | interface iPost { 36 | url:string 37 | } 38 | 39 | test("然后有很多地方使用了 iData", function () { 40 | let a :iPost = { 41 | url: "https://github.com/nimoc/blog/issues/33" 42 | } 43 | console.log(a) 44 | let b :iPost = { 45 | url: "https://github.com/nimoc" 46 | } 47 | console.log(b) 48 | }) 49 | 50 | ``` 51 | 52 | 如果此时 iPost 新增了属性 title,则会导致 a b 声明时编译期报错 53 | 54 | ```ts 55 | interface iPost { 56 | url:string 57 | title: string 58 | } 59 | 60 | test("然后有很多地方使用了 iData", function () { 61 | // TS2741: Property 'title' is missing in type '{ url: string; }' but required in type 'iPost'. 62 | let a :iPost = { 63 | url: "https://github.com/nimoc/blog/issues/33" 64 | } 65 | console.log(a) 66 | // TS2741: Property 'title' is missing in type '{ url: string; }' but required in type 'iPost'. 67 | let b :iPost = { 68 | url: "https://github.com/nimoc" 69 | } 70 | console.log(b) 71 | }) 72 | ``` 73 | 74 | 想要解决这个问题就需要在 a b 两处声明的地方加上 title 属性,如果不只是 a b 两次,而是由几十处就会变得非常麻烦. 75 | 76 | 虽然你可以认为类型系统就应该这样严格,但是在这个场景下我更希望不需要改几十处代码 77 | 78 | 如果不想改几十处可能会导致我们写出不好的代码,例如修改 iPost 为 79 | 80 | ```ts 81 | interface iPost { 82 | url:string 83 | title?: string 84 | } 85 | ``` 86 | 87 | 这种做法虽然不需要些几十处了,但是 [nimo](https://github.com/nimoc) 认为这种方式会引入不必要的 undefined . 88 | 导致明明用了类型系统,结果还要处理最繁琐的 undefined 问题. 89 | 90 | ## 空值设计 (zero values) 91 | 92 | 我们借鉴 [golang](http://golang.org/) 中zero values 的设计,来解决上述2个问题. 93 | 94 | 95 | 请看代码: 96 | ```ts 97 | 98 | 99 | interface iPerson { 100 | name:string 101 | age:number 102 | } 103 | interface iMakePerson { 104 | name?:string 105 | age?:number 106 | } 107 | 108 | function Person(v: iMakePerson) :iPerson { 109 | return { 110 | name: v.name || "", 111 | age: v.age || 0 112 | } 113 | } 114 | test("响应数据空值填充", function () { 115 | let response = Person(JSON.parse(`{"name":"nimo"}`)) 116 | // 不会出现 response.age 是 undefined 导致的 NaN 的情况 117 | expect(response.age + 1).toBe(1) 118 | }) 119 | 120 | test("多处使用 Person ", function () { 121 | let a = Person({ 122 | name: "nimo", 123 | }) 124 | expect(a).toStrictEqual({name:"nimo",age:0}) 125 | let b = Person({ 126 | age: 18, 127 | }); 128 | expect(b).toStrictEqual({name:"",age:18}) 129 | }) 130 | 131 | ``` 132 | 133 | 如果要新增属性则只需在 iPerson 和 iPerson中分别增加新属性 134 | 135 | 比如新增了 nikename 136 | 137 | ```ts 138 | interface iPerson { 139 | name:string 140 | age:number 141 | nikename:string 142 | } 143 | interface iMakePerson { 144 | name?:string 145 | age?:number 146 | nikename?:string 147 | } 148 | function Person(v: iMakePerson) :iPerson { 149 | return { 150 | name: v.name || "", 151 | age: v.age || 0, 152 | nikename: v.nikename || "", 153 | } 154 | } 155 | ``` 156 | 157 | 使用所有 Person不会报错,因为接口定义了 nikename?:string 158 | 159 | ```ts 160 | let a = Person({ 161 | name: "nimo", 162 | }) 163 | expect(a).toStrictEqual({name:"nimo",age:0,nikename:""}) 164 | ``` 165 | 166 | 167 | 如果新增了 gender ,并且要求 gender 是必填的那么可以这样修改 iPerson 168 | 169 | ```ts 170 | interface iPerson { 171 | name:string 172 | age:number 173 | nikename:string 174 | gender:string 175 | } 176 | interface iMakePerson { 177 | name?:string 178 | age?:number 179 | nikename?:string 180 | gender:string 181 | } 182 | function Person(v: iMakePerson) :iPerson { 183 | return { 184 | name: v.name || "", 185 | age: v.age || 0, 186 | nikename: v.nikename || "", 187 | gender: v.gender, 188 | } 189 | } 190 | ``` 191 | 192 | 注意此时在 iPerson 中 `gender` 不是 `gender?`,没有通过 ? 定义可以为undefined. 这样在所有调用 Person 的地方都需要定义 gender 193 | 194 | ```ts 195 | // 编译期报错 196 | // TS2345: Argument of type '{ name: string; }' is not assignable to parameter of type 'iPerson'. 197 | Person({ 198 | name: "nimo", 199 | }) 200 | 201 | // 不报错 202 | Person({ 203 | name: "nimo", 204 | gender: "male", 205 | }) 206 | ``` 207 | 208 | 209 | **通过空值设计可以消除代码中的 undefined , 提高开发效率,增加项目稳定性** 210 | 211 | 基于空值make函数你可以略过部分属性的声明,不必要写大量的重复代码,但请切记一点在 make 函数中空值只能有 212 | 213 | ```js 214 | "" 215 | 0 216 | false 217 | [] 218 | 另外一个 make 函数 219 | ``` 220 | 221 | 这是因为如果你在 make 中定义了以上其他的值,会让调用 make 函数的人不明白到底make后属性默认值是什么. 222 | 223 | > 不能用 {} 是因为 另外一个make函数替代了空值对象. 224 | 225 | 另外一个make 函数请看下面的例子 226 | 227 | ```ts 228 | interface iSon { 229 | name:string 230 | } 231 | interface iMakeSon { 232 | name?:string 233 | } 234 | function Son (v :iMakeSon):iSon { 235 | return { 236 | name: v.name || "" 237 | } 238 | } 239 | interface iFamily { 240 | unity:boolean 241 | son: iSon 242 | } 243 | interface iMakeFamily { 244 | unity?: boolean 245 | son?: iSon 246 | } 247 | function Family(v :iMakeFamily):iFamily { 248 | return { 249 | unity: v.unity || false, 250 | son: v.son || Son({}) 251 | } 252 | } 253 | 254 | test("多层mark",function () { 255 | let data = Family({ 256 | unity: true, 257 | // son: Son({}) // 此行可有可无,根据实际场景决定 258 | }) 259 | expect(data).toStrictEqual({ 260 | "son": { 261 | "name": "" 262 | }, 263 | "unity": true 264 | }) 265 | }) 266 | 267 | ``` 268 | 269 | 270 | ---- 271 | 272 | 多说一句,在 ts 中还可以将 son 直接包括在 family 中 273 | ```ts 274 | interface iSome { 275 | unity:boolean 276 | son: { 277 | name:string 278 | } 279 | } 280 | ``` 281 | 282 | --- 283 | 284 | 使用 ts 要带入静态类型的思维,虽然会写出很多在js角度看起来很麻烦的类型代码,但是这些代码会让你的项目稳定性更高. 285 | 而空值设计可以让编写 ts 更加轻松稳定. 286 | 287 | > 如果你学习 typescript 发现怎么用都不顺手,我建议先学习一门纯粹的强静态类型语言.用于掌握强静态类型语言编程思维. 288 | > 因为 TypeScript 是 对 JavaScript进行 类型批注,而不是真正意义上的静态类型语言. 289 | 290 | 291 | 如果你觉得空值函数的设计不错,请将本文推荐给你的朋友或同事 292 | 293 | 这样能让更多人提供更安全的make函数. 294 | 295 | 在Github发表评论: https://github.com/nimoc/fe/discussions/59 296 | --------------------------------------------------------------------------------