深入理解JS的类型、值、类型转换

195 阅读11分钟

一、七种内置类型和常见引用类型


插个图,来自于《JavaScript语言精髓与编程实践》第三章P184页,后来想想有点多而杂,所以就自己画了些重点内容如上图

二、特殊的null

typeof来检查上述七种类型时,返回的是对应的类型字符串值 

 但,有一个例外

typeof null === 'ogject' // true

null是唯一一个用typeof检测会返回'object'基本类型值(注意‘基本’两字)

具体的原因,当面试官问到,可以这样吹一波

不同的对象在底层都表示为二进制 
在JavaScript中二进制前三位为0的话都会被判断为object类型 > null的二进制表示全是0,自然前三位也是0 
所以 typeof null === “object”


三、引用类型的子类型:typeof [引用类型] === what ?

上面的图中虽然列出了七种引用类型,但是 typeof ‘引用类型’ === ‘object’ 一定成立吗?

不,还有一种情况:typeof ‘某些引用类型’ === ‘function’

还是先直接看一些测试吧,看下答案跟你预想的是不是一回事? 

如果全都胸有成竹,那下面这一小节你可以跳过了

typeof Function; // 'function'
typeof new Function(); // 'function'
typeof function() {}; // 'function'

typeof Array; // 'function'
typeof Array(); // 'object'
typeof new Array(); // 'object'
typeof []; // 'object'

typeof Boolean; // "function"
typeof Boolean(); // "boolean"
typeof new Boolean(); // "object"

typeof Math; // 'object'
typeof Math(); // Math is not a function
typeof new Math(); // Math is not a constructor

1、引用类型中的函数

先看前三句,原来typeof除了能判断基本类型object之外,还能判断function类型,函数也属于对象

2、引用类型的子类型

Array举例子

typeof Array; // 'function'
typeof Array(); // 'object'
typeof new Array(); // 'object'
typeof []; // 'object'

Array是个构造函数,所以直接打印出function 

但构造出来的Array()却又是另一回事了,构造出来的结果是个数组,自然属于引用类型,所以也就打印出了‘object’

构造函数 Array(..) 不要求必须带 new 关键字。不带时,它会被自动补上。 因此 Array(1,2,3) 和 new Array(1,2,3) 的效果是一样的

3、引用类型中的基本包装类型

typeof Boolean; // "function"
typeof Boolean(); // "boolean"
typeof new Boolean(); // "object"

Boolean是个构造函数,第一句没问题 

Boolean()直接执行,得出了布尔值,所以得到了‘boolean’

new出来的是个Boolean对象,具体来说就是:通过构造函数创建出来的是封装了基本类型值的封装对象,好好理解一下这句话

这里用String来举个例子吧,看到了吗,一个封装对象


但是,这里不推荐使用这种封装对象,看个例子

var a = new Boolean(false);
if (!a) {
  console.log('Oops'); // 执行不到这里
}

a是个对象,对象永远是真,所以……你懂了


个人建议不要轻易去碰包装类型,日常开发直接用字面量就好了(大牛自动忽略这段话)


4、Math到底是什么类型?

Math和Global(浏览器中替代为window)都是内置的对象,并不是引用类型的一种

typeof Math; // 'object'
typeof Math(); // Math is not a function
typeof new Math(); // Math is not a constructor

不是函数,不是构造器,这个应该能理解了吧。


四、typeof的安全防范机制

首先,我们需要知道underfinedundeclared的区别:未定义与未声明

但是,对于typeof来说,这两者都一样,返回的都是underfined

var a;
typeof a; // 'underfined'
typeof b; // 'underfined'

很明显,我们知道b就是undeclared(未声明的),但在typeof看来都是一样

这个特性,可以拿来做些什么呢?

举个简单的例子,在程序中使用全局变量 DEBUG 作为“调试模式”的开关。在输出调试信 息到控制台之前,我们会检查 DEBUG 变量是否已被声明。顶层的全局变量声明 var DEBUG = true 只在 debug.js 文件中才有,而该文件只在开发和测试时才被加载到浏览器,在生产环 境中不予加载。

