likes
comments
collection
share

看看Vue2中"判断一个值是否为有效的数组索引"是怎么实现的

作者站长头像
站长
· 阅读数 16

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

【若川视野 x 源码共读】第24期 | vue2工具函数点击了解本期详情一起参与

一、前言

今天在看Vue2的基础工具函数,其中一个函数是isValidArrayIndex(判断一个值是否为有效的数组索引),具体实现如下:

export function isValidArrayIndex(val: any): boolean {
    const n = parseFloat(String(val));
    return n >= 0 && Math.floor(n) === n && isFinite(val);
}

isValidArrayIndex的实现依据是:有效的数组索引值是非负整数,且小于数组长度。当然,这里没有对数组长度进行限制,而是限制数值小于无穷。

二、对isValidArrayIndex实现的一些理解

2.1 为什么使用parseFloat,而不是parseInt、Number、+(一元加)?

parseFloat(str):解析一个字符串(如果不是字符串先转换为字符串)并返回一个浮点数或NaN。其解析规则为:

  • str中有正号(+)、负号(-)、数字(0-9)、小数点(.)、或者科学记数法中的指数(e 或 E)以外的字符,会忽略该字符以及之后的所有字符,返回当前已经解析到的浮点数。
  • str中第2个小数点的出现也会使解析停止(在这之前的字符都会被解析)。
  • 忽略前导和尾随空格/行终止符。
  • str的第1个字符不能被解析成为数字,则返回 NaN
  • 可以解析并返回Infinity
  • 解析BigIntNumber会丢失精度。因为末位 n 字符被丢弃。

parseInt(str, radix):解析一个字符串(如果不是字符串先转换为字符串)并返回指定基数的十进制整数或NaN。radix 是 2-36 之间的整数,表示被解析字符串的基数。其解析规则为:

  • str中有正号(+)、负号(-)、数字(0-9)以外的字符或str中字符不是指定radix参数中的数字,会忽略该字符以及之后的所有字符,返回当前已经解析到的整数。
  • 忽略前导和尾随空格/行终止符。
  • str的第1个字符不能被解析成为数字,则返回 NaN
  • 解析BigIntNumber会丢失精度。因为末位 n 字符被丢弃。
  • 如果radix是undefined、0或未指定:
    1. 如果str以0x或0X开头,radix被假定为16;
    2. 如果str以0开头,radix被假定为8或10(ECMAScript 5中澄清了应该使用10)。具体取哪个取决于实现,所以建议使用parseInt时都指定好radix值
    3. 如果str以其他值开头,radix为10。
  • parseInt将数字截断为整数值。

Number(value):转换字符串或者其他值为Number类型。如果该值不能被转换,它会返回NaN。其转换规则为:

  • 对于Number类型则总是返回其自身。
  • undefined变成NaN
  • null变成 0
  • true 变成 1false 变成0
  • 如果是字符串中包含数字字面量,会将其当数值解析。如果解析失败(除前后空格外,任何一个字符导致整个字符串不能被完整转为数值,都会失败,如:2个小数点...),返回的结果为 NaN。与实际数字字面量相比,它们有一些细微的差别:
    1. 忽略前导和尾随空格/行终止符。
    2. 前导数值 0 不会导致该数值成为八进制文本(或在严格模式下被拒绝)。
    3. + 和 - 允许在字符串的开头指示其符号,但该标志只能出现一次,不得后跟空格。
    4. Infinity 和 -Infinity 被当作是字面量。在实际代码中,它们是全局变量。
    5. 空字符串或仅空格字符串转换为 0
    6. 不允许使用数字分隔符。
  • Symbol抛出 TypeError
  • 对象首先按顺序调用@@toPrimitive(将 "number" 作为 hint)、valueOf() 和 toString() 方法将其转换为原始值。然后将生成的原始值转换为数值。

+一元加:与Number几乎相同(除了+转换BigInt会抛出 TypeError,而Number转换BigInt不报错,返回Number类型值(精度可能会丢失))。

一些测试用例:

// parseFloat可以正确识别+、-、.(小数点)、e、前后空格。
parseFloat('   +1.23e1bcbc    '); // 12.3
parseFloat('   -1.23e1bcbc    '); // -12.3
parseFloat('- 10'); // NaN, 虽然可以识别+、-,但后面必须紧跟数字

// parseFloat在第2个小数点处会停止解析
parseFloat('2.2.2.'); // 2.2

