js中的数据类型及类型检测方法

3,455 阅读10分钟

动态类型

JavaScript 是一种弱类型或者说动态语言。这意味着你不用提前声明变量的类型,在程序运行过程中,类型会被自动确定。这也意味着你可以使用同一个变量保存不同类型的数据:

let a = 18;  // a 现在是一个数字
a = "js";  // a 现在是一个字符串
a = true;  // a 现在是一个布尔值

数据类型

JavaScript 语言中类型集合由 原始值对象 组成。

目前 JavaScript 中的原始值即基本数据类型包括:Boolean(布尔类型), Number(数字类型), String(字符串类型), Undefined, Null, BigInt, Symbol(符号类型)。其中BigIntSymbol是 ES6 新增的数据类型。

JavaScript 中的对象即引用类型包括Object类型, Function类型, Array类型, Date类型, RegExp类型和基本包装类型

本质区别

基本数据类型和引用类型在内存中的存储方式不同

  • 基本数据类型是直接存储在栈中的简单数据段,占据空间小,属于被频繁使用的数据。
  • 引用类型是存储在堆内存中,占据空间大。引用类型在栈中存储了指针,该指针指向堆中该实体的起始地址,当解释器寻找引用值时,会检索其在栈中的地址,取得地址后从堆中获得实体。

原始值

除对象类型(object)以外的其它任何类型定义的不可变的值(值本身无法被改变)。

布尔类型

在计算机科学中,布尔值 是一种取值仅能为  或  的数据类型,它赋予了编程语言在逻辑上表达 或  的能力。

数字类型

目前 ECMAScript 标准定义了两种数值类型:Number(数字类型)和 BigInt(见下方)。

数字类型是一种基于 IEEE 754 标准的双精度 64 位二进制格式的值(从 -(2^53-1) 到 (2^53-1) 之间的数字)。除了能够表示浮点数外,还有三个带符号的值:+Infinity-Infinity 和 NaN(非数值,Not a Number)。

全局属性 Infinity 是一个数值,表示无穷大。

字符串类型

JavaScript 的字符串类型用于表示文本数据。它是一组 16 位的无符号整数值的“元素”。在字符串中的每个元素占据了字符串的位置。第一个元素的索引为 0,下一个是索引 1,依此类推。字符串的长度是它的元素的数量。

Undefined类型

undefined全局对象的一个属性。也就是说,它是全局作用域的一个变量。undefined的最初值就是原始数据类型undefined

一个没有被赋值的变量的类型是 undefined。如果方法或者是语句中操作的变量没有被赋值,则会返回 undefined

Null类型

null 是一个字面量,不像 undefined,它不是全局对象的一个属性。null 是表示缺少的标识,指示变量未指向任何对象。把 null 作为尚未创建的对象,也许更好理解。在 API 中,null 常在返回类型应是一个对象,但没有关联的值的地方使用。

BigInt类型

BigInt 是一种内置对象,特点是数据涵盖的范围大,能够解决超出普通数据类型范围报错的问题。它提供了一种方法来表示大于 2^53-1 的整数。这原本是 Javascript 中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。

使用 BigInt,应用程序不再需要变通方法或库来安全地表示 Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER 之外的整数。现在可以在标准JS中执行对大整数的算术运算,而不会有精度损失的风险。

console.log(Number.MAX_SAFE_INTEGER);  // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER);  // -9007199254740991

使用方法:

  1. 整数末尾直接 +n
  2. 调用 BigInt()
let a = 29292929291188181818n;
let b = BigInt(121212321312342323);
typeof a;  // bigint
typeof b;  // bigint

Symbol类型

这种数据类型的特点是没有重复的数据,可以作为 objectkey

let sym1 = Symbol('zzz');
let sym2 = Symbol('zzz');
console.log(sym1 == sym2);  // false

数据的创建方法 Symbol(),因为它的构造函数不够完整,所以不能使用 new Symbol() 创建数据。由于 Symbol() 创建数据具有唯一性,所以 Symbol() !== Symbol(), 同时使用 Symbol 数据作为 key 不能使用 for 循环获取到这个 key,需要使用 Object.getOwnPropertySymbols(obj) 获得这个对象中 key 类型。

