# 4. 变量、作用域与内存

基本类型与引用类型的值,一个存的是真实的值,一个存的是地址,类似C指针。

# 基本数据类型与引用类型的区别

  • 只有引用类型值可以动态的添加属性
  • 复制变量值时,基本类型相互不影响,引用类型值复制时,只是复制了值得地址,他们指向同一个堆内存区域,原先的值属性发生改变,新的值也会有对应的改变
  • 做函数参数时,引用类型的变量有可能改变值本身,基本类型不会
  • 基本数据类型保存在栈内存里,引用值是对象,存储在堆内存上。
function setName(obj) {    // 这里的obj是个局部变量,存的值指向堆内存对应的区域
  obj.name = "zuo";   // 修改的堆上的内容, 修改的值会影响到person
  obj = new Object(); // 改变了obj这个局部变量,指向另一个对象
  obj.name = "hello"; // 这里修改的值不会影响到person
}

var person = new Object();
setName(person);
alert(person.name);   // "zuo"

# 检测引用类型值的具体类型

基本类型用 typeof 就可以,引用类型用typeof都是object,还有一些引用类型是基于最基础的object的。如Array, Date或自定义对象。可以使用 instanceof 来判断这些类型的值是否属于某个引用类型

alert(person instanceof Object); // 变量person是Object吗, 如果是返回true
alert(colors instanceof Array); // 变量colors是Arrary吗
alert(pattern instanceof RegExp);

// JS里面所有引用类型的值都是Object的实例,任何引用类型 instanceof Object 都是true

在判断数据类型时,基本数据类型使用 typeof,引用类型使用 instanceof

# 执行上下文和作用域

执行上下文(execution context, 之前为执行环境、JS高程4将翻译改为 执行上下文,简称 上下文,看个人理解,选择自己比较好理解的一个翻译即可)定义了变量或函数有权访问的数据,决定了它们各自的行为。每一个执行上下文都有一个关联的变量对象(variable object),用来存储该环境中定义的所有变量和函数

  • 全局执环境是最外层的一个执行环境。根据宿主环境不同,表示执行环境的对象也不一样,web里,全局执行环境为Window对象,所有的全局变量和函数都是作为window对象的属性和方法创建的。全局执行环境直到浏览器关闭才会被销毁。
  • 非全局执行环境(函数)在执行结束后销毁,保存在其中的变量和函数定义也随之销毁
  • 每个函数都有自己的执行环境。当进入一个函数执行时,函数的环境会被push到一个环境栈中,执行完成后再pop函数的环境,返回原来的执行环境。
  • 代码在一个环境(上下文)中执行时。会创建变量对象的作用域链(scope chain),保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端始终是当前执行代码所在环境的变量对象。(如果这个环境是函数,最开始时变量对象只有一个arguments对象),作用域的下一个变量对象来自包含(外部)环境,一直延续到全局执行环境。全局执行环境的变量对象始终是作用域链的最后一个对象。
var color = "blue";

function changeColor() {
  var anotherColor = "red";

  function swapColors() {
    var tempColor = anotherColor;
    anotherColor = color;
    color = tempColor;
    // swapColors()局部执行环境,可访问 tempColor、anotherColor、color
  }

  // changeColor() 局部执行环境,可以访问 antherColor、color
  swapColors();
}

// 全局执行环境,只能访问 color
changeColor()

上面的例子中,内部的环境可以通过作用域链访问所有的外部环境,外部环境不能访问内部环境的变量。

  • 函数swapColors()的作用域链包含三个变量对象:他自己的变量对象(其中定义着arguments对象),changeCOlor()的变量对象,全局执行环境的变量对象
  • 函数changeColor()的作用域链包含两个变量对象:他自己的变量对象(其中定义着arguments对象),全局执行环境的变量对象

4_0_作用域链.png

# 延长作用域链

当执行环境下列任何一个语句时,作用域链就会得到加长

  • try-catch语句的catch块 (会创建一个新的变量对象,包含错误对象的声明)
  • with语句
function buildUrl() {
  var qs = "?debug=true";

  with(location) {  // 会将指定的对象location添加到作用域链前端,就可以直接访问location.href属性
    var url = href + qs
  }

  return url
}

# var 没有块级作用域

除函数外,有{}封闭的代码块没有自己的作用域。

  • 函数内容用var定义的,在函数执行完毕后,会直接销毁,如果函数内容使用了未定义的变量,会直接被添加到全局环境中。
  • 函数中访问一个变量的值,会优先从当前作用域去找,然后会一级一级向全局作用域链查找,直到找到为止。找到了就会停止,直接使用该值。
// 示例1:
if (true) {
  var color = "blue";
}
alert(color); // "blue"

// 示例2:
for(var i = 0; i < 10; i++) {
  doSomethind(i);
}
alert(i); // 10

# let、const 块级作用域

第四版新增内容,使用 let、const 声明的变量,他们的作用域是块级的({}括起来的部分)。

// 示例 1
if (true) {
  let color = "blue"
}
alert(color) // Uncaught ReferenceError: color is not defined
// 引用错误,color 未定义

// 示例 2
for (var i = 0; i < 10; i++) {}
console.log(i) // 10
for (let i = 0; i < 10; i++) {}
console.log(i) // ReferenceError: i is not defined

const 声明变量的同时必须初始化一个值,不然就无法修改了。其他和 let 基本一致。更多

