JavaScript 变量、作用域、内存

原始值与引用值

ECMAScript 分为两种数据类型: 1、原始值:最简单的数据(undefined null boolean number string symbol)。 2、引用值:由多个值构成的对象,保存在内存中的对象(js 不能直接访问内存地址,也就不能操作对象所在内存空间,所以实际操作只是对该对象的引用操作)。

原始值不能有属性

1const person = 'Tomiaa'
2person.age = 17 // 添加并不会报错
3console.log(person.age) // undefind
4
5const person1 = new String('Tomiaa')
6person1.age = 17
7console.log(person1.age) // 17
8
9console.log(typeof person) // string
10console.log(typeof person1) // object

复制值

1const num = 6
2const num2 = num // num2 得到 6,num2 与 num 是完全独立的,互不影响。创建了该值的副本
3
4const obj = {} // 引用值储存在堆内存上
5const obj1 = obj // 只会复制指针
6obj1.name = 'tom'
7console.log(obj.name) // 'tom';
8// 引用值赋值是储存的是该值的内存地址,obj1 = obj时只是把地址赋值,指向的是同一个内存地址。访问的都是指向同一个对象。

函数传参

  • 原始值传递
1function add(num) {
2  // 函数内部相当于 let num = undefind;
3  num += 10
4  return num
5}
6const count = 20
7
8const result = add(count)
9console.log(count) // 20,没有变化
10console.log(result) // 30
  • 引用值传递
1function setName(obj) {
2  // 同样是赋值了,但赋值的是引用地址,操作的是同一个内存地址
3  obj.name = 'tomiaa'
4}
5const person = {}
6setName(person)
7console.log(person.name) // "tomiaa"

例 2:

1function setName(obj) {
2  obj.name = 'greg' // 对传入的地址赋值
3  obj = {} // obj 被赋值了新的内存地址
4  obj.name = 'tom'
5}
6const person = {}
7setName(person)
8console.log(person.name) // "greg"

上下文

“上下文”在 js 中非常重要。它决定了变量和函数访问的数据以及行为,上下文储存在variable object对象上,但无法通过代码访问,后台处理时会用到它。

全局上下文:在 ECMA 所述的宿主环境,全局上下文对象可能不一样,浏览器中为window对象,node.js环境下则是global对象。通过var声明的全局变量都会成为window对象的属性或方法。let、const 则不会,但是在作用域链效果是一样的。

eval()调用内部存在第三种上下文。

1const color = 'blue'
2function changeColor() {
3  const redColor = 'red'
4  function swapColors() {
5    const tempColor = redColor
6    // 这里可以访问color、redColor 和tempColor
7  }
8  // 这里可以访问color 和redColor,但访问不到tempColor
9}
10// 这里只能访问color

改变作用域

1with (Promise) {
2  console.log(all === Promise.all); // true
3}

with语句将Promise作为当前作用域的上下文。这里的all访问的就是Promise中的实例对象(不能是原型对象上的属性或方法)。

变量声明

1(function temp() {
2  const str = 'str1'
3})()
4console.log(str); // 错误。无法访问函数作用域中的变量
5
6(function temp1() {
7  name = 'tom' // 省略了 var 会被添加到全局上下文,函数执行结束后变量依然存在
8})()
9console.log(name) // 'tom'

标识符查找

1const color = 'blue'
2function getColor() {
3  return color // 当前函数作用域不存在 color,就会往上一级作用域查找,直到全局上下文。
4}
5
6function getColor1() {
7  const color1 = 'red'
8  {
9    const color1 = 'green'
10    return color1 // green 这里查找 color1 标识符要比上一个函数快,因为不要切换作用域,js 引擎在查找标识符做了很多工作,未来可能微不足道了
11  }
12}

垃圾回收

JavaScript 通过自动内存管理内存的分配和闲置资源的回收:确定哪个变量不会再使用,释放它的内存。这个过程每隔一段时间或预定时间就会自动运行。但这个过程是不完美的方案,在一个代码块内哪些变量是否还有用是一个“无法判定”的问题。

以一个函数作用域周期为例,执行函数时,会分配该函数到栈或堆内存中保存对应的值,函数内部使用了变量,退出。此时就可以释放局部的变量了。但并不会这么明显,垃圾回收需要跟踪哪些变量还会继续使用:在浏览器的发展史上,用到过标记清理和引用计数。

