小陈同学の前端笔记 | 关于8种数据类型的那些事儿你还记得吗?

349 阅读9分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

前言

JavaScript是弱类型编程语言,即在声明变量的时候,不需要指定其数据类型。那么JavaScript一共包含多少种数据类型呢?答案是 8种,分为基本数据类型和引用数据类型。

  • null
  • undefined
  • Boolean
  • String
  • Number
  • Symbol
  • BigInt
  • Object(引用数据类型)

基本数据类型和引用数据类型

基本数据类型保存在栈内存中,其占用空间小,大小固定,通过按值来访问,属于被频繁使用的数据。

引用数据类型保存在堆内存中,其占用空间通常较大,且大小不固定。在栈内存中存储的是指针,该指针指向堆中的地址,即在中存地址,中存内容

堆栈内存示意图

八大数据类型简介

null

null类型只有一个值,即特殊值null。逻辑上讲,null值表示一个空对象指针,因此typeof null === "object"

undefined

undefined类型也同样只有一个值,即特殊值undefined。当时用var或者let声明了变量却没有初始化的时候,就相当于给变量赋予了undefined值,但需要注意的是,包含undefined值的变量跟未定义变量是有区别的。

let name;
// let age;
console.log(name); // 输出undefined
console.log(age); // 报错

Boolean

Boolean布尔类型是非常常用的数据类型之一,它有两个字面值:truefalse。这两个布尔值不同于数值,因此true不等于1,false不等于0。

虽然布尔值只有两个,但所有其他ECMAScript类型的值都有相应布尔值的等价形式。

Truthy表示为真Falsy表示为假
truefalse
非0数正负0
[](空数组)""(空字符串)
{}(空对象)undefined
Infinitynull
......NaN

String

String字符串类型表示零或多个16位Unicode字符序列。字符串可以使用双引号("")、单引号('')或反引号(``)表示。但是以某种引号作为字符串开头,必须仍然以该种引号作为字符串结尾。例如下面的写法会导致语法的错误

let name = '小陈同学吗"; // 语法错误,引号不匹配,连我这个注释都变红色了

ECMAScript中的字符串是不可变的,意思是一旦创建,它们的值就不能变了。要修改某个变量中的字符串值,则必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量。

Number

Number数值类型蛮有意思,可用于表示整数和浮点数,不同的数值类型相应地也有不同的数值字面量格式。

let year = 1998; // 十进制整数
let birthday = 0x1AB // 十六进制的427,0x为16进制数的前缀
let age = 027; // 八进制的23,0为8进制数的前缀

要定义浮点数,数值中必须包含小数点,而且小数点后面必须至少有一个数字。因为存储浮点数使用的内存空间是存储整数值的两倍,所以ECMAScript总是想方设法地把值转化为整数。类似地,如果数值本身就是整数,只是小数点后面跟着0(如1.0),那它也会被转换为整数。

let floatNum1 = 1.; // 小数点后面没有数字,被当做整数1来处理
let floatNum2 = 5.0; // 小数点后面是零,被当做整数10来处理

对于非常大或非常小的数值,浮点数也可以用科学记数法来表示。

let floatNum3 = 3.14125e7; // 等于31412500
let floatNum4 = 3e-17; // 等于0.00000000000000003

此外,有一个特殊的数值叫NaN,意思是“不是数值”(Not a Number),用于表示本来要返回数值的操作失败了,而不是抛出错误。

console.log(0/0); // NaN
console.log(-0/+0); // NaN

若分子是非0值,分母是有符号0或无符号0,则会返回Infinity-Infinity

console.log(5/0); // Infinity
console.log(5/-0); // -Infinity

NaN有几个独特的属性。首先,任何涉及NaN的操作始终返回NaN(如NaN/10)。其次,NaN不等于包括NaN在内的任何值,即NaN != NaN。那么我们该如何判断一个变量是否为NaN呢?ECMAScript提供了isNaN()函数,该函数接收一个参数,可以是任意数据类型,然后判断这个参数是否“不是数值”。该函数会尝试把参数转换为数值,任何不能转换为数值的值都会导致这个函数返回true

console.log(isNaN(NaN));           // true
console.log(isNaN(10));            // false,10是数值
console.log(isNaN("10"));          // false,"10"可以转换为数值10
console.log(isNaN("小陈同学吗"));   // true,不可转换为数值
console.log(isNaN(true));          // false,可以转换为数值1

但我认为,isNaN()函数仅仅做到的是判断一个变量能否转换为数值,若能则返回false,然则反之。它不能用来严格判断是否为NaN,因为小陈同学也可以返回true

ES6提供了Number.isNaN()方法用来判断一个值是否严格等于NaN,它会首先判断传入的值是否为数字类型,如不是,直接返回false。当且仅当参数为NaN时才返回true

Symbol

Symbol是ES6新增的数据类型,它用于表示独一无二的值,确保对象属性使用的唯一标识符,不会发生属性冲突的危险。

它需要使用Symbol()函数初始化,也可以传入一个字符串参数作为对Symbol的描述。

let sym1 = Symbol();
let sym2 = Symbom();

let sym3 = Symbol('小陈同学');
let sym4 = Symbol('小陈同学');

console.log(sym1 == sym2); // false
console.log(sym3 == sym4); // false

下面是使用Symbol作为属性的一个例子。

let mySymbol = Symbol();

// 第一种写法
let obj = {};
obj[mySymbol] = '小陈同学吗';

// 第二种写法
let obj = {
  [mySymbol]: '小陈同学吗'
};

// 第三种写法
let obj = {};
Object.defineProperty(obj, mySymbol, { value: '小陈同学吗' });

