【重学前端】002-JavaScript类型:关于类型,有哪些你不知道的细节

本文深入探讨JavaScript的运行时类型,包括7种基本类型(Undefined、Null、Boolean、String、Number、Symbol、Object)的详细特性。特别关注了类型转换,如StringToNumber、装箱和拆箱转换的规则。此外,还讨论了void运算符的用途以及如何避免JavaScript中的类型陷阱。
摘要由CSDN通过智能技术生成

【重学前端】002-JavaScript类型:关于类型,有哪些你不知道的细节

一、运行时类型

1、概述

运行时类型 - 思维导图

运行时类型

运行时类型 = 代码实际执行过程中的类型;

JavaScript 代码运行过程中产生的数据都具有运行时类型

运行时类型一共有 7 种;

二、类型

1、概述

7 种类型 - 思维导图

7 种类型

JavaScript 语言的每一个值都属于某一种数据类型

JavaScript 语言规定了 7 种语言类型;

这 7 种类型广泛应用于变量、函数参数、表达式、函数返回值等场合;

这 7 种类型分别是:

  1. Undefined;
  2. Null;
  3. Boolean;
  4. String;
  5. Number;
  6. Symbol;
  7. Object。

2、Undefined、Null

Undefined - 思维导图

Undefined

Undefined 表示未定义,它的类型只有一个值,就是 undefined ;

任何变量在赋值前都是 Undefined 类型,值是 undefined ;

一般我们可以使用全局变量 undefined 来表示这个值;

我们也可以使用 void 运算符把任意一个表达式变成 undefined 值;(# void 运算符细究)

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

Undefined - 代码示例

// 正常的函数
function say() {
  console.log("hello");
}
// [Function: say]
console.log(say);
// 正常调用
say(); // hello

// 正常打印:function
console.log(
  typeof function say1() {
    console.log("hello111");
    return 1;
  }
);

// 此时打印结果为:undefined
console.log(
  typeof void function say1() {
    console.log("hello111");
    return 1;
  }
);

// 此时打印结果为:undefined
console.log(void (1 === 1));

思维导图 - void 运算符细究

void 运算符细究

参考网址:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/void

含义: 对给定的表达式进行求值,并返回一个 undefined ;

语法: void expression

概述: 这个运算符能向期望一个表达式的值是 undefined 的地方插入会产生副作用的表达式。

产生副作用: 指的是强制让这个表达式的返回值变成 undefined

一般场景: void 运算符通常只用于获取 undefined的原始值,一般使用void(0)(等同于void 0)。当然也可以直接使用全局变量 undefined

JavaScript URIs :当用户点击一个以 javascript: 开头的 URI 时,它会执行 URI 中的代码,然后用返回的值替换页面内容,除非返回的值是 undefinedvoid运算符可用于返回 undefined

<a href="javascript:void(0);">
  这个链接点击之后不会做任何事情,如果去掉 void(),
  点击之后整个页面会被替换成一个字符 0。
</a>
<p> chrome 中即使<a href="javascript:0;">也没变化,firefox 中会变成一个字符串 0 </p>
<a href="javascript:void(document.body.style.backgroundColor='green');">
  点击这个链接会让页面背景变成绿色。
</a>

注意,虽然这么做是可行的,但利用 javascript: 伪协议来执行 JavaScript 代码是不推荐的,推荐的做法是为链接元素绑定事件。

在箭头函数中避免泄露: 箭头函数标准中,允许在函数体不使用括号来直接返回值。如果原本没有返回值的函数有了返回值可能会导致一些问题,此时可以使用 void 预算符强制其返回值为 undefined !

Null - 思维导图

Null

Null 类型也只有一个值,即 null ,表示”定义了,但为空“;

一般编程时不会将变量赋值为 undefined ,这样就可以保证值为 undefined 的变量都是从未赋值的自然状态;

与 undefined 不同,null 是一个关键字,所以在任何代码中都可以放心使用 null 关键字获取 null 值

3、Boolean

Boolean 类型有两个值, true 和 false,它用于表示逻辑意义上的真和假,同样有关键字 true 和 false 来表示两个值。

4、String

思维导图 - String

String

String 用于表示文本数据

String 的最大长度为 2^53 - 1 ;

这个最大长度并不完全是字符数,因为 String 的意义并非”字符串“,而是字符串的 UTF16 编码,我们操作字符串的 charAt 、 charCodeAt、length 等方法针对的都是 UTF16 编码,所以字符串的最大长度,实际上受字符串的编码长度影响;

现行的字符集国际标准,字符是以 Unicode 的方式表示的,每一个Unicode 的码点表示一个字符,理论上,Unicode 的范围是无限的。UTF 是 Unicode 的编码方式,规定了码点在计算机中的表示方法,常见的有 UTF16 和 UTF8 。 Unicode 的码点通常用 U+??? 来表示,其中 ??? 是十六进制的码点值。 0-65536(U+0000 - U+FFFF)的码点被称为基本字符区域BMP)。

