程序员应该具备的数学抽象能力
案例一: CSS 的值展开
以之前碰到过的 CSS 值展开代码为例,它的输入是个不定长,以空格作为分割的字符串,输出的是一个长度为 4 的字符串数组:如字符串只有一个元素(没有空格),输出数组4个元素都是那个字符串;有两个元素(一个空格),则输出数组则是两个元素的顺序重复;有三个元素(两个空格),则输出数字前三是三个元素,第四则是第二个元素;有四个元素(三个空格),则按顺序平铺。
这通常在 CSS 的 padding、margin 上使用,可以想一下是不是实现相同?
原代码是:
function expandValueToFourValues(value) {
var trimValue = value.replace(/\s+/, " ");
var attribValues = trimValue.split(" ");
var valueExpanded = [];
if(attribValues.length == 4) {
valueExpanded = attribValues;
} else if (attribValues.length == 3) {
valueExpanded[0] = attribValues[0];
valueExpanded[1] = attribValues[1];
valueExpanded[2] = attribValues[2];
valueExpanded[3] = attribValues[1];
} else if (attribValues.length == 2) {
valueExpanded[0] = attribValues[0];
valueExpanded[1] = attribValues[1];
valueExpanded[2] = attribValues[0];
valueExpanded[3] = attribValues[1];
} else if (attribValues.length == 1) {
valueExpanded[0] = attribValues[0];
valueExpanded[1] = attribValues[0];
valueExpanded[2] = attribValues[0];
valueExpanded[3] = attribValues[0];
}
return valueExpanded;
}
如果要简化,应该怎么去做?这里直接给出答案,包含函数声明和注释只有 6 行:
function expandValueToFourValues(value) {
// split 方法可以接受一个正则表达式或者函数作为拆分条件,没必要先替换字符串,可以直接切,/\s+/ 表示适配任意白字符
const attribValues = value.split(/\s+/);
// 把输入转换为矩阵,就可以看出单行宽度和索引值是有规律的,直接通过 Array.from() 生成长度为4的数组即
return Array.from({length: 4}, (n, i) => attribValues[i] || attribValues[i-2] || attribValues[0]);
}
这是怎么做到的?注释上写着,把字符串拆分后当成一个矩阵来找规律,就是:
输入字符串 | 输出数组第0元素 | 输出数组第1元素 | 输出数组第2元素 | 输出数组第3元素 |
---|---|---|---|---|
a | a | a | a | a |
a b | a | b | a | b |
a b c | a | b | c | b |
a b c d | a | b | c | d |
这时再来看那个表达式 (n, i) => attribValues[i] || attribValues[i-2] || attribValues[0]
,将几个值代入:
当只有一个元素时,假设 i 只有 0 有值:
- [0] attributes[i] 有值,正常加入
- [1..3] attributes[i] 和 attributes[i-2] 都没有值,走到最后 attributes[0]
当有两个元素时,i 只有 0 和 1 有值,按照下面就做到了两个元素重复添加:
- [0,1] attributes[i] 有值,将前两个元素添加进入返回数组
- [2] attributes[i] 没有值,但是 attributes[i-2 = 0] 返回第一个元素
- [3] attributes[i] 没有值,但是 attributes[3-2 = 1] 返回第二个元素
当有三个元素时,i 只有 0、1、2 有值:
- [0,1,2] attributes[i] 有值,将前两个元素添加进入返回数组
- [3] attributes[i] 没有值,则取 attributes[3-2] 刚好取第 index 1 的元素,这就需求惊人的巧合了。
当有四个元素时:
- [0..3] 都有值,依次加入即可
其实这是 CSS 里颜色展开的算法,我怀疑浏览器里可能也是这么简洁实现的,相对原来的代码,这种写法明显简单干净,少了很多判断。
案例二:Canvas 的 textDecoration 实现
Canvas 的 textDecoration 实现并不复杂,只需要处理三条线的起终点即可,但是它有一个问题是它跟 textBaseline 相交,因为 Canvas 的文本画线是从 textBaseline 开始画起的,所以它的装饰线也要考虑到起点:
出于简化说明,这里不考虑 textBaseline 为
alphabetic
的情况,它是从英文的四线格第三线开始画,公式会不太一样。
初看这个图,是不是又要写一堆 if else 构成?不,我们把它展开成一个二维矩阵:
textBaseline\textDecoration | overline | lineThrough | underline |
---|---|---|---|
top | 0 | fontSize/2 | fontSize |
middle | -fontSize/2 | 0 | fontSize/2 |
bottom | -fontSize | -fontSize/2 | 0 |
最简单的办法是把 key 合并,成为一维哈希直接取值:
const decorationBaseLineMap = {
'overline top': 0,
'lineThrough top': fontSize / 2,
'underline top': fontSize,
'overline middle': -fontSize / 2,
// ...
}
// 获取 Y 轴起点
const y = decorationBaseLineMap[`${textDecoration} ${textBaseline}`];
但这样还是很啰嗦,而且我从来不觉得拼接字符串是个很快的事情。我们尝试从这个二维数组中发现几何规律,其实它是个很完美的以 middle 和 lineThrough 为中心的镜像:
- fontSize = unerline/top
- fontSize / 2 = lineThrough/top = underline/middle
- 0 = overline/top = lineThrough/middle = underline/bottom
- -fontSIze / 2 = overline/middle = lineThrough/bottom
- -fontSize = overline/bottom =
此时第一反应是给二维数组代数进去,使之前后相等(代什么数字进去,我是凭感觉[狗头]),然后 X 轴和 Y 轴相加结果如下:
textBaseline\textDecoration | overline [0] | lineThrough [-1] | underline [-2] |
---|---|---|---|
top [0] | 0 | fontSize/2 [-1] | fontSize [-2] |
middle [1] | -fontSize/2 [1] | 0 | fontSize/2 [-1] |
bottom [2] | -fontSize [2] | -fontSize/2 [1] | 0 |
此时已经呼之欲出了,有一个隐藏的系数存在于这个公式里,通过它可以做到镜像取反,假设 fontSize = 1
,代入到多元一次方程很容易算出来:x = -.5
(0.5 去掉整数部分),代入到其它几个公式里,继续验证一下正确性:
- underline/top:
x(underline+top)fontSize = -.5*(-1+0)*1 = .5(正数)
- overline/middle:
x(overline+middle)fontSize = -.5*(0+1)*1 = -.5(负数)
. - lineThrough/middle:
x(lineThrough+middle)fontSize = -.5*(-1+1)*1 = 0
- underline/middle:
x(underline+middle)fontSize = -.5*(-2+1)*1 = .5(正数)
- overline/bottom:
x(overline+bottom)fontSize = -.5*(0+2)*1 = -1(负数)
- lineThrough/bottom:
x(lineThrough+bottom)fontSize = 0.5*(-1+2)*1=-.5(负数)
- underline/bottom:
x(underline+bottom)fontSize = -.5*(-2+2)*1 = 0
写成实际的代码:
const decorationDim = ({
overline: 0,
lineThrough: -1,
underline: -2,
})[textDecoration];
const baselineDim = ({
top: 0,
middle: 1,
bottom: 2,
})[textBaseline];
// 获取 Y 轴值, -.5 是 xAxis 和 yAxis 相加后与字体大小的系数
const y = fontSize * (decorationDim + baselineDim) * -.5;
如果写了一堆 if else,会发现这个结果也是相符的,我写了个可公开的范例:Canvas textDecoration (codepen.io)。
总结
希望大家能在开发过程中多进行思考,不要按照需求翻译成代码,而是具备更高的抽象能力,通过一些数学方法,提高代码的执行效率。
版权所有丨转载请注明出处:https://kxq.io/archives/developer-math-abstraction