10|JS有哪8种数据类型,你需要注意什么?
下载APP
关闭
渠道合作
推荐作者
10|JS有哪8种数据类型,你需要注意什么?
2022-10-11 石川 来自北京
《JavaScript进阶实战课》
课程介绍
讲述:石川
时长16:11大小14.78M
你好,我是石川。
JavaScript 的数据类型看上去不难理解,但是你确定真的理解并且会用它们吗?实际上,如果不系统地理解数据类型的概念,很可能会导致一些隐藏问题时不时地冒出来,给我们写的程序挖坑。比如简单的加法计算可能就会带来意想不到的结果值;或者没有很好地利用到 JS 的一些长处,让开发更便捷,比如说通过内置的包装对象快速获取值的属性。
在正式开始之前,我还是要先说明一下,虽然现在 JS 介绍数据类型的书和资料不在少数,我们也不会在这里做理论赘述。但我还是会先带你快速建立起对值的基本认识框架,然后通过对问题的深入了解,以此达成扬长避短的目标。
那么,JavaScript 当中有几种类型的值呢?答案是 8 种。如果再归归类,我们还可以把它们分为两大类,分别是原始类型(Primitive Type)和对象类型(Object Type)。
其中,原始数据类型包含了数字、布尔、字符串、BigInt、null、undefined,以及后来新增的 symbol,这个数据类型的值都是不可变的(immutable)。
对象数据类型则包含我们经常说的对象,对象的值是可变的(mutable)。它是一个大类,如果再细分,它又包含了我们常用的数组(array)、函数(function)、Date、RegExp,以及后来新增的 Map 和 Set。没错,我们经常用的数组、函数作为值都属于对象数据类型。
好,在了解了数据类型的划分后,接下来,我们会通过不同类型在实际应用时会出现的各种问题,来进一步了解它们的原理和使用方法,打下底层基础,为后面的学习铺平道路。
下面,我们就从原始类型数据开始说起。
原始类型
原始类型是我们经常用到的数据类型,这里有基础的类型,比如数字、字符串和布尔,也有特殊的类型,比如 null 和 undefined。相比对象类型,原始类型数据从值的角度来看是理论和实际偏差最大的部分,另外因为它看上去比较简单,所以也同时是很容易被忽略的部分。
所以,我们这节课的重点,就是要把这些问题用放大镜放大,深入浅出地了解它的核心原理和解决方案。
number 数字:为什么 0.1+0.2 不等于 0.3?
我们先来做个小实验:在 Chrome 开发者工具中输入 0.1+0.2,得到的结果是什么?惊不惊喜,意不意外?最后的结果竟然不是 0.3,后面还有 。
为什么会出现这样的情况?我们可以先回头看下这个数据类型脑图,JavaScript 中的数字类型包括了两类:浮点数和整数。
而以上运算使用的就是浮点数。那和浮点数对应的是什么呢?是定点数。
定点数的好处是可以满足日常的小额计算。但它也有很大的缺点,就是在进行很小或很大的数字计算时,会浪费很大的空间。比如我们想表达中国有 14 亿人口, 如果写起来,就是 1,400,000,000,14 后面 8 个零。所以要解决这个问题,就需要用到科学计数法。
浮点数是采用科学计数法来表示的,由尾数(significand mantissa)、基数(base)和指数(exponent)三部分组成。上面的数字如果是用科学计数法表示,只需要写成 即可。对于小数,就是反之,拿 0.002 举例,数字是 2,那么小数就是 10 的负 3 次方。
上面我们看的是十进制的例子,而 JavaScript 所采用的IEEE 754 是二进制浮点数算术标准。这个标准里规定了 4 种浮点数算术方式:单精确度、双精确度、延伸单精确度与延伸双精确度。JavaScript 在这里选择的又是双精确度(64 位)这种方式,通常也叫 double 或者 float64 类型。
这种方式顾名思义,有 64 位比特。其中包含了 1 个比特的符号位(sign)、11 个比特的有偏指数(exponent)、还有 52 个比特的小数部分(fraction)。
因为把十进制转化为二进制的算法是用十进制的小数乘以 2 直到没有了小数为止,所以十进制下的有些小数无法被精确地表示成二进制小数。而既然这里的浮点数是二进制,因此小数就会存在精度丢失的问题。
而且当我们使用加减法的时候,由于需要先对齐(也就是把指数对齐,过程中产生移位),再计算,所以这个精度会进一步丢失。并且根据 JavaScript 引擎实际返回的小数点后的位数,可能会出现第三次丢失。这样下来,最后的结果就和实际相加减的数有偏离。
现在,我们就了解了精度丢失的问题,这并不是一个 bug,而是符合标准设计的。同时,我们也了解了出现这种情况的原因和实际运算的步骤,那我们需要做什么才能解决由它引起的精度问题呢?
通常对于这个问题的处理,是通过按比例放大再缩小。我们来看个例子,假如要设置一个 19.99 元的商品,我们可以把它先变成 1999,这样就可以做加法计算,之后再缩小。
NaN:如何判断一个值是不是数字?
如果说浮点数给我们带来了意料之外、情理之中的惊喜,那么 NaN 带给我们的则是意料之外且情理之外的惊喜了。
惊喜 1
在 IEEE 754 中,NaN 虽然代表的是“不是数字”的意思,但是如果我们用 typeof NaN 来获取,会发现它返回的是 number。
惊喜 2
原始类型有个特点,就是两个数据的数值一样,会被当做是等同的。而对象类型则相反,即使两个数据的数值一样,也会被当做是不同的数值,每一个数值都有一个唯一的身份。
我们可以通过下面的例子来看一下。当我们严格比较两个数字时,返回的就是 true;当我们严格比较两个对象字面量时,返回的结果就是 false。
按照这样的原则,既然 NaN 是数字的值,如果我们输入 NaN 严格比较 NaN,原则上应该返回的是 true,可实际 NaN 返回的却是 false。
惊喜 3
JavaScript 中会通过 isNaN 来判断一个值是不是数字,但是当我们输入一个字符串,它也会被当做是一个数字。因为在这个过程中,“0”这个字符串被转换成了数字。
所以,从这些惊喜中我们可以发现,想通过 NaN 和 isNaN 来判断一个值是不是数字的做法,是很不靠谱的。
那么,如何才能更正确地判断一个值是不是数字呢?
我们可以通过判断值的类型,并加上一个 isFinite 这种方式来判断。isFinite 是 JavaScript 中的一个内置函数,通过它,我们可以过滤掉 NaN 和 Infinity。
但是要注意,和惊喜 3 一样,它会把括号中的值比如字符串转化成数字,所以我们需要再通过 typeof 来确保这种被转换的问题不会被漏掉。
string 字符串:一串字符有多长?
我们知道,原始类型的数据除了 undefined 和 null 以外,都有内置的包装对象(object wrapper)。那么下面就让我们通过字符串,来看一下它是怎么工作的。
可以看到在这个例子中,我们是用 new String() 这样的 constructor 的方式创建一个字符串。而当我们想要获取它的长度时,就可以采用 str.length 方法来获取。
但是,即使你不用 constructor 这种方式,也仍然可以用字面量的方式来获取字符串的长度(length)。在这个过程中,你同样可以看到长度结果的返回。而且,当你再用 typeof 来获取它的类型时,收到的结果仍然是字符串,而不是对象。
这是因为在你使用 length 这个方法的时候,JavaScript 引擎临时创建了一个字符串的包装对象。当这个字符串计算出长度后,这个对象也就消失了。所以当你再回过头来看 str 的类型时,返回的是字符串而不是对象。
boolean 布尔:你分得清真假吗?
在 Java 中,布尔类型的数据包含了真值 true 和假值 false 两个值。但需要注意的是,在 JavaScript 中,除了 false 以外,undefined、null、0、NaN 和“‘’”也都是假值。
这里你一定会问,那么真值有哪些呢?其实你可以使用排除法,除了假值以外的,都可以认为是真值。为了方便查询,你可以参考下面这个列表。
null:什么,你是个对象?
我们前面说过,null 是六种原始数据类型中独立的一种。可当我们用 typeof 来获取 null 的种类时,返回的结果是’object’,也就是说它是属于对象类型。
这是一个 bug,但是也并非完全没有逻辑的 bug,因为 null 实际是一个空的对象指针。
那我们要如何判断一个值是不是 null 呢?解决这个问题方法,其实就是不用 typeof,而是直接将值和null做严格比较。
除了 null 以外,另外一个和它类似的是 undefined。如果说 null 代表值是空对象,undefined 代表的就是没有值。但是当我们对比它们的值时,它们却是相等的;另外严格比较它们的数据类型的时候,又会发现它们是不同的。
所以,我们判断值是否为空,可以是 if (x === undefined || x === null) {},也可以是 if (!x) {…}。
那么我们什么时候用 undefined,什么时候用 null 呢?通常我们是不用 undefined 的,而是把它作为系统的返回值或系统异常。比如当我们声明了一个变量,但是没有赋值的情况下,结果就是 undefined。而当我们想特意定义一个空的对象的时候,可以用 null。
对象类型
说完了原始类型,我们再来看看对象类型。原始类型的问题都是非黑即白的,比如我们前面看到的问题都是由于 JavaScript 设计中的某种限制、缺陷或 bug 造成的。
而对象类型的问题,更多是在不同场景下、不同的使用方式所体现出的优劣势。
为什么基于对象创建的实例 instanceOf 返回错误?
你要创建一个对象,既可以通过字面量也可以通过 constructor 的模式(我们在后面讲设计范式的时候还会更具体地提到这一点,这里我们只需要了解它的不同使用方式)。但这里你需要注意的问题是,如果你进一步基于一个对象创建实例,并且用到我们之前讲面向对象编程模式中提到的 Object.create() 的话,这样的情况下,你没法用 instanceOf 来判断新的实例属于哪个对象。
因为这里的两个对象间更像是授权而不是继承的关系,之间没有从属关系,所以返回的是错误。而通过经典的基于原型的继承方式创建的实例,则可以通过 instanceOf 获取这种从属关系。
这里我们可以来对比下字面量,constructor 以及基于原型的继承的使用,具体你可以参考以下代码示例:
其实,不光是对象,数组和函数也都可以不用 constructor,而是通过字面量的方式创建。反之,我们说的数字、字符串和布尔除了字面量,也都可以通过 constructor 创建。
我们知道,原始类型是不可改变的,而对象类型则是可变的。比如下面这个例子,当定义一个字符串,通过 toUpperCase() 把它变成大写后,再次获取它的值时,会发现其仍然是小写。
而如果我们尝试给一个对象增加一个属性,那么再次获取它的属性的时候,其属性就会改变。
另外一点需要注意的是,对象数据在栈中只被引用,而实际存放在堆中。举个例子,假如我们有两个变量,变量 personA 赋值为{name: “John”, age:25},当我们将 personB 赋值为 personA,然后修改 personA 的名称,那么 personB 的名字也会改。因为它们引用的都是堆中的同一个对象。
如何识别一个数组?
前面我们说过,数组其实是对象,那我们怎么能判断一个值是数组还是对象呢?其实就是我刚刚提到过很多次的 typeof,它可以帮我们了解一个“值”的种类。当我们用 typeof 来获取对象和数组的种类时,返回的都是 object。
但是,前面我们也说过,null 返回的也是 object,这样我们用 typeof 是没有办法来实际判断一个值是不是对象的。不过,因为 null 是一个假值,所以我们只要加一个真假判断,和 typeof 结合起来,就可以排除 null,来判断是否一个值实际是不是对象。
好,上面的例子能筛选对象了,可我们仍然没法判断一个对象是不是数组。
其实,在 ES5 以后,JavaScript 就有了 isArray 的内置功能,在此之前,人们都是通过手写的方式来判断一个值是不是数组。但在这儿,为了更好地理解原理,我们可以在上面例子的基础上,做一个 isArray 的功能。
这里我们用到了数组的特性,数组虽然从数据类型上看也是一种对象,但是它和对象值相比,区别在于是否可以计算长度。
除此之外,我们也可以用在前面讲到面向对象时,讲到的“基于原型的继承”中学过的原型来判断。
function 函数字面量:是声明还是表达?
函数在 JavaScript 中主要有两种写法,表达式和声明式。
声明式函数
我们先来看看在 JavaScript 中,什么是声明,都有哪些形式的声明。
如下图所示,我们大致把声明分为了变量、常量、函数和类这四种类型。从中可以看出,这四大类声明几乎都是我们接触过的常用的语句,比如变量、常量和函数都是我们在讲到函数式和不可变时有提到过的,类则是我们在面向对象编程时讲过的。
说完声明,我们再看看声明式函数。声明式函数最大的好处就是可以用于函数提升 hoisting。可以说,除了有特殊需求会用到表达式以外,声明式就是默认的写法,下面是一个声明式函数的抽象语法树 AST(abstract syntax tree)。
表达式函数
下面我们再来看看表达式函数。首先同样的问题,什么是表达式?一个语句里面可以有很多的表达,而表达式函数其实就是把函数作为这样的一个表达。在表达式函数中,又可以分为两种写法:
第一种是把函数当一个字面量的值来写,它的好处是可以自我引用。
第二种是在 ES6 中,函数表达也可以通过箭头函数的方式来写。它的好处是可以用于 this、参数和 super 相关的绑定。
延伸:类型之间如何转换?
另外,在 JavaScript 中,我们还会经常遇到的一个问题是:如何将一个值从一种类型转换到另外一种类型呢?
其实这里可以用到强制多态 coercion。强制多态一般分为两种方式:一种是通过显性 explicit 的;另外一种是通过隐性 implicit 的。比如说,我们可以通过显式 explicit,将一个字符串转化成为一个数字。
以上面这段代码为例,当 a = 42 时,它是一个数字,b 通过隐性的强制多态,把它变成了一个字符串。c 则通过显性的强制多态,把它变成了一个字符串。在ECMAScript 的官方文档中,有相关的运行时的判断条件,实际浏览器会通过类似以下的算法来判断对应的处理。
总结
通过这一讲,希望你对 JavaScript 的数据类型有了更系统的了解,也对不同数据类型的相关问题有了更好的解决方式,从而扬长避短。
但除了解决执行层面的问题,也更希望你能发现 JavaScript 之美和不足之处。如果你之前看过《黄金大镖客》,可能知道它的英文名字叫The Good, the bad, the ugly。在 JavaScript 里,通过对数据类型的认识,我们同样可以看到它的美和不足之处,这同时也才是它的精彩之处。
它的美在于简单、灵活。我们可以看到数字、字符串虽然简单,但又可以灵活地使用内置的包装对象,来返回属性和使用强大的调用功能;对象、函数、数组虽然是复杂的数据类型,但又都可以通过字面量来表示。
同时,JS 也有着不足之处,特别是原始数据的一些缺陷和 bug,但正是它的灵活和没有那么多条条框框,当初才吸引了很多开发者。除了语言本身的进化外,工程师们通过各种方式克服了这些缺陷。
最后,我们再来丰富下开篇中的脑图,一起总结下今天所学到的内容。让我们在记忆宫殿中将这些关键的信息通过这张脑图,放到我们信息大厦里的“JS 之法”这一层吧!
思考题
课程中我们总结过有些类型的数据既可以用字面量也可以用 constructor 来创建,那你觉得哪种方式在不同场景下更适合呢?
欢迎在留言区分享你的答案和见解,我们一起交流讨论。另外,也欢迎你把今天的内容分享给更多的朋友。我们下期再见!
分享给需要的人,Ta购买本课程,你将得18元
生成海报并分享
赞 2
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
09|面向对象:通过词法作用域和调用点理解this绑定
下一篇
11|通过JS引擎的堆栈了解闭包原理
精选留言(6)
- Geek_0031682022-10-13 来自北京一般原始类型就很少用构造函数创建。 字面量创建的对象都是一次性的,创建同类的对象需要多次创建,自定义构造函数可以实现同类的只用定义一次就好了,降低了代码冗余度。1
- AbyssKR2022-11-17 来自海南原始类型是不可改变的,下面的示例代码: var str = 'hello'; str.toUpperCase; // 应该是 str.toUpperCase();
作者回复: 谢谢指正!已经修改了。
- Silence2022-10-27 来自北京对象类型的图有问题,Symbol 没有构造函数,new 的话会报 Symbol is not a constructor
作者回复: 图里没有new吧,Symbol也是一个constructor。https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/Symbol。不过提醒大家用Symbol()的时候不要加new,这是一个好的点,值得注意,谢谢提出。
- 英雄各有见2022-10-25 来自北京看过某位大佬的博客,定义undefined严格意义上要使用 void 0 好些,undefined不是关键字是window对象的一个属性,可以赋值的...
作者回复: 这是很好的一个点,但是从ES5开始,window的undefined就不能赋值了。 如果要避免本地变量用undefined命名的话,可以用ESLint rule no-undefined来避免。 很多前端的minify工具也可以在优化时做到void 0。考虑到代码还是给其它程序员读的,建议可以用undefined,把void 0的转换交给优化工具来处理。
共 4 条评论 - Hello,Tomrrow2022-10-20 来自上海原始类型,我们通过更习惯使用功能字面量的形式来创建(除symbol外),主要是考虑到编码习惯;数组和对象,更多的使用的也是字面量形式,简洁清晰。
- 天择2022-10-14 来自新加坡以对象为例,字面量的方式可以一次性初始化很多字段,包括复杂字段和函数,前提是要了解对象的定义和结构;而contructor则依赖定义时的参数,如果参数不够,则需要显式地通过赋值或者方法初始化。不过constructor的方式可以做一些额外的工作,能做的事情不局限于字段初始化,字面量的方式无能为力。