一、数据存储
下面我们就先来看看 JavaScript 中的数据是如何存储的。
1. 语言类型
说到JavaScript的数据存储机制,首先我们需要知道,JavaScript 究竟是什么类型的语言?
一般情况下可以根据声明变量的特点,将语言分为静态语言和动态语言:
- 静态语言: 在使用之前就需要确认其变量数据类型的称为静态语言;
- 动态语言: 相反的,在运行过程中需要检查数据类型的语言称为动态语言。
很显然,JavaScript 就是一门动态语言,因为在声明变量之前并不需要确认其数据类型。
对于一个变量,我们既可以给他设置为一个数字,也可以给他设置为一个字符串,还可以让字符串类型的变量和数值类型的变量相加。在这个过程中,可以发生隐式的类型转化。弱类型语言可以发生隐式类型转换,而强类型语言不能发生隐式类型转换。而JavaScript就是弱类型语言。
现在我们知道了,JavaScript是一种弱类型、动态的语言。这就意味着,无需告诉JavaScript引擎变量是什么类型,JavaScript 引擎在运行代码时会自己计算出来。同时,我们可以使用同一个变量保存不同类型的数据。
2. 堆栈空间
对于 JavaScript 中的 8 种数据类型,其中前7种都是基础类型,最后1种是引用类型。
这些数据主要存储在栈空间和堆空间中,下面来看看栈空间和堆空间的概念。
下面来看一段代码:
function fn() { var a = "hello"; var b = a; var c = { name: "CUGGZ"}; var d = c; } fn() 复制代码
当这段代码执行时,需要先进行编译,并创建执行上下文,最后在按照顺序执行代码。当执行到第三行时,调用栈执行状态如下:
此时变量 a 和 b 的值都被保存在执行上下文中, 而执行上下文又被压入到了栈中,所以变量 a 和 b 的值都是存放在栈中 的。
接下来继续执行后面的代码。当执行到第四行代码时,JavaScript 引擎判断变量 c 的值是一个引用类型,这时JavaScript 引擎会将该对象分配到堆空间里,分配后该对象会有一个在堆中的地址,然后再将该数据的地址写进 c 的变量值,最终分配好内存的执行上下文如下:
可以看到,对象类型存储在堆空间中,在栈空间中只保留了对象的引用地址,当 JavaScript 访问该数据时,会通过栈中的引用地址来访问。
所以,基本数据类型的值直接保存在栈中,引用类型的值会存放在堆中。
那为什么要区分堆空间和栈空间呢?将数据都存在栈空间中不行吗?
答案肯定是不可以的。JavaScript 引擎需要使用栈来维护程序执行期间上下文的状态,如果将所有数据都放在栈空间中,就会影响到上下文切换的效率,进而影响到整个程序的执行效率。
所以,通常情况下,栈空间不会设置的很大,主要用来存放一些基本类型的小数据。由于引用类型的数据占用空间都比较大,所以这类数据会被存放到堆中,堆空间比较大,能存放很多较大的数据。
最后,我们再看看上面实例代码中第五行,也就是将变量 c 赋值给变量 d 是怎么执行的。在 JavaScript 中,原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。 所以d = c 的操作就是把 c 的引用地址赋值给 d,如下图所示:
可以看到,变量 c 和 d 都指向了同一个堆中的对象,当我们修改c的值时,d也会发生变化。
二、数据类型
说完JavaScript数据的存储方式,下面再来看看JavaScript中8种数据类型的一些细节,一些概念这里就不再提了。
1. Undefined
Undefined 类型表示未定义,它的类型只有一个值,就是 undefined。任何变量在赋值前是 Undefined 类型、值为 undefined,可以用全局变量 undefined 来表达这个值。可以通过以下方式来得到 undefined: (1)声明了一个变量,但没有赋值
var foo; //undefined 复制代码
(2)引用未定义的对象属性
var obj = {} obj.b // undefined 复制代码
(3)函数定义了形参,但没有传递实参
function fn(a) { console.log(a); //undefined } fn(); 复制代码
(4)执行 void 表达式;
void 0 // undefined 复制代码
推荐通过 void 表达式来得到 undefined 值,因为这种方式既简便又不需要引用额外的变量和属性;同时它作为表达式还可以配合三目运算符使用,代表不执行任何操作。
如下面的代码就表示满足条件 x 大于 0 且小于 5 的时候执行函数 fn,否则不进行任何操作:
x > 0 && x < 5 ? fn() : void 0; 复制代码
那如何判断一个变量的值是否为 undefined 呢?可以通过 typeof 关键字获取变量 x 的类型,然后与 'undefined' 字符串做真值比较:
if(typeof x === 'undefined') { ... } 复制代码
2. Null
Null 数据类型和 Undefined 类似,只有一个值 null,表示变量被置为空对象,而非一个变量最原始的状态。null 是 JavaScript 保留关键字,而 undefined 只是一个常量。也就是说可以声明名称为 undefined 的变量,但将 null 作为变量使用时则会报错。
对于null,还有一个比较关键的问题,来看代码:
typeof null == 'object' // true 复制代码
实际上,null 有自己的类型 Null,而不属于Object类型,typeof 之所以会判定为 Object 类型,如下:
在 JavaScript 第一个版本中,所有值都存储在 32 位的单元中,每个单元包含一个小的 类型标签(1-3 bits) 以及当前要存储值的真实数据。类型标签存储在每个单元的低位中,共有五种数据类型:
000: object - 当前存储的数据指向一个对象。 1: int - 当前存储的数据是一个 31 位的有符号整数。 010: double - 当前存储的数据指向一个双精度的浮点数。 100: string - 当前存储的数据指向一个字符串。 110: boolean - 当前存储的数据是布尔值。 复制代码
如果最低位是 1,则类型标签标志位的长度只有一位;如果最低位是 0,则类型标签标志位的长度占三位,为存储其他四种数据类型提供了额外两个 bit 的长度。
有两种特殊数据类型:
- undefined的值是 (-2)30(一个超出整数范围的数字);
- null 的值是机器码 NULL 指针(null 指针的值全是 0)。
那也就是说null的类型标签也是000,和Object的类型标签一样,所以会被判定为Object。
可以通过另一种方法获取 null 的真实类型:
Object.prototype.toString.call(null) ; // [object Null] 复制代码
当然 undefined 类型也可以通过这种方式来获取:
Object.prototype.toString.call(undefined) ; // [object Undefined] 复制代码
当通过 “==” 来比较null 和 undefined 是否相等时,得到的结果是 true:
undefined == null; //true 复制代码
在 Javascript 规范中提到,要比较相等性之前,不能将 null 和 undefined 转换成其他任何值,并且规定null 和 undefined 是相等的。null 和 undefined都代表着无效的值。
3. Boolean
Boolean 数据类型只有两个值:true 和 false,分别代表真和假。很多时候我们需要将各种表达式和变量转换成 Boolean 数据类型来当作判断条件。
下面是将星期数转换成中文的函数,比如输入数字 1,函数就会返回“星期一”,输入数字 2 会返回“星期二”,以此类推,如果未输入数字则返回 undefined:
function getWeek(week) { const dict = ['日', '一', '二', '三', '四', '五', '六']; if(week) return `星期${dict[week]}`; } 复制代码
这里在 if 语句中会进行类型转换,将 week 变量转换成 Boolean 数据类型,而 0、空字符串、null、undefined 在转换时都会返回 false。所以在输入 0 并不会返回“星期日”,而会返回 undefined。这是我们需要注意的问题。