4.核心JavaScript笔记:变量、作用域、内存问题

本系列内容由ZouStrong整理收录

整理自《JavaScript权威指南(第六版)》,《JavaScript高级程序设计(第三版)》

JavaScript变量弱类型(松散类型)的本质,决定了它只是在特定时间用于保存特定值的一个名字而已(更确切的说,是内存中的一块空间)

一.基本类型和引用类型

在将一个值赋给变量时,解析器必须确定这个值是基本类型还是引用类型

1. 变量访问

  • 基本数据类型是按值访问的,操作的是保存在变量中的实际的值
  • 基本类型值在内存中占据固定大小的空间,因此被保存在栈内存
  • 基本类型值所在的变量被复制时,会创建该值的一个副本,此后,这两个变量可以参与任何操作而不会相互影响
  • 基本类型值是不可变的

区别

  • 引用数据类型(对象)是按引用访问的,变量中包含的不是对象本身,而是一个指向对象的引用(也叫指针,它保存在栈内存中),这个指针指向真实的对象(保存在堆内存
  • 引用数据类型值所在的变量被复制时,复制的其实是指针,导致两个变量最终都指向同一个对象,因此,改变其中一个变量中的对象,就会影响另一个变量保存的对象
  • 引用类型值是可变的

2. 变量比较

对于保存基本类型值的变量,只要这两个变量保存的数据相同,就认为这两个变量是相同的,这是因为,基本类型值是按值访问,直接比较数据

var a="strong";
var b="strong";
a==b            //true

对于保存引用类型值的变量,尽管有时候两个变量保存的数据看起来相同,这两个变量也是不一定相同的(除非保存的是同一个对象的引用),因为任何两个独立的对象永不相等

var obj1 = new Object();
var obj2 = new Object();
obj1.name="zsz";
obj2.name="zsz";
obj1==obj2;                //false
obj1.name==obj2.name;     //true

这是因为,对象是按引用访问的,除了比较值之外,还要比较引用的地址是否相同,由于每次创建对象都会实例化一个新对象,而两个独立对象的内存地址不同,所以导致不相同

3. 参数传递

虽然访问变量分为按值访问和按引用访问,但是函数的参数都是按值传递的:把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样

在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(arguments对象中的一个元素),在向参数传递引用类型的值时,会把这个值在栈内存中的地址(指针)复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部

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

在局部作用域中修改的对象会在全局作用域中反映出来,难道参数是按引用传递的?

function setName(obj) { 
	obj.name = "Nicholas"; 
	obj = new Object();   //局部对象,函数结束后被销毁
	obj.name = "Greg"; 
} 
var person = new Object(); 
setName(person); 
alert(person.name);   //"Nicholas" 如果是按引用传递的,此处应该是Greg

4. 类型检测

typeof运算符

typeof运算符通常是检测基本数据类型的最佳工具,但在检测引用类型的值时,这个操作符的用处不大,因为我们并不仅仅想知道某个值是对象,而是想进一步知道它是什么类型的对象

instanceof运算符

如果变量是给定引用类型(根据它的原型链来识别)的实例,那么instanceof运算符就会返回true

实例  instanceof  构造函数

person instanceof Object   // 变量person中的值是Object的实例吗?
colors instanceof Array    // 变量colors中的值是Array的实例吗?

所有引用类型的值都是Object的实例。因此,在检测一个引用类型值和Object构造函数时,instanceof操作符始终会返回true

类特性检测

instanceof运算符并不会检查对象是否由构造函数初始化的,而回检查对象是否继承自构造函数的prototype

var a=[1,2,3];
a instanceof Array      //true
a instanceof Object    //true

所以这个运算符也不保险,最准确的检测某个对象类型的方法——类特性检测(后述)

二. 作用域和作用域链

所有变量(或函数)都存在于一个作用域(执行环境)当中,这个作用域决定了变量的生命周期,以及哪一部分代码可以访问其中的变量

每个作用域都有一个与之关联的变量对象(variable object),作用域中定义的所有变量和函数都保存在这个对象中(无法直接访问这个变量对象)

1. 作用域(执行环境)

(现阶段而言)作用域只有全局作用域和函数作用域之分,ECMAScript6新增了块级(局部)作用域

全局作用域是最外围的一个作用域(在Web浏览器中,全局作用域是window对象,在NodeJS中,全局作用域是global对象),所有全局变量和函数都是作为window对象的属性和方法存在的

每个函数内部也都有自己的作用域,当函数被调用或执行时,函数的作用域就会被推入一个作用域栈中。而在函数执行完毕之后,栈将其作用域弹出,把控制权返回给之前的作用域(ECMAScript程序中的执行流正是由这个机制控制)

某个作用域中的所有代码执行完毕后,该作用域才会被销毁,保存在其中的所有变量和函数定义也随之销毁(全局作用域直到应用程序退出——例如关闭网页或浏览器——时才会被销毁)

作用域有助于确定应该何时释放内存

2. 作用域链

当代码在一个作用域中执行时,会创建由变量对象组成的一个作用域链(scope chain),作用域链的用途,用于搜索变量和函数,且保证对作用域有权访问的所有变量和函数的有序访问

作用域链的前端,始终都是当前代码所在作用域的变量对象。如果这个作用域是函数,则将其活动对象作为变量对象(活动对象在最开始时只包含一个属性,即arguments对象)作用域链中的下一个变量对象来自包含(外部)作用域,而再下一个变量对象则来自下一个包含作用域。这样,一直延续到全局作用域,全局作用域的变量对象(简称全局对象)始终都是作用域链中的最后一个对象

  • 每个作用域都可以向上搜索作用域链,以查询变量和函数名
  • 但任何作用域都不能通过向下搜索作用域链而进入另一个作用域

即函数的局部作用域不仅有权访问函数作用域中的变量,而且有权访问其包含(父)作用域,乃至全局作用域;全局作用域只能访问在全局作用域中定义的变量和函数,而不能直接访问局部作用域中的任何数据

标识符解析是沿着作用域链一级一级的向上查找的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)