问题是如何在程序中检查全局变量 DEBUG 才不会出现 ReferenceError 错误。这时 typeof 的 安全防范机制就成了我们的好帮手:

// 这样会抛出错误
if (DEBUG) {
  console.log('Debugging is starting');
}
// 这样是安全的
if (typeof DEBUG !== 'undefined') {
  console.log('Debugging is starting');
}

这不仅对用户定义的变量(比如 DEBUG)有用,对内建的 API 也有帮助:

if (typeof atob === "undefined") {
         atob = function() { /*..*/ };
}

这样的安全防范机制在各式源码中非常常见,可见,大作们早已经把一些基础的东西弄得非常透彻并运用到实践中,所以说,看源码是我们快速提高的一个方式,应该错不了。


五、值

这一part引用自[一、内存空间详解 · Sample GitBook]

JS的执行上下文生成之后,会创建一个叫做变量对象的特殊对象(关于变量对象在我的其他文章中有讲到),JS的基础类型都保存在变量对象中

> 严格意义上来说,变量对象也是存放于堆内存中,但是由于变量对象的特殊职能,我们在理解时仍然需要将其于堆内存区分开来。

但引用数据类型的值是保存在堆内存中的对象。JavaScript不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间。 

在操作对象时,实际上是在操作对象的引用而不是实际的对象。

因此,引用类型的值都是按引用访问的。 这里的引用,我们可以理解为保存在变量对象中的一个地址,该地址与堆内存的实际值相关联。


看到这里,应该就能比较好的理解深浅拷贝、引用传参之类的相关问题了,这属于延伸思考,google去吧,学会自我思考和搜索也是一种技能。


六、强制类型转换

《you don’t know JS》中 第一部分第4章

类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时(runtime)。

然而在 JavaScript 中通常将它们统称为强制类型转换,我个人则倾向于用“隐式强制类型转换”(implicit coercion)和“显式强制类型转换”(explicit coercion)来区分。

1、抽象值操作

介绍显式和隐式强制类型转换之前,我们需要先掌握字符串、数字和布尔值之间类型转换的基本规则

1️⃣ ToString

toString() 可以被显式调用,或者在需要字符串化时自动调用

null 转换为 "null"

undefined 转换为 "undefined"

true 转换为 "true"

数字的字符串化则遵循通用规则,极小和极大的数字使用指数形式:

// 1.07 连续乘以七个 1000
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
// 七个1000一共21位数字 
a.toString(); // "1.07e21"

数组的默认 toString() 方法经过了重新定义,将所有单元字符串化以后再用 "," 连接起 来

var a = [1,2,3];
 a.toString(); // "1,2,3"


2️⃣ ToNumber

其中 true 转换为 1,false 转换为 0。

undefined 转换为 NaN,null 转换为 0。 

处理失败 时返回 NaN(处理数字常量失败时会产生语法错误)


3️⃣ ToBoolean

先看什么是假值

• undefined
• null
• false
• +0、-0 和 NaN
• ""

假值的布尔强制类型转换结果为 false。 

从逻辑上说,假值列表以外的都应该是真值(truthy)


再看下假值对象(这东西太有意思了😂) 

不是说规定所有的对象都是真值,怎么还会有假值对象呢?

var a = new Boolean(false);
var b = new Number(0);
var c = new String('');

var d1 = Boolean( a && b && c );
var d2 = a && b && c;

看看d1和d2有什么不同?是不是特有意思? 


> 如果假值对象并非封装了假值的对象,那它究竟是什么? 
> 值得注意的是,虽然 JavaScript 代码中会出现假值对象,但它实际上并不属于 JavaScript 语 言的范畴。 
> 浏览器在某些特定情况下,在常规 JavaScript 语法基础上自己创建了一些外来(exotic) 值,这些就是“假值对象”。
> 假值对象看起来和普通对象并无二致(都有属性,等等),但将它们强制类型转换为布尔 值时结果为 false。

