PC端,想做一个同一设备防止重复注册的功能。
想过用ip的方法,但是现在ip是动态的重启可能会变。
也想过获取mac地址但是浏览器有兼容性。
至于获取手机验证码,自己的项目,预算有限。。。
所以有好的解决方法么?
没有足够的数据
(゚∀゚ )
暂时没有任何数据
(゚∀゚ )
暂时没有任何数据
LeapFE 收藏了问题 · 2月23日
PC端,想做一个同一设备防止重复注册的功能。
想过用ip的方法,但是现在ip是动态的重启可能会变。
也想过获取mac地址但是浏览器有兼容性。
至于获取手机验证码,自己的项目,预算有限。。。
所以有好的解决方法么?
PC端,想做一个同一设备防止重复注册的功能。想过用ip的方法,但是现在ip是动态的重启可能会变。也想过获取mac地址但是浏览器有兼容性。至于获取手机验证码,自己的项目,预算有限。。。
LeapFE 关注了问题 · 2月23日
PC端,想做一个同一设备防止重复注册的功能。
想过用ip的方法,但是现在ip是动态的重启可能会变。
也想过获取mac地址但是浏览器有兼容性。
至于获取手机验证码,自己的项目,预算有限。。。
所以有好的解决方法么?
关注 9 回答 6
LeapFE 赞了文章 · 2月22日
2021年你需要知道的HTML标签和属性
Web开发人员都在广泛的使用HTML。无论你使用什么框架或者选择哪个后端语言,框架在变,但是HTML始终如一。尽管被广泛使用,但还是有一些标签或者属性是大部分开发者不熟知的。虽然现在有很多的模版引擎供我们使用,但是我们还是需要尽可能的熟练掌握HTML内容,就像CSS一样。
在我看来,最好尽可能使用HTML特性来实现我们的功能,而不是使用JavaScript实现相同的功能,尽管我承认编写HTML可能会是重复的和无聊的。
尽管许多开发人员每天都在使用HTML,但他们并没有尝试改进自己的项目,也没有真正利用HTML的一些鲜为人知的特性。
下面这5个通过HTML标签/属性实现的功能我觉得需要了解一下:
图片懒加载可以帮助提升网站的性能和响应能力。图片懒加载可以避免立即加载那些不在屏幕中立即显示的图片素材,当用户滚动临近图片时再去开始加载。
换言之,当用户滚动到图片出现时再进行加载,否则不加载。这就降低了屏幕内容展示过程中的图片素材的请求数量,提升了站点性能。
往往我们都是通过javascript来实现的,通过监听页面滚动事件来确定加载对应的资源。但是,在不完全考虑兼容性的场景下,我们其实可以直接通过HTML来直接实现。
注:本篇的提到的标签和属性的兼容性需要大家根据实际场景来选取是否使用
可以通过为图片文件添加loading="lazy"
的属性来实现:
<img data-original="image.png" loading="lazy" alt="lazy" width="200" height="200" />
当用户在进行输入搜索功能时,如果能够给出有效的提示,这会大大提升用户体验。输入建议和自动完成功能现在到处可见,我们可以使用Javascript添加输入建议,方法是在输入框上设置事件侦听器,然后将搜索到的关键词与预定义的建议相匹配。
其实,HTML也是能够让我们来实现预定义输入建议功能的,通过<datalist>
标签来实现。需要注意的是,使用时这个标签的id属性需要和input元素的list属性一致。
<label for="country">请选择喜欢的国家:</label>
<input list="countries" name="country" id="country">
<datalist id="countries">
<option value="UK">
<option value="Germany">
<option value="USA">
<option value="Japan">
<option value="India">
<option value=“China”>
</datalist>
你是否遇到过在不同场景或者不同尺寸的设备上面的时候,图片展示适配问题呢?我想大家都遇到过。
针对只有一个尺寸的图片素材的时候,我们往往可以通过CSS的object-fit
属性来进行裁切适配。但是有些时候需要针对不同的分辨率来显示不同尺寸的图片的场景的时候,我们是否可以直接通过HTML来实现呢?
HTML提供了<picture>
标签,允许我们来添加多张图片资源,并且根据不同的分辨率需求来展示不同的图片。
<picture>
<source media="(min-width:768px)" srcset="med_flower.jpg">
<source media="(min-width:495px)" srcset="small_flower.jpg">
<img data-original="high_flower" style="width: auto;" />
</picture>
我们可以定义不同区间的最小分辨率来确定图片素材,这个标签的使用有些类似<audio>
和<video>
标签。
当我们的页面有大量的锚点跳转或者静态资源加载时,并且这些跳转或者资源都在统一的域名的场景时,我们可以通过<base>
标签来简化这个处理。
例如,我们有一个列表需要跳转到微博的不同大V的主页,我们就可以通过设置<base>
来简化跳转路径
<head>
<base href="https://www.weibo.com/" target="_blank">
</head>
<body>
<a href="jackiechan">成龙</a>
<a href="kukoujialing">贾玲</a>
</body>
<base>
标记必须具有href
和target
属性。
当我们希望实现一段时间后或者是立即重定向到另一个页面的功能时,我们可以直接通过HTML来实现。
我们经常会遇到有些站点会有这样一个功能,“5s后页面将跳转”。这个交互可以嵌入到HTML中,直接通过<meta>
标签,设置http-equiv="refresh"
来实现
<meta http-equiv="refresh" content="4; URL='https://google.com' />
这里content
属性指定了重定向发生的秒数。值得一提的是,尽管谷歌声称这种形式的重定向和其他的重定向方式一样可用,但是使用这种类型的重定向其实并不是那么的优雅,往往会显得很突兀。
因此,最好在某些特殊的情况下使用它,比如在长时间用户不活动之后再重定向到目标页面。
HTML和CSS是非常强大的,哪怕我们仅仅使用这两种技术也能创建出一些奇妙的网站。虽然它们的使用量很大很普遍,还是有很多的开发者并没有真正的深入了解他们,还有很多的内容需要我们深入的去学习和理解,实践,有很多的技巧等待着我们去发现。
查看原文Web开发人员都在广泛的使用HTML。无论你使用什么框架或者选择哪个后端语言,框架在变,但是HTML始终如一。尽管被广泛使用,但还是有一些标签或者属性是大部分开发者不熟知的。虽然现在有很多的模版引擎供我们使用,但是我们还是需要尽可能的熟练掌握HTML内容,就像CS...
赞 37 收藏 28 评论 2
LeapFE 赞了文章 · 2月21日
经常看一些 LOL 比赛直播的小伙伴,肯定都知道,在一些弹幕网站(Bilibili、虎牙)中,当人物与弹幕出现在一起的时候,弹幕会“巧妙”的躲到人物的下面,看着非常的智能。
简单的一个截图例子:
其实,这里是运用了 CSS 中的 MASK 属性实现的。
之前在多篇文章都提到了 mask,比较详细的一篇是 -- 奇妙的 CSS MASK,本文不对 mask 的基本概念做过多讲解,向下阅读时,如果对一些 mask 的用法感到疑惑,可以再去看看。
这里只简单介绍下 mask 的基本用法:
最基本,使用 mask 的方式是借助图片,类似这样:
{
/* Image values */
mask: url(mask.png); /* 使用位图来做遮罩 */
mask: url(masks.svg#star); /* 使用 SVG 图形中的形状来做遮罩 */
}
当然,使用图片的方式后文会再讲。借助图片的方式其实比较繁琐,因为我们首先还得准备相应的图片素材,除了图片,mask 还可以接受一个类似 background 的参数,也就是渐变。
类似如下使用方法:
{
mask: linear-gradient(#000, transparent) /* 使用渐变来做遮罩 */
}
那该具体怎么使用呢?一个非常简单的例子,上述我们创造了一个从黑色到透明渐变色,我们将它运用到实际中,代码类似这样:
下面这样一张图片,叠加上一个从透明到黑色的渐变,
{
background: url(image.png) ;
mask: linear-gradient(90deg, transparent, #fff);
}
应用了 mask 之后,就会变成这样:
这个 DEMO,可以先简单了解到 mask 的基本用法。
这里得到了使用 mask 最重要结论:添加了 mask 属性的元素,其内容会与 mask 表示的渐变的 transparent 的重叠部分,并且重叠部分将会变得透明。
值得注意的是,上面的渐变使用的是 linear-gradient(90deg, transparent, #fff)
,这里的 #fff
纯色部分其实换成任意颜色都可以,不影响效果。
了解了 mask 的用法后,接下来,我们运用 mask,简单实现视频弹幕中,弹幕碰到人物,自动被隐藏过滤的例子。
首先,我简单的模拟了一个召唤师峡谷,以及一些基本的弹幕:
方便示意,这里使用了一张静态图,表示了召唤师峡谷的地图,并非真的视频,而弹幕则是一条一条的 <p>
元素,和实际情况一致。伪代码大概是这样:
<!-- 地图 -->
<div class="g-map"></div>
<!-- 包裹所有弹幕的容器 -->
<div class="g-barrage-container">
<!-- 所有弹幕 -->
<div class="g-barrage">6666</div>
...
<div class="g-barrage">6666</div>
</div>
为了模拟实际情况,我们再用一个 div 添加一个实际的人物,如果不做任何处理,其实就是我们看视频打开弹幕的感受,人物被视频所遮挡:
注意,这里我添加了一个人物亚索,并且用 animation 模拟了简单的运动,在运动的过程中,人物是被弹幕给遮挡住的。
接下来,就可以请出 mask 了。
我们利用 mask 制作一个 radial-gradient
,使得人物附近为 transparent
,并且根据人物运动的 animation,给 mask 的 mask-position
也添加上相同的 animation 即可。最终可以得到这样的效果:
.g-barrage-container {
position: absolute;
mask: radial-gradient(circle at 100px 100px, transparent 60px, #fff 80px, #fff 100%);
animation: mask 10s infinite alternate;
}
@keyframes mask {
100% {
mask-position: 85vw 0;
}
}
实际上就是给放置弹幕的容器,添加一个 mask
属性,把人物所在的位置标识出来,并且根据人物的运动不断的去变换这个 mask 即可。我们把 mask 换成 background,原理一看就懂。
background 透明的地方,即 mask 中为 transparent 的部分,实际就是弹幕会被隐藏遮罩的部分,而其他白色部分,弹幕不会被隐藏,正是完美的利用了 mask 的特性。
其实这项技术和视频本身是无关的,我们只需要根据视频计算需要屏蔽掉弹幕的位置,得到相应的 mask 参数即可。如果去掉背景和运动的人物,只保留弹幕和 mask,是这样的:
需要明确的是,使用 mask,不是将弹幕部分给遮挡住,而是利用 mask
,指定弹幕容器之下,哪些部分正常展示,哪些部分透明隐藏。
最后,完整的 Demo 你可以戳这里:
CodePen Demo -- mask 实现弹幕人物遮罩过滤
当然,上面我们简单的还原了利用 mask 实现弹幕遮罩过滤的效果。但是实际情况比上述的场景复杂的多,因为人物英雄的位置是不确定的,每一刻都在变化。所以在实际生产环境中,mask 图片的参数,其实是由后端实时对视频进行处理计算出来的,然后传给前端,前端再进行渲染。
对于运用了这项技术的直播网站,我们可以审查元素,看到包裹弹幕的容器的 mask 属性,每时每刻都在发生变化:
返回回来的其实是一个 SVG 图片,大概长这个样子:
这样,根据视频人物的实时位置变化,不断计算新的 mask,再实时作用于弹幕容器之上,实现遮罩过滤。
本文到此结束,希望对你有帮助 :),本文介绍了 CSS mask 的一个实际生产环境中,非常有意义的一次实践,也表明很多新的 CSS 技术,运用得当,还是能给业务带来非常有益的帮助的。
想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄
更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。
如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。
查看原文经常看一些 LOL 比赛直播的小伙伴,肯定都知道,在一些弹幕网站(Bilibili、虎牙)中,当人物与弹幕出现在一起的时候,弹幕会“巧妙”的躲到人物的下面,看着非常的智能。
赞 41 收藏 22 评论 7
LeapFE 发布了文章 · 2月19日
使用基于工具的轻量级代码检查代码更改(又名现代代码审查)已成为广泛的规范,应用于各种开源和产业系统。在本文中,我们对Google的现代代码审查进行了探索性研究。 Google很早就引入了代码审查并经过多年的发展;我们的研究揭示了为什么Google引入了这种做法并分析了当前的状态,经过数十年的代码变更和数百万条代码审核。通过12次访谈,对44位受访者的调查以及分析的900万条已审核变更的审核日志,我们调查了Google进行代码审查的动机,当前做法,以及开发人员的满意度和挑战。
对等代码审查,是除了作者以外的开发者们对源代码进行手动检查,被认为是对提高软件项目质量的一种有价值的工具。在 1976年,Fagan正式制定了高度结构化的代码审查流程——代码检查。多年来,研究人员提供了有关代码检查的优势的证据,特别是在发现缺陷上,但麻烦的是费时,这种方法的同步性特点阻碍了它在实践中的推广。现今,大多数组织都采用更轻量级的代码审查实践来限制检查效率低下。现代代码审查是(1)非正式(与Fagan风格相反),(2)基于工具的,(3)异步,(4)聚焦于审查代码更改。
一个开放的研究挑战是:在这种新颖的背景下,了解哪些实践代表了宝贵而有效的审查方法。 Rigby和Bird定量分析了来自跨领域软件项目以及组织的代码审查数据,发现五个高度趋同的方面,他们猜想其他的项目也有这个规律性。Rigby和Bird的分析基于有价值的广泛的观点(分析了多个来自不同的环境的项目)。对于由Basili倡导的知识实证体系的发展,同样重要的是要考虑对单个案例进行分析的集中和纵向的观点。本文扩展了Rigby和Bird的工作,着重于审查实践和特色,主要是:在Google成立,一家公司拥有数十年的代码审查的历史和大量的每日审查借鉴。本文可以(1)对从业人员规定进行代码审查和(2)吸引研究人员想了解和支持这一新颖过程的人。
从Google非常早期的历史开始,代码审查就已经成为软件开发中必不可少的一部分;因为它很早就引入了,已经成为了Google文化的一个核心部分。在Google,代码审查的过程和工具经过反复完善了十多年,应用在每天全球数十个办公室中,超过25,000名开发人员的超过20,000行源代码的更改。
我们以探索性调查形式进行分析聚焦于代码审查的三个方面,并扩展了Rigby和Bird的成果:(1)驱动代码审查的动机,(2)当前实践,以及(3)开发人员对代码审查的感受,专注于特定审查遇到的挑战(审核过程中的中断)和满意度。我们的研究方法结合了来自多个数据源的输入:对Google开发人员进行12次半结构化的访谈;一个内部调查,发送给了最近做个变更审查的工程师,并收到44条回复;一组来自两年间的Google代码审查工具产生的900万条评论的数据。
我们发现:相比其他情况,Google的流程明显更轻松,基于一位审阅者,快速迭代,小更改,以及集成紧密代码审查工具。由于围绕代码审查发生的交互是复杂的,中断审查仍然存在。不过,开发人员认为此过程很有价值,确信当规模很大的时候,可以很好地发挥作用,并且,进行这个过程有若干原因,也取决于作者与审查者之间的关系。最后,我们发现关于使用代码审查工具,不仅可以协作审查,而且还有个一个很重要的发现:代码审查可以作为一种教学工具。
我们描述了文献研究中的审查过程,然后我们详细介绍这些融合代码审查的做法流程。
代码检查。软件检查是第一个正式的代码审查流程。这个高度结构化的过程涉及计划,概述,准备,检查会议,返工和跟进。代码检查的目标是在同步检查会议中发现缺陷,作者和审稿人坐在同一会议室内来检查代码更改。 Kollanus和Koskinen汇编了有关代码检查的最新文献调查研究。他们发现绝大多数关于代码检查的研究本质上是经验性的。代码检查作为一种发现缺陷的技术的整体价值和促使检查者读代码的价值存在共识。总体而言,与Internet的普及和异步代码审查流程的增长,自2005年以来,代码检查的研究有所下降。
通过电子邮件进行异步审查。直到2000年代后期,大多数大型OSS项目都采用一种远程异步审查的形式,这依赖于补丁发送的通讯渠道,如邮件列表和问题跟踪系统。在这种情况下,项目成员评估贡献的补丁程序并通过这些渠道请求修改。当补丁被认为质量足够高时,核心开发人员会将其提交给代码库。受信任的提交者可能具有提交到审查过程,而不是进行提交前的审查。 Rigby等人,是最早在这种环境下进行广泛工作的人之一;他们发现,这种类型的审查“与[代码检查]几乎没有共同点,只是相信同行会有效地发现软件缺陷” 。 Kononenko等人,分析了这种情况,并且发现审查响应时间和接受程度和社会因素相关,例如,审查者的工作量和改变作者的经验,这些是代码检查所无法反映的。
基于工具的审查。为了使修补程序审查的过程结构更加合理,OSS和工业设置中出现了几种工具。这些工具支持审阅过程的持久工作:(1)补丁的作者将其提交给代码审阅工具,(2)审阅者可以看到建议代码与更改的差异,(3)可以与作者和其他审稿人就特定的话题展开讨论,然后(4)作者可以提出修改意见,以解决评审者的意见。此反馈周期将持续到每个人都满意或补丁被丢弃为止。不同的项目使用了他们的工具来支持他们的过程。 Microsoft使用CodeFlow,该工具跟踪每个人(作者或审阅者)的状态以及他们在进程中的位置(签名,等待,审阅); CodeFlow不会阻止作者未经批准而提交更改,并支持在评审线程中聊天。 Google的Chromium项目(以及其他几个OSS项目)依赖于外部可用的Gerrit;在Chromium中,只有经过审阅者的明确批准并自动确认更改不会破坏构建,更改才会合并到master分支中。在Gerrit中,未分配审查者也可以发表评论。 VMware开发了开源的ReviewBoard,它将静态分析集成到审查过程中。这种集成依赖于变更作者手动请求分析,并且已经证明可以提高代码审查的质量。 Facebook的代码审查系统Phabricator,使审查者可以“接管”更改并自行提交,并提供了挂钩以进行自动静态分析或持续的构建/测试集成。
在基于工具审查的背景下,研究人员调查了代码更改接受或响应时间与更改后的代码和作者的特征之间的关系,以及审阅者之间的协议。 根据工业和OSS开发人员的意见,还进行了定义什么构成良好的代码审查。
基于分叉模式的开发模型。在GitHub上,拉请求的过程中,开发人员想要对现有的git仓库进行更改,就要对其fork进行更改。在发出拉请求后,会出现在拉请求的列表中展示项目中的问题,任何可以看到该项目的人都可以看到。 Gousios等人对拉请求集成者和贡献者的实践和碰到的问题进行了定性研究,发现与基于工具的代码审查有类似的方法。
Rigby和Bird提出了第一项也是最重要的工作,这些工作试图跨几个代码审查过程和上下文确定融合的实践。 他们考虑了使用基于电子邮件审查的OSS项目,使用Gerrit的OSS项目,使用基本代码审查工具的AMD项目以及使用CodeFlow的Microsoft。 他们分析了这些项目的过程和数据,以描述多个角度,例如迭代开发,审查者选择和审查讨论。 他们确定了所有已考虑项目都融合到的五种现代代码审查实践(表1)。 我们将使用其ID(例如CP1)来引用这些做法。 基本上,他们在快速,轻量级过程(CP1,CP2,CP3)方面达成了一致,很少有人参与(CP4)进行小组问题解决(CP5)。
id | 融合实践 |
---|---|
CP1 | 当时同行审查遵循轻量级,灵活的流程 |
CP2 | 审查要提早(在提交更改之前),快速且频繁地进行 |
CP3 | 变更范围很小 |
CP4 | 两名审查者找大量的缺陷 |
CP5 | 审查已从发现缺陷活动变为小组解决问题活动 |
本节描述了我们研究的问题和背景;它还概述了我们的研究方法及其局限性。
这项研究的总体目标是调查Google的现代代码审查,这一过程涉及成千上万的开发人员,并且经过了十多年的改进。 为此,我们进行了探索性调查,围绕三个主要研究问题进行了结构设计。
RQ1:Google进行代码审查的动机是什么?Rigby和Bird发现动机是现代代码审查的融合特征之一(CP5)。 在此可以了解,动机和期望推动了Google的代码审查。 特别是,我们既考虑了引入现代代码审查的历史原因(因为Google是最早使用现代代码审查的公司之一),也考虑了当前的期望。
RQ2:Google的代码审查实践是做什么? Rigby和Birdregard根据流程(CP1),速度和频率(CP2),分析变更的大小(CP3)和审阅者数量(CP4)来执行流程本身。 我们对Google的这些方面进行了分析,以调查与以前的研究相比,对于具有更长代码审查历史,明确文化和大量审查记录的公司来说,同样的发现是否成立。
RQ3:Google开发人员如何看待代码审查?最后,在我们的最后一个研究问题中,我们有兴趣了解Google开发人员如何看待其公司中已实施的现代代码审查。为了更好的理解实践(因为感知驱动行动)和指导未来的研究,这个探索很必需。我们聚焦于两方面:一些特定的审查,比如:开发人员有中断审查的经历;在面临一些挑战时开发人员对审查是否满意。
我们简要描述了关于我们方法论的研究背景。 有关Google代码审查过程和工具的详细说明,请参见第5.1节。
Google的大多数软件开发都在一个整体的源代码存储库中(mono-repo),通过内部版本控制系统访问。 由于Google代码审查是必需的,因此,每次向Google源代码控制系统提交代码,都先要使用CRITIQUE进行代码审查,CRITIQUE是一个内部开发的,集中的,基于Web的代码检查工具。 开发工作流程基于Google的整体代码库,包括代码审查过程,是非常统一的。就像第2节中描述的工具一样,CRITIQUE允许审查者看到提议更改代码和开始讨论前明确的代码行处。CRITIQUE提供了大量的登录功能;记录开发者使用该工具的交互(包括打开工具,查看差异,创建评论和接受更改)。
为了回答我们的研究问题,我们采用定性和定量相结合的方法,该方法结合了以下几种来源的数据:在与Google从事软件开发工作的员工进行的半结构化访谈,来自代码审查工具的日志以及对其他员工的调查。我们使用访谈作为一种工具来收集有关进行代码审查(RQ1)动机的多样性(与频率相对比)的数据,并激发开发人员对代码审查及其挑战(RQ3)的理解。 我们使用CRITIQUE的日志来量化和描述当前的审查实践(RQ2)。最后,我们使用调查来确认访谈(RQ1)中出现的代码审查的多种动机,和激发开发人员对过程的满意度的因素。
会谈。我们与选定的Google员工进行了一系列面对面的半结构化访谈,每次访谈大约需要1个小时。最初的可能参加者是使用滚雪球采样法来选择的,首先是论文作者所知道的开发人员。从此库中,选择参与者以确保团队的分散,技术领域,工作角色,公司内部的时间长度以及在代码审核过程中的角色。访谈脚本包括有关代码审查的动机,最近审查/撰写的变更,以及最佳/最差审查经历的问题。 在每次访谈之前,我们都会回顾参与者的审查历史,并找到要在访谈中会讨论的更改; 我们选择这些更改,是根据互动次数,参与对话的人数,以及是否有很多令人惊讶审查点评。在访谈的观察部分中,要求参与者在审阅即将发生的变更时思考,并提供一些明确的信息,例如开始审阅的切入点。 访谈一直持续到达到饱和,并且访谈提出了大致相似的概念。 总体而言,我们对从Google工作1个月到10年(平均5年),软件工程和站点可靠性工程的员工进行了12次面试。他们包括技术主管,经理和个人贡献者。每次访谈涉及三到四个人:参与者和2-3个受访者(其中两个是本文的作者)。采访由一名采访者实时转录,而另一名采访者提出问题。
采访数据的开放编码。为了确定采访数据中出现的广泛主题,我们进行了一次开放编码。 两位作者讨论了访谈笔录,以确立共同的主题,然后将其转换为编码方案。 然后,另一位作者对讨论的注释进行了封闭编码,注释是对确认的主题。我们对其中不止一个采访进行了迭代,直到我们就该计划达成协议。 我们还跟踪了上下文(审稿人与作者之间的关系)中提到的这些主题。问题设计和分析过程的结合意味着我们可以讨论结果中的稳定主题,但不能有意义地讨论发生的相对频率。
审查数据的分析。我们定量的分析数据,数据是代码审查过程中使用CRITIQUE产生的日志。我们主要关注Rigby和Bird发现的融合实践(CP)相关的指标。为了方便对比,我们不考虑没有审查者的更改,因为我们对有明确代码审查过程的更改感兴趣。我们将“审阅者”视为批准代码更改的任何用户,而不论更改作者是否明确要求他们进行审阅。我们使用基于名称的启发式的方法来自动化流程产生的更改。我们专门关注Google主要代码库中发生的更改。我们还排除了在研究时尚未落实的更改,以及我们的差异工具报告的源码零行变化量的更改,例如,仅修改二进制文件的更改。在Google,平均每个工作日,提交了约20,000个符合上述过滤条件的更改。 我们的最终数据集包括2014年1月至2016年7月,由25,000多名作者和审阅者创建的符合这些标准的大约900万项更改,以及从2014年9月至2016年7月之间的所有更改中收集的大约1300万条审查评论。
调查。我们创建了一个在线调查表,并发送给了98位最近提交了代码更改的工程师。代码更改已经过审核,因此我们定制了调查表,以询问受访者如何看待代码审核,关于他们最近的特定更改;这种策略使我们可以减轻召回偏见,但仍可以收集全面的数据。 该调查包括三个关于收到的评论的价值的Likertscale问题,一个关于评论对其更改影响的多项选择(基于访谈产生的期望)和一个可选的“其他”回答,以及一个开放式- 最终提出质疑,以引起受访者对所收到的评论,代码评论工具和/或整个过程的意见。 我们收到了44份有效的调查问卷答复(45%的答复率,在软件工程研究中被认为很高了)。
我们描述了研究方法所带来的对有效性和工作成果局限性的威胁,以及为缓解这些挑战所采取的行动。
内部有效性——可信度。关于评论数据的定量分析,我们使用启发式方法从定量分析中滤除机器人撰写的更改,但这些启发式方法可能允许某些机器人撰写的更改; 我们对此进行了缓解,因为我们仅包括具有人工审核者的由机器人撰写的更改。关于定性调查,我们使用了开放式编码来分析受访者的答案。该编码可能会受到编写该编码的作者的经验和动机的影响,尽管会通过让多个编码人员参与来减轻这种偏见。决定参加我们的访谈并自由选择调查的员工决定这样做,从而引入了自我选择偏见的风险。 因此,对于不选择参与的开发人员而言,结果可能会有所不同;为减轻此问题,我们将访谈和调查中的信息相结合。 此外,我们使用雪球抽样方法来确定要面试的工程师,这有抽样偏差的风险。尽管我们试图通过面试具有各种工作角色和职责的开发人员来减轻这种风险,但我们访谈的开发人员可能有其他因素在整个公司中并不适用。 为了减轻主持人的接受偏见,参与定性数据收集的研究人员不属于CRITIQUE团队。 社会可取性偏见可能已经影响了答案,使其更适合Google文化。 但是,在Google鼓励人们批评和改进发现的工作流程,从而减少这种偏见。 最后,我们没有采访与专家评审员(例如安全评审)进行交互的研究科学家或开发人员,因此我们的结果偏向于一般开发人员。
通用性——可移植性。我们的结果可能无法推广到其他情况,而是我们对多年实践和数百万次细化检查后,仍会发生的多种多样的做法和审查中断感兴趣。鉴于基本代码检查机制在多个公司和OSS项目中的相似性, 有理由认为,如果审查过程达到相同的成熟度并使用可比较的工具,则开发人员将具有类似的经验。
在我们的第一个研究问题中,我们首先要研究导致这一过程的原因,从而寻求理解开发人员在Google进行代码审查时的动机和期望。
Google的代码审查最早是由第一批员工之一引入的; 本文的第一作者采访了该员工(以下简称 E),以更好地理解代码审查及其演变的最初动机。E 解释了代码审查引入的主要推动力是:迫使开发人员编写其他开发人员可以理解的代码 ; 这被认为很重要,因为代码必须作为未来开发人员的老师。Google在代码审查中的引入标志着从研究代码库(已优化为快速原型开发)向生产代码库的过渡,在此基础上考虑未来工程师阅读源代码。代码审查也被认为能够确保不止一个人熟悉每一段代码,从而增加了知识在公司中的驻留机会。
E 重申了这样一个概念,即尽管审阅者发现错误是很棒的,但在Google引入codereview的首要原因是为了提高代码的可理解性和可维护性。但是,除了最初进行代码审查的教育动机外,E 解释说,开发人员的三个其他好处很快就在内部对开发人员变得显而易见:检查样式和设计的一致性;确保足够的测试;通过确保没有任何开发人员可以在没有监督的情况下提交任意代码来提高安全性。
通过对访谈数据进行编码,我们确定了Google开发人员期望从代码审查中获得的四个关键主题:教育,维护规范,把关和事故预防。 教育从代码审查中学习或学习,并与引入代码审查的最初原因保持一致; 规范是指组织对自由选择的偏好(例如格式或API使用模式); 网守涉及围绕源代码,设计选择或其他工件的边界的建立和维护; 事故是指引入错误,缺陷或其他与质量相关的问题。
这些是审查过程中的主要主题,但是代码审查也用于追溯历史。开发人员在审查过程完成后对其进行评估;代码审查可以浏览历史记录 代码更改的内容,包括发生了什么注释以及更改如何演变。 我们还注意到开发人员使用代码回顾历史来了解错误的引入方式。 从本质上讲,代码审查使将来的变更审核成为可能。
在我们的调查中,我们进一步验证了这种编码方案。 他们可以选择四个主题中的一个或多个主题和/或自己撰写。 较早确定的四个主题中的每个主题都是在特定代码审查的背景下由8至11个受访者选择的,因此,可以更加确信上述编码方案与开发人员对代码审查价值的理解相一致。
尽管这些期望可以覆盖以前在Microsoft [4]上获得的期望,但正如我们的参与者所解释的那样,Google的主要重点是教育以及代码的可读性和可理解性,这与历史动因相吻合。 因此,关注点与Rigby和Bird的关注点不一致(即,小组解决问题的活动)[33]。
如前所述,在对访谈笔录进行编码时,我们还跟踪了提到主题的评论上下文,我们发现这些不同主题的相对重要性取决于作者与评论者之间的关系(图1)。例如,维护工程师与具有不同资历的工程师(项目负责人,专家可读性审阅者或“新”团队成员)之间的规范冲突,而与同伴或其他团队相比则少一些,而看门人和事故预防则是主要的。具有广泛的价值,并包含多种不同的关系。
图1. 关系图,描述了哪些评论期望主题主要出现在特定作者/评论者上下文中。
在我们的第二个研究问题中,我们描述了代码重审过程,并将其定量方面的内容与先前工作中发现的趋同做法进行了比较[33]。
Google的代码审查与两个概念相关:所有权和可读性。 我们首先介绍它们,然后描述审阅过程的流程,然后得出内部审阅工具CRITIQUE与其他审阅工具不同的特点。
所有权。Google代码库以树结构排列,其中每个目录都由一组人员明确拥有。 尽管任何开发人员都可以提议对代码库的任何部分进行更改,但是相关目录(或父目录)的所有者必须在提交更改之前对其进行审核和批准; 甚至目录所有者也要在提交之前检查其代码。
可读性。Google定义了一个称为可读能力的概念,该概念很早就引入了,以确保代码库中的代码风格和规范保持一致。 开发人员可以使用特定语言获得可读性认证。 为了应用可读性,开发人员将更改发送给一组有可读能力的审阅者。 一旦这些审阅者确信开发人员了解某种语言的代码风格和最佳实践,便会为开发人员授予该语言的可读性。 每次更改都必须由具有所使用语言可读性证明的人员编写或审阅。
代码审查流程。审查流程与评论紧密结合,其工作方式如下:
1。 创建:作者开始修改,添加或删除某些代码; 一旦准备好,他们就会进行更改。
2。 预览:作者然后使用CRITIQUE来查看更改的差异,并查看自动代码分析器的结果(例如,来自Tricorder [36])。 准备就绪后,作者将更改发送给一个或多个审阅者。
3。 评论:审阅者可以在Web UI中查看差异,并随时起草评论。 程序分析结果(如果存在)也对审阅者可见。未解决的评论显示为变更作者必须解决的操作项目。已解决的评论包括可选或信息性评论,可能不需要变更作者采取任何行动。
4。 解决反馈:作者现在可以通过更新更改或通过回复评论来处理注释。更新更改后,作者将上载新快照。 作者和审阅者可以查看任意一对快照之间的差异,以了解发生了什么变化。
5。 批准:解决所有评论后,评论者会批准该更改并将其标记为“ LGTM”(对我来说很好 Looks Good To Me)。 要最终进行更改,开发人员通常必须至少获得一名审阅者的批准。通常,只需一名审阅者即可满足上述所有权和可读性要求。
我们尝试量化“轻量级”审阅的方式(CP1)。 我们通过检查变更作者邮寄了一组可解决以前未解决的注释的评论来衡量评论中来回的次数。我们假设一个迭代对应于一个作者解决某个评论的一个实例; 零重复意味着作者可以立即提交。我们发现所有更改中有80%以上最多涉及解决评论的重复。
建议审阅者。要确定最佳的人来重新审阅更改,CRITIQUE依靠一种工具来分析变更并建议可能的审阅者。 此工具确定满足更改中所有文件的审阅要求所需的最小审阅者集。 请注意,通常只需要一名审阅者,因为更改通常是由拥有文件查询所有权和/或可读权的人创作的。 该工具对最近编辑和/或审阅所包含文件的审阅者进行优先级排序。 由于尚未建立新的团队成员,因为他们尚未建立审核/编辑历史记录,因此已明确添加为他们。 未分配的审阅者还可以对更改发表评论(并可能批准)。寻找审阅者的工具支持通常仅在文件更改超出特定团队的情况下才需要。 在一个团队内,开发人员知道向谁发送更改。 为了将可能发送给团队中任何人的更改,许多团队使用一种系统,该系统将循环发送到团队电子邮件地址的审阅分配给配置的团队成员,同时考虑到审阅负载和休假。
代码分析结果。CRITIQUE将代码分析结果显示为注释以及人工注释(尽管颜色不同)。分析人员(或审阅者)可以提供建议的编辑,这些编辑可以被提议,也可以通过评论应用于变更。 为了在更改提交之前审核更改,Google的开发还包括预提交挂钩:检查失败需要开发人员显式覆盖以启用提交的地方。 提交前检查包括基本的自动样式检查和运行与变更相关的自动测试套件。 所有预提交检查的结果在代码查看工具中可见。 通常,会自动触发预提交检查。 这些检查是可配置的,以便团队可以强制实施特定于项目的不变量,并自动将电子邮件列表添加到更改中,以提高意识和透明度。 除了预先提交结果外,CRITIQUE还可以通过Tricorder [36]显示各种自动代码分析的结果,这些分析可能不会阻止提交更改。 分析结果包括简单的样式检查,更复杂的基于编译器的分析通过以及特定于项目的检查。 目前,Tricorder包括110个分析仪,其中5个是用于数百次附加检查的插件系统,总共可分析30多种语言。
我们复制了Rigby和Bird发现的CP2-4的定量分析,以便将这些实践与Google融合的特征进行比较。
审查频率和速度。Rigby和Bird发现快节奏的迭代开发也适用于现代代码审查:在他们的项目中,开发人员的工作间隔非常短。 为了找到答案,他们分析了评论的频率和速度。
在Google,就频率而言,我们发现处于中位数上的开发者每周大约进行3次更改,而80%的开发者每周进行少于7次更改。 同样,开发人员每周审核的变更中位数为4,而80%的审阅者每周审核的变更少于10。 在速度方面,我们发现开发人员必须等待对其更改的初步反馈,对于较小的更改,平均时间少于一小时,对于较大的更改,平均时间少于5小时。 整个审阅过程的总体(所有代码大小)中值延迟小于4小时。 这比Rigby和Bird [33]报告的平均批准时间要低得多,AMD的批准时间中位数为17.5小时,Chrome OS为15.7小时,三个Microsoft项目为14.7、19.8和18.9小时。 另一项研究发现,微软批准的平均时间为24小时[14]。
审查规模。Rigby和Bird认为,只有通过较小的变更来审查并随后分析审查规模,才能实现快速审查时间。在Google,正在考虑的更改中,超过35%仅修改一个文件,而大约90%的修改少于10个文件。超过10%的更改仅修改一行代码,而修改的行数的中位数为24。更改位数的中位数显着低于Rigby和Bird对AMD(44行),Lucent(263行)和Bing等公司的报告。 ,Microsoft的Office和SQLServer(在这些界限之间的某个位置),但符合开放源代码项目中的更改大小[33]。
审查者和评论的数量。甚至在经过深入研究的代码检查中,研究人员的最佳人数一直存在争议[37]。 Rigby和Bird调查了所考虑的项目是否收敛到了类似数量的参与评审人员。 他们发现这个数字是两个,无论是否明确邀请了审阅者(例如,在Microsoft项目中,邀请的中位数最多为4个审阅者),或者是否公开广播了更改以进行审阅[33]。
相比之下,在Google中,只有不到25%的更改拥有多于一名审阅者,而超过99%的更改最多具有五名审阅者,中位审阅者人数为1。较大的更改通常平均会拥有更多的审阅者。 但是,即使平均变化非常大,平均也需要不到两名审稿人。
Rigby和Bird还发现“当活跃于[超过2]位审稿人时,有关更改的评论数量最少” [33],并得出结论,两名审稿人发现缺陷的最佳数量。 在Google,情况有所不同:审阅人数越多,对更改的评论平均数就越多。 此外,每次更改的平均注释数随行数的变化而增加,对于大约1250行的更改,每个更改的最高注释数为12.5。 大于此的更改通常包含自动生成的代码或较大的删除,从而导致平均注释数较低。
我们最后一个研究问题是通过Google的代码审查来调查开发人员的挑战和满足感。
以前的研究调查了整个审查过程中的挑战[4,26],并提供了令人信服的证据,这也被我们作为工程师的经验所证实,理解要审查的代码是一个主要障碍。 为了拓宽我们的经验知识体系,我们在这里集中讨论特定审查(“审查中断”)中遇到的挑战,例如延误或分歧。
对我们的访谈数据的分析提出了五个主要主题。 前四个主题认为审查中断在过程中的相关因素有:
距离:受访者从两个角度感知代码审阅的距离:地理(即作者与审阅者之间的物理距离)和组织(例如不同团队或不同角色之间的物理距离)。这两种类型的距离都被认为是导致审阅过程延迟或导致误解的原因。
社会互动:受访者认为代码审阅中的交流有可能从两个方面导致问题:措辞和权力。 措辞是指有时作者对评论发表敏感的事实。 对评论的情感分析提供了证据,表明带有负面语气的评论不太可能有用[11]。 权利是指使用代码审查过程来诱使他人改变自己的行为; 例如,拖延审核或保留批准。 措辞或权利在审查中,可能会使开发人员对检查过程感到不舒服或沮丧。
审查主题:访谈提到了关于代码审查是否是重新审查某些方面的最合适上下文(尤其是设计审查)的分歧。 这导致期望值不匹配(例如,某些团队希望大多数设计在第一次审阅之前完成,其他团队希望在审阅中讨论设计),这可能导致参与者之间以及过程中产生摩擦。
背景:受访者让我们看到,由于不知道是什么导致了这种变化,所以会产生误解; 例如,如果变更的理由是解决生产问题的紧急解决方案或“有个不错的改进”。 预期结果的不匹配会导致延迟或沮丧。
最后一个主题是工具本身:
定制化:一些团队对代码审查有不同的要求,例如,关于需要多少审查者。 这是技术上的审查中断,因为批评中并不总是支持任意定制,并且可能引起对这些政策的误解。 根据反馈,CRITIQUE最近发布了一项新功能,该功能允许更改作者要求所有审阅者签名。
为了了解已确定问题的重要性,我们使用了调查的一部分来调查代码审查是否总体上被认为是有价值的。
我们发现(表2)在Google内部代码审查被普遍认为是有价值和有效的–所有受访者都同意代码审查很有价值的说法。我们对CRITIQUE进行的内部满意度调查反映了这种观点:97%的开发人员对此感到满意。
在特定变化的背景下,情绪变化更大。最不满意的答复与很小的更改(1个字或2行)或与实现某些其他目标所需的更改(例如,从源代码的更改触发过程)相关。但是,大多数受访者认为,他们所做的更改的反馈量是适当的。在这3个中,有8位受访者认为注释无济于事,并指出,所审查的更改是小的配置更改,对代码审核没有影响。只有2位受访者表示评论中有bug。
bad | good | ||||
---|---|---|---|---|---|
对于此更改,审核过程很好地利用了我的时间 | 2 | 4 | 14 | 11 | 13 |
总的来说,我认为Google的代码审查很有价值 | 0 | 0 | 0 | 14 | 30 |
对于此更改,反馈量为 | 2 | 2 | 34 | 5 | 0 |
表2. 用户满意度调查结果
为了根据满意度对答案进行情境化,我们还调查了开发人员花费在审阅代码上的时间。为了准确量化审阅者所花费的时间,我们跟踪了开发人员与CRITIQUE的互动(例如,打开选项卡,查看了差异,评论,批准了更改),以及其他工具来估算开发人员每周花费多长时间来审核代码。我们将开发人员交互的顺序分组为一定的时间段,将“审阅会话”视为与变更提交者以外的其他开发人员进行的,与同一未提交变更相关的交互顺序,每次相隔不超过10分钟。 从2016年10月开始的五周内,所有审核会话所花的总小时数,然后计算每周每位用户的平均值,过滤出我们在过去五周内都没有数据的用户。 我们发现开发人员平均花费3.2(平均每周2.6个小时)来审查更改。 与OSS项目的6.4小时/周的自我报告时间相比,这个数字很低[10]。
我们讨论了这项调查中出现的主题,这些主题可以启发从业人员建立代码审查流程,并激发研究人员在未来的调查中。
现代代码审查的诞生是它减轻了繁琐的代码检查的负担[4]; 实际上,Rigby和Bird在他们对整个系统的调查中都证实了这一特征(CP1)。 在Google,代码审查已汇聚到一个更加轻量级的过程,开发人员发现该过程既有价值又可以很好地利用他们的时间。
Google的审查时间中位数比其他项目要短得多。 我们假定这些差异是由于Google在代码审查方面的文化(严格的审查标准和对快速审阅时间的期望)。 此外,审稿人人数也有很大差异(其中一个在Google中,而其他两个在其他项目中);我们认为拥有一名审阅者可以使审阅变得快速而轻便。
审阅时间短和审阅者人数少可能是由于代码审阅是开发人员工作流程中必不可少的一部分;它们也可能源于小的更改。 OSS项目的中位数变化范围从11到32行变化,具体取决于项目。 在公司中,此更改大小通常较大,有时高达263行。 我们发现Google的更改大小与OSS更接近:大多数更改很小。变更的大小分布是代码审查过程质量的重要因素。 先前的研究发现,随着更改大小的增加,有用评论的数量会减少,审阅延迟会增加。 大小也会影响开发人员对代码审查过程的理解; Mozilla投稿人的调查发现,开发人员认为与大小相关的因素对审核延迟的影响最大。 Google确认变更大小与评论质量之间的相关性,并强烈鼓励开发人员进行小的增量更改(大删除和自动重构除外)。 这些发现和我们的研究支持审查小的更改的价值以及对研究和工具的需求,以帮助开发人员创建如此小的独立代码更改以进行审查。
Google进行代码审查的部分做法与软件工程研究中提出的做法保持一致。 例如,微软公司对代码所有权的一项研究发现,应认真审查较小贡献者所做的更改,以提高代码质量。 我们发现,此概念是在Google上通过要求所有者批准而强制实施的。 同样,以前的研究表明,通常有一个变更的审阅者将负责检查代码是否符合常规。 可读性使此过程更加明确。 在下文中,我们将重点介绍使其成为“下一代代码审查工具” 的CRITIQUE的功能。
审查者的建议。研究人员发现,对审稿代码具有先验知识的审稿人会提供更多有用的评论,因此工具可以为审稿人选择提供支持。我们已经看到,审阅者推荐功能受工具支持,从而优先考虑那些最近编辑/审阅受审文件的人员。这证实了最近的研究,即频繁的审稿人对模块的发展做出了巨大的贡献,应与频繁的编辑者一并纳入。在Google中,实际上,找到合适的审稿人似乎没有问题,实际上,实施推荐的模型很简单,因为它可以以编程方式识别所有者。这与其他用于标识审阅者的提议的工具相反,审阅者已经审阅了具有相似名称的文件或考虑了诸如审阅中包含的评论数量之类的功能。 Google的工作重点是处理审稿人的工作量和暂时缺勤(与Microsoft的研究一致)。
静态分析集成。对88位Mozilla开发人员进行的定性研究发现,静态分析集成是代码审查最常用的功能。 自动进行的分析使审阅者可以专注于更改的可理解性和可维护性,而不会因为琐碎的评论(例如关于格式)而分心。 我们在Google的调查向我们展示了在代码审阅工具中进行静态分析集成的实际含义。CRITIQUE为分析作者集成了反馈渠道:审阅者可以选择在分析生成的评论上单击“请修复”,以表示作者应修复该问题,作者或审稿人都可以单击“无用”以标记对审阅过程无用的分析结果。具有较高“无用”点击率的分析仪已固定或禁用。我们发现,这种反馈循环对于维持开发人员对分析结果的信任至关重要。
协作审查之外的审查工具。最后,我们找到了有力的证据,表明CRITIQUE的使用超出了审查代码。 变更作者使用CRITIQUE来检查差异并浏览分析工具的结果。 在某些情况下,代码审查是变更开发过程的一部分:一个审查者可能会发送未完成的变更,以便决定如何完成实施。此外,开发人员还使用CRITIQUE来检查提交的更改的历史,只要这些更改被批准即可。 这与Sutherland和Venolia设想的将代码审查数据用于开发的有益用法相一致。 将来的工作可以调查代码审查工具的这些意外的和潜在的有影响的非审查使用。
知识转移是Rigby和Bird提出的主题。 为了衡量由于代码审查而导致的知识转移,他们从先验工作的角度出发,通过测量变更,审查和两个集合的不同文件数来衡量专业知识,以更改的文件数为依据 。他们发现开发人员通过代码审查了解更多文件。
在Google,知识转移是代码审查教育动机的一部分。 我们试图通过查看评论和编辑/审阅的文件来量化此效果。 随着开发人员积累了在Google工作的经验,他们对其更改发表的评论平均减少了(图2)。在过去一年内开始工作的Google开发人员,每次更改的注释通常多于两倍。先前的工作发现,作者认为来自审阅者的注释无用,而无用注释的数目则随着经验的增加而减少。 我们假设评论的减少是由于审阅者在使用代码库建立谱系关系时需要询问较少的问题的结果,并佐证了代码审阅的教育方面可能随着时间的流逝而得到回报的假说。此外,我们可以看到, 由Google的工程师编辑和审查的文件,以及这两个集合的结合,随着资历的增加而增加(图3),并且看到的文件总数明显大于编辑的文件数。 在公司工作(以3个月为增量),然后计算他们已编辑和审阅的文件数。在以后的工作中,更好地了解审阅文件如何影响开发人员的流利性将是很有意思的。
图2. 审查者的评论和开发者在Google的任职年限
图3. 全职员工随时间查看(编辑或审阅,或两者都有)的不同文件的数量。
我们的研究发现,代码审查是Google开发工作流程的重要方面。担任所有职务的开发人员都将其视为提供多种好处的环境,并且在此上下文中,开发人员可以相互学习代码库,维护团队代码库的完整性以及搭建,建立和发展确保代码库可读性和一致性的规范。开发人员报告说,他们对审查代码的要求感到满意。大部分更改很小,只有一名审阅者,除了提交授权外没有其他评论。在一周中,有70%的更改在邮寄出去进行初审后不到24小时内就会提交。这些特征使代码审查比其他采用类似过程的项目更轻便。此外,我们发现Google在其实践中包含了一些研究思想,从而使当前研究趋势的实践意义显而易见。
原文地址:https://dl.acm.org/doi/10.114...
查看原文使用基于工具的轻量级代码检查代码更改(又名现代代码审查)已成为广泛的规范,应用于各种开源和产业系统。在本文中,我们对Google的现代代码审查进行了探索性研究。 Google很早就引入了代码审查并经过多年的发展;我们的研究揭示了为什么Google引入了这种做法并分析...
赞 11 收藏 4 评论 0
LeapFE 发布了文章 · 2020-10-30
图”由节点和边组成。上面的地铁线路图中从“芍药居”出发到“太阳宫”需要3分种可以用下“图”表示。“图”中描述了“A”和“B”互为邻节点,其中3代表从节点“A”到“B”那条边的权重,边有权重的图称为“加权图”,不带权重的图称为“非加权图”。边上的剪头代表只能从A到B且需要的成本为3,这种边代有方向的图称为“有向图”。
如果“A”能到“B”同时“B”也可以到”A”且成本同样为3则称为“无向图”
如果存在节“C”使得“A”到 “B”,“B”可以到“C”,“C”又可以到“A”则称“A”、“B”、“C”为一个“环”。 无向图中每一条边最可看为一个环。
2.算法流程
有如下“有向加权图”, 我们要从“起点”出发到“终点”。
首先需要四个表,用于存储相关信息。
表一: 用于存储“图”信息 “图”信息
表二: 用于存储每结点从起点出发的最小成本, 开始时只有“起点”成本为0
表三:最小开销路径上每结点的父结点
表四:记录结点处理状态
算法流程如下:
1) 从表二及表四中找出最小开销的未处理节点,开始时只有“起点”
2) 从表一中看到从起点出发可以到达A和B开销分别为5和3,更新表二
3) 更新表三记录当前到达A、B点的最小开销父结点为起点
4) 更新表四记录已处理过起点,完成对一个节点的处理
5) (第二轮)从表二及表四中找出未处理过的最小开销的节点“B”(到达成本3)
6) 从表一中看到从B出发可以到达节点A和终点开销分别为1和4
7) 记录B点已处理过
8) (第三轮)从表二及表四中找出未处理过的最小开销的节点“A”
9) 从点A出表可到达终点,点A当前最小到达成本为4 加上A到终点的开销1小于表二中终点当前的最小开销,所以更新表二中终点的开销为5 并更新表三中终点父节点为A
10) 记录A点已处理
11) (第四轮) 从表二及表四中找出未处理过的最小开销的节点:“终点“
12) 由于终点无指向结点无需再处理,支接标记已处理完成终点
13) (第五轮)已无未处理结点完成操作
14) 最终结果
从表二中我们知道终点的最小到达开销为5
从表三中我们可以从终点的父结点一路推出最小开销路径为: 终点 < A < B < 起点
4.代码实现(TypeScript)
/** * 狄克斯特拉查找结果 */
export interface DijkstraFindResult<T_node> {
/** 差找的图 */
graph: Map<T_node, Map<T_node, number>>;
/** 开始节点 */
startNode: T_node;
/** 结束节点 */
endNode: T_node;
/** 是否找到 */
isFind: boolean;
/** 最小成本路径节点链*/
parents: Map<T_node, T_node>;
/** 结果路径 */
path: T_node[];
/** 每节点最小到达成本 */
arriveCosts: Map<T_node, number>;
}
/**
* 查找未处理过的最小成本节点
* @param costs key:节点信息, value:当前到达成本
* @param processed key:节点信息 value: 是否已处理过
*/
function findMinCostNode<T_node>( costs: Map<T_node, number>, processed: Map<T_node, boolean>): T_node | null {
var minCost: number = Number.MAX_VALUE;
var minCostNode: T_node | null = null;
for (const [node, cost] of costs) {
if (cost < minCost && !processed.get(node)) {
minCost = cost;
minCostNode = node;
}
}
return minCostNode;
}
/**
* 返回从开始节点到结束节点路径
* @param endNode 结束节点
* @param parents key:节点A value:节点A父节点
*/
function getPath<T_node>( endNode: T_node, parents: Map<T_node, T_node>): T_node[] {
let path = [endNode];
let nParent = parents.get(endNode);
while (nParent) {
path.push(nParent);
nParent = parents.get(nParent);
}
path.reverse();
return path;
}
/**
* 狄克斯特拉查找(找出成本最短路径)
* - 用于加权(无负权边)有向图无环图
* @param graph 要查找的"图", Map<节点 ,Map<相邻节点,到达成本>>
* @param startNode 开始节点
* @param endNode 结束节点
*/
export function dijkstraFind<T_node>(
graph: Map<T_node, Map<T_node, number>>,
startNode: T_node,
endNode: T_node): DijkstraFindResult<T_node> {
/** 到节点最小成本 * k:节点 * v:从出发点到节点最小成本 */
let arriveCosts: Map<T_node, number> = new Map();
/** 最小成本路径父节点 k:节点A v: 节点A在最小成本路径上的父节点 */
let parents: Map<T_node, T_node> = new Map();
/** 已处理节点 k: 节点 v: 是否已处理过 */
let processedNode: Map<T_node, boolean> = new Map();
// 设置起点成本为零
arriveCosts.set(startNode, 0);
// 当前节点
let currentNode: T_node | null = startNode;
// 当前节点到达成本
let currentNodeCost: number = 0;
// 当前节点邻节点
let neighbors: Map<T_node, number>;
let isFind: boolean = false;
while (currentNode) {
// 标记是否找到目标结点
if (currentNode === endNode) isFind = true;
// 这里costs中一定会有node对映值所以强制转型成number
currentNodeCost = <number>arriveCosts.get(currentNode);
neighbors = graph.get(currentNode) || new Map();
//遍历邻节点更新最小成本
for (const [neighborNode, neighborCost] of neighbors) {
// 邻节点之前算出的最小到达成本
let tmpPrevMinCost = arriveCosts.get(neighborNode);
let prevCost: number = tmpPrevMinCost === undefined ? Number.MAX_VALUE : tmpPrevMinCost;
// 邻节点经过当前节点的成本
let newCost = currentNodeCost + neighborCost;
// 如果经当前结点成本更小,更新成本记录及邻节点最小成本路径父结点
if (newCost < prevCost) {
arriveCosts.set(neighborNode, newCost);
parents.set(neighborNode, <T_node>currentNode);
}
}
// 记录已处理结点
processedNode.set(<T_node>currentNode, true);
// 找出下一个未处理的可到达最小成本结点
currentNode = findMinCostNode(arriveCosts, processedNode);
}
// 从起始点到终点路径
let path: T_node[] = [];
if (isFind) {
path = getPath(endNode, parents);
}
return {
isFind: isFind,
path: path,
graph: graph,
arriveCosts: arriveCosts,
parents: parents,
startNode: startNode,
endNode,
};
} //eof dijkstraFind
// 测试
function objToMap(obj: any): Map<string, number> {
let map: Map<string, number> = new Map();
for (let k in obj) {
map.set(k, obj[k]);
}
return map;
}
/** 图 */
const graph: Map<string, Map<string, number>> = new Map();
graph.set("start", objToMap({ a: 5, b: 3 }));
graph.set("a", objToMap({ end: 1 }));
graph.set("b", objToMap({ a: 1, end: 4 }));
graph.set("end", new Map());
let result = dijkstraFind(graph, "start", "end");
console.log(result);
// 输出
/*
{
isFind: true,
path: [ 'start', 'b', 'a', 'end' ],
graph: Map {
'start' => Map { 'a' => 5, 'b' => 3 },
'a' => Map { 'start' => 5, 'end' => 1, 'b' => 1 },
'b' => Map { 'start' => 3, 'end' => 4, 'a' => 1 },
'end' => Map { 'a' => 1, 'b' => 4 }
},
arriveCosts: Map {
'start' => 0,
'a' => 4,
'b' => 3,
'end' => 5
},
parents: Map {
'a' => 'b',
'b' => 'start',
'end' => 'a'
},
startNode: 'start',
endNode: 'end'
}
*/
把上例中的“图”看成一个地换线路图:现在我们要人A站到D站
将狄克斯特拉算法应用于地铁图对比上面的例子有几个问题.
问题1: 地铁为一个无向图,如A可以到B,B也可以到A ,所以描述图信息时双向的图信息都 要录入,如:
问题2:图中第条边都是一个环,且如A,B,C也可组成一个环是否会对结果产生影响?
不会,因为算法中每次选出的处理节点都是到达成本最小的节点,只有从这个节出发到下一个节点成本更底时才会更新最小成本表和父节点表,且处理过的结点不会再次处理。
问题3: 如何处理换乘线路用时问题?
如:1号线换5号线需要2分种, 5号线换2号线要1分钟。
上图中我们可以看出不考虑换乘从A到D的最少用时路径为:
A > B > C > D
如果算上换乘线路时间最短用时路径为:
A > C > D
那么如何处理呢?我们可以把换乘站内的换乘路径看成一个局部的图并将其嵌入地铁图中,如:
上图中B结点由 B_A,B_D, B_C 三个结点代替。其中 B_A到B_C,B_D 到B_C 权重相同(也可以不同)代表从1号线换5号线用时2分钟,B_A到B_D权重为0代表从A经由B到D不需要换乘。将上图作为新的算法输入数据就可算出考虑换乘用时的最少用时路径。
参考:
《算法图解》【美】Aditya Dhargava
注:
狄克斯特拉算法部分主要参考算法图解
查看原文图”由节点和边组成。上面的地铁线路图中从“芍药居”出发到“太阳宫”需要3分种可以用下“图”表示。“图”中描述了“A”和“B”互为邻节点,其中3代表从节点“A”到“B”那条边的权重,边有权重的图称为“加权图”,不带权重的图称为“非加权图”。边上的剪头代表只能从A到B且需要的成本...
赞 8 收藏 7 评论 0
LeapFE 发布了文章 · 2020-10-30
QuickType 是一款可以根据 json 文本生成指定语言(如 Type Script,C++,,Java,C#,Go 等)类型声明代码的工具。
例如我们在写接口调用处理收到响应数据的逻辑时一般分为如下两步: 1.根据接口返回的 JSON 格式写一个对应的类型 2.写 JSON 格式验证与解析逻辑来根据收到的数据生成对应的类对象
使用 QuickType 工具就可以根据 JSON 文本帮助我们自动生成以上两部分的代码。
以如下 JSON 为例:
使用 QuickType 生成 TypeScript 语言的接口声明代码如下:
可以通过桌面应用、web 页、 IDE 插件、命令行 4 种方式使用 QuickType 工具。
其中 web 页(https://app.quicktype.io/ 可能被墙)和桌面应用使用方式基本一致这里不做介绍。
打开 App Store 搜索 “Paste JSON as Code”下载安装即可
软件使用很简单,软件时时生成目标代码,按如下步骤操作:
QuickType 提供了 Xcode,VSCode, Visual Studio 三种开发工具的扩展。下载地址如下:
下面以 VSCode 扩展的安装与使用为例
vscode 中 Paste JSON 有两种使用方式。
# 查看帮助
quicktype
# json字符串生成C# 声明
echo '{ "name": "David" }' | quicktype -l csharp
# json字符串生成Go类声名文件 ints.go
echo '[1, 2, 3]' | quicktype -o ints.go
# 从json文件生成swift类文件
quicktype person.json -o Person.swift
# 可选参数
quicktype \
--src person.json \
--src-lang json \
--lang swift \
--top-level Person \
--out Person.swift
# 从返回 JSON 的接口生成 java类文件
quicktype https://api.somewhere.com/data -o Data.java
查看原文QuickType 是一款可以根据 json 文本生成指定语言(如 Type Script,C++,,Java,C#,Go 等)类型声明代码的工具。
赞 3 收藏 2 评论 0
LeapFE 发布了文章 · 2020-10-28
本文档主要介绍四种工具的特点, 包括优点、缺点、 输入、输出、能够处理的文件类型,针对不同文件类型的处理方式, 以及其适用场景。
简介
Rollup 是一个模块打包工具, 可以将我们按照 ESM (ES2015 Module) 规范编写的源码构建输出如下格式:
<script>
标签加载RequireJS
加载Webpack
加载Webpack
, Rollup
加载优点:
支持动态导入。
支持tree shaking。仅加载模块里用得到的函数以减小文件大小。
Scope Hoisting。 rollup可以将所有小文件生成到一个大文件中,所有代码都在同一个函数作用域里:, 不会像 Webpack 那样用很多函数来包装模块。
没有其他冗余代码, 执行很快。除了必要的 cjs
, umd
头外,bundle 代码基本和源码差不多,也没有奇怪的 __webpack_require__
, Object.defineProperty
之类的东西,
缺点:
不支持热更新功能;对于commonjs模块,需要额外的插件将其转化为es2015供rollup 处理;无法进行公共代码拆分。
输入:
options.input 单/多文件入口点
输出:
rollup支持生成 iife、cjs、amd 、esm、umd格式的文件; 单/多js文件输出
文件资源处理:
rollup 通过插件来编译处理各类静态资源:
基本使用参考
https://www.cnblogs.com/tugenhua0707/p/8179686.html
适用场景:
由纯js开发的第三方库; 需要生成单一的umd文件的场景
纯js/ts编写的第三方库:
React、Vue
UI组件库 evergreen
使用 babel 将 js/ts 编译成 esm 和 cjs 格式的模块文件, 使用 rollup 将库打包成 umd 格式的 evergreen.min.js 和 evergreen.js , 打包出来的代码比较干净。
简介
前端构建工具,gulp是基于Nodejs,自动化地完成 javascript、coffee、sass、less、html/image、css 等文件的测试、检查、合并、压缩、格式化、浏览器自动刷新、部署文件生成,并监听文件在改动后重复指定的这些步骤。
借鉴了Unix操作系统的管道(pipe)思想,前一级的输出,直接变成后一级的输入,使得在操作上非常简单。
gulp基于流式操作,通过各种 Transform Stream 来实现文件不断处理 输出。
优点:
gulp文档简单,学习成本低,使用简单;对大量源文件可以进行流式处理,借助插件,可以对文件类型进行多种操作处理。
缺点:
不支持tree-shaking、热更新、代码分割等。 gulp 对 js 模块化方案无能为力,只是对静态资源做流式处理,处理之后并未做有效的优化整合。
输入:
输入(gulp.src) js,ts,scss,less 等源文件
输出:
对输入源文件依次执行打包(bundle)、编译(compile)、压缩、重命名等处理后输出(gulp.dest)到指定目录中去
适用场景:
静态资源密集操作型场景,主要用于css、图片等静态资源的处理操作。
文件处理:
gulp通过各种中间件处理静态资源的编译:
package.json
中的 gulp 插件案例:
antd
gulp + webpack + tsc / babel
gulp的作用主要是打包流程管理, 拷贝文件(less/ts/ts类型声明文件),处理less, 拷贝并转译less 为css。
tsc及babel 则用于转译 静态ts文件, 逐个输出到指定目录es/lib目录下
webpack主要用于模块化处理,将打包后的模块编译到 dist下的 antd.js antd.min.js 以及及其他css文件等。
简介:
Webpack 是一种前端资源模块化管理和打包 工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分割,等到实际需要的时候再异步加载。
优点:
基本之前gulp 可以进行的操作处理,现在webpack也都可以做。同时支持热更新,支持tree shaking 、Scope Hoisting、动态加载、代码拆分、文件指纹、代码压缩、静态资源处理等,支持多种打包方式。(优点有很多,在这不做过多赘述)
缺点:
不支持 打包出esm格式的代码 (打包后的代码再次被引用时tree shaking 困难), 打包后亢余代码多,配置较为复杂。
输入:
入口文件 js/ts
输出:
js、css、 img等静态资源文件
适用场景:
应用程序开发
案例:
react-bootstrap 使用babel进行tsx文件的编译,并且按照原有目录输出到 lib esm/cjs目录下;
同时使用shell 工具 拷贝 TS类型声明文件 到对应目录;
对于umd文件,则采用webpack打包生成了 react-bootstrap.min.js 及 react-bootstrap.js 输出到dist下。
打包umd方式非常简单,但文件中保留了许多webpack使用的到的冗余代码。生成效果不如上述 的 evergreen 纯净。
简介
tsc/babel 可以将 ts 代码编译 js 代码。支持编译成 esm、cjs、amd 格式的文件
优点:
编译速度快,可以保留原有的目录相对位置,分目录保存各模块的代码,便于按需引用加载;
缺点:
只对语言本身进行编译转换,不支持tree shaking 等高级功能。
输入:
ts/js 文件
输出:
ts/ts对应的js文件,且一一对应
案例分析:
tsc/babel常与其他工具配合使用
第三方js类库:
UI类库开发(按需加载)
生成esm tsc/babel + gulp
生成cjs tsc/babel + gulp
生成umd rollup (js + css的合并文件)
开发应用程序
webpack + loader + plugin
上述打包方式各有其特点,根据当前需求及开发便利,酌情选择打包编译方式。
查看原文本文档主要介绍四种工具的特点, 包括优点、缺点、 输入、输出、能够处理的文件类型,针对不同文件类型的处理方式, 以及其适用场景。Rollup简介Rollup 是一个模块打包工具, 可以将我们按照 ESM (ES2015 Module) 规范编写的源码构建输出如下格式:IIFE: 自执行函数, ...
赞 4 收藏 4 评论 0
LeapFE 赞了文章 · 2020-09-17
阿宝哥第一次使用 TypeScript 是在 Angular 2.x 项目中,那时候 TypeScript 还没有进入大众的视野。然而现在学习 TypeScript 的小伙伴越来越多了,本文阿宝哥将从 16 个方面入手,带你一步步学习 TypeScript,感兴趣的小伙伴不要错过。
TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。
TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件。下图显示了 TypeScript 与 ES5、ES2015 和 ES2016 之间的关系:
TypeScript | JavaScript |
---|---|
JavaScript 的超集用于解决大型项目的代码复杂性 | 一种脚本语言,用于创建动态网页 |
可以在编译期间发现并纠正错误 | 作为一种解释型语言,只能在运行时发现错误 |
强类型,支持静态和动态类型 | 弱类型,没有静态类型选项 |
最终被编译成 JavaScript 代码,使浏览器可以理解 | 可以直接在浏览器中使用 |
支持模块、泛型和接口 | 不支持模块,泛型或接口 |
社区的支持仍在增长,而且还不是很大 | 大量的社区支持以及大量文档和解决问题的支持 |
命令行的 TypeScript 编译器可以使用 npm 包管理器来安装。
$ npm install -g typescript
$ tsc -v
# Version 4.0.2
$ tsc helloworld.ts
# helloworld.ts => helloworld.js
当然,对刚入门 TypeScript 的小伙伴来说,也可以不用安装 typescript
,而是直接使用线上的 TypeScript Playground 来学习新的语法或新特性。通过配置 TS Config 的 Target,可以设置不同的编译目标,从而编译生成不同的目标代码。
下图示例中所设置的编译目标是 ES5:
(图片来源:https://www.typescriptlang.or...)
如你所见,在上图中包含 3 个 ts 文件:a.ts、b.ts 和 c.ts。这些文件将被 TypeScript 编译器,根据配置的编译选项编译成 3 个 js 文件,即 a.js、b.js 和 c.js。对于大多数使用 TypeScript 开发的 Web 项目,我们还会对编译生成的 js 文件进行打包处理,然后在进行部署。
新建一个 hello.ts
文件,并输入以下内容:
function greet(person: string) {
return 'Hello, ' + person;
}
console.log(greet("TypeScript"));
然后执行 tsc hello.ts
命令,之后会生成一个编译好的文件 hello.js
:
"use strict";
function greet(person) {
return 'Hello, ' + person;
}
console.log(greet("TypeScript"));
观察以上编译后的输出结果,我们发现 person
参数的类型信息在编译后被擦除了。TypeScript 只会在编译阶段对类型进行静态检查,如果发现有错误,编译时就会报错。而在运行时,编译生成的 JS 与普通的 JavaScript 文件一样,并不会进行类型检查。
let isDone: boolean = false;
// ES5:var isDone = false;
let count: number = 10;
// ES5:var count = 10;
let name: string = "semliker";
// ES5:var name = 'semlinker';
const sym = Symbol();
let obj = {
[sym]: "semlinker",
};
console.log(obj[sym]); // semlinker
let list: number[] = [1, 2, 3];
// ES5:var list = [1,2,3];
let list: Array<number> = [1, 2, 3]; // Array<number>泛型语法
// ES5:var list = [1,2,3];
使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript 支持数字的和基于字符串的枚举。
enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dir: Direction = Direction.NORTH;
默认情况下,NORTH 的初始值为 0,其余的成员会从 1 开始自动增长。换句话说,Direction.SOUTH 的值为 1,Direction.EAST 的值为 2,Direction.WEST 的值为 3。
以上的枚举示例经编译后,对应的 ES5 代码如下:
"use strict";
var Direction;
(function (Direction) {
Direction[(Direction["NORTH"] = 0)] = "NORTH";
Direction[(Direction["SOUTH"] = 1)] = "SOUTH";
Direction[(Direction["EAST"] = 2)] = "EAST";
Direction[(Direction["WEST"] = 3)] = "WEST";
})(Direction || (Direction = {}));
var dir = Direction.NORTH;
当然我们也可以设置 NORTH 的初始值,比如:
enum Direction {
NORTH = 3,
SOUTH,
EAST,
WEST,
}
在 TypeScript 2.4 版本,允许我们使用字符串枚举。在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。
enum Direction {
NORTH = "NORTH",
SOUTH = "SOUTH",
EAST = "EAST",
WEST = "WEST",
}
以上代码对应的 ES5 代码如下:
"use strict";
var Direction;
(function (Direction) {
Direction["NORTH"] = "NORTH";
Direction["SOUTH"] = "SOUTH";
Direction["EAST"] = "EAST";
Direction["WEST"] = "WEST";
})(Direction || (Direction = {}));
通过观察数字枚举和字符串枚举的编译结果,我们可以知道数字枚举除了支持 从成员名称到成员值 的普通映射之外,它还支持 从成员值到成员名称 的反向映射:
enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dirName = Direction[0]; // NORTH
let dirVal = Direction["NORTH"]; // 0
另外,对于纯字符串枚举,我们不能省略任何初始化程序。而数字枚举如果没有显式设置值时,则会使用默认规则进行初始化。
除了数字枚举和字符串枚举之外,还有一种特殊的枚举 —— 常量枚举。它是使用 const
关键字修饰的枚举,常量枚举会使用内联语法,不会为枚举类型编译生成任何 JavaScript。为了更好地理解这句话,我们来看一个具体的例子:
const enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dir: Direction = Direction.NORTH;
以上代码对应的 ES5 代码如下:
"use strict";
var dir = 0 /* NORTH */;
异构枚举的成员值是数字和字符串的混合:
enum Enum {
A,
B,
C = "C",
D = "D",
E = 8,
F,
}
以上代码对于的 ES5 代码如下:
"use strict";
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
Enum[Enum["B"] = 1] = "B";
Enum["C"] = "C";
Enum["D"] = "D";
Enum[Enum["E"] = 8] = "E";
Enum[Enum["F"] = 9] = "F";
})(Enum || (Enum = {}));
通过观察上述生成的 ES5 代码,我们可以发现数字枚举相对字符串枚举多了 “反向映射”:
console.log(Enum.A) //输出:0
console.log(Enum[0]) // 输出:A
在 TypeScript 中,任何类型都可以被归为 any 类型。这让 any 类型成为了类型系统的顶级类型(也被称作全局超级类型)。
let notSure: any = 666;
notSure = "semlinker";
notSure = false;
any
类型本质上是类型系统的一个逃逸舱。作为开发者,这给了我们很大的自由:TypeScript 允许我们对 any
类型的值执行任何操作,而无需事先执行任何形式的检查。比如:
let value: any;
value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK
在许多场景下,这太宽松了。使用 any
类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any
类型,就无法使用 TypeScript 提供的大量的保护机制。为了解决 any
带来的问题,TypeScript 3.0 引入了 unknown
类型。
就像所有类型都可以赋值给 any
,所有类型也都可以赋值给 unknown
。这使得 unknown
成为 TypeScript 类型系统的另一种顶级类型(另一种是 any
)。下面我们来看一下 unknown
类型的使用示例:
let value: unknown;
value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK
对 value
变量的所有赋值都被认为是类型正确的。但是,当我们尝试将类型为 unknown
的值赋值给其他类型的变量时会发生什么?
let value: unknown;
let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error
unknown
类型只能被赋值给 any
类型和 unknown
类型本身。直观地说,这是有道理的:只有能够保存任意类型值的容器才能保存 unknown
类型的值。毕竟我们不知道变量 value
中存储了什么类型的值。
现在让我们看看当我们尝试对类型为 unknown
的值执行操作时会发生什么。以下是我们在之前 any
章节看过的相同操作:
let value: unknown;
value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error
将 value
变量类型设置为 unknown
后,这些操作都不再被认为是类型正确的。通过将 any
类型改变为 unknown
类型,我们已将允许所有更改的默认设置,更改为禁止任何更改。
众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,其工作方式类似于数组。
元组可用于定义具有有限数量的未命名属性的类型。每个属性都有一个关联的类型。使用元组时,必须提供每个属性的值。为了更直观地理解元组的概念,我们来看一个具体的例子:
let tupleType: [string, boolean];
tupleType = ["semlinker", true];
在上面代码中,我们定义了一个名为 tupleType
的变量,它的类型是一个类型数组 [string, boolean]
,然后我们按照正确的类型依次初始化 tupleType 变量。与数组一样,我们可以通过下标来访问元组中的元素:
console.log(tupleType[0]); // semlinker
console.log(tupleType[1]); // true
在元组初始化的时候,如果出现类型不匹配的话,比如:
tupleType = [true, "semlinker"];
此时,TypeScript 编译器会提示以下错误信息:
[0]: Type 'true' is not assignable to type 'string'.
[1]: Type 'string' is not assignable to type 'boolean'.
很明显是因为类型不匹配导致的。在元组初始化的时候,我们还必须提供每个属性的值,不然也会出现错误,比如:
tupleType = ["semlinker"];
此时,TypeScript 编译器会提示以下错误信息:
Property '1' is missing in type '[string]' but required in type '[string, boolean]'.
某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。当一个函数没有返回值时,你通常会见到其返回值类型是 void:
// 声明函数返回值为void
function warnUser(): void {
console.log("This is my warning message");
}
以上代码编译生成的 ES5 代码如下:
"use strict";
function warnUser() {
console.log("This is my warning message");
}
需要注意的是,声明一个 void 类型的变量没有什么作用,因为在严格模式下,它的值只能为 undefined
:
let unusable: void = undefined;
TypeScript 里,undefined
和 null
两者有各自的类型分别为 undefined
和 null
。
let u: undefined = undefined;
let n: null = null;
object 类型是:TypeScript 2.2 引入的新类型,它用于表示非原始类型。
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
create(o: object | null): any;
// ...
}
const proto = {};
Object.create(proto); // OK
Object.create(null); // OK
Object.create(undefined); // Error
Object.create(1337); // Error
Object.create(true); // Error
Object.create("oops"); // Error
Object 类型:它是所有 Object 类的实例的类型,它由以下两个接口来定义:
// node_modules/typescript/lib/lib.es5.d.ts
interface Object {
constructor: Function;
toString(): string;
toLocaleString(): string;
valueOf(): Object;
hasOwnProperty(v: PropertyKey): boolean;
isPrototypeOf(v: Object): boolean;
propertyIsEnumerable(v: PropertyKey): boolean;
}
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
/** Invocation via `new` */
new(value?: any): Object;
/** Invocation via function calls */
(value?: any): any;
readonly prototype: Object;
getPrototypeOf(o: any): any;
// ···
}
declare var Object: ObjectConstructor;
Object 类的所有实例都继承了 Object 接口中的所有属性。
{} 类型描述了一个没有成员的对象。当你试图访问这样一个对象的任意属性时,TypeScript 会产生一个编译时错误。
// Type {}
const obj = {};
// Error: Property 'prop' does not exist on type '{}'.
obj.prop = "semlinker";
但是,你仍然可以使用在 Object 类型上定义的所有属性和方法,这些属性和方法可通过 JavaScript 的原型链隐式地使用:
// Type {}
const obj = {};
// "[object Object]"
obj.toString();
never
类型表示的是那些永不存在的值的类型。 例如,never
类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {}
}
在 TypeScript 中,可以利用 never 类型的特性来实现全面性检查,具体示例如下:
type Foo = string | number;
function controlFlowAnalysisWithNever(foo: Foo) {
if (typeof foo === "string") {
// 这里 foo 被收窄为 string 类型
} else if (typeof foo === "number") {
// 这里 foo 被收窄为 number 类型
} else {
// foo 在这里是 never
const check: never = foo;
}
}
注意在 else 分支里面,我们把收窄为 never 的 foo 赋值给一个显示声明的 never 变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事修改了 Foo 的类型:
type Foo = string | number | boolean;
然而他忘记同时修改 controlFlowAnalysisWithNever
方法中的控制流程,这时候 else 分支的 foo 类型会被收窄为 boolean
类型,导致无法赋值给 never 类型,这时就会产生一个编译错误。通过这个方式,我们可以确保
controlFlowAnalysisWithNever
方法总是穷尽了 Foo 的所有可能类型。 通过这个示例,我们可以得出一个结论:使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。
有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。
通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。
类型断言有两种形式:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 !
可以用于断言操作对象是非 null 和非 undefined 类型。具体而言,x! 将从 x 值域中排除 null 和 undefined 。
那么非空断言操作符到底有什么用呢?下面我们先来看一下非空断言操作符的一些使用场景。
function myFunc(maybeString: string | undefined | null) {
// Type 'string | null | undefined' is not assignable to type 'string'.
// Type 'undefined' is not assignable to type 'string'.
const onlyString: string = maybeString; // Error
const ignoreUndefinedAndNull: string = maybeString!; // Ok
}
type NumGenerator = () => number;
function myFunc(numGenerator: NumGenerator | undefined) {
// Object is possibly 'undefined'.(2532)
// Cannot invoke an object which is possibly 'undefined'.(2722)
const num1 = numGenerator(); // Error
const num2 = numGenerator!(); //OK
}
因为 !
非空断言操作符会从编译生成的 JavaScript 代码中移除,所以在实际使用的过程中,要特别注意。比如下面这个例子:
const a: number | undefined = undefined;
const b: number = a!;
console.log(b);
以上 TS 代码会编译生成以下 ES5 代码:
"use strict";
const a = undefined;
const b = a;
console.log(b);
虽然在 TS 代码中,我们使用了非空断言,使得 const b: number = a!;
语句可以通过 TypeScript 类型检查器的检查。但在生成的 ES5 代码中,!
非空断言操作符被移除了,所以在浏览器中执行以上代码,在控制台会输出 undefined
。
在 TypeScript 2.7 版本中引入了确定赋值断言,即允许在实例属性和变量声明后面放置一个 !
号,从而告诉 TypeScript 该属性会被明确地赋值。为了更好地理解它的作用,我们来看个具体的例子:
let x: number;
initialize();
// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error
function initialize() {
x = 10;
}
很明显该异常信息是说变量 x 在赋值前被使用了,要解决该问题,我们可以使用确定赋值断言:
let x!: number;
initialize();
console.log(2 * x); // Ok
function initialize() {
x = 10;
}
通过 let x!: number;
确定赋值断言,TypeScript 编译器就会知道该属性会被明确地赋值。
类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。 换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。目前主要有四种的方式来实现类型保护:
interface Admin {
name: string;
privileges: string[];
}
interface Employee {
name: string;
startDate: Date;
}
type UnknownEmployee = Employee | Admin;
function printEmployeeInformation(emp: UnknownEmployee) {
console.log("Name: " + emp.name);
if ("privileges" in emp) {
console.log("Privileges: " + emp.privileges);
}
if ("startDate" in emp) {
console.log("Start Date: " + emp.startDate);
}
}
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
typeof
类型保护只支持两种形式:typeof v === "typename"
和 typeof v !== typename
,"typename"
必须是 "number"
, "string"
, "boolean"
或 "symbol"
。 但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。
interface Padder {
getPaddingString(): string;
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) {}
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) {}
getPaddingString() {
return this.value;
}
}
let padder: Padder = new SpaceRepeatingPadder(6);
if (padder instanceof SpaceRepeatingPadder) {
// padder的类型收窄为 'SpaceRepeatingPadder'
}
function isNumber(x: any): x is number {
return typeof x === "number";
}
function isString(x: any): x is string {
return typeof x === "string";
}
联合类型通常与 null
或 undefined
一起使用:
const sayHello = (name: string | undefined) => {
/* ... */
};
例如,这里 name
的类型是 string | undefined
意味着可以将 string
或 undefined
的值传递给sayHello
函数。
sayHello("semlinker");
sayHello(undefined);
通过这个示例,你可以凭直觉知道类型 A 和类型 B 联合后的类型是同时接受 A 和 B 值的类型。此外,对于联合类型来说,你可能会遇到以下的用法:
let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';
以上示例中的 1
、2
或 'click'
被称为字面量类型,用来约束取值只能是某几个值中的一个。
TypeScript 可辨识联合(Discriminated Unions)类型,也称为代数数据类型或标签联合类型。它包含 3 个要点:可辨识、联合类型和类型守卫。
这种类型的本质是结合联合类型和字面量类型的一种类型保护方法。如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块。
可辨识要求联合类型中的每个元素都含有一个单例类型属性,比如:
enum CarTransmission {
Automatic = 200,
Manual = 300
}
interface Motorcycle {
vType: "motorcycle"; // discriminant
make: number; // year
}
interface Car {
vType: "car"; // discriminant
transmission: CarTransmission
}
interface Truck {
vType: "truck"; // discriminant
capacity: number; // in tons
}
在上述代码中,我们分别定义了 Motorcycle
、 Car
和 Truck
三个接口,在这些接口中都包含一个 vType
属性,该属性被称为可辨识的属性,而其它的属性只跟特性的接口相关。
基于前面定义了三个接口,我们可以创建一个 Vehicle
联合类型:
type Vehicle = Motorcycle | Car | Truck;
现在我们就可以开始使用 Vehicle
联合类型,对于 Vehicle
类型的变量,它可以表示不同类型的车辆。
下面我们来定义一个 evaluatePrice
方法,该方法用于根据车辆的类型、容量和评估因子来计算价格,具体实现如下:
const EVALUATION_FACTOR = Math.PI;
function evaluatePrice(vehicle: Vehicle) {
return vehicle.capacity * EVALUATION_FACTOR;
}
const myTruck: Truck = { vType: "truck", capacity: 9.5 };
evaluatePrice(myTruck);
对于以上代码,TypeScript 编译器将会提示以下错误信息:
Property 'capacity' does not exist on type 'Vehicle'.
Property 'capacity' does not exist on type 'Motorcycle'.
原因是在 Motorcycle 接口中,并不存在 capacity
属性,而对于 Car 接口来说,它也不存在 capacity
属性。那么,现在我们应该如何解决以上问题呢?这时,我们可以使用类型守卫。下面我们来重构一下前面定义的 evaluatePrice
方法,重构后的代码如下:
function evaluatePrice(vehicle: Vehicle) {
switch(vehicle.vType) {
case "car":
return vehicle.transmission * EVALUATION_FACTOR;
case "truck":
return vehicle.capacity * EVALUATION_FACTOR;
case "motorcycle":
return vehicle.make * EVALUATION_FACTOR;
}
}
在以上代码中,我们使用 switch
和 case
运算符来实现类型守卫,从而确保在 evaluatePrice
方法中,我们可以安全地访问 vehicle
对象中的所包含的属性,来正确的计算该车辆类型所对应的价格。
类型别名用来给一个类型起个新名字。
type Message = string | string[];
let greet = (message: Message) => {
// ...
};
在 TypeScript 中交叉类型是将多个类型合并为一个类型。通过 &
运算符可以将现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };
let point: Point = {
x: 1,
y: 1
}
在上面代码中我们先定义了 PartialPointX
类型,接着使用 &
运算符创建一个新的 Point
类型,表示一个含有 x 和 y 坐标的点,然后定义了一个 Point
类型的变量并初始化。
那么现在问题来了,假设在合并多个类型的过程中,刚好出现某些类型存在相同的成员,但对应的类型又不一致,比如:
interface X {
c: string;
d: string;
}
interface Y {
c: number;
e: string
}
type XY = X & Y;
type YX = Y & X;
let p: XY;
let q: YX;
在上面的代码中,接口 X 和接口 Y 都含有一个相同的成员 c,但它们的类型不一致。对于这种情况,此时 XY 类型或 YX 类型中成员 c 的类型是不是可以是 string
或 number
类型呢?比如下面的例子:
p = { c: 6, d: "d", e: "e" };
q = { c: "c", d: "d", e: "e" };
为什么接口 X 和接口 Y 混入后,成员 c 的类型会变成 never
呢?这是因为混入后成员 c 的类型为 string & number
,即成员 c 的类型既可以是 string
类型又可以是 number
类型。很明显这种类型是不存在的,所以混入后成员 c 的类型为 never
。
在上面示例中,刚好接口 X 和接口 Y 中内部成员 c 的类型都是基本数据类型,那么如果是非基本数据类型的话,又会是什么情形。我们来看个具体的例子:
interface D { d: boolean; }
interface E { e: string; }
interface F { f: number; }
interface A { x: D; }
interface B { x: E; }
interface C { x: F; }
type ABC = A & B & C;
let abc: ABC = {
x: {
d: true,
e: 'semlinker',
f: 666
}
};
console.log('abc:', abc);
以上代码成功运行后,控制台会输出以下结果:
由上图可知,在混入多个类型时,若存在相同的成员,且成员类型为非基本数据类型,那么是可以成功合并。
TypeScript | JavaScript |
---|---|
含有类型 | 无类型 |
箭头函数 | 箭头函数(ES2015) |
函数类型 | 无函数类型 |
必填和可选参数 | 所有参数都是可选的 |
默认参数 | 默认参数 |
剩余参数 | 剩余参数 |
函数重载 | 无函数重载 |
myBooks.forEach(() => console.log('reading'));
myBooks.forEach(title => console.log(title));
myBooks.forEach((title, idx, arr) =>
console.log(idx + '-' + title);
);
myBooks.forEach((title, idx, arr) => {
console.log(idx + '-' + title);
});
// 未使用箭头函数
function Book() {
let self = this;
self.publishDate = 2016;
setInterval(function () {
console.log(self.publishDate);
}, 1000);
}
// 使用箭头函数
function Book() {
this.publishDate = 2016;
setInterval(() => {
console.log(this.publishDate);
}, 1000);
}
function createUserId(name: string, id: number): string {
return name + id;
}
let IdGenerator: (chars: string, nums: number) => string;
function createUserId(name: string, id: number): string {
return name + id;
}
IdGenerator = createUserId;
// 可选参数
function createUserId(name: string, id: number, age?: number): string {
return name + id;
}
// 默认参数
function createUserId(
name = "semlinker",
id: number,
age?: number
): string {
return name + id;
}
在声明函数时,可以通过 ?
号来定义可选参数,比如 age?: number
这种形式。在实际使用时,需要注意的是可选参数要放在普通参数的后面,不然会导致编译错误。
function push(array, ...items) {
items.forEach(function (item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
// type Combinable = string | number;
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
在以上代码中,我们为 add 函数提供了多个函数类型定义,从而实现函数的重载。在 TypeScript 中除了可以重载普通函数之外,我们还可以重载类中的成员方法。
方法重载是指在同一个类中方法同名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。所以类中成员方法满足重载的条件是:在同一个类中,方法名相同且参数列表不同。下面我们来举一个成员方法重载的例子:
class Calculator {
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: string, b: number): string;
add(a: number, b: string): string;
add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
}
const calculator = new Calculator();
const result = calculator.add('Semlinker', ' Kakuqo');
这里需要注意的是,当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。另外在 Calculator 类中,add(a: Combinable, b: Combinable){ }
并不是重载列表的一部分,因此对于 add 成员方法来说,我们只定义了四个重载方法。
let x: number; let y: number; let z: number;
let five_array = [0,1,2,3,4];
[x,y,z] = five_array;
let two_array = [0, 1];
let five_array = [...two_array, 2, 3, 4];
let colors: string[] = ["red", "green", "blue"];
for (let i of colors) {
console.log(i);
}
let person = {
name: "Semlinker",
gender: "Male",
};
let { name, gender } = person;
let person = {
name: "Semlinker",
gender: "Male",
address: "Xiamen",
};
// 组装对象
let personWithAge = { ...person, age: 33 };
// 获取除了某些项外的其它项
let { name, ...rest } = person;
在面向对象语言中,接口是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类去实现。
TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。
interface Person {
name: string;
age: number;
}
let semlinker: Person = {
name: "semlinker",
age: 33,
};
interface Person {
readonly name: string;
age?: number;
}
只读属性用于限制只能在对象刚刚创建的时候修改其值。此外 TypeScript 还提供了 ReadonlyArray<T>
类型,它与 Array<T>
相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
有时候我们希望一个接口中除了包含必选和可选属性之外,还允许有其他的任意属性,这时我们可以使用 索引签名 的形式来满足上述要求。
interface Person {
name: string;
age?: number;
[propName: string]: any;
}
const p1 = { name: "semlinker" };
const p2 = { name: "lolo", age: 5 };
const p3 = { name: "kakuqo", sex: 1 }
接口和类型别名都可以用来描述对象的形状或函数签名:
接口
interface Point {
x: number;
y: number;
}
interface SetPoint {
(x: number, y: number): void;
}
类型别名
type Point = {
x: number;
y: number;
};
type SetPoint = (x: number, y: number) => void;
与接口类型不一样,类型别名可以用于一些其他类型,比如原始类型、联合类型和元组:
// primitive
type Name = string;
// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };
// union
type PartialPoint = PartialPointX | PartialPointY;
// tuple
type Data = [number, string];
接口和类型别名都能够被扩展,但语法有所不同。此外,接口和类型别名不是互斥的。接口可以扩展类型别名,而反过来是不行的。
Interface extends interface
interface PartialPointX { x: number; }
interface Point extends PartialPointX {
y: number;
}
Type alias extends type alias
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };
Interface extends type alias
type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }
Type alias extends interface
interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };
类可以以相同的方式实现接口或类型别名,但类不能实现使用类型别名定义的联合类型:
interface Point {
x: number;
y: number;
}
class SomePoint implements Point {
x = 1;
y = 2;
}
type Point2 = {
x: number;
y: number;
};
class SomePoint2 implements Point2 {
x = 1;
y = 2;
}
type PartialPoint = { x: number; } | { y: number; };
// A class can only implement an object type or
// intersection of object types with statically known members.
class SomePartialPoint implements PartialPoint { // Error
x = 1;
y = 2;
}
与类型别名不同,接口可以定义多次,会被自动合并为单个接口。
interface Point { x: number; }
interface Point { y: number; }
const point: Point = { x: 1, y: 2 };
在面向对象语言中,类是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的属性和方法。
在 TypeScript 中,我们可以通过 Class
关键字来定义一个类:
class Greeter {
// 静态属性
static cname: string = "Greeter";
// 成员属性
greeting: string;
// 构造函数 - 执行初始化操作
constructor(message: string) {
this.greeting = message;
}
// 静态方法
static getClassName() {
return "Class name is Greeter";
}
// 成员方法
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
那么成员属性与静态属性,成员方法与静态方法有什么区别呢?这里无需过多解释,我们直接看一下编译生成的 ES5 代码:
"use strict";
var Greeter = /** @class */ (function () {
// 构造函数 - 执行初始化操作
function Greeter(message) {
this.greeting = message;
}
// 静态方法
Greeter.getClassName = function () {
return "Class name is Greeter";
};
// 成员方法
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
// 静态属性
Greeter.cname = "Greeter";
return Greeter;
}());
var greeter = new Greeter("world");
在 TypeScript 3.8 版本就开始支持ECMAScript 私有字段,使用方式如下:
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}
let semlinker = new Person("Semlinker");
semlinker.#name;
// ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.
与常规属性(甚至使用 private
修饰符声明的属性)不同,私有字段要牢记以下规则:
#
字符开头,有时我们称之为私有名称;在 TypeScript 中,我们可以通过 getter
和 setter
方法来实现数据的封装和有效性校验,防止出现异常数据。
let passcode = "Hello TypeScript";
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode == "Hello TypeScript") {
this._fullName = newName;
} else {
console.log("Error: Unauthorized update of employee!");
}
}
}
let employee = new Employee();
employee.fullName = "Semlinker";
if (employee.fullName) {
console.log(employee.fullName);
}
继承(Inheritance)是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系。
继承是一种 is-a 关系:
在 TypeScript 中,我们可以通过 extends
关键字来实现继承:
class Animal {
name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
constructor(name: string) {
super(name); // 调用父类的构造函数
}
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
let sam = new Snake("Sammy the Python");
sam.move();
使用 abstract
关键字声明的类,我们称之为抽象类。抽象类不能被实例化,因为它里面包含一个或多个抽象方法。所谓的抽象方法,是指不包含具体实现的方法:
abstract class Person {
constructor(public name: string){}
abstract say(words: string) :void;
}
// Cannot create an instance of an abstract class.(2511)
const lolo = new Person(); // Error
抽象类不能被直接实例化,我们只能实例化实现了所有抽象方法的子类。具体如下所示:
abstract class Person {
constructor(public name: string){}
// 抽象方法
abstract say(words: string) :void;
}
class Developer extends Person {
constructor(name: string) {
super(name);
}
say(words: string): void {
console.log(`${this.name} says ${words}`);
}
}
const lolo = new Developer("lolo");
lolo.say("I love ts!"); // lolo says I love ts!
在前面的章节,我们已经介绍了函数重载。对于类的方法来说,它也支持重载。比如,在以下示例中我们重载了 ProductService
类的 getProducts
成员方法:
class ProductService {
getProducts(): void;
getProducts(id: number): void;
getProducts(id?: number) {
if(typeof id === 'number') {
console.log(`获取id为 ${id} 的产品信息`);
} else {
console.log(`获取所有的产品信息`);
}
}
}
const productService = new ProductService();
productService.getProducts(666); // 获取id为 666 的产品信息
productService.getProducts(); // 获取所有的产品信息
软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。
泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。
对于刚接触 TypeScript 泛型的读者来说,首次看到 <T>
语法会感到陌生。其实它没有什么特别,就像传递参数一样,我们传递了我们想要用于特定函数调用的类型。
参考上面的图片,当我们调用 identity<Number>(1)
,Number
类型就像参数 1
一样,它将在出现 T
的任何位置填充该类型。图中 <T>
内部的 T
被称为类型变量,它是我们希望传递给 identity 函数的类型占位符,同时它被分配给 value
参数用来代替它的类型:此时 T
充当的是类型,而不是特定的 Number 类型。
其中 T
代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T
可以用任何有效名称代替。除了 T
之外,以下是常见泛型变量代表的意思:
其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U
,用于扩展我们定义的 identity
函数:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity<Number, string>(68, "Semlinker"));
除了为类型变量显式设定值之外,一种更常见的做法是使编译器自动选择这些类型,从而使代码更简洁。我们可以完全省略尖括号,比如:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity(68, "Semlinker"));
对于上述代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T 和 U,而不需要开发人员显式指定它们。
interface GenericIdentityFn<T> {
(arg: T): T;
}
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};
为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。出于篇幅考虑,这里我们只简单介绍 Partial 工具类型。不过在具体介绍之前,我们得先介绍一些相关的基础知识,方便读者自行学习其它的工具类型。
在 TypeScript 中,typeof
操作符可以用来获取一个变量声明或对象的类型。
interface Person {
name: string;
age: number;
}
const sem: Person = { name: 'semlinker', age: 33 };
type Sem= typeof sem; // -> Person
function toArray(x: number): Array<number> {
return [x];
}
type Func = typeof toArray; // -> (x: number) => number[]
keyof
操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。
interface Person {
name: string;
age: number;
}
type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type K3 = keyof { [x: string]: Person }; // string | number
在 TypeScript 中支持两种索引签名,数字索引和字符串索引:
interface StringArray {
// 字符串索引 -> keyof StringArray => string | number
[index: string]: string;
}
interface StringArray1 {
// 数字索引 -> keyof StringArray1 => number
[index: number]: string;
}
为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类。其中的原因就是当使用数值索引时,JavaScript 在执行索引操作时,会先把数值索引先转换为字符串索引。所以 keyof { [x: string]: Person }
的结果会返回 string | number
。
in
用来遍历枚举类型:
type Keys = "a" | "b" | "c"
type Obj = {
[p in Keys]: any
} // -> { a: any, b: any, c: any }
在条件类型语句中,可以用 infer
声明一个类型变量并且对它进行使用。
type ReturnType<T> = T extends (
...args: any[]
) => infer R ? R : any;
以上代码中 infer R
就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。
有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束。
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:
loggingIdentity(3); // Error, number doesn't have a .length property
这时我们需要传入符合约束类型的值,必须包含必须的属性:
loggingIdentity({length: 10, value: 3});
Partial<T>
的作用就是将某个类型里的属性全部变为可选项 ?
。
定义:
/**
* node_modules/typescript/lib/lib.es5.d.ts
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
在以上代码中,首先通过 keyof T
拿到 T
的所有属性名,然后使用 in
进行遍历,将值赋给 P
,最后通过 T[P]
取得相应的属性值。中间的 ?
号,用于将所有属性变为可选。
示例:
interface Todo {
title: string;
description: string;
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}
const todo1 = {
title: "Learn TS",
description: "Learn TypeScript",
};
const todo2 = updateTodo(todo1, {
description: "Learn TypeScript Enum",
});
在上面的 updateTodo
方法中,我们利用 Partial<T>
工具类型,定义 fieldsToUpdate
的类型为 Partial<Todo>
,即:
{
title?: string | undefined;
description?: string | undefined;
}
需要注意的是,若要启用实验性的装饰器特性,你必须在命令行或 tsconfig.json
里启用 experimentalDecorators
编译器选项:
命令行:
tsc --target ES5 --experimentalDecorators
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
类装饰器声明:
declare type ClassDecorator = <TFunction extends Function>(
target: TFunction
) => TFunction | void;
类装饰器顾名思义,就是用来装饰类的。它接收一个参数:
看完第一眼后,是不是感觉都不好了。没事,我们马上来个例子:
function Greeter(target: Function): void {
target.prototype.greet = function (): void {
console.log("Hello Semlinker!");
};
}
@Greeter
class Greeting {
constructor() {
// 内部实现
}
}
let myGreeting = new Greeting();
(myGreeting as any).greet(); // console output: 'Hello Semlinker!';
上面的例子中,我们定义了 Greeter
类装饰器,同时我们使用了 @Greeter
语法糖,来使用装饰器。
友情提示:读者可以直接复制上面的代码,在 TypeScript Playground 中运行查看结果。
有的读者可能想问,例子中总是输出 Hello Semlinker!
,能自定义输出的问候语么 ?这个问题很好,答案是可以的。
具体实现如下:
function Greeter(greeting: string) {
return function (target: Function) {
target.prototype.greet = function (): void {
console.log(greeting);
};
};
}
@Greeter("Hello TS!")
class Greeting {
constructor() {
// 内部实现
}
}
let myGreeting = new Greeting();
(myGreeting as any).greet(); // console output: 'Hello TS!';
属性装饰器声明:
declare type PropertyDecorator = (target:Object,
propertyKey: string | symbol ) => void;
属性装饰器顾名思义,用来装饰类的属性。它接收两个参数:
趁热打铁,马上来个例子热热身:
function logProperty(target: any, key: string) {
delete target[key];
const backingField = "_" + key;
Object.defineProperty(target, backingField, {
writable: true,
enumerable: true,
configurable: true
});
// property getter
const getter = function (this: any) {
const currVal = this[backingField];
console.log(`Get: ${key} => ${currVal}`);
return currVal;
};
// property setter
const setter = function (this: any, newVal: any) {
console.log(`Set: ${key} => ${newVal}`);
this[backingField] = newVal;
};
// Create new property with getter and setter
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class Person {
@logProperty
public name: string;
constructor(name : string) {
this.name = name;
}
}
const p1 = new Person("semlinker");
p1.name = "kakuqo";
以上代码我们定义了一个 logProperty
函数,来跟踪用户对属性的操作,当代码成功运行后,在控制台会输出以下结果:
Set: name => semlinker
Set: name => kakuqo
方法装饰器声明:
declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol,
descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;
方法装饰器顾名思义,用来装饰类的方法。它接收三个参数:
废话不多说,直接上例子:
function log(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
let originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log("wrapped function: before invoking " + propertyKey);
let result = originalMethod.apply(this, args);
console.log("wrapped function: after invoking " + propertyKey);
return result;
};
}
class Task {
@log
runTask(arg: any): any {
console.log("runTask invoked, args: " + arg);
return "finished";
}
}
let task = new Task();
let result = task.runTask("learn ts");
console.log("result: " + result);
以上代码成功运行后,控制台会输出以下结果:
"wrapped function: before invoking runTask"
"runTask invoked, args: learn ts"
"wrapped function: after invoking runTask"
"result: finished"
下面我们来介绍一下参数装饰器。
参数装饰器声明:
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol,
parameterIndex: number ) => void
参数装饰器顾名思义,是用来装饰函数参数,它接收三个参数:
function Log(target: Function, key: string, parameterIndex: number) {
let functionLogged = key || target.prototype.constructor.name;
console.log(`The parameter in position ${parameterIndex} at ${functionLogged} has
been decorated`);
}
class Greeter {
greeting: string;
constructor(@Log phrase: string) {
this.greeting = phrase;
}
}
以上代码成功运行后,控制台会输出以下结果:
"The parameter in position 0 at Greeter has been decorated"
TypeScript 4.0 带来了很多新的特性,这里我们只简单介绍其中的两个新特性。
当 noImplicitAny
配置属性被启用之后,TypeScript 4.0 就可以使用控制流分析来确认类中的属性类型:
class Person {
fullName; // (property) Person.fullName: string
firstName; // (property) Person.firstName: string
lastName; // (property) Person.lastName: string
constructor(fullName: string) {
this.fullName = fullName;
this.firstName = fullName.split(" ")[0];
this.lastName = fullName.split(" ")[1];
}
}
然而对于以上的代码,如果在 TypeScript 4.0 以前的版本,比如在 3.9.2 版本下,编译器会提示以下错误信息:
class Person {
// Member 'fullName' implicitly has an 'any' type.(7008)
fullName; // Error
firstName; // Error
lastName; // Error
constructor(fullName: string) {
this.fullName = fullName;
this.firstName = fullName.split(" ")[0];
this.lastName = fullName.split(" ")[1];
}
}
从构造函数推断类属性的类型,该特性给我们带来了便利。但在使用过程中,如果我们没法保证对成员属性都进行赋值,那么该属性可能会被认为是 undefined
。
class Person {
fullName; // (property) Person.fullName: string
firstName; // (property) Person.firstName: string | undefined
lastName; // (property) Person.lastName: string | undefined
constructor(fullName: string) {
this.fullName = fullName;
if(Math.random()){
this.firstName = fullName.split(" ")[0];
this.lastName = fullName.split(" ")[1];
}
}
}
在以下的示例中,我们使用元组类型来声明剩余参数的类型:
function addPerson(...args: [string, number]): void {
console.log(`Person info: name: ${args[0]}, age: ${args[1]}`)
}
addPerson("lolo", 5); // Person info: name: lolo, age: 5
其实,对于上面的 addPerson
函数,我们也可以这样实现:
function addPerson(name: string, age: number) {
console.log(`Person info: name: ${name}, age: ${age}`)
}
这两种方式看起来没有多大的区别,但对于第一种方式,我们没法设置第一个参数和第二个参数的名称。虽然这样对类型检查没有影响,但在元组位置上缺少标签,会使得它们难于使用。为了提高开发者使用元组的体验,TypeScript 4.0 支持为元组类型设置标签:
function addPerson(...args: [name: string, age: number]): void {
console.log(`Person info: name: ${args[0]}, age: ${args[1]}`);
}
之后,当我们使用 addPerson
方法时,TypeScript 的智能提示就会变得更加友好。
// 未使用标签的智能提示
// addPerson(args_0: string, args_1: number): void
function addPerson(...args: [string, number]): void {
console.log(`Person info: name: ${args[0]}, age: ${args[1]}`)
}
// 已使用标签的智能提示
// addPerson(name: string, age: number): void
function addPerson(...args: [name: string, age: number]): void {
console.log(`Person info: name: ${args[0]}, age: ${args[1]}`);
}
compilerOptions 支持很多选项,常见的有 baseUrl
、 target
、baseUrl
、 moduleResolution
和 lib
等。
compilerOptions 每个选项的详细说明如下:
{
"compilerOptions": {
/* 基本选项 */
"target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
"module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
"lib": [], // 指定要包含在编译中的库文件
"allowJs": true, // 允许编译 javascript 文件
"checkJs": true, // 报告 javascript 文件中的错误
"jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
"declaration": true, // 生成相应的 '.d.ts' 文件
"sourceMap": true, // 生成相应的 '.map' 文件
"outFile": "./", // 将输出文件合并为一个文件
"outDir": "./", // 指定输出目录
"rootDir": "./", // 用来控制输出目录结构 --outDir.
"removeComments": true, // 删除编译后的所有的注释
"noEmit": true, // 不生成输出文件
"importHelpers": true, // 从 tslib 导入辅助工具函数
"isolatedModules": true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).
/* 严格的类型检查选项 */
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
"strictNullChecks": true, // 启用严格的 null 检查
"noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
"alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'
/* 额外的检查 */
"noUnusedLocals": true, // 有未使用的变量时,抛出错误
"noUnusedParameters": true, // 有未使用的参数时,抛出错误
"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
"noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)
/* 模块解析选项 */
"moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
"baseUrl": "./", // 用于解析非相对模块名称的基目录
"paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
"rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
"typeRoots": [], // 包含类型声明的文件列表
"types": [], // 需要包含的类型声明文件名列表
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。
/* Source Map Options */
"sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
"inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
"inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性
/* 其他选项 */
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true // 为装饰器提供元数据的支持
}
}
简介:TypeScript 官方提供的在线 TypeScript 运行环境,利用它你可以方便地学习 TypeScript 相关知识与不同版本的功能特性。
除了 TypeScript 官方的 Playground 之外,你还可以选择其他的 Playground,比如 codepen.io、stackblitz 或 jsbin.com 等。
简介:一款在线 TypeScript UML 工具,利用它你可以为指定的 TypeScript 代码生成 UML 类图。
简介:一款 TypeScript 在线工具,利用它你可以为指定的 JSON 数据生成对应的 TypeScript 接口定义。
除了使用 jsontots 在线工具之外,对于使用 VSCode IDE 的小伙们还可以安装 JSON to TS 扩展来快速完成 JSON to TS 的转换工作。
简介:利用 Schemats,你可以基于(Postgres,MySQL)SQL 数据库中的 schema 自动生成 TypeScript 接口定义。
简介:一款 TypeScript AST 在线工具,利用它你可以查看指定 TypeScript 代码对应的 AST(Abstract Syntax Tree)抽象语法树。
对于了解过 AST 的小伙伴来说,对 astexplorer 这款在线工具应该不会陌生。该工具除了支持 JavaScript 之外,还支持 CSS、JSON、RegExp、GraphQL 和 Markdown 等格式的解析。
简介:TypeDoc 用于将 TypeScript 源代码中的注释转换为 HTML 文档或 JSON 模型。它可灵活扩展,并支持多种配置。在线地址:https://typedoc.org/
简介:使用 TypeScript ESLint 可以帮助我们规范代码质量,提高团队开发效率。
对 TypeScript ESLint 项目感兴趣且想在项目中应用的小伙伴,可以参考 “在Typescript项目中,如何优雅的使用ESLint和Prettier” 这篇文章。
能坚持看到这里的小伙伴都是 “真爱”,如果你还意犹未尽,那就来看看本人整理的 Github 上 1.8K+ 的开源项目:awesome-typescript。
https://github.com/semlinker/...
阿宝哥第一次使用 TypeScript 是在 Angular 2.x 项目中,那时候 TypeScript 还没有进入大众的视野。然而现在学习 TypeScript 的小伙伴越来越多了,本文阿宝哥将从 16 个方面入手,带你一步步学习 TypeScript,感兴趣的小伙伴不要错过。
赞 140 收藏 107 评论 8
查看全部 个人动态 →
(゚∀゚ )
暂时没有
(゚∀゚ )
暂时没有
注册于 2019-02-22
个人主页被 3.5k 人浏览
推荐关注