3. 作用域链延长

通过在作用域链的前端临时增加一个变量对象,就可以延长作用域链,该变量对象会在代码执行后被移除

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

  • try/catch语句的catch块(会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明)
  • with语句

在with语句的代码块内部,每个变量首先被认为是一个局部变量,而如果在局部作用域中找不到该变量的定义,就会查询with指定的对象中是否有同名的属性(该对象成为了变量对象)如果发现了同名属性,则以该对象属性的值作为变量的值,否则,就继续沿着作用域链向上查找

with(document){
	var write=3;
	write("ss");   //报错,write是一个保存着数值的变量,不是函数
}

with(document){
	write("strong");    //document.write("strong")
}

var write=3;
with(document){
	write("strong");   //document.write("strong")
}

虽然with延长了作用域链,但是with内部的代码并没有自己的作用域,其作用域仍然属于其外部执行环境

with(document){
	var a=3;
}
alert(a);   // 3

由于width会延长作用域链,影响性能,因此严格模式下不允许使用with语句

4. 没有块级作用域

其他语言中,由花括号封闭的代码块都有自己的作用域,即花括号内的变量不能在外部访问到,JavaScript没有块级作用域

ECMAScript 6 新增了块级作用域(使用let声明)

5. 再谈变量声明

使用var声明的变量会自动被添加到最接近的作用域中。在函数内部,最接近的作用域就是函数的局部作用域;在with语句中,最接近的作用域是其外部作用域。如果初始化变量时没有使用var声明,该变量会自动被添加到全局作用域

因此,在初始化变量之前,一定要先声明(除非刻意创建全局),严格模式下,变量不经声明而直接使用会报错

访问局部变量要比访问全局变量更快,因为不用向上搜索作用域链(这也就是,为什么在创建立即执行函数时,会在实参部分传入window,document等本不需要传递的对象,就是为了减少查询的时间和次数,提高性能)

这样,当多次访问该对象时,就只需要全局查询一次了

;(function(win){
})(window);

三. 垃圾回收

JavaScript具有自动垃圾回收机制

