介绍
可展开表格
今天闲来无事想改Teams头像,但是找了半天也没有发现Mac该如何拍照,于是同事说:都是干前端的,何不用javascript来拍照呢?于是一顿操作猛如虎,终于拍到了照片。一起来看看。
首先要确认你的电脑有摄像头,然后我们先写一个打开摄像头的函数。在javascript中,我们可以使用navigator.mediaDevices.getUserMedia来访问摄像头。拿到摄像头的stream之后,将它赋值给一个提前定义好的video元素,这样就可以在页面上显示摄像头的画面了。
1 | function openCamera(videoElement) { |
在来写一个关闭摄像头的函数。关闭摄像头其实就是停止stream中的所有轨道,并将video元素的srcObject设置为null。
1 | function closeCamera(videoElement) { |
1 | <!DOCTYPE html> |
在前端项目开发过程中,经常要生成列表数据,用于展示或者测试之用,今天介绍一个快速生成列表数据的方法。假设我们正在开发一个用户管理系统,有一个页面用于展示系统中的用户,用户的类型定义如下:
1 | interface User = { |
在项目开发初期,API还未开发完成,前端只能先自己Mock数据用来展示页面。假设我们要生成50条用户数据,你会怎么做?大多数人都会想到如下方案。
1 | const users = []; |
中规中矩的方法,也是我以前常用的方法,今天在做code review时发现了另一个写法,感觉非常新颖,与大家分享。
1 | const users = Array.from({ length: 50 }, (_, i) => ({ |
想要看懂上面的代码,首先需要了解Array.from方法,这个方法可以根据一个已经存在的数组或者类数组对象创建一个新的数组,它的定义如下,我们用的是第二种形式。
1 | Array.from(items) |
看一下参数:items: 可以是一个数组或者类数组对象。mapFn: 可选参数,是一个映射函数,用于对每个元素进行转换。thisArg: 可选参数,指定mapFn函数中的this上下文。
还需要了解一下类数组对象,可以看看这篇:xxx, 类数组对象有两个特征:
length属性。好了,基础知识讲解完毕,回到上面的代码。
1 | const users = Array.from({ length: 50 }, (_, i) => ({ |
{length: 50}实际上创建了一个类数组对象,它具有50个元素,但这些元素都是undefined。(_, i) => ({ ... })是一个映射函数,它接收两个参数,第一个参数是当前元素(在这里是undefined),第二个参数是当前元素的索引。注意:javascript中有一个不成文的约定,如果一个变量没有用到但是必须占位,那么就用_作为变量名。仔细观察代码可知,我们只需要下标i,所以第一个参数用_占位。函数内部的实现就比较简单了,直接根据下标i生成对应的用户数据对象。由于使用的是箭头函数且返回的是对象,所以需要用括号包裹对象字面量。
今天的内容比较简单,但也涉及了类数组对象,Array.from方法以及箭头函数的用法。每一段优雅代码的背后,都是无数个小知识点的积累。
祝大家编程愉快,我们明天再见。
今天滨海的天气特别凉快,弹弓小队三人刚刚结束练弓,回家就写了这篇,一会儿还要洗衣服,准备迎接明天的训练。
今天接到一个需求,需要自定义一个Input输入框,我们的项目使用React + Ant Design进行开发,按理说Ant Design已经提供了非常丰富的组件样式,但有时候还是无法满足特定的需求,比如我们的输入框要求渲染成下面的样子

而Ant Design的Input组件默认的渲染结果是这样的。
仔细观察这两个输入框,他们的区别如下:
下面我们依次来实现这两个需求:
Ant Design的Input提供了variant属性来控制输入框的样式,我们可以将其设置为underline来实现下划线样式。
1 | <Input |
Ant Design的Input组件并未提供相关设置,但是Form组件提供了requiredMark属性来控制必选标志的样式,我们可以通过这个属性来自行渲染必选标志,这个方式非常灵活,可以渲染成任何你想要的样子。
首先定义一个函数用来渲染自定义的必选标志,这个函数接受两个参数,一个是标签,另一个是boolean变量,用来指示当前控件是否为必选项。
函数逻辑也十分简单,首先渲染label。然后当required为true时,渲染一个红色的星号,否则不渲染任何内容。
1 | const customizeRequiredMark = (label: React.ReactNode, { required }: { required: boolean }) => ( |
接下来,将Input组件用Form包裹起来,并应用上面的自定义函数即可。
1 | // 使用requiredMark属性来应用自定义的必选标志 |
完整代码如下:
1 | import { Form, Input } from 'antd'; |
今天就到这里了,我们明天见,码字不易,如果觉得有用就关注一下,您的关注,是我持续输出的动力!
typescript类型体操是近两年非常流行的编程实践,所谓类型体操,是指通过类型系统来实现一些复杂的逻辑操作。
typescript使用extend和infer来实现分支操作。通过条件类型,可以根据类型的不同进行不同的处理。
typescript使用K in T来实现循环遍历操作。通过映射类型,可以对类型进行遍历和转换。
typescript使用keyof来获取类型的属性。通过keyof操作符,可以获取一个类型的所有属性名,并将其转换为联合类型。
typescript使用T[keyof T]来获取类型的值。通过索引访问类型,可以获取一个类型的所有属性值,并将其转换为联合类型。
在昨天的文章中,我们介绍了如何使用typescript中的枚举类型Enum来处理状态值。今天,我们将探讨一个更好的实践:使用const代替枚举。为什么说const是更好的方案呢,因为typeScript中的枚举类型有一些弊端。
对于数字类型的枚举,无法实现真正的类型安全,比如下面的枚举类型定义:
1 | enum JobStatus { |
这个类型定义了五种状态,分别用数字1到5来表示。但是这里有一个隐藏的问题,你可以将任意数字赋值给JobStatus类型的变量,而不管这个数字是否在1到5之间。例如:
1 | let status: JobStatus = 1; // 这是合法的 |
以上代码可以成功编译,没有任何问题,这是一个很大的隐患,如果我们手滑写错了一个数字,那就会导致程序出现bug。
一个更好的做法是使用字符串类型代替数字类型:
1 | const enum JobStatus { |
这样就可以将状态值限制为枚举类型定义的字符串,任何其他字符串都会引起编译错误。
1 | const status: JobStatus = JobStatus.PENDING; // 这是合法的 |
Tree-Shaking是指在打包时去除未使用的代码。TypeScript的枚举类型在编译后会生成一个对象,这个对象包含了所有枚举成员的映射关系。即使程序中没有使用这个枚举类型,编译后的代码中仍然会保留这个对象。这会导致打包后的代码体积增大。
const代替枚举上面提到的两点问题,使用const都可以很好地解决。下面使用const来实现同样的功能,首先定义Job状态常量,因为要兼顾后端接口的整数类型和前端显示的字符串类型,我们索性将他们封装到一起。用code表示后端返回的状态值,用label表示前端要显示的状态名称。
1 | const JobStatus = { |
接下来定义Job接口,使用JobStatusType来描述状态类型。
1 | interface Job { |
后端返回的数据和先前一样用数字类型来表示Job状态。
1 | const jobs = [ |
最后是打印Job的函数,这里涉及到一个问题,我们不能像之前一样直接使用字符串来访问状态的显示名称,因为现在状态是一个对象,我们需要通过状态的code来获取对应的label。
1 | function printJobStatus(job: Job) { |
现在我们可以调用这个函数来打印Job的状态:
1 | jobs.forEach(printJobStatus); |
之前的映射函数getJobDisplayName也可以省略掉了,因为我们在定义JobStatus时已经将状态码(code)和显示名称(label)封装在一起了。
1 | // 该函数可以省略 |
弊端是多了一个查找的过程,但这个查找过程是非常轻量级的,因为我们只需要遍历一次JobStatus对象来找到对应的状态。
const代替枚举的好处在于:
const可以确保状态值只能是预定义的状态,而不能是其他任意值。const支持Tree-Shaking,只有实际使用才会被保留,从而减小打包后的代码体积。好了,今天就到这里了,我们明天见。以上文章为纯古法手打,如果觉得有用,就关注一下,感谢支持。
这篇文章来谈一下 TypeScript 中的枚举类型(Enum)以及一些最佳实践。事情的起因是这样的,今天看到自己之前写的一段代码,感觉不是很好,于是想优化一下,期间用到了枚举类型,遂记录一下。为了方便理解,我将原来的例子简化一下。
业务需求是这样的:我们要实现一个Job系统,你可以将其想象为Jenkins Job类似的东西,每个Job有一个状态,状态可以是以下几种:
PENDING:等待执行RUNNING:正在执行SUCCESS:执行成功FAILED:执行失败CANCELED:执行被取消Job的状态信息由后端返回,前端只负责展示,也不需要实时刷新。很简单的需求,对吧?我的原始代码如下:
前端数据类型定义, 首先定义一个字面量用来保存Job状态,然后定义一个Job接口来描述Job对象。
1 | // 定义Job状态字面量 |
后端返回数据如下,可以看到后端是用数字类型来表示状态的。
1 | const jobData = [ |
为了将后端返回的数字类型和前端定义的Job Status对应起来,我又额外写了一个映射函数:
1 | function mapJobStatus(status: number): JobStatus { |
接下来就是展示了,展示Job状态时,用户不想看到全大写的状态,而是想看到首字母大写的状态,所以我又写了一个函数来处理这个问题:
1 | function getJobDisplayName(status: JobStatus): string { |
好了,下面我们停下来思考一下,以上这些代码都解决了哪些问题,为什么需要两个转换函数,有没有更好的解决方式?
为了完成这个需求,上述代码做了以下几件事:
对于第一点,可以使用枚举类型来实现,这样就不需要手动维护状态码和状态字面量之间的映射关系了。
对于第二点,原本的实现是将全大写的状态转换为首字母大写的形式,这种转换方式比较简单,但实际业务中,可能会有更复杂的需求,比如用户希望看到不同的展示字符串(例如将RUNNING显示为In progress)。因此,使用一个映射表来处理这种转换会更加灵活。
我们可以使用 TypeScript 的枚举类型来简化代码。首先定义一个枚举来表示 Job 状态:
1 | enum JobStatus { |
这样就可以省去第一个转换函数mapJobStatus,因为枚举本身就提供了状态码到状态字面量的映射,可以直接使用这个枚举来定义 Job 接口:
1 | interface Job { |
接下来,重写getJobDisplayName, 这里使用typescript的Record类型来创建一个映射表(Record类型相当于一个键值对的映射,只不过键和值都是类型化的),将枚举值映射到展示字符串,与原本的实现方式相比,这种方式更加简洁易维护。
1 | const getJobDisplayName: Record<JobStatus, string> = { |
最后是调用代码,如下:
1 | const jobs = [ |
使用枚举类型的好处是:
有没有更好的实现方式?很想听听大家的想法,欢迎留言讨论。
今天就到这里了,我们明天见。
昨天在网上看到一道面试题,是关于JavaScript中的+元算符的,如下:
1 | [] + 0 = ? |
要解决这道题,我们首先要了解JavaScript中+运算符的行为,+元算符在JavaScript中主要有三种用途:一是用于数字相加,二是用于字符串连接,三是用于类型转换。
1 | 1 + 2 = 3 // 数字相加 |
再回到面试题,可以看出,这并非常规的加法操作,因为运算符两侧的操作数并非都是数字类型,而是包含了数组和对象。难道是字符串连接吗?不确定,是类型转换?好像也不是。
追本溯源,我们先看看MDN上关于+的运行规则吧:
如果+元算符的操作数包含非基本类型(比如对象,数组等),先将其转换为基本类型(primitive type)。
在JavaScript中,基本类型包括
undefined、null、boolean、number、string、BigInt、Symbol。
当+元算符两侧都是基本类型时,执行规则如下:
BigInt时,将另一个操作数也转换为BigInt,并执行加法;举几个列子:
1 | 1 + `2` = '12' // 满足规则1,将数字1转换为字符串'1',执行字符串连接 |
注意以上3条规则是按顺序执行的,字符串连接的优先级高于数字加法,所以字符串和数字相加时,永远会转换为字符串连接。
现在来看[] + 0该如何执行,首先[]是数组,不属于基本类型,所以先将它转换为基本类型,对象类型转换为基本类型的操作如下:
toPrimitive方法;toPrimitive方法,则调用valueOf方法;valueOf方法返回的值不是基本类型,则调用toString方法;toString方法返回的值仍不是基本类型,则抛出错误。所以[] + 0的执行过程如下:
[]没有toPrimitive方法,所以调用valueOf方法。valueOf方法返回值仍然是数组对象[]。toString方法,返回空字符串''。因此,[] + 0等价于'' + 0, 此时+两侧都是基本类型了,并且满足有一侧是字符串的条件,所以将另一侧的操作数0也转换为字符串,执行字符串连接,结果为'' + '0' = '0'。
再来看{} + 0, {}和[]一样,都是对象类型,所以先将其转换为基本类型。
{}没有toPrimitive方法,所以调用valueOf方法,返回值仍然是对象{}。toString方法,返回字符串'[object Object]''[object Object]'与0进行字符串连接,结果为'[object Object]' + '0' = '[object Object]0'。哈哈,但是我要告诉你,这个答案是错误的,这个分析是没有问题的,但是JavaScript解释器不同意,当它看到{}时,会将其解释为一个空的代码块,而不是一个空对象,因此,{} + 0实际上等于下面的代码:
1 | {} |
{}被视为一个空代码块, 没有返回任何结果,而+ 0被解释为一条独立的语句,返回值是0,最终结果是0。
如果要让代码按照我们上面分析的过程执行,那么就要防止JavaScript将{}解释为空代码块,可以用()将其包裹起来。
1 | ({}) + 0 // 结果为 '[object Object]0' |
1 | [] + 0 = '0' |
说实话这道题目比较偏门,但是对于了解JavaScript中+运算符的行为还是很有帮助的,通过一道题,能了解一个知识点,还是很值得的。
有的时候,不要光纠结问题的答案,更应该关注的是问题背后的原理的规则,就比如这道题,在没有写这篇文章之前,如果让我回答,我是答不上来的,我需要查阅资料,了解+运算符的行为规则,才能得出正确的答案。我觉得相比知道答案,更有意思的是分析的过程,这个过程体现了一个程序员处理问题的逻辑思维能力,小到一道面试题,大到一个复杂的系统设计,都是如此。那么如何培养这种能力呢,我也一直在寻找答案…
最后给大家留几道思考题:
1 | [] + [] = ? |
今天就到这里了,我们明天见。
今天周六,准备出去逛逛,奈何天气太热,动也不想动,只能呆在家里了。锁凤十代打了两天,手感非常不错,准备留下了,之前买的猛禽就退了吧,现在赚钱不易,还是要精打细算的好。