程序员应该具备的数学抽象能力

案例一: 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元素
aaaaa
a babab
a b cabcb
a b c dabcd

这时再来看那个表达式 (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 的情况,它是从英文的四线格第三线开始画,公式会不太一样。

textdecorations.png

初看这个图,是不是又要写一堆 if else 构成?不,我们把它展开成一个二维矩阵:

textBaseline\textDecorationoverlinelineThroughunderline
top0fontSize/2fontSize
middle-fontSize/20fontSize/2
bottom-fontSize-fontSize/20

最简单的办法是把 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\textDecorationoverline [0]lineThrough [-1]underline [-2]
top [0]0fontSize/2 [-1]fontSize [-2]
middle [1]-fontSize/2 [1]0fontSize/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