本系列内容由ZouStrong整理收录
整理自《JavaScript权威指南(第六版)》,《JavaScript高级程序设计(第三版)》
JavaScript变量弱类型(松散类型)的本质,决定了它只是在特定时间用于保存特定值的一个名字而已(更确切的说,是内存中的一块空间)
在将一个值赋给变量时,解析器必须确定这个值是基本类型还是引用类型
区别
对于保存基本类型值的变量,只要这两个变量保存的数据相同,就认为这两个变量是相同的,这是因为,基本类型值是按值访问,直接比较数据
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
这是因为,对象是按引用访问的,除了比较值之外,还要比较引用的地址是否相同,由于每次创建对象都会实例化一个新对象,而两个独立对象的内存地址不同,所以导致不相同
虽然访问变量分为按值访问和按引用访问,但是函数的参数都是按值传递的:把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样
在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(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
typeof运算符通常是检测基本数据类型的最佳工具,但在检测引用类型的值时,这个操作符的用处不大,因为我们并不仅仅想知道某个值是对象,而是想进一步知道它是什么类型的对象
如果变量是给定引用类型(根据它的原型链来识别)的实例,那么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),作用域中定义的所有变量和函数都保存在这个对象中(无法直接访问这个变量对象)
(现阶段而言)作用域只有全局作用域和函数作用域之分,ECMAScript6新增了块级(局部)作用域
全局作用域是最外围的一个作用域(在Web浏览器中,全局作用域是window对象,在NodeJS中,全局作用域是global对象),所有全局变量和函数都是作为window对象的属性和方法存在的
每个函数内部也都有自己的作用域,当函数被调用或执行时,函数的作用域就会被推入一个作用域栈中。而在函数执行完毕之后,栈将其作用域弹出,把控制权返回给之前的作用域(ECMAScript程序中的执行流正是由这个机制控制)
某个作用域中的所有代码执行完毕后,该作用域才会被销毁,保存在其中的所有变量和函数定义也随之销毁(全局作用域直到应用程序退出——例如关闭网页或浏览器——时才会被销毁)
作用域有助于确定应该何时释放内存
当代码在一个作用域中执行时,会创建由变量对象组成的一个作用域链(scope chain),作用域链的用途,用于搜索变量和函数,且保证对作用域有权访问的所有变量和函数的有序访问
作用域链的前端,始终都是当前代码所在作用域的变量对象。如果这个作用域是函数,则将其活动对象作为变量对象(活动对象在最开始时只包含一个属性,即arguments对象)作用域链中的下一个变量对象来自包含(外部)作用域,而再下一个变量对象则来自下一个包含作用域。这样,一直延续到全局作用域,全局作用域的变量对象(简称全局对象)始终都是作用域链中的最后一个对象
即函数的局部作用域不仅有权访问函数作用域中的变量,而且有权访问其包含(父)作用域,乃至全局作用域;全局作用域只能访问在全局作用域中定义的变量和函数,而不能直接访问局部作用域中的任何数据
标识符解析是沿着作用域链一级一级的向上查找的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)
通过在作用域链的前端临时增加一个变量对象,就可以延长作用域链,该变量对象会在代码执行后被移除
当执行流进入下列任何一个语句时,作用域链就会得到加长
在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语句
其他语言中,由花括号封闭的代码块都有自己的作用域,即花括号内的变量不能在外部访问到,JavaScript没有块级作用域
ECMAScript 6 新增了块级作用域(使用let声明)
使用var声明的变量会自动被添加到最接近的作用域中。在函数内部,最接近的作用域就是函数的局部作用域;在with语句中,最接近的作用域是其外部作用域。如果初始化变量时没有使用var声明,该变量会自动被添加到全局作用域
因此,在初始化变量之前,一定要先声明(除非刻意创建全局),严格模式下,变量不经声明而直接使用会报错
访问局部变量要比访问全局变量更快,因为不用向上搜索作用域链(这也就是,为什么在创建立即执行函数时,会在实参部分传入window,document等本不需要传递的对象,就是为了减少查询的时间和次数,提高性能)
这样,当多次访问该对象时,就只需要全局查询一次了
;(function(win){
})(window);
JavaScript具有自动垃圾回收机制
执行环境负责管理代码执行过程中使用的内存。找出那些不再继续使用的变量,然后释放其占用的内存,垃圾回收器会按照固定的时间间隔(或代码执行中预定的回收时间),周期性地执行这一操作
局部变量只在函数执行的过程中存在。而在这个过程中,会为局部变量在内存上分配相应的空间;然后在函数中使用这些变量,直至函数执行结束,此时,局部变量就没有存在的必要了,因此可以释放它们的内存
垃圾回收器必须跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存,通常有两个策略
JavaScript 中最常用的垃圾回收方式是标记清除(mark-and-sweep)
当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,而当变量离开环境时,则将其标记为“离开环境”
这样,当垃圾回收器下次再运行时,它就会释放那些标记为“离开环境”的值所占用的内存
另一种垃圾回收策略叫做引用计数(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;
垃圾回收器是周期性运行的,而且如果为变量分配的内存数量很大,那么回收工作量也是相当大的。在这种情况下,确定垃圾回收的时间间隔是一个非常重要的问题
IE6的垃圾收集器是根据内存分配量运行的,具体一点说就是 256个变量、 4096个对象(数组)或者64KB的字符串,达到
上述任何一个临界值,垃圾收集器就会运行,如果一个脚本在其生命周期中一直保有那么多的变量。这样一来,垃圾收集器就不得不频繁地运行
IE7改变了这一点:触发垃圾收集的内存分配量临界值被调整为动态修正,IE7中的各项临界值在初始时与 IE6 相等。如果
垃圾收集例程回收的内存分配量低于 15%,则变量、字面量和(或)数组元素的临界值就会加倍。如果例程回收了 85%的内存分配量,则将各种临界值重置回默认值。这一看似简单的调整,极大地提升了IE在运行包含大量 JavaScript 的页面时的性能
JavaScript在进行内存管理及垃圾回收时面临的问题就是:分配给Web浏览器的可用内存数量通常要比分配给桌面应用程序的少。这样做的目的是防止运行JavaScript的网页耗尽全部系统内存而导致系统崩溃。内存限制问题不仅会影响给变量分配内存,同时还会影响调用栈以及在一个线程中能够同时执行的语句数量
因此,确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为null来释放其引用——解除引用
这一做法适用于大多数全局变量和全局对象的属性。局部变量会在它们离开执行环境时自动被解除引用
function test(name){
var local = new Object();
local.name = name;
return local;
}
var global = test("strong");
global = null; // 手工解除global的引用
不过,解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾回收器下次运行时将其回收
为了确保有效地回收内存,应该及时解除不再使用的全局对象、全局对象属性以及循环引用变量的引用