js中的变量、作用域和内存

126 阅读8分钟

JavaScript中的变量、作用域和内存问题

1、基本类型和引用类型的值

基本类型:Undefined、null、Boolean、Number和String; 引用类型:保存再内存中的对象,object

可以为引用对象动态的添加属性,但是不能给基本类型添加属性。

2、复制变量值

复制基本类型的变量值时,复制的变量值是可以独立于被复制变量值的,操作两者时,互相不受影响,即复制该值时只是被复制变量值的一个副本

var num15(nubmer类型)

复制变量后的值:

var num25(nubmer类型)
var num15(nubmer类型)

当复制一个引用类型的变量值时,同样也会将存储在对象中的值复制一份放在为新变量分配的空间中,复制操作结束以后,这个复制的值本质上为一个指针,而这个指针的指向存储再堆中的对象,即改变一个变量会影响到另外一个变量。

3、传递参数

ECMAScript中函数的参数传递是按值传递的。基本类型的值的传递和基本变量的复制一样,而引用类型值的传递,则如同引用类型变量的复制一样,但是在函数传参的过程中,参数的传递只能通过按值传递的方式进行。例子如下:

function addTen(num){
  num += 10;
  return num;
}
var count = 10;
var result = addTen(count);	// 30
alert(count);	// 20

关于引用类型的例子:

function setName(obj) {
  obj.name = "Nicholas";
}
var person = new Object();
setName(person);
alert(person.name);	// 输出'Nicholas'

在setName函数的内部,obj引用的是传入的person对象,obj通过引用访问同一个对象,即在函数内部为obj添加一个属性户,函数外部的对象也会有所反映,解释原因如下:函数外部的对象(即person)指向的对象在堆内存中只有一个,而且是全局对象,查看以下代码:

function setName(obj) {
  obj.name = 'Nicholas';
  obj = new object();
  obj.name = 'Alsion';
}
var person = new Object();
setName(person);
alert(person.name);	//输出'Nicholas'

在上述代码示例中,如果person是按照引用传递的,则person的name属性会被自动修改为Alison,说明即使在函数内部修改了参数的值,但是原始的引用没有改变,实际上,当在函数内部重写obj时,这个变量的引用就是一个局部对象了。

4、执行环境以及作用域

执行环境时JavaScript中最重要的一个概念,执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为,每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中

全局执行环境是最外围的一个执行环境,根据ECMAScript实现的宿主环境不同,表示执行环境的对象也不同,在web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的,某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的函数和变量也会被销毁。

在代码在一个环境中执行时,会创建变量对象的一个作用域链,作用域链的用途是,保证对执行环境有权访问的所有变量和函数的有序访问,作用域链的前端始终都是当前执行代码所在环境的变量对象,入股偶环境是函数,则将器活动对象作为变量对象,活动对象在最开始时只包含一个对象,即arguments对象。作用域链中的下一个变量对象来自包含(外部)环境,而在下一个变量对象则来自下一个包含环境,这样一直延续到全局执行环境,全局执行环境的变量对象始终都是作用域中的最后一个对象。

4.1延长作用域链

有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在海代码执行后移除。在代码执行进入以下两种情况时作用域链会延长:

  • try-catch语句
  • with语句 两个语句都会在作用域的前端增加一个变量对象,对于with语句而言,会将指定的对象添加到作用域链中,如下为with例子:
function builder() {
 var qs = "?debug=true";
 with(location) {
    var url = href + qs;	 
  }
  return url;
}

with语句接收的是location对象,因此其变量对象中就包含了location对象的所有属性和方法,而这个变量对象被添加到了作用域的前端。buildUrl函数中定义了一个变量qs。当在with语句中引用变量href时,实际上引用的时location.href,可以在当前执行环境的变量对象中找到,当引用变量qs时,引用的则是在buildUrl()中定义的那个变量,而该变量位于函数环境的变量对象中。至于with语句内部,则定义了一个名为url的变量,因而url就变成了函数执行环境的一部分,所以可以作为函数的值被返回。

没有块级作用域

5、垃圾收集

局部变量只在函数执行中存在,而在这个过程中,会在局部变量在栈(堆)内存上分配相应的空间,以便存储他们的值,在函数中引用这些变量,知道函数执行结束,此时,局部变量就没有存在的必要了,因此可以释放它们的内存以便将来使用。垃圾收集器必须跟踪哪个变量有用哪个变量无用,对于不再使用的变量打上标记,以备将来回收其内存。

5.1 标记清除

当变量进入环境时,就将这个变量标记为“进入环境”,从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。而当变量离开环境时,则将其标记为“离开环境”。

垃圾收集器在运行的时候会给存储在内存中的所有变量加上标记(当然,可以使用任何标记方式),然后它会去掉环境中的变量以及被环境中的变量引用的变量的标记,而在此之后在被加上标记的变量被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了,最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存。

5.2 引用计数

引用计数的含义是跟踪记录每个值被引用的次数,当声明了一个变量并将一个引用类型赋值给变量时,则这个值的引用次数就是1。如果同一个值又被赋值给另外一个变量,则该变量的引用次数加1,相反,如果包含这个值的引用又取了另外一个值,则这个值的引用次数减1。

在为了确保占用最少的内存获取最好的性能,而优化内存最好的方式就是为了执行中代码只保存必要的数据,一旦数据不再使用就将其值设为null来释放其引用,这个做法叫做解除引用。而局部变量会在它们离开执行环境时自动被解除引用。

小结

JavaScript变量可以用啦保存以下两种类型的值:基本类型值和引用类型值,基本类型的值又以下5种基本数据类型:Undefined、null、Boolean、Number和String。两者具有以下特点:

  • 基本类型值在内存种占据固定大小的空间,因此被保存在栈内存中;
  • 从一个变量向另外一个变量复制基本类型的值,会创建这个值的一个副本;
  • 引用类型的值是对象,会保存在堆内存中;
  • 包含引用类型值的变量实际上包含的并不是对象本身,而是指向该对象的指针;
  • 从一个变量向另外一个变量复制引用类型的值,复制的实际上就是指针,因此两个变量最终都指向同一个变量;
  • 确定一个值是哪种基本类型可以使用typeof,而确定一个值是哪种引用类型可以用instanceof操作符。
  • 执行环境有全局执行环境和函数环境之分;
  • 每次进入一个鑫的执行环境,都会创建一个用于搜索变量和函数的作用域链;
  • 函数的局部环境可以访问函数作用域中的变量,也可以访问其包含(父)环境,乃至全局环境;
  • 全局环境只能访问全局环境中的函数和变量,不能访问局部变量中的任何数据;
  • 离开作用域的值将被自动标记为可回收,因此将在垃圾收集期间被删除;
  • 目前主流的垃圾回收算法是“标记清除”,这种算法的思想是给不被使用的值加上标记,然后再回收其内存;
  • 另一种垃圾收集算法是“引用计数”,这种算法的思想是跟踪记录所有值被引用的次数;
  • 解除变量的引用可以有助于消除循环引用的现象,也可以堆垃圾收集有好处。为了确保有效的回收内存,应及时解除不在使用的全局对象、全局对象属性和循环引用变量的引用。