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 | var a = 1 |
首先,浏览器通过声明提升,先声明变量 a
和 b
,都是 undefined,然后再将 1
赋给 a
,此时 a = 1
,再将 a
的值 赋给 b
,此时 b = 1
,在内存中可表示为:
执行第三行代码时,将 2
赋给 b
,也就是说 b
的 Stack 里的 1
,变成了2
,在内存中可表示为:
所以,a
的值到最后还是 1
。
面试题2:
1 | var a = {name: 'a'} |
第一行代码将一个对象赋给 a
,此时 a
的 Stack 里边存的是该对象数据所在 Heap 的地址,Heap 里边存储着对应地址的具体数据。
第二行代码将 a
的 Stack 里边的内容复制一份,赋给了 b
,也就是放到了 b
的 Stack中,此时a
和b
的 Stack 共同指向同一个 Heap 地址,在内存中可表示为:
第三行代码将一个新的对象 {name: 'b'}
赋给了b
,因为{name: 'b'}
是一个新的对象值,它在 Heap 中会有一个新的地址,所以 b
的 Stack 的值也会是一个指向新 Heap 的地址,在内存中可表示为:
所以,a.name
的值是 'a'
。
面试题3:
1 | var a = {name: 'a'} |
前两行代码同面试题2一样,a
和b
的 Stack 共同指向同一个 Heap 地址,当运行到第三行代码时,b.name
访问的是 heap 中该对象的key:'name'
,并且将 'b'
赋值给了所对应的 value,也就是改变了该键值对的值,在内存中可表示为:
所以,a.name
的值是 'b'
。
面试题4:
1 | var a = {name: 'a'} |
前两行代码同之前一样,a
和b
的 Stack 共同指向同一个 Heap 地址,当运行到第三行代码时,将 null
赋给了 b
,由于 null
是普通类型的值,所以这个值会保存在 b
的 Stack 中,而 a
和 Heap 都没有变化。在内存中可表示为:
所以,a
的值还是{name: 'a'}
。
面试题5:
1 | var a = { n:1 } |
执行第一行和第二行代码之后,a
和 b
的 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.x
中 a
中的地址仍然是之前保存 { n:1 }
的地址,所以这一步计算在之前的 Heap 里,添加了一个新的键值对,key 是 x
,值是存有 { n:2 }
的 Heap 地址。
最终的结果为:
所以,a.x
的结果为undefined
,b.x
的结果为[object Object]
。在代码中要尽量避免a.x = a = { n: 2}
这种写法。
普通类型在栈内存中存储,复杂类型是在 Stack 内存中存储 Heap 地址,在地址所对应的 Heap 地址中存储该类型,通过引用建立关系。值类型之间传递的是值,引用类型之间传递的是地址(引用)。值类型作为函数的参数传递的是值,引用类型作为函数的参数传递的是地址(引用)。
2.深拷贝与浅拷贝
1 | var a = 1 |
对于简单类型的数据来说,赋值就是深拷贝。
对于复杂类型的数据(对象)来说,才要区分浅拷贝和深拷贝。1
2
3
4var 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
这个临时对象。同理,其他普通类型的的方法也都是这样。