普通类型和对象的区别

JavaScript 一共有七种数据类型,其中六种 string、number、boolean、undefined、symbol、null 称为普通类型(原始类型),object(对象)称为复杂类型(引用类型)。
当我们打开浏览器时(以Chrome为例),Chrome 打开即占用一定大小的内存,当我们打开多个页面时,Chrome 给每个网页分配一定数量的内存,这些内存要分给页面渲染器、网络模块、浏览器外壳和 JS 引擎(V8引擎),JS 引擎将内存分为代码区和数据区,我们只研究数据区。

1. 两种类型在内存中的形式

数据区分为 Stack(栈内存) Heap(堆内存),普通类型的数据直接存在 Stack 里,复杂类型的数据是把 Heap 地址存在 Stack 里,如图:

上图中,变量 a、b、c 都是普通类型的数据,它们的数据都存在 Stack 里,数字使用64位存储,字符使用16位(32位)。而 o1 和 o2 为复杂类型,它们的 Satck 存储数据的 Heap 地址,真正的数据存储在地址所对应的 Heap 中。

为了更清楚的理解普通类型和复杂类型在内中的作用机制,可以通过面试题的几个代码举例。

面试题1:

1
2
3
4
var a = 1
var b = a
b = 2
请问 a 显示是几?

首先,浏览器通过声明提升,先声明变量 ab ,都是 undefined,然后再将 1 赋给 a,此时 a = 1,再将 a 的值 赋给 b,此时 b = 1,在内存中可表示为:

执行第三行代码时,将 2 赋给 b,也就是说 b 的 Stack 里的 1,变成了2,在内存中可表示为:

所以,a 的值到最后还是 1

面试题2:

1
2
3
4
var a = {name: 'a'}
var b = a
b = {name: 'b'}
请问现在 a.name 是多少?

第一行代码将一个对象赋给 a,此时 a 的 Stack 里边存的是该对象数据所在 Heap 的地址,Heap 里边存储着对应地址的具体数据。
第二行代码将 a 的 Stack 里边的内容复制一份,赋给了 b,也就是放到了 b 的 Stack中,此时ab的 Stack 共同指向同一个 Heap 地址,在内存中可表示为:

第三行代码将一个新的对象 {name: 'b'} 赋给了b,因为{name: 'b'}是一个新的对象值,它在 Heap 中会有一个新的地址,所以 b 的 Stack 的值也会是一个指向新 Heap 的地址,在内存中可表示为:

所以,a.name 的值是 'a'

面试题3:

1
2
3
4
var a = {name: 'a'}
var b = a
b.name = 'b'
请问现在 a.name 是多少?

前两行代码同面试题2一样,ab的 Stack 共同指向同一个 Heap 地址,当运行到第三行代码时,b.name 访问的是 heap 中该对象的key:'name' ,并且将 'b' 赋值给了所对应的 value,也就是改变了该键值对的值,在内存中可表示为:

所以,a.name 的值是 'b'

面试题4:

1
2
3
4
var a = {name: 'a'}
var b = a
b = null
请问现在 a 是什么?

前两行代码同之前一样,ab的 Stack 共同指向同一个 Heap 地址,当运行到第三行代码时,将 null 赋给了 b,由于 null 是普通类型的值,所以这个值会保存在 b 的 Stack 中,而 a 和 Heap 都没有变化。在内存中可表示为:

所以,a 的值还是{name: 'a'}

面试题5:

1
2
3
4
5
6
var a = { n:1 }
var b = a
a.x = a = { n:2 }

alert(a.x) // -->undefined
alert(b.x) // -->[object Object]

执行第一行和第二行代码之后,ab 的 Stack 中的地址都指向 Heap 中的{ n:1 }(ADD:369),在执行第三行代码时,浏览器先从左往右阅读代码,此时 a.x 中的 a= 后边的 a 的值相等(ADD:369)。

然后浏览器从右往左计算(赋值),先执行a = { n:2 },将 { n:2 } 赋给了中间的a,此时,a 的 Stack 中的地址因为指向新赋予的对象已经改变为(ADD:567)。

再计算a.x = a ,而a.xa 中的地址仍然是之前保存 { n:1 } 的地址,所以这一步计算在之前的 Heap 里,添加了一个新的键值对,key 是 x,值是存有 { n:2 } 的 Heap 地址。

最终的结果为:

所以,a.x 的结果为undefinedb.x 的结果为[object Object]。在代码中要尽量避免a.x = a = { n: 2} 这种写法。

普通类型在栈内存中存储,复杂类型是在 Stack 内存中存储 Heap 地址,在地址所对应的 Heap 地址中存储该类型,通过引用建立关系。值类型之间传递的是值,引用类型之间传递的是地址(引用)。值类型作为函数的参数传递的是值,引用类型作为函数的参数传递的是地址(引用)。

2.深拷贝与浅拷贝

1
2
3
4
5
var a = 1
var b = a
b = 2 //这个时候改变 b
a 完全不受 b 的影响
那么我们就说这是一个深拷贝

对于简单类型的数据来说,赋值就是深拷贝。
对于复杂类型的数据(对象)来说,才要区分浅拷贝和深拷贝。

1
2
3
4
var a = {name: 'a'}
var b = a
b.name = 'b'
a.name === 'b' // true

上述代码对 b 操作后,a 也变了,所以是浅拷贝。
对 Heap 内存进行完全的拷贝,称为深拷贝。

3. GC 垃圾回收机制

在浏览器给 JS 分配的内存中,如果 Heap 中的某个对象没有被任何其他对象或者变量进行引用,则会被浏览器视为垃圾,删除该对象,回收该内存地址。

4. 为什么简单类型也可以调用方法

在我们使用 JavaScript 的时候,通过 var 声明一个普通类型的时候,发现也可以调用相关的方法,如图所示。

但普通类型并不具有方法,这是因为当我们执行n.toString() 的时候,JS 会在内存中创建一个临时的对象 temp = new Numer(n)temp 是 n 的复杂类型的封装,实际上n.toString() 等于 temp.toString(),然后把temp.toString() 的值作为n.toString() 的值返回,然后再消除掉temp 这个临时对象。同理,其他普通类型的的方法也都是这样。