JavaScript 的字符串是永远无法被改变的(类似 Java 字符串),因此字符串具有”值类型“的特征;

JavaScript 字符串把每个 UTF16 单元当作一个字符来处理,所以处理非 BMP(超出U+0000 - U+FFFF 范围)的字符时,你应该格外小心;(现实中很少用到 BMP 之外的字符!)

JavaScript 这个设计继承自 Java,最新标准中的解释:这样设计是为了“性能和尽可能实现起来简单”。

5、Number

Number - 思维导图

Number

Number 即表示数字,范围大概是有理数的范围,在计算机中有一定精度限制

JavaScript 中的 Number 类型有 18437736874454810627(即 264-253+3) 个值;

JavaScript 中的 Number 类型基本符合 IEEE 754-2008 规定的双精度浮点数规则,但是JavaScript 为了表达几个额外的语言场景(比如不让除以 0 出错,而引入了无穷大的概念);

NaN,占用了 9007199254740990,这原本是符合 IEEE 规则的数字;
Infinity,无穷大;
-Infinity,负无穷大。

JavaScript 中有 +0 和 -0,在加法类运算中它们没有区别,但是除法的场合则需要特别留意区分,“忘记检测除以 -0,而得到负无穷大”的情况经常会导致错误,而区分 +0 和 -0 的方式,正是检测 1/x 是 Infinity 还是 -Infinity。

根据双精度浮点数的定义,Number 类型中有效的整数范围是 -0x1fffffffffffff 至 0x1fffffffffffff,所以 Number 无法精确表示此范围外的整数。

根据浮点数的定义,非整数的 Number 类型无法用 = 也不行) 来比较;

这就是为什么 0.1 + 0.2 == 0.3 返回 false !

浮点数运算的精度问题导致等式左右的结果并不是严格相等!

正确比较浮点数的方法:

解决方案: Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON

检查等式左右两边差的绝对值是否小于最小精度,才是正确的比较浮点数的方法。

6、Symbol*

备注:这个东西有些难以理解,需深刻研究之!

Symbol - 思维导图

Symbol

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

Symbol 可以具有字符串类型的描述,但是即使描述相同,Symbol 也不相等。

我们创建 Symbol 的方式是使用全局的 Symbol 函数。

var mySymbol = Symbol("my symbol");

一些标准中提到的 Symbol,可以在全局的 Symbol 函数的属性中找到。

例如,我们可以使用 Symbol.iterator 来自定义 for…of 在对象上的行为:

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

代码中我们定义了 iterator 之后,用 for(var v of o) 就可以调用这个函数,然后我们可以根据函数的行为,产生一个 for…of 的行为

这里我们给对象 o 添加了 Symbol.iterator 属性,并且按照迭代器的要求定义了一个 0 到 10 的迭代器,之后我们就可以在 for of 中愉快地使用这个 o 对象啦。

这些标准中被称为“众所周知”的 Symbol,也构成了语言的一类接口形式。它们允许编写与语言结合更紧密的 API。

7、Object*

Object - 思维导图

Object

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

Object 表示对象的意思,它是一切有形和无形物体的总称

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

属性分为:数据属性 + 访问器属性,二者都是 key-value 的结构,key 可以是字符串或者 Symbol 的类型;

JavaScript 中对象的类:”类“仅仅是运行时对象的一个私有属性,JavaScript 是无法自定义类型的;

JavaScript 的基本类型,都在对象类型中有一个**”亲戚“**,它们是:Number、String、Boolean、Symbol 。

所以,必须认识到,3 与 new Number(3) 是不同的,前者是 Number 类型,后者是对象类型。

Number、String 和 Boolean ,三个构造器是两用的,当跟 new 搭配时,它们产生对象;当直接使用时,它们表示强制类型转换

Symbol 函数比较特殊,直接使用 new 调用它会抛出错误,但它仍然是 Symbol 对象的构造器

JavaScript 语言设计上试图模糊对象和基本类型之间的关系,我们日常代码可以把对象的方法在基本类型上使用。

举个例子:console.log(“abc”.charAt(0)); // a

在原型上添加的方法也可用于基本类型!

Symbol.prototype.hello = () => console.log("hello");
 
var a = Symbol("a");
console.log(typeof a); // symbol,a 并非对象
a.hello(); // hello

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

