时间: 2019-11-19阅读: 64标签: 代码
Anders Hejlsberg: Introducing TypeScript
在 9102 年年初,一位室友问我一个问题,如何才能够提升写代码的能力?
https://channel9.msdn.com/posts/Anders-Hejlsberg-Introducing-TypeScript
可惜的是: 当时仅仅回复了一些自己的想法,如多看开源代码,多读书,多学习,多关注业界的动向与实践,同时也列了一些原则。但是这些并没有所总结,又或者说没有例子的语言始终是空泛的。所以在今年年底之际,对应着今年中遇到的形形色色的代码问题来一一讲解一下。
好代码的用处实际上本书建立在一个相当不可靠的前提之上:好的代码是有意义的。我见过太多丑陋的代码给他们的主人赚着大把钞票,所以在我看来,软件要取得商业成功或者广泛使用,“好的代码质量”既不必要也不充分。即使如此,我仍然相信,尽管代码质量不能保证美好的未来,他仍然有其意义:有了质量良好的代码以后,业务需求能够被充满信心的开发和交付,软件用户能够及时调整方向以便应对机遇和竞争,开发团队能够再挑战和挫折面前保持高昂的斗志。总而言之,比起质量低劣,错误重重的代码,好的代码更有可能帮助用户取得业务上的成功。
image.png
以上文字摘抄于《实现模式》的前言,距离本书翻译已经时隔 10 年了,但是这本书仍旧有着很大的价值。同时对于上述言论,我并不持否认意见。但是我认为,坏代码比好代码更加的费财(嗯,没打错,我确定)。对于相同的业务需求,坏代码需要投入的精力,时间更多,产出反而会更少。同时根据破窗理论( 此理论认为环境中的不良现象如果被放任存在,会诱使人们仿效,甚至变本加厉 ),坏代码会产生更坏的代码。这是一个恶性循环,如果不加以控制,完成需求的时间会慢慢失去控制。需要完成需求的人也会失落离开。
也就是说,好代码可以实现多赢,能够让用户爽,能够让老板爽,能够让开发者爽。总之,大家爽才是真的爽。
image.png
怎么写出好代码少即使多
利用开源出来的设计与代码来减轻来自于业务线的时间压力。
image.png
The best way to write secure and reliable applications. Write nothing; deploy nowhere.
TypeScript 是 JavaScript 的超集,TypeScript结合了类型检查和静态分析,显式接口。TypeScript是微软的开源项目,它是由C#之父Anders Hejlsberg发起的。
以上取自 github 上最火的项目之一nocode。懒惰是程序员的美德之一。所以学习业务,理解业务,拒绝不必要的需求也是一个程序员的必修功课。详情可以参考如何杜绝一句话需求?这一篇 blog,当然,在大部分场景下,我们是不具备对需求说不的能力与权力的,但是无论如何,深度的理解业务,对客户有同理心是对程序员的更高要求。解决问题才是一个程序员需要做的事情。能够理解好题意才能解决问题。
为什么会有 TypeScript?
JavaScript 只是一个脚本语言,并非真正设计用于开发大型 Web 应用,JavaScript 没有提供类和模块等概念,对于一个真正的应用开发,TypeScript 扩展JavaScript 并实现了这些特性。TypeScript 主要特点包括:
- TypeScript 是微软推出的开源语言,使用 Apache 授权协议
- TypeScript 是 JavaScript 的超集.
- TypeScript 增加了可选类型、类和模块
- TypeScript 可以编译成可读的、标准的 JavaScript
- TypeScript 支持开发大规模 JavaScript 应用,支持所有浏览器,主机和操作系统
- TypeScript 设计用于开发大型应用,并保证编译后的 JavaScript 代码兼容性
- TypeScript 扩展了 JavaScript 的语法,因此已有的 JavaScript 代码可直接与 TypeScript 一起运行无需更改
- TypeScript 文件扩展名是 ts,而 TypeScript 编译器会编译成 js 文件
- TypeScript 语法与 JScript .NET 相同
- TypeScript 非常易学和易于理解
对于软件开发而言,时间一定是最宝贵,最有价值的资源。相应的,尽量把时间耗费在解决新的问题,而不是对已经存在确切解决方案的问题老调重弹。所以,尽量不要自己写代码,而是借用别人的设计与实现。而在事实上,你也很难在极短的时间压力下设计并完成比开源更加合适的代码。
语言特性
类
接口
模块
类型注解
编译时类型检查
Arrow 函数 (类似 C# 的 Lambda 表达式)
当然,开源作者一定是想让他的产品有更多的受众,所以从设计上而言,会采用较为通用的设计,如果你的需求较为特殊并且你觉得不能说服作者帮你“免费打工”(或者作者拒绝了),那么你也只需要在特定之处进行包装与改写,但是要比完全重写要简单太多了。
JavaScript 与 TypeScript 的区别
TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法,因此现有的 JavaScript 代码无需做任何修改便可与TypeScript一起使用,TypeScript 通过类型注解提供编译时的静态类型检查。TypeScript 可处理已有的 JavaScript 代码,并只对其中的 TypeScript 代码进行编译。
核心TypeScript编译器
语法分析器(Parser):
以一系列原文件开始, 根据语言的语法, 生成抽象语法树(AST)
联合器(Binder):
使用一个
Symbol
将针对相同结构的声明联合在一起(例如:同一个接口或模块的不同声明,或拥有相同名字的函数和模块)。这能帮助类型系统推导出这些具名的声明。类型解析器与检查器(Type resolver / Checker):
解析每种类型的构造,检查读写语义并生成适当的诊断信息。
生成器(Emitter):
从一系列输入文件(.ts和.d.ts)生成输出,它们可以是以下形式之一:JavaScript(.js),声明(.d.ts),或者是source maps(.js.map)。
预处理器(Pre-processor):
“编译上下文”指的是某个“程序”里涉及到的所有文件。上下文的创建是通过检查所有从命令行上传入编译器的文件,按顺序,然后再加入这些文件直接引用的其它文件或通过
import
语句和/// <reference path=... />
标签间接引用的其它文件。
沿着引用图走下来你会发现它是一个有序的源文件列表,它们组成了整个程序。 当解析导出(import)的时候,会优先选择“.ts”文件而不是“.d.ts”文件,以确保处理的是最新的文件。 编译器会进行与Nodejs相似的流程来解析导入,沿着目录链查找与将要导入相匹配的带.ts或.d.ts扩展名的文件。 导入失败不会报error,因为可能已经声明了外部模块。
独立编译器(tsc):
批处理编译命令行界面。主要处理针对不同支持的引擎读写文件(比如:Node.js)。
语言服务:
“语言服务”在核心编译器管道上暴露了额外的一层,非常适合类编辑器的应用。
语言服务支持一系列典型的编辑器操作比如语句自动补全,函数签名提示,代码格式化和突出高亮,着色等。
基本的重构功能比如重命名,调试接口辅助功能比如验证断点,还有TypeScript特有的功能比如支持增量编译(在命令行上使用--watch
)。
语言服务是被设计用来有效的处理在一个长期存在的编译上下文中文件随着时间改变的情况;在这样的情况下,语言服务提供了与其它编译器接口不同的角度来处理程序和源文件。
请参考 [[Using the Language Service API]] 以了解更多详细内容。
当然,调研新的技术方案并且使用到项目中是一种能力,但是千万不要因为一个小功能添加一个非常大的项目。
数据结构
Node:
抽象语法树(AST)的基本组成块。通常
Node
表示语言语法里的非终结符;一些终结符保存在语法树里比如标识符和字面量。SourceFile:
给定源文件的AST。
SourceFile
本身是一个Node
;它提供了额外的接口用来访问文件的源码,文件里的引用,文件里的标识符列表和文件里的某个位置与它对应的行号与列号的映射。Program:
SourceFile
的集合和一系列编译选项代表一个编译单元。Program
是类型系统和生成代码的主入口。Symbol:
具名的声明。
Symbols
是做为联合的结果而创建。Symbols
连接了树里的声明节点和其它对同一个实体的声明。Symbols
是语义系统的基本构建块。Type:
Type
是语义系统的其它部分。Type
可能被命名(比如,类和接口),或匿名(比如,对象类型)。Signature:
一共有三种
Signature
类型:调用签名(call),构造签名(construct)和索引签名(index)。
笔者在之前就遇到过其他小伙伴因为无法使用数字四舍五入。说 fixed 方法有问题而使用 math.js 的小伙伴。
编译过程概述
整个过程从预处理开始。
预处理器会算出哪些文件参与编译,它会去查找如下引用(/// <reference path=... />
标签和import
语句)。
语法分析器(Parser)生成抽象语法树(AST)Node
.
这些仅为用户输出的抽象表现,以树的形式。
一个SourceFile
对象表示一个给定文件的AST并且带有一些额外的信息如文件名及源文件内容。
然后,联合器(Binder)处理AST节点,结合并生成Symbols
。
一个Symbol
会对应到一个命名实体。
这里有个一微妙的差别,几个声明节点可能会是名字相同的实体。
也就是说,有时候不同的Node
具有相同的Symbol
,并且每个Symbol
保持跟踪它的声明节点。
比如,一个名字相同的class
和namespace
可以合并,并且拥有相同的Symbol
。
联合器也会处理作用域,以确保每个Symbol
都在正确的封闭作用域里创建。
生成SourceFile
(还带有它的Symbols
们)是通过调用createSourceFile
API。
到目前为止,Symbol
代表的命名实体可以在单个文件里看到,但是有些声明可以从多文件合并,因此下一步就是构建一个全局的包含所有文件的视图,也就是创建一个Program
。
一个Program
是SourceFile
的集合并带有一系列CompilerOptions
。
通过调用createProgram
API来创建Program
。
通过一个Program
实例创建TypeChecker
。
TypeChecker
是TypeScript类型系统的核心。
它负责计算出不同文件里的Symbols
之间的关系,将Type
赋值给Symbol
,并生成任何语义Diagnostic
(比如:error)。
TypeChecker
首先要做的是合并不同的SourceFile
里的Symbol
到一个单独的视图,创建单一的Symbol
表,合并所有普通的Symbol
(比如:不同文件里的namespace
)。
在原始状态初始化完成后,TypeChecker
就可以解决关于这个程序的任何问题了。
这些“问题”可以是:
- 这个
Node
的Symbol
是什么? - 这个
Symbol
的Type
是什么? - 在AST的某个部分里有哪些
Symbol
是可见的? - 某个函数声明的
Signature
都有哪些? - 针对某个文件应该报哪些错误?
TypeChecker
计算所有东西都是“懒惰的”;为了回答一个问题它仅“解决”必要的信息。
TypeChecker
仅会检测和这个问题有关的Node
,Symbol
或Type
,不会检测额外的实体。
对于一个Program
同样会生成一个Emitter
。
Emitter
负责生成给定SourceFile
的输出;它包括:.js
,.jsx
,.d.ts
和.js.map
。
(11.545).toFixed(2) // "11.54"
术语
如果想要了解 fixed 方法为何有问题的,可以参考为什么(2.55).toFixed(1)等于2.5?作者以 v8 源码来解释为何会有这样的问题,以及提供了部分修正 fixed 的方案。
完整开始/令牌开始(Full Start/Token Start)
令牌本身就具有我们称为一个“完整开始”和一个“令牌开始”。“令牌开始”是指更自然的版本,它表示在文件中令牌开始的位置。“完整开始”是指从上一个有意义的令牌之后扫描器开始扫描的起始位置。当关心琐事时,我们往往更关心完整开始。
函数 | 描述 |
---|---|
ts.Node.getStart |
取得某节点的第一个令牌起始位置。 |
ts.Node.getFullStart |
取得某节点拥有的第一个令牌的完整开始。 |
事实上如果没有很大的精度需求,前端完完全全利用一个函数便可以解决的问题,完全不需要复杂的math 这种高精度库。
琐碎内容(Trivia)
语法的琐碎内容代表源码里那些对理解代码无关紧要的内容,比如空白,注释甚至一些冲突的标记。
因为琐碎内容不是语言正常语法的一部分(不包括ECMAScript API规范)并且可能在任意2个令牌中的任意位置出现,它们不会包含在语法树里。但是,因为它们对于像重构和维护高保真源码很重要,所以需要的时候还是能够通过我们的APIs访问。
因为EndOfFileToken
后面可以没有任何内容(令牌和琐碎内容),所有琐碎内容自然地在非琐碎内容之前,而且存在于那个令牌的“完整开始”和“令牌开始”之间。
虽然这个一个方便的标记法来说明一个注释“属于”一个Node
。比如,在下面的例子里,可以明显看出genie
函数拥有两个注释:
var x = 10; // This is x.
/**
* Postcondition: Grants all three wishes.
*/
function genie([wish1, wish2, wish3]: [Wish, Wish, Wish]) {
while (true) {
}
} // End function
这是尽管事实上,函数声明的完整开始是在var x = 10;
后。
我们依据Roslyn's notion of trivia
ownership处理注释所有权。通常来讲,一个令牌拥有同一行上的所有的琐碎内容直到下一个令牌开始。任何出现在这行之后的注释都属于下一个令牌。源文件的第一个令牌拥有所有的初始琐碎内容,并且最后面的一系列琐碎内容会添加到end-of-file
令牌上。
对于大多数普通用户,注释是“有趣的”琐碎内容。属于一个节点的注释内容可以通过下面的函数来获取:
函数 | 描述 |
---|---|
ts.getLeadingCommentRanges |
提供源文件和一个指定位置,返回指定位置后的第一个换行与令牌之间的注释的范围(与ts.Node.getFullStart 配合会更有用)。 |
ts.getTrailingCommentRanges |
提供源文件和一个指定位置,返回到指定位置后第一个换行为止的注释的范围(与ts.Node.getEnd 配合会更有用)。 |
做为例子,假设有下面一部分源代码:
debugger;/*hello*/
//bye
/*hi*/ function
function
关键字的完整开始是从/*hello*/
注释,但是getLeadingCommentRanges
仅会返回后面2个注释:
d e b u g g e r ; / * h e l l o * / _ _ _ _ _ [CR] [NL] _ _ _ _ / / b y e [CR] [NL] _ _ / * h i * / _ _ _ _ f u n c t i o n
↑ ↑ ↑ ↑ ↑
完整开始 查找 第一个注释 第二个注释 令牌开始
开始注释
适当地,在debugger
语句后调用getTrailingCommentRanges
可以提取出/*hello*/
注释。
如果你关心令牌流的更多信息,createScanner
也有一个skipTrivia
标记,你可以设置成false
,然后使用setText
/setTextPos
来扫描文件里的不同位置。
function round(number, precision) { return Math.round(+number + 'e' + precision) / Math.pow(10, precision); }
参考资料
当然,也有小伙伴来找我询问大量数据的表格优化,我第一反应就是React Infinite或者vue-infinite-scroll此类解决方案。但是对方能够多提供一些信息包括上下文,采用的技术栈,当前数据量大小,未来可能需要达到的大小,当前表格是否需要修改等。得到了这些信息,结合业务来看,相比于增加一个库,是否如下方式更为便捷与快速。
TypeScript入门指南(JavaScript的超集)
https://tutorialzine.com/2016/07/learn-typescript-in-30-minutes
TypeScript Handbook(中文版):
http://www.runoob.com/manual/gitbook/TypeScript/_book/index.html
http://www.tslang.cn/#download-links
有人说:“TypeScript 让 JavaScript 又变成了 Java,而我们不需要另一个 Java,所以我们不需要 TypeScript“。这样说的人一定不知道,TypeScript 的类型系统中有:Intersection TypesUnion Types & Discriminated Unions (aka "Algebraic Data Types.")String Literal TypesPolymorphic this TypesIndex TypesMapped Types...这些吧。特别是 Generic Types 组合上 Mapped Types 描述能力爆表。如果你的代码超过 1000 行,而且你不打算浪费时间,那么试试 TypeScript。当然前提是你是有经验的开发人员,如果是编程初学者,建议还是先从 JavaScript 开始。
作者:林建入
链接:https://www.zhihu.com/question/21879449/answer/233768634
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
// 因为 vue 模型的原因,使用 Object.freeze 性能可以有很大增益 this.xxx = Object.freeze(xxx);
随着堆积业务,代码的增长。管理复杂度的成本与日俱增,把依赖降低。 利用开源代码使得任务更容易实现。时间就是成本。关键是让收益可以最大化。
学习更多是为了做的更少。
统一
不同的人由于编码经验和编码偏好不同,项目中同一个功能的实现代码可能千差万别。但是如果不加以约束,让每一个人都按照自己的偏好写自己的模块,恐怕就会变成灾难。
所以每次在学习一些新技术的时候,我总是想多看看作者的实例代码,作者是如何理解的,社区又是如何理解的。以求实现起来代码风格不至于偏离社区太多,这样的话可以提高沟通与协作的效率。类似于 《阿里巴巴Java开发手册》 或者vue 风格指南这种取自大公司或社区的经验之谈,要多读几遍。因为他们所遇到的问题和业务更加复杂。
对于公司内部开发来说,写一个组件时候,生命周期的代码放在文件上面还是放在最下面,如何把代码的一个功能点集中放置。通用型代码的修改。代码行数的限制。能够列出统一的方案,多利而少害。
化繁为简(抽象)
抽象是指从具体事物抽出、概括出它们共同的方面、本质属性与关系等,而将个别的、非本质的方面、属性与关系舍弃的思维过程。
如果你面对一个较大的系统,你会发现重构并不能解决根本问题,它仅仅只能减少少许的代码的复杂度以及代码行数,只有抽象才可以解决实质性问题。
无论是数据库设计,架构设计,业务设计,代码设计,但凡设计都离不开抽象。抽象能力强的所面临的困难会比能力弱的少很多。
或者说抽象能力弱一些的小伙伴遇到一些问题甚至需要重新推翻然后再设计,这个是在时间和业务开发中是不能被接受的。
这里就谈谈代码,以下也举个例子,如axios库中有拦截器与本身业务,在没有看到源码之前,我一直认为他是分 3 阶段处理:
请求拦截业务处理响应拦截
但如果你去看源码,你就会发现其实在作者看来,这 3 个阶段其实都是在处理一个 Promise 队列而已。
// 业务处理 var chain = [dispatchRequest, undefined]; var promise = Promise.resolve(config); this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { // 前置请求拦截 chain.unshift(interceptor.fulfilled, interceptor.rejected); }); this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { // 后置响应拦截 chain.push(interceptor.fulfilled, interceptor.rejected); }); while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); } return promise;
这就是一种代码抽象能力。让自己的代码可以适应更多的场景是程序员需要思考的。代码不是给机器看的,是给人看的,更高的要求是: 代码不仅仅是给人看的,更是给人用的。需要考虑到协作的人与事,灵活的配置也是必须要考虑到的。就拿前端的 虚拟 dom 来说。能够适配更多的平台。
当然了,抽象能力需要时间,需要经验,需要学习大量的设计。
注意!:不要过早的抽象业务代码,甚至不要抽象业务代码。多写一点代码无所谓,千万别给自己找事做。 在业务上尽量保持简单和愚蠢。除非你是业务专家,确认当前业务不太会产生变化。
权责对等(拆分与合并)
责任与义务本质上就是对等的,且越对等的就越稳定。这几年,微服务架构,中台,微前端理论层出不穷,本质上就是为了权责对等,对于更加基础的服务,更有产出的业务投入更高的人力与物力以保证更稳定的运行是很正常的一件事。而不是之前的大锅饭(单体应用)。
从代码上来看,某个模块是否承担了它不应该做的事情,或者某个模块过于简单,徒增复杂度。
当然,事实上有些东西目前是做不到的让所有人都觉得满意,增一分则肥,减一分则瘦,刚刚好很难界定。就像 Dan Abramov 说的那样:
Flux libraries are like glasses: you’ll know when you need them.只做一件事
Unix 哲学,这个很好理解,就像我今年想做的事情太多,反而什么都没有做(或者说都做了,但都不好)。
代码上来看,不要因为一点点性能的原因,把几件事合在一起去做。例如在一次 for 循环中解决所有问题,或者将所有代码写在一个函数中,例如:
created() { const {a,b,c,d} = this.data // ... 三件事情彼此有交互同时需要 a,b,c,d // 完成之后的逻辑 }
改造后:
本文由10bet发布于Web前端,转载请注明出处:10bet好代码的用处,怎么写出好代码?
关键词: