JavaScript: 关于类型,有哪些你不知道的细节?

从运行时的角度去看 JavaScript 的类型系统

Posted by chanweiyan on October 26, 2020

运行时类型是代码实际执行过程中我们用到的类型。所有的类型数据都会属于 7 个类型之一。从变量、参数、返回值到表达式中间结果,任何 JavaScript 代码运行过程中产生的数据,都具有运行时类型。

问题

  • 为什么有的编程规范要求用 void 0 代替 undefined?
  • 字符串有最大长度吗?
  • 0.1 + 0.2 不是等于 0.3 么?为什么 JavaScript 里不是这样的?
  • ES6 新加入的 Symbol 是个什么东西?
  • 为什么给对象添加的方法能用在基本类型上?

为什么有的编程规范要求用 void 0 代替 undefined?

因为 JavaScript 的代码 undefined 是一个变量,而并非是一个关键字,这是 JavaScript 语言公认的设计失误之一,所以,我们为了避免无意中被篡改,我建议使用 void 0 来获取 undefined 值。

字符串有最大长度吗?

String 有最大长度是 2^53 - 1,这在一般开发中都是够用的,但是有趣的是,这个所谓最大长度,并不完全是你理解中的字符数。因为 String 的意义并非“字符串”,而是字符串的 UTF16 编码,我们字符串的操作 charAt、charCodeAt、length 等方法针对的都是 UTF16 编码。所以,字符串的最大长度,实际上是受字符串的编码长度影响的。

0.1 + 0.2 不是等于 0.3 么?为什么 JavaScript 里不是这样的?

console.log(0.1 + 0.2 == 0.3);

这里输出的结果是 false,说明两边不相等的,这是浮点运算的特点,也是很多同学疑惑的来源,浮点数运算的精度问题导致等式左右的结果并不是严格相等,而是相差了个微小的值。

所以实际上,这里错误的不是结论,而是比较的方法,正确的比较方法是使用 JavaScript 提供的最小精度值:

console.log(Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);

检查等式左右两边差的绝对值是否小于最小精度,才是正确的比较浮点数的方法。这段代码结果就是 true 了。

ES6 新加入的 Symbol 是个什么东西?

Symbol 是 ES6 中引入的新类型,它是一切非字符串的对象 key 的集合,在 ES6 规范中,整个对象系统被用 Symbol 重塑。

我们创建 Symbol 的方式是使用全局的 Symbol 函数。例如: var mySymbol = Symbol("my symbol");

一些标准中提到的 Symbol,可以在全局的 Symbol 函数的属性中找到。例如,我们可以使用 Symbol.iterator 来自定义 for…of 在对象上的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    var o = new Object

    o[Symbol.iterator] = function() {
        var v = 0
        return {
            next: function() {
                return { value: v++, done: v > 10 }
            }
        }
    };

    for(var v of o)
        console.log(v); // 0 1 2 3 ... 9

为什么给对象添加的方法能用在基本类型上?

Object 是 JavaScript 中最复杂的类型,也是 JavaScript 的核心机制之一。

在 JavaScript 中,对象的定义是“属性的集合”

JavaScript 中的几个基本类型,都在对象类型中有一个“亲戚”。它们是:

  • Number;
  • String;
  • Boolean;
  • Symbol。

运算符提供了装箱操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对应对象的方法。

类型转换

最为臭名昭著的是 JavaScript 中的“ == ”运算,因为试图实现跨类型的比较,它的规则复杂到几乎没人可以记住。

这里我们当然也不打算讲解 == 的规则,它属于设计失误,并非语言中有价值的部分,很多实践中推荐禁止使用“ ==”,而要求程序员进行显式地类型转换后,用 === 比较。

所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。

我们定义一个函数,函数里面只有 return this,然后我们调用函数的 call 方法到一个 Symbol 类型的值上,这样就会产生一个 symbolObject。

1
2
3
4
5
6
    var symbolObject = (function(){ return this; }).call(Symbol("a"));

    console.log(typeof symbolObject); //object
    console.log(symbolObject instanceof Symbol); //true
    console.log(symbolObject.constructor == Symbol); //true

拆箱转换在 JavaScript 标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即,拆箱转换)。

Symbol.toPrimitive 是一个内置的 Symbol 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。——MDN

在 ES6 之后,还允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。

1
2
3
4
5
6
7
8
9
10
11
12
    var o = {
        valueOf : () => {console.log("valueOf"); return {}},
        toString : () => {console.log("toString"); return {}}
    }

    o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}


    console.log(o + "")
    // toPrimitive
    // hello

有一个说法是:程序 = 算法 + 数据结构,运行时类型包含了所有 JavaScript 执行时所需要的数据结构的定义,所以我们要对它格外重视。

不用原生的Number和parseInt实现StringToNumber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let StringToNumber = str => {
    let arr = str.trim().split('');
    let sign = arr[0] === '-' ? -1 : 1;
    if (arr[0] === '-' || arr[0] === '+') {
        arr.shift()
    }
    return sign * arr.reduce((total, cur)=>(
        total * 10 + (cur >= '0' && cur <= '9' ? (cur - '0') : NaN)
    ));
}

let res = []
res.push(StringToNumber(' 123456 '))
res.push(StringToNumber(' -9237 '))
res.push(StringToNumber(' + 9237 '))
res.push(StringToNumber(' 7 545 '))
res.push(StringToNumber(' 34 234 '))
res.push(StringToNumber(' 12@3 '))
res.push(StringToNumber(' 234*235 '))
console.log(res) // [123456, -9237, 9237, NaN, NaN, NaN, NaN]

参考

  1. https://time.geekbang.org/column/article/78884