最后再看真值是什么 

真值就是假值列表之外的值 

再来看一段有意思的代码

var a = 'false';
var b = '0';
var c = "''";
var d1 = Boolean(a && b && c);
var d2 = a && b && c


到目前为止,我们得出的一个结论是:[]、{} 和 function(){} 都不在假值列表中,因此它们都 是真值

再看几个常用的吧

var a = "0";
var b = [];
var c = {};
var d = "";
var e = 0;
var f = null;
var g;
Boolean( a ); // true  特别注意这个,字符串0和空字符串不一样
Boolean( b ); // true
Boolean( c ); // true
Boolean( d ); // false  和第一个比,空字符串是false
Boolean( e ); // false
Boolean( f ); // false
Boolean( g ); // false

那是不是记住假值,就知道哪些是真值了? 

理论上是的…… 

那实际上是什么? 

真正掌握类型转换!


2、显式类型转换

这个其实很好理解

// 字符串转换
var a = 42;
var b = String(a);
// 数字转换
var c = '3.14';
var d = Number(c);
// 布尔值转换
var e = [];
var f = Boolean(e)


3、隐式强制类型转换

1️⃣字符串和数字之间的隐式转换

多的不谈了,简单来说就是,如果 + 的其中一个操作数是字符串(或者通过以上步骤可以得到字符串), 则执行字符串拼接;否则执行数字加法。

var a = '42';
var b = '0';
var c = 42;
var d = 0;
a + b; // "420" 这个地方,注意一下
c + d; // 42

这里有个小坑,可以当做程序员饭后趣谈

console.log([] + {}); // [object object]
console.log({} + []); // ?这会是多少呢?

《you don’t know JS 》中5.1.3章节是这样说的

还有一个坑常被提到(涉及强制类型转换,参见第 4 章) 

[] + {}; // "[object Object]" 
{} + []; // 0

表面上看 + 运算符根据第一个操作数([] 或 {})的不同会产生不同的结果,实则不然。 第一行代码中,{} 出现在 + 运算符表达式中,因此它被当作一个值(空对象)来处理。第4 章讲过 [] 会被强制类型转换为 "",而 {} 会被强制类型转换为 "[object Object]"。 
但在第二行代码中,{} 被当作一个独立的空代码块(不执行任何操作)。代码块结尾不需 要分号,所以这里不存在语法上的问题。最后 + [] 将 [] 显式强制类型转换(参见第 4 章) 为 0。

但目前的chrome浏览器控制台是这样的


对此,你怎么看?😏 

{} 其实应该当成一个代码块,而不是一个 Object,当你在console.log使用的时候,{} 被当成了一个 Object


这下是不是印象更深刻了?


2️⃣ 隐式强制类型转换为布尔值

下面的情况会发生 布尔值隐式强制类型转换。 

  •  (1)if (..)语句中的条件判断表达式。 
  •  (2)for ( .. ; .. ; .. )语句中的条件判断表达式(第二个)。 
  •  (3) while (..) 和 do..while(..) 循环中的条件判断表达式。 
  •  (4)? :中的条件判断表达式。 
  •  (5) 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判断表达式)。


3️⃣ || 与 &&

就一句话,理解了就万岁,称之为“操作数选择器”

a || b;
// 大致相当于(roughly equivalent to): a ? a : b;
a && b;
// 大致相当于(roughly equivalent to): a ? b : a;

只选择其中一个


4、== 与 ===

常见的误区是“== 检查值是否相等,=== 检查值和类型是否相等” 
正确的解释是:“== 允许在相等比较中进行强制类型转换,而 === 不允许。”

两个完全截然不同的理解方向,果然,看书还是要看权威的书好


这一段,看完后我只想总结一句,放弃 == ,拥抱 ===,其他的不谈了



我在学习过程中喜欢做记录,分享的是自己在前端之路上的一些积累和思考,希望能跟大家一起交流与进步。 这是我的[github博客],欢迎star