let key = Symbol('key')
let obj = { [key]: 'symbol'}
let keyArray = Object.getOwnPropertySymbols(obj)  // 返回一个数组[Symbol('key')]
obj[keyArray[0]]  // 'symbol'

对象

在计算机科学中, 对象(object)是指内存中的可以被标识符引用的一块区域。

对象拥有两种属性:数据属性访问器属性

Object类型

常见的创建 Object 实例的方式有两种:

一种方式是使用对象字面量表示法

// 注意使用逗号分隔不同属性
var person = {
    name: '小明',
    age: 20
};

另一种是 new Object

var person = new Object();
person.name = "小明";
person.age = 20;

Function类型

每个函数都是 Function 类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此 函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。函数通常是使用函数声明语法定义的,如下面的例子所示:

function sum(num1, num2) {
    return num1 + num2;
}

下面使用函数表达式定义函数:

var sum = function(num1, num2) {
    return num1 + num2;
}

还有一种用 Function 构造函数定义函数的方法,Function 构造函数可以接收任意数量的参数,但最后一个参数始终都被看成是函数体,而前面的参数则枚举出了新函数的参数。例如:

var sum = new Function("num1", "num2", "return num1 + num2");
sum(1, 2);  // 3

函数内部属性

在函数内部,有两个特殊的对象:arguments 和 this

其中 arguments 是一个类数组对象,包含着传入函数中的所有参数。arguments 的主要用途是保存函数参数,但这个对象还有一个名叫 callee 的属性,该属性是一个指针,指向拥有这个 arguments 对象的函数。

另一个特殊对象是 this,其行为与 Java 和 C# 中的 this 大致类似。换句话说,this 引用的是函数据以执行的环境对象——或者也可以说是 this 值(当在网页的全局作用域中调用函数时,this 对象引用的就是 window )。

Array类型

ECMAScript 数组的每一项可以保存任何类型的数据。例如,可以用数组的第一个位置来保存字符串,用第二位置来保存数值。而且,ECMAScript 数组的大小是可以动态调整的,可以随着数据的添加自动增长以容纳新增数据。

创建数组的基本方式有两种。第一种是使用数组字面量表示法:

var colors = ["red", "blue", "green"];  // 创建一个包含 3 个字符串的数组
var names = [];  // 创建一个空数组

第二种是使用 Array 构造函数:

var arr = new Array();

Date类型

要创建一个日期对象,使用 new 操作符和 Date 构造函数即可,如下所示:

var now = new Date();

在调用 Date 构造函数而不传递参数的情况下,新创建的对象自动获得当前日期和时间。若根据特定的日期和时间创建日期对象,必须传入表示该日期的毫秒数(即从 UTC 时间 1970 年 1 月 1 日午夜起至该日期止经过的毫秒数)。

为了简化这一计算过程,ECMAScript 提供了两个方法: Date.parse()Date.UTC()

Date.parse() 方法接收一个表示日期的字符串参数,然后尝试根据这个字符串返回相应日期的毫秒数。若该字符串不能表示日期,那么它会返回 NaN 。实际上,如果直接将表示日期的字符串传递给 Date 构造函数,也会在后台调用 Date.parse()

Date.UTC() 方法同样也返回表示日期的毫秒数。参数分别是年份、基于 0 的月份(一月是 0,二月是 1,以此类推)、月中的哪一天(1 到 31)、小时数(0 到 23)、分钟、秒以及毫秒数。

RegExp类型

ECMAScript 通过 RegExp 类型来支持正则表达式。RegExp 对象用于将文本与一个模式匹配。

有两种方法可以创建一个 RegExp 对象,一种是字面量:

// var regex = /pattern/modifiers
var regex = /^[0-9]+$/g;

另一种是构造函数 new RegExp()

// var regex = new RegExp("pattern", "modifiers") 
var regex = new RegExp("^[0-9]+$", "g");

关于正则表达式的详细介绍请参考:JS正则表达式完整教程(略长)

基本包装类型

var str = 'hello';  // string类型
str.charAt(0);  // h