三、类型转换

1、概述

概述

JS 是弱类型语言,因此类型转换发生频繁,大部分转换符合直觉

”==“是重大设计缺陷 ,其试图实现跨类型的比较,它的规则复杂到几乎没有人能够记住它!

推荐禁止使用”“,而要求进行显式地类型转换后,用 ”=“ 比较。

类型转换图示

较为复杂的部分: Number 和 String 之间的转换,以及对象跟基本类型之间的转换。

image-20220607201358495

2、StringToNumber

StringToNumber - 思维导图

StringToNumber

字符串到数字的类型转换,存在一个语法结构,类型转换支持十进制、二进制、八进制和十六进制,比如:

30;0b111;0o13;0xFF

支持的字符串语法还包括正负号科学计数法,可以使用大写或者小写的 e 来表示:

1e3;-1e-2。

parseInt 和 parseFloat 并不使用这个转换,所以支持的语法跟这里不尽相同;

在不传入第二个参数的情况下,parseInt 只支持 16 进制前缀 “0x” ,而且会忽略非数字字符,也不支持科学计数法

在一些古老的浏览器环境中,parseInt 还支持 0 开头的数字作为 8 进制前缀,这是很多错误的来源。所以在任何环境下,都建议传入 parseInt 的第二个参数,而 parseFloat 则直接把原字符串作为十进制来解析,它不会引入任何的其他进制。

多数情况下,Number 是比 parseInt 和 parseFloat 更好的选择。

3、NumberToString

在较小的范围内,数字到字符串的转换是完全符合你直觉的十进制表示

当 Number 绝对值较大或者较小时,字符串表示则是使用科学计数法表示的。

4、装箱转换*

不好理解,需要时需要细细研究!下面对教程进行摘录!

每一种基本类型 Number、String、Boolean、Symbol 在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。

前文提到,全局的 Symbol 函数无法使用 new 来调用,但我们仍可以利用装箱机制来得到一个 Symbol 对象,我们可以利用一个函数的 call 方法来强迫产生装箱。

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

我们可以用 console.log 看一下这个东西的 type of,它的值是 object,我们使用 symbolObject instanceof 可以看到,它是 Symbol 这个类的实例,我们找它的constructor 也是等于 Symbol 的,所以我们无论从哪个角度看,它都是 Symbol 装箱过的对象:

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

装箱机制会频繁产生临时对象,在一些对性能要求较高的场景下,我们应该尽量避免对基本类型做装箱转换。

使用内置的 Object 函数,我们可以在 JavaScript 代码中显式调用装箱能力。

var symbolObject = Object(Symbol("a"));
 
console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true

每一类装箱对象皆有私有的 Class 属性,这些属性可以用 Object.prototype.toString 获取:

var symbolObject = Object(Symbol("a"));
 
console.log(Object.prototype.toString.call(symbolObject)); //[object Symbol]

在 JavaScript 中,没有任何方法可以更改私有的 Class 属性,因此 Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。

但需要注意的是,call 本身会产生装箱操作,所以需要配合 typeof 来区分基本类型还是对象类型。

5、拆箱转换*

不好理解,需要时需要细细研究!下面对教程进行摘录!

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

对象到 String 和 Number 的转换都遵循“先拆箱再转换”的规则。通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 String 或者 Number。

拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。

var o = {
    valueOf : () => {console.log("valueOf"); return {}},
    toString : () => {console.log("toString"); return {}}
}

o * 2
// valueOf
// toString
// TypeError

我们定义了一个对象 o,o 有 valueOf 和 toString 两个方法,这两个方法都返回一个对象,然后我们进行 o*2 这个运算的时候,你会看见先执行了 valueOf,接下来是 toString,最后抛出了一个 TypeError,这就说明了这个拆箱转换失败了。

到 String 的拆箱转换会优先调用 toString。我们把刚才的运算从 o*2 换成 String(o),那么你会看到调用顺序就变了。

var o = {
    valueOf : () => {console.log("valueOf"); return {}},
    toString : () => {console.log("toString"); return {}}
}

String(o)
// toString
// valueOf
// TypeError

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

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

四、结语

除了这七种语言类型,还有一些语言的实现者更关心的规范类型

  • List 和 Record: 用于描述函数传参过程。
  • Set:主要用于解释字符集等。
  • Completion Record:用于描述异常、跳出等语句执行过程。
  • Reference:用于描述对象属性访问、delete 等。
  • Property Descriptor:用于描述对象的属性。
  • Lexical Environment 和 Environment Record:用于描述变量和作用域。
  • Data Block:用于描述二进制数据。

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值