// 第1个字符不能被解析成为数字返回NaN
parseFloat('z2.2.2.'); // NaN

// 可以解析并返回`Infinity`
parseFloat('Infinity'); // Infinity
parseFloat('-Infinity'); // -Infinity

// BigInt中n被丢弃
parseFloat('8888n'); // 8888

// -------分割线--------

// parseInt可以正确识别+、-、前后空格, 但不能识别小数点(.)、e。
parseInt('   +1.23e1bcbc    '); // 1
parseInt('   +123e1bcbc    '); // 123
parseInt('   -1.23e1bcbc    '); // -1
parseInt('- 10'); // NaN, 虽然可以识别+、-,但后面必须紧跟数字

// 第1个字符不能被解析成为数字返回NaN
parseInt('z2.2.2.'); // NaN

// BigInt中n被丢弃
parseInt('8888n'); //8888

// radix的解析
parseInt('0xb'); // 11, radix被当作16
parseInt('09'); // 9, radix被当作10
parseInt('09',0); // 9, radix被当作10
parseInt('09',undefined); // 9, radix被当作10

// 数字被截断为整数
parseInt('11.71',10); // 11

// -------分割线--------

Number(undefined); // NaN
+undefined; // NaN

Number(null); // 0
+null; // 0

Number(true); // 1
Number(false); // 0
+true; // 1
+false; // 0

// 传入字符串
// 忽略前后空格
Number('   34  '); // 34
+'   34  '; // 34

Number('0100'); // 100, 识别为10进制
+'0100'; // 100, 识别为10进制

Number('Infinity');// Infinity
+'Infinity'; // Infinity
Number('-Infinity'); // -Infinity
+"-Infinity"; // -Infinity

Number(''); // 0
Number('      '); // 0
+''; // 0
+"   "; // 0

// 使用数字分隔符,返回NaN
Number("10_000"); // NaN
+"10_000"; //NaN

// 对BigInt
Number(100n); // 100
+100n; // TypeError
Number('100n'); // NaN
+'100n'; // NaN

// 对Symbol
Number(Symbol()); // TypeError
+(Symbol()); // TypeError

// 对对象
const x = {
    toString: () => {
        return 3;
    },
    valueOf: () => {
     	return 2;
    },
    [Symbol.toPrimitive]: (hint) => {
        // Symbol.toPrimitive 是内置的symbol属性,其指定了一种接受首选类型并返回对象原始值的表示的方法。
        // 它被所有的强类型转换制算法优先调用。
        // hint值为:"number"、"string" 和 "default" 中的任意一个

        if (hint === "number") {
            return 1;
        }
        if (hint === "string") {
            return "1";
        }
        return true;
    },
};

Number(x); // 1, 优先级:Symbol.toPrimitive > valueOf > toString, 且Number为强制数值类型转换,hint为"number"

+x; // 1, 同Number(x)

综上所述,对于"判断一个值是否为有效的数组索引"这个场景而言:

  • parseFloat优于parseInt:因为parseFloat可以精确解析的范围更广;
  • parseInt优于Number或+:因为parseInt更可靠,而Number或+会将false、""、" "、null转为0,而parseInt或者parseFloat都是转为NaN
2.2 为什么要在将val传给parseFloat之前执行String()方法?

对parseFloat而言,如果入参不是string类型,则会首先调用ToString抽象操作将其转换为字符串类型,这看起来与使用String()是一样的效果,大部分情况下也确实如此。但对Symbol类型有所不同:详见MDN

  1. Symbol类型进行强制类型转换为字符串时,抛出TypeError;
  2. String(Symbol())时,不会抛出TypeError错误,而是直接返回字符串的"Symbol()"
parseFloat(Symbol()); // TypeError
parseFloat(String(Symbol())); // NaN
2.3 为什么const x = [1,2]; console.log(x[1.0]===x[1]); // true

在 JavaScript 代码中,像1这样的数值字面值其实是一个浮点值,而不是整数。

因为JavaScript的Number 类型是一个双精度64位二进制格式IEEE 754值。一个双精度的浮点数转为十进制的数字时,只要它转回来的双精度浮点数不变,精度取最短的那个就行。所以:

1.0===1; // true,也就是为啥x[1.0]===x[1]了

参考资料汇总

转载自:https://juejin.cn/post/7235916193185366053
评论
请登录