上面的 str 是一个基本数据类型,为什么它却能调用 charAt() 方法呢?

是因为在执行第二行代码时,后台会自动进行下面的步骤:

  1. 自动创建 String 类型的一个实例(和基本类型的值不同,这个实例就是一个基本包装类型的对象)
  2. 调用实例(对象)上指定的方法
  3. 销毁这个实例

包装对象,就是当基本数据类型以对象的方式去使用时,JavaScript 会转换成对应的包装类型,相当于 new 一个对象,内容和基本类型的内容一样,然后当操作完成再去访问的时候,这个临时对象会被销毁。

number、string、boolean 都有对应的包装类型。

因为有了基本包装类型,所以 JavaScript 中的基本类型值可以被当作对象来访问。

检测数据类型

检测数据类型的方法:

typeof

// 基本数据类型 
console.log(typeof "123");  // string
console.log(typeof 1);  // number
console.log(typeof true);  // boolean
console.log(typeof undefined);  // undefined
console.log(typeof null);  // object 
console.log(typeof Symbol('sym'));  // symbol
console.log(typeof 9n);  // bigint
// 引用类型 
console.log(typeof []);  // object
console.log(typeof function(){});  // function
console.log(typeof {});  // object

为什么 typeof null 结果为 object

这个 bug 是第一版 Javascript 留下来的。在这个版本,数值是以 32 字节存储的,由标志位(1~3个字节)和数值组成。标志位存储的是低位的数据。

在 JavaScript 中二进制前三位都为 0 的话会被判断为 object 类型, null 的二进制表示是全 0,自然前三位也是 0,所以执行 typeof 时会返回 object。详情请参考: The history of “typeof null”

instanceof

instanceof 可以用于引用类型的检测,但对于基本数据类型是不生效的。

另外,不能用于检测nullundefined,会报错。

// 基本数据类型
console.log('1' instanceof String);  // false
console.log(1 instanceof Number);  // false
console.log(true instanceof Boolean);  // false
console.log(undefined instanceof undefined);  // TypeError
console.log(null instanceof null);  // TypeError
console.log(typeof Symbol('sym') instanceof Symbol);  // false
console.log(typeof 9n instanceof BigInt);  // false
// 引用类型
console.log([] instanceof Array);  // true
console.log(function (){} instanceof Function);  // true 
console.log({} instanceof Object);  // true

Object.prototype.toString.call

// 基本数据类型
console.log(Object.prototype.toString.call(1));  // [object Number]
console.log(Object.prototype.toString.call('Hello'));  // [object String]
console.log(Object.prototype.toString.call(true));  // [object Boolean]
console.log(Object.prototype.toString.call(undefined));  // [object Undefined]
console.log(Object.prototype.toString.call(null));  // [object Null]
console.log(Object.prototype.toString.call(1n));  // [object BigInt]
console.log(Object.prototype.toString.call(Symbol('sym')));  // [object Symbol]
// 引用类型
console.log(Object.prototype.toString.call(console.log));  // [object Function]
console.log(Object.prototype.toString.call(new Date));  // [object Date]
console.log(Object.prototype.toString.call([1, 2, 3]));  // [object Array]
console.log(Object.prototype.toString.call(new Object));  // [object Object]

在任何值上调用 Object 原生的 toString() 方法,都会返回一个 [object NativeConstructorName] 格式的字符串。每个类在内部都有一个 [[Class]] 属性,这个属性中就指定了上述字符串中的构造函数名。但是它不能检测非原生构造函数的构造函数名。

constructor

// 基本数据类型
console.log((1).constructor === Number);  // true
console.log('hello'.constructor === String);  // true
console.log(true.constructor === Boolean);  // true
console.log((1n).constructor === BigInt);  // true
console.log(Symbol('sym').constructor === Symbol);  // true
// 引用类型
console.log(console.log.constructor === Function);  // true
console.log((new Date).constructor === Date);  // true
console.log((new Object).constructor === Object);  // true
console.log([1, 2, 3].constructor === Array);  // true

注意:constructor 不能判断 undefinednull

相关参考: