在学习JavaScript的基本类型与引用类型的相关知识之前 , 我们可以先了解几个关于内存的预备知识 , 以便更好地理解后面的内容。
由C/C++编译的程序的内存分配
堆区(heap)
一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式类似 于链表 , 堆区的数据是存放在二级缓存中的,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)
栈区(stack)
由编译器自动分配释放,存放函数的参数值,局部变量的值等 , 其操作方式类似于数据结构中的栈, 栈区的数据使用的是一级缓存, 它们通常都是被调用时处于存储空间中,调用完毕立即释放.
全局区(静态区)(static)
全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域 , 程序结束后由系统释放.
文字常量区
常量字符串就是放在这里的 , 程序结束后由系统释放 。
程序代码区
存放函数体的二进制代码。
堆区与栈区的区别
栈区
先进后出 , 驻留于常规RAM(随机访问存储器)区域,这是一种特别快、特别有效的数据保存方式,仅次于寄存器。
堆区
顺序随意, 一种常规用途的内存池(也在RAM区域), 它最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必 知道存储的数据要在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。
以上是关于C/C++ 编译的程序的内存分配的相关知识, 那么同样在JS中也存在堆区(heap) 与 栈区(stack)这种概念, 大家都知道JS是一门解释型脚本语言, 在浏览器中需要由解释器(例如V8, Rhin, SpiderMonkey等等)一边编译一边执行, 那肯定就没有那么多种内存分区了, JavaScript中只有堆区与栈区, 理解起来也很简单, 下面就开始介绍JavaScript的相关知识
JavaScript中的基本类型与引用类型
JavaScript 中的变量可以包含两种不同数据类型的值 : 基本类型值 和 引用类型值
基本类型值
指的是 5 种简单的数据段: undefined、null、boolean、number、string,它们被储存在栈区中,操作变量,就会影响实际的值。
引用类型值
引用类型是一种数据结构,它的值包括 Object、Array、Date、RegExp、Boolean、Number、String,引用类型值是引用类型的一个实例。
例如:
let a = new Object
let b = new String
那么 a
和 b
就分别是 Object 类型 和 String 类型的一个实例
引用类型的值被保存在堆区中,它不允许被直接操作,因此需要通过保存在栈区中的相应的指针(Pointer)来操作。
可以用下面这张图来理解:
基本数据类型值占据空间小、大小固定,属于被频繁使用的数据,所以直接将值存储在栈区中
引用数据类型值占据空间大、大小不固定 , 如果也存储在栈区中,将会影响程序性能 , 所以将实际值储存在堆区中 , 将指针储存在栈区中
当解释器寻找引用数据类型值时,会首先检索其在栈区中的指针,取得指针后从堆区中获得相应实际值
以上是相关概念, 下面用实际例子来说明这2种类型的区别
基本类型的复制
var foo = 'Leo';
var bar = foo;
foo = 'Bridy';
console.log(foo); // output: 'Bridy'
console.log(bar); // output: 'Leo'
在复制基本类型的时,JS 会直接在栈区中创建一个新的副本 , 因此当第一个变量的值发生改变, 并不会被影响第二个变量
下图很形象地展示了基本类型值的复制过程 :
引用类型的复制
var foo = new Object();
var bar = foo;
foo.name = 'Leo';
console.log(bar.name); // output: 'Leo'
如上所示,在声明引用类型值时,JS 会在堆区创建对象的实际值,并在栈区创建该值对应的指针。
而在复制对象时,JS 则会在栈区中创建一个指针的副本,两个指针都指向堆区中的同一个对象,因此无论用哪个变量,它们都操作的是堆区中的同一个对象,所以当声明foo的 name 属性为 Leo 时,bar 的 name 属性也是 Leo
下图很形象地展示了引用类型值的复制过程 :
延伸拓展 , 函数的参数传递
function sum(e){
e += 10;
return e;
}
var foo = 10;
var bar = sum(foo);
console.log(foo); // output: 10
console.log(bar); // output: 20
这个例子很好理解, JS中所有的函数都是按值传递的, 将变量foo传入函数sum中, 就相当于复制了一份变量foo给参数e, 所以参数e无论如何都不会影响到变量foo
函数中使用引用类型
function setName(e){
e.name = 'Leo';
e = new Object();
e.name = 'Birdy';
}
var person = new Object();
setName(person);
console.log(person.name); // output:
这个例子稍微复杂一点, 先猜猜看答案是谁?
如果你对 JS 非常熟悉, 那对此一眼就能看出答案, 首先声明变量 person
为一个对象, 将其传入函数 setName
中, 就相当于复制了一份栈内存中的指针给参数 e
, 所以参数 e
就可以修改堆内存中对象的 name
属性为 Leo
之后再重新将参数 e
赋值为一个新对象, 此时堆内存中会加入一个新对象,这和之前的对象就是 2 个不同的对象了,所以修改其中一个对象的属性,另一个对象不会被影响,所以正确答案就是 Leo,嘻嘻,猜对了没?
在函数内部声明的对象是局部对象, 在函数运行完毕后垃圾收集器会将其标记为可回收, 下次回收垃圾时会将其销毁并回收所占用的内存
确定一个值是哪种基本类型可以使用 typeof 操作符,而确定一个值是哪种引用类型可以使用 instanceof 操作符。