# 标识符查找

标识符就是变量名。当出现一个变量名时,需要搜索作用域链,从当前作用域像外层作用域查找,确定标识符表示什么,找到了就结束。没找到就提示未定义。因此理论上访问局部变量的速度比访问全局变量的速度要快。但经过 JS 引擎的优化,这个差异可能就微不足道了。

# 垃圾收集

JS是一门具有自动垃圾收集机制的编程语言,开发人员不必关心内存分配和回收问题。

  • JS中最常用的垃圾收集方式是标记清除(mark-and-sweep),这种算法的思想是给当前不使用的值加上标记,然后再回收其内存。
  • 当进入执行环境时,当前环境定义的变量函数都会标记为"进入环境",当前的执行环境结束后,再标记为"离开环境",**离开作用域的值被标记为可以回收,然后在垃圾回收期间被删除。
  • 另一种不太常见的垃圾收集策略是引用计数(reference counting),这种算法的思想是跟踪记录所有值被引用的次数。次数为0时,回收。在循环引用时会有问题,导致内存无法回收造成内存泄漏。
  • 解除变量的引用,有助于消除循环引用、内存回收。为确保有效的回收内存,应及时解除不再使用的全局对象、全局对象属性等变量的引用

# 内存管理

虽然JS自带标记清除的垃圾回收机制,我们一般不需要管理内存管理。但需要注意:分片给浏览器的内存通常比分配给桌面软件的要少很多,分配给移动端浏览器的就更少了。这更多是出于安全考虑。将内存占用保持在一个较小的值,可以让页面性能能好。

  1. 解除引用,解除引用并不会自动导致相关内存被回收,它会在下次垃圾回收时被回收。
let obj = new Obj('name')

// 解除 obj 对值得引用
obj = null

TIP

下面是第四版新增的内容。通过 const 和 let 声明提升性能、隐藏类和删除操作、内存泄漏、静态分片与对象池。

  1. 通过 const 和 let 声明提升性能。相比 var 的非块级作用域,ES6新增的 let、const 拥有块级作用域,不仅有助于改善代码风格,还有利于改进垃圾回收的过程。可能会更早的让垃圾回收程序介入,尽早回收内存。

  2. V8隐藏类: 避免动态新增或删除类实例属性

V8引擎在解释执行JS代码时,会利用 隐藏类,如果代码非常注重性能,这一点对你可能很重要。

什么是隐藏类?在下面的例子中。V8会在后台配置,两个 Article 的类实例 a1、a2 会共享相同的隐藏类。因为这两个实例共享同一个构造函数和原型。

function Article() {
  this.title = 'Some Title'
}
let a1 = new Article()
let a2 = new Article()

假设之后又加入了下面的代码,那么两个 Article 实例就会对应两个不同的隐藏类。根据这种操作的频率和隐藏内的大小,可能会对内存有明显影响。

a2.author = 'zhangsan'

解决方法是:避免动态新增或删除实例的属性,如果要新增属性,最好在定义的时候就声明,如果要删除实例属性,最好将实例属性设置为 null

  1. 内存泄漏

一般主要发生在下面两种情况:

  • 未定义就直接使用的变量被提升到 window 全局,导致函数结束内存没回收
  • 闭包的情况,注意闭包引用的内容不要过大。
// 未定义变量就使用
function setName() {
  name = 'zhangsan'
}

// 闭包,定时器内部引用外部变量,定时器不结束,内存不会回收
let name = 'zhangsan'
setInterval(() => {
  console.log(name)
}, 1000)

// 闭包,函数使用了除自身作用域外部的变量
let outer = function() {
  let name = 'zhangsan'
  return function() {
    return name
  }
}

假设 name 的内容很大,不止是一个小字符串,那可能就是大问题了。

  1. 静态分配与对象池

WARNING

静态分配是一种极端的形式,如果应用程序不是被垃圾回收严重的拖了后腿,就不需要使用。大多数情况,这都属于过早优化,因此不用考虑

为了提升 JS 性能,最后要考虑的一点就是压榨浏览器了。核心是 减少不必要的垃圾回收,合理的使用分配的内存,避免多余的垃圾回收,就可以保住因释放内存而损失的性能。

function add(a, b) {
  let res = new Obj()
  res.x = a.x + b.x
  res.y = a.y + b.y
  return res
}

上面的例子中,每次调用这个函数,都会在堆上面创建一个新的对象,然后修改它。再返回给调用者。如果这个对象的生命周期很短,很快成为可以被回收的值。当这个方法被频繁使用时,会使浏览器更频繁的安排垃圾回收。

解决方法是 不要动态创建对象,使用一个已有的对象即可,怎么让数据不乱、且尽少触发回收呢?可以使用对象池,用来管理 res 实例。

function add(a, b, res) {
  res.x = a.x + b.x
  res.y = a.y + b.y
  return res
}

对象池逻辑:

  • 先分配多个可回收的对象实例,比如 res1、res2、res3。
  • 调用 add 方法需要使用时,就从对象池里取,如果没有就新增 res4,再使用,所以需要合理的分配对象池实例个数。
  • 如果用完了 res1,通知对象池已释放,这样可以分配给下次调用 add 时使用了。

感觉逻辑就比较复杂了,主要是一种思维方法,绝大多数场景都没必要使用,属于过早优化。

上次更新: 2020/11/8 19:55:50