执行环境负责管理代码执行过程中使用的内存。找出那些不再继续使用的变量,然后释放其占用的内存,垃圾回收器会按照固定的时间间隔(或代码执行中预定的回收时间),周期性地执行这一操作

局部变量只在函数执行的过程中存在。而在这个过程中,会为局部变量在内存上分配相应的空间;然后在函数中使用这些变量,直至函数执行结束,此时,局部变量就没有存在的必要了,因此可以释放它们的内存

垃圾回收器必须跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存,通常有两个策略

1. 标记清除

JavaScript 中最常用的垃圾回收方式是标记清除(mark-and-sweep)

当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,而当变量离开环境时,则将其标记为“离开环境”

这样,当垃圾回收器下次再运行时,它就会释放那些标记为“离开环境”的值所占用的内存

2.引用计数

另一种垃圾回收策略叫做引用计数(reference counting)

记录每个值被引用的次数,将一个引用类型值赋给该变量时,则这个值的引用次数就是1;如果同一个值又被赋给另一个变量,则该值的引用次数加1;相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1;当这个值的引用次数变成0时,就可以将其占用的内存空间回收回来

这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为零的值所占用的内存

循环引用

循环引用指的是对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用

function problem(){
	var obj1 = {};
	var obj2 = {};
	obj1.name = obj2;
	obj2.names = obj1;
}

在采用标记清除策略时,由于函数执行之后,这两个对象都离开了作用域,因此这种相互引用不是个问题

两个对象通过各自的属性相互引用(引用次数都是1),在采用引用计数的实现中,当函数执行完毕后, 引用还将继续存在,引用次数永远不会是 0;假如这个函数被重复多次调用,就会导致大量内存得不到回收(这只是一个说明,几乎所有浏览器都不再采用引用计数了)

但是, 在低版本IE中,对于BOM和DOM对象采用的就是引用计数。因此只要低版本IE涉及BOM和DOM对象,就会存在循环引用的问题

var element = document.getElementById("s");
var obj = {};
obj.name = element;
element.names = obj;

由于存在循环引用,即使上面的DOM从页面中移除,它也永远不会被回收,为了避免类似这样的循环引用问题,最好是在不使用它们的时候手工断开原生JavaScript对象与DOM 元素之间的连接,当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存

obj.name = null;
element.names = null;

3. 性能问题

垃圾回收器是周期性运行的,而且如果为变量分配的内存数量很大,那么回收工作量也是相当大的。在这种情况下,确定垃圾回收的时间间隔是一个非常重要的问题

IE6的垃圾收集器是根据内存分配量运行的,具体一点说就是 256个变量、 4096个对象(数组)或者64KB的字符串,达到
上述任何一个临界值,垃圾收集器就会运行,如果一个脚本在其生命周期中一直保有那么多的变量。这样一来,垃圾收集器就不得不频繁地运行

IE7改变了这一点:触发垃圾收集的内存分配量临界值被调整为动态修正,IE7中的各项临界值在初始时与 IE6 相等。如果
垃圾收集例程回收的内存分配量低于 15%,则变量、字面量和(或)数组元素的临界值就会加倍。如果例程回收了 85%的内存分配量,则将各种临界值重置回默认值。这一看似简单的调整,极大地提升了IE在运行包含大量 JavaScript 的页面时的性能

4. 管理内存

JavaScript在进行内存管理及垃圾回收时面临的问题就是:分配给Web浏览器的可用内存数量通常要比分配给桌面应用程序的少。这样做的目的是防止运行JavaScript的网页耗尽全部系统内存而导致系统崩溃。内存限制问题不仅会影响给变量分配内存,同时还会影响调用栈以及在一个线程中能够同时执行的语句数量

因此,确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为null来释放其引用——解除引用

这一做法适用于大多数全局变量和全局对象的属性。局部变量会在它们离开执行环境时自动被解除引用

function test(name){ 
	var local = new Object(); 
	local.name = name; 
	return local; 
} 
var global = test("strong"); 
global = null;      // 手工解除global的引用

不过,解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾回收器下次运行时将其回收

为了确保有效地回收内存,应该及时解除不再使用的全局对象、全局对象属性以及循环引用变量的引用

你可能感兴趣的:(JavaScript)