JavaScript 实现字节单位转换 —— Byte 转 KB、MB、GB、TB ...

雷尚林  更新于:
创建于:

背景

将字节(Bytes)转成 KB、MB、GB、TB 等,是十分常见的需求,它的实现方法有多种,本文列出了常见的四种实现,最推荐的是第4种:BigInt 版

方法1:if else

一般做法是,通过判断 bytes / 1024 是否大于 1024,来决定是否提升单位:

function formatBytes(bytes: number) {
  const K = 1024;

  if (bytes < K) {
    return `${bytes} B`;
  } else if ((bytes /= K) < K) {
    return `${bytes.toFixed(2)} KB`;
  } else if ((bytes /= K) < K) {
    return `${bytes.toFixed(2)} MB`;
  } else if ((bytes /= K) < K) {
    return `${bytes.toFixed(2)} MB`;
  } else if ((bytes /= K) < K) {
    return `${bytes.toFixed(2)} GB`;
  } else if ((bytes /= K) < K) {
    return `${bytes.toFixed(2)} TB`;
  } else if ((bytes /= K) < K) {
    return `${bytes.toFixed(2)} PB`;
  } else if ((bytes /= K) < K) {
    return `${bytes.toFixed(2)} EB`;
  } else if ((bytes /= K) < K) {
    return `${bytes.toFixed(2)} ZB`;
  } else {
    return `${bytes.toFixed(2)} YB`;
  }
}

formatBytes(1024 * 1024); // "1.00 MB"

(bytes /= K) < K 是一个简写,等同于:

bytes = bytes / K;
if (bytes < K) {
  // ...
}

通过 if-else 实现略显繁琐,下面我们将用 while 循环改写。

方法2:while

function formatBytes(bytes: number) {
  if (bytes === 0) {
    return "0 B";
  }

  const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
  const K = 1024;
  let i = 0;

  while (bytes >= 1024 && i < sizes.length - 1) {
    bytes /= K;
    i++;
  }

  return `${bytes.toFixed(2)} ${sizes[i]}`;
}

formatBytes(1024 * 1024); // "1.00 MB"

方法3:Math.log

相比 if-else,使用 while 会“清爽”许多,但还有更好的方法:Math.log

function formatBytes(bytes: number) {
  if (bytes === 0) {
    return "0 B";
  }
  const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
  const K = 1024;

  const i = Math.floor(Math.log(bytes) / Math.log(K));
  bytes /= Math.pow(K, i);

  return `${bytes.toFixed(2)} ${sizes[i]}`;
}

formatBytes(1024 * 1024); // "1.00 MB"

相比 while,Math.log 在常数时间(O(logn))内完成,通过对数计算直接得到 i,显得更加“高级”。

对数运算是指数运算的“逆运算”,我们想要得到的 i,本质上,是获取以 K 为底,bytes 的对数,然后再向下取整,得到在 sizes 中对应的下标。

由于 JavaScript 没有提供计算任意底数的指数方法,所以用到了指数“换底公式”:

对数的换底公式
对数的换底公式

Math.log 是 JavaScript 中求自然对数的内置方法,通过 Math.log(bytes) / Math.log(K) 可以获取到目标单位的索引(下标)。

限制有效范围

到目前为止,我们的 formatBytes 函数还没有对参数进行限制,虽然有 TypeScript 的约束,但它只在开发阶段有效,并且无法判断参数是否为非负整数,所以补充以下代码:

function formatBytes(bytes: number) {
  // 当 bytes 不是非负整数,或者 bytes 大于 2 ** 53 - 1 时,抛出一个异常
  if (
    !(Number.isInteger(bytes) && bytes >= 0 && bytes <= Number.MAX_SAFE_INTEGER)
  ) {
    throw new Error(
      "Invalid input: `bytes` must be a non-negative integer within Number.MAX_SAFE_INTEGER",
    );
  }

  // ...
}

formatBytes(Number.MAX_SAFE_INTEGER); // "8.00 PB"
formatBytes(Number.MAX_SAFE_INTEGER + 1); //  Error

Number.MAX_SAFE_INTEGER 是一个常量,等同于 2 的 53 次方减 1,这是 JavaScript 能够精确计算的最大整数,对应还有 Number.MIN_SAFE_INTEGER,表示 JavaScript 能够精确计算的最小整数:

Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1; // true
Number.MIN_SAFE_INTEGER === -(2 ** 53 - 1); // true

方法 4: BigInt(推荐)

如果你足够细心,会发现我们的 formatBytes 能够处理的最大数量大约是 8.00 PB,也就是 Number.MAX_SAFE_INTEGER 个字节,再大就会丢失精度:

2 ** 53 - 1; // 正常 9007199254740991
2 ** 53; // 正常 9007199254740992
2 ** 53 + 1; // 异常:9007199254740992
2 ** 53 + 2; // 异常:9007199254740994

这个问题并不常见,因为 8.00 PB 已经非常大了,足够日常使用,如果还要支持更大的数量级,可以用 ES2020 引入的基础数据类型:BigInt,下面是改写后的代码:

function formatBytes(bytes: string | bigint | number) {
  try {
    bytes = BigInt(bytes);
    if (bytes < 0n) {
      throw new Error();
    }
  } catch {
    throw new Error(
      "Invalid input: `bytes` must be an integer, BigInt, or an integer string, and they all must be non-negative",
    );
  }

  if (bytes === 0n) {
    return "0 B";
  }

  const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; // 等等 ...
  const K = 1024n;
  let i = 0n;
  const originBytes = bytes;
  while (bytes >= K && i < sizes.length - 1) {
    bytes /= K;
    i++;
  }

  const intPart = bytes;
  const level = K ** i;
  const decimalPart = originBytes - level * intPart;
  const num = Number(intPart) + Number((decimalPart * 1000n) / level) / 1000;

  if (num > Number.MAX_SAFE_INTEGER) {
    throw new Error(
      `Invalid input: \`bytes\` exceeds the maximum precise value that ${
        sizes[sizes.length - 1]
      } can represent`,
    );
  }

  return `${num.toFixed(2)} ${sizes[Number(i)]}`;
}

// formatBytes(1024n ** 8n * BigInt(Number.MAX_SAFE_INTEGER)); // 9007199254740991.00 YB
// formatBytes(1024n ** 8n * BigInt(Number.MAX_SAFE_INTEGER + 1)); // Error

注意:

这种方法较为理想,因为不受浮点数精度的影响,无最大值限制。