标记清理

当变量在进入上下文时,从逻辑上讲只要在上下文中就不应该释放它们的内存,只要上下文在运行就可能用到它。当离开上下文时就会被加上离开上下文的标记。如“在上下文中”和“不在上下文中”两个列表。

在垃圾回收程序运行时,它会将所有在当前上下文中变量及被上下文中引用的变量标记去掉,之后再被加上标记的变量就是待删除了,原因是在任何上下文中的变量都访问不到它们了。之后垃圾回收程序会做一次清理,清除带有标记的值并回收它们的内存。

引用计数

引用计数没有标记清理那么常用。在声明一个变量时,这个值引用次数为 1。如果这个值被赋值到另一个值,引用数就会加 1。相反,这个值被新的值覆盖,引用数就会减 1。但引用数为 0 时就没办法访问这个值了。等待垃圾回收程序运行时就会释放引用数为 0 的值。

但引用计数有一个严重的问题:循环引用

1function fn() {
2  const obj1 = {}
3  const obj2 = {}
4  obj1.data = obj2
5  obj2.list = obj1
6}

上面的两个变量互相引用,引用数都是 2。在标记清理策略下会被回收,但在引用计数下,这两个值还会存在。他们的引用数永远不会变成 0。这个函数被调用多次就会造成很多内存不会释放。因此,早期的网景浏览器就放弃了引用计数。

但引用计数的问题在 IE8 之前也有许多问题,BOM 和 DOM 对象是 C++实现的,并非 js 引擎的标记清理,只要设计了 DOM 和 BOM 对象就无法避开引用的问题。

1const dom = document.getElementById('app')
2const obj = {}
3obj.element = dom
4dom.data = obj
5
6// 需要手动切断循环引用
7obj.element = null
8dom.data = null

在 IE9 中把 BOM 和 DOM 对象都改成了 js 对象,从而避免了两套垃圾回收算法与内存泄露的问题。

警告在 IE 中

window.CollectGarbage()方法会立即触发垃圾回收。在 Opera 7 及更高版本中,调用window.opera.collect()也会启动垃圾回收程序。这些方法有可能触发垃圾回收(不推荐)。

内存管理

在系统中,分配给浏览器的内存一般比桌面软件要少很多。

如果数据不再需要,那么把它赋值为null

1function fn() {
2  const obj = { name: 'tom' }
3  return obj // 返回了 obj 的引用
4  // 在函数执行完毕后,超出上下文后 obj 就会被自动解除引用,无需手动解除
5}
6
7let globalObj = fn() // 全局变量
8
9// 解除引用
10globalObj = null

使用constlet可以更早的让垃圾回收程序处理。

隐藏类

chrome的 V8 JavaScript 引擎解释 js 时会利用隐藏类。

1function fn() {
2  this.name = 'tom'
3}
4const o1 = new fn()
5const o2 = new fn()

o1o2共享相同的隐藏类、构造函数以及原型。

如果后续代码做了添加操作:

1o1.age = 12

此时两个类的实例就会对应不同的隐藏类。

解决方案(避免先创建再新增):

1function fn(age) {
2  this.name = 'tom'
3  this.age = age
4}
5const o1 = new fn()
6const o2 = new fn(12)

如果后续代码做了删除操作:

1delete o2.age

此时两个类的实例就会对应不同的隐藏类(与动态添加的后果是一样的)。

解决方案(把不想要的属性设置为null):

1function fn(age) {
2  this.name = 'tom'
3  this.age = age
4}
5const o1 = new fn()
6const o2 = new fn(12)
7
8o2.age = null
9// 这样可以保持隐藏类不变,并且继续共享

内存泄露

  • 没有加声明关键字会导致属性被添加到window上,只要window没有被清除属性就不会消失。
1function fn() {
2  name = 'tom' // 相当于 window.name = 'tom'
3}
  • 定时器也会导致内存泄漏
1const name = 'tom'
2setInterval(() => {
3  console.log(name)
4}, 1000)

定时器一直执行就会导致name一直被引用。

  • 闭包也会造成内存泄漏
1function globalFun() {
2  const obj = { name: 'tom' }
3  return () => obj
4}

调用globalFun方法返回的函数只要一直引用它,内部的obj也不会被清理掉。