// 以上写法都得到同样结果
obj[mySymbol] // "小陈同学吗"

BigInt

BigInt数据类型的目的是比Number数据类型支持的范围更大的整数值(此前Number所能表示的最大值为2^53 - 1)。在对大整数执行数学运算时,以任意精度表示整数的能力尤为重要。使用BigInt,整数溢出将不再是问题。

BigInt是在ES2020中引入的,我在JS高程这本书的目录中并没有看到关于BigInt的内容,了解基础用法如下。

// 方式一:加n
console.log(1n);              // 1n

// 方式二:使用构造函数 BigInt
console.log(BigInt(1));       // 1n

1n === BigInt(1)              // true

typeof 1n;                    // bigint

Object

Object是引用数据类型。它是一组数据和功能的集合,对象通过new操作符后跟对象类型的名称来创建。

// 写法1:
let obj = {name:'小陈同学吗',gender:'男'}

// 写法2:
let obj1 = new Object({name:'小陈同学吗',gender:'男'})

// 写法3:
let obj2 = {}
obj2.name = '小陈同学吗'
obj2.gender = '男'

判断数据类型的方法

typeof

typeof undefined          // "undefined"
typeof true               // "boolean"
typeof "小陈同学吗"        // "string"
typeof 123                // "number"
typeof NaN                // "number" (注意NaN也是Number类型的)
typeof 123n               // "bigint"
typeof Symbol()           // "symbol"

typeof console.log        // "function"
typeof null               // "object" (这很特殊)
typeof {}                 // "object"
typeof [1,2,3]            // "object"

细心的朋友已经发现了,使用typeof检测的类型中,并没有null类型,这是因为,null的本身含义就是表示一个空对象指针,因此就会返回object类型。

那怎么还多了个function类型?

根据红宝书上写的内容,函数在ECMAScript中被认为是对象,并不代表一种数据类型。可是,函数也有自己特殊的属性。为此,就有必要通过typeof操作符来区分函数和其他对象。

使用typeof检测数据类型还是会出现一个问题,那就是无法区分数组和对象,因为数组的打印值也是"object",所以typeof并不是最优解,他是来判断基本类型的,下面再来介绍一下instanceof操作符。

instanceof

instanceof操作符的原理是:通过查找原型链来检测某个变量是否为某个类型数据的实例。

a instanceof A     // 只要右边变量A的 `prototype` 在左边变量a的原型链上即可。

需要注意的是,instanceof只适用于对象类型,不适用原始类型的值。

那么我们来解决刚才的问题:如何检测数组Array类型?

[] instanceof Array    // true
{} instanceof Array    // false
(console.log) instanceof Function // true

可以发现它是可以检测出Array类型的数组,但是这里还是会存在一个问题,Array这个构造函数的实例的原型链上也存在Object,也就是说[] instanceof Object也是返回true。那么这里就存在一个顺序的问题,我们可以写一个函数,先检测是不是Array类型的,再检测Object,我们的函数如下所示。

function checkArray(obj) {
    if(obj instanceof Array) return 'Array';
    if(obj instanceof Object) return 'Object';
    else return 'Others'
}

但其实挺麻烦的,那么到底有没有更具普适性的检测方法呢?有的。

Object.prototype.toString()

Object.prototype.toString()可以说是判断数据类型的究极解决方案了。

Object.prototype.toString.call({})              // '[object Object]'
Object.prototype.toString.call([])              // '[object Array]'
Object.prototype.toString.call(console.log)     // '[object Function]'
Object.prototype.toString.call('小陈同学吗')     // '[object String]'
Object.prototype.toString.call(123)             // '[object Number]'
Object.prototype.toString.call(true)            // '[object Boolean]'
Object.prototype.toString.call(Symbol())        // '[object Symbol]'
Object.prototype.toString.call(null)            // '[object Null]' !!!
Object.prototype.toString.call(undefined)       // '[object Undefined]'

Object.prototype.toString.call(new Date())      // '[object Date]'
Object.prototype.toString.call(Math)            // '[object Math]'
Object.prototype.toString.call(new Set())       // '[object Set]'
Object.prototype.toString.call(new WeakSet())   // '[object WeakSet]'
Object.prototype.toString.call(new Map())       // '[object Map]'
Object.prototype.toString.call(new WeakMap())   // '[object WeakMap]'

该方法不仅仅检测出Array类型,就连null也攻破了,而且还可以检测出DateMathSet等。

但经查阅相关资料,需要说明的是

  • 传入基本数据类型也能够判定出结果是因为对值进行了包装。
  • null 和 undefined 能够输出结果是内部实现有做处理。
  • 自定义类型结果会打印"[object Object]",所以需要用instanceof来判断。

再写个方法把多余部分去掉,只剩下数据类型就好了。

function test(obj){
    let res = Object.prototype.toString.call(obj)
    let str = res.split(" ")[1]
    return str.substring(0,str.length-1)
}
// 测试
// test([]) 打印 Array

结语

JavaScript的数据类型,平时使用的时候感觉还是挺简单的(甚至都没管它),但深挖进去你会发现还是有很多细节上的东西,它会涉及到数据的存储方式(堆栈)、浮点数如何存储与表示、原型链等知识。我也只是通过写文章的形式来巩固我学过的东西,加深记忆。

🎉 好了,小陈在这预祝看到这篇文章或还没看到的朋友们,国庆快乐!

参考资料

  • 《JavaScript高级程序设计(第4版)》
  • 《JavaScript权威指南(第7版)》
  • 网上其他优秀作者的文章