JS基础知识回顾:变量、作用域和内存问题

ECMAScript变量可能包含两种不同数据类型的值:基本类型值和引用类型值。

基本类型值指的是简单的数据段,而引用类型值指的是那些可能由多个值构成的对象。

引用类型的值是保存在内存中的对象,与其他语言不同,JavaScript不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。

在操作对象时,实际上是在操作对象的引用而不是实际的对象。

在很多语言中,字符串以对象的形式来表示,因此被认为是引用类型的,ECMAScript放弃了这一传统。

定义基本类型值和引用类型值的方式是类似的:创建一个变量并为该变量赋值。

但是,当这个值保存到变量当中之后,对于不同类型的值可以执行的操作却大相径庭。

对于引用类型的值,我们可以为其添加属性和方法,也可以改变和删除其属性和方法。

例如:var person=new Object();person.name="Name";alert(person.name);//"Name"(如果对象不被销毁或者这个属性不被删除,则这个属性将一直存在)

但是我们不能为基本类型的值添加属性,尽管这样做也不会导致任何错误。

例如:var name="Name";name.age=27;alert(name.age);//undefined

除了保存的方式不同之外,在从一个变量向另一个变量复制基本类型值和引用类型值时,也存在不同。

如果从一个变量向另一个变量复制基本类型的值,会在变量对象上创建一个新值,然后把该值复制到新变量分配的位置上。

例如:var num1=5;var num2=num1;//尽管此时num1和num2中保存的值都是5,但是两个变量是完全独立的,接下来可以参与任何操作而不互相影响

如果从一个变量向另一个变量复制引用类型的值,也会将存储在变量中的值复制一份放到为新变量分配的空间中,只不过这个值的副本实际上是一个指针,复制结束后,两个变量实际上将引用同一个对象,改变其中一个,就会影响到另外一个。

例如:var obj1=new Object();var obj2=obj1;obj1.name="Name";alert(obj2.name);//"Name"(因为二者指向同一个对象,所以改变一个会影响另外一个)

尽管ECMAScript中访问对象有按值访问和按引用访问两种,但是所有函数的参数都是按值传递的。

也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。

在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数,或者用ECMAScript的概念来说,就是arguments对象的一个元素)。

在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反应在函数的外部。

可以把ECMAScript函数的参数想象成局部变量,很多人错误的一位在局部作用域中修改的对象会在全局作用域中反映出来,就说明参数是按引用传递的,下面这个例子可以很好的证明这个想法是错误的:

function setName(obj){obj.name="Name";obj=new Object();obj.name="Greg";}

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

即使在函数内部修改了参数的值,但原始的引用仍然保持不变。

实际上,在函数内部重写obj时,这个变量引用的就是一个局部对象了,而这个局部对象会在函数执行完毕后立即被销毁。

在检测基本数据类型时typeof是非常得力的助手,但由于它只能检测出对象而不能检测出对象的类型,因此它在检测引用类型的值是作用不大。

为此ECMAScript提供了instanceof操作符:result=variable instanceof constructor

如果变量是给定引用类型的实例,那么instanceof操作符会返回true。

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

当然,如果使用instanceof操作符检测基本类型的值,该操作符始终会返回false,因为基本类型不是对象。

执行环境(execution context)定义了变量或函数有权访问的其他数据,决定了它们各自的行为。

每个执行环境都有一个与之相关的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象当中。

执行环境分为全局(最外围的执行环境)和局部(每个函数各自的执行环境)两种。

当执行流进入一个函数时,函数的环境就会被推入一个环境栈当中,而在函数执行完成后,栈将其环境弹出,把控制权返回给之前的执行环境。

某个执行环境中的所有代码被执行完成后,该环境被销毁,保存在其中的变量和函数定义也随之销毁。

当代码在一个环境中执行时,会创建变量对象的作用域链,用来保证执行环境有权访问的所有变量和函数的有序访问。

内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。

这些环境之间的联系是线性的、有次序的,每个环境都可以向上搜索作用域链,以查询变量和函数名,但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。

函数的参数也被当做变量来对待,因此其访问规则与执行环境中的其他变量相同。

在执行流进入下列两种语句中时,作用域链会得到加长:try-catch语句的catch块,with语句。

因为这个种语句都会在作用域链的前端添加一个变量对象。

对于with语句来说,会将指定的对象添加到作用域链中。

对于catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。

在IE8及之前版本的JavaScript实现中,存在一个与标准不一致的地方,即在catch语句中捕获的错误对象会被添加到执行环境的变量对象,而不是catch语句的变量对象中,所以在catch块的外部也可以访问到错误对象。IE9修复了这个问题。

JavaScript中没有块级作用域的概念,所以在循环语句中定义的变量在循环语句的块级作用域结束之后并不会被销毁,其中的变量可以在其所属的函数的执行环境中被访问。

使用var声明的变量会自动被添加到最接近的环境中,在函数内部,最近接的环境就是函数的局部环境;

如果初始化变量时没有使用var声明,该变量会自动被添加到全局环境,但由于这种做法可能导致意想不到的错误且为调试造成麻烦,所以并不推荐使用。

当在某个环境中读取或写入一个标识符时,必须通过搜索来确定该标识符实际代表什么。

搜索过程由作用域链的前端开始,向上逐级查询与给定名字匹配的标识符,如果在局部环境中找到了标识符,搜索停止变量就绪,如果未找到则会一直向上追溯到全局环境的变量对象,如果在全局环境中仍未找到,则意味着该变量尚未声明。

在这个搜索过程中,如果存在一个局部的变量的定义,则搜索会自动停止,不再进入另一个变量对象,所以,如果局部环境中存在着同名标识符,就不会使用位于父环境中的标识符。

变量查询也是有代价的,访问局部变量明显要比访问全局变量速度更快,因为不同向上搜索作用域链,不过由于JavaScript引擎在查询标识符方面一直在不断的优化,这个差别在未来应该就可以忽略不计了。

JavaScript具有自动垃圾收集机制,执行环境会负责管理代码执行过程中使用的内存。

具体到浏览器中的实现,则通常有标记清除(mark-and-sweep)和引用计数(reference counting)两种策略。

标记清除是JavaScript中最常用的垃圾收集方式:当变量进入环境时,就将这个变量标记为”进入环境“,而当变量离开环境时,则将其标记为”离开环境“,最后垃圾收集器完成内存清除工作,销毁那些带有”离开环境“标记的值并收回它们所占用的内存空间。

到2008年为止,IE、Firefox、Opera、Chrome、Safari的JavaScript实现使用的都是标记清除式的垃圾收集策略,只是垃圾收集的事件间隔互不相同。

另外一种不太常见的垃圾收集策略叫做引用计数:当声明一个变量并讲一个引用类型值赋给该变量时,这个值的引用次数被记做1,如果同一个值又被赋给其他变量,则该值的引用次数加1,相反,如果包含对这个值引用的变量又取得了另外一个值,那么这个值的引用次数减1,当这个值的引用次数变为0时,则说明无法再访问这个值了,此时就可以将其占用的内存空间收回。

可是在出现循环引用的时候,这种垃圾收集机制显然就会出现问题,Netscape3是最早引用计数策略的浏览器,不过由于这样的问题它在4.0中放弃了该方式。

另外,IE中有一部分对象并不是原生JavaScript对象,而是用COM对象的形式实现的,而COM对象的垃圾收集机制采用的就是引入计数策略,所以只要IE涉及COM对象就会存在循环引用的问题,为了避免这样的问题,最好是在不使用它们的时候手动断开原生JavaScript对象与DOM元素之间的连接(将变量设置为null就意味着切断变量与它们此前引用的值之间的连接)。

为了解决上述问题,IE9把BOM和DOM对象都转换成了真正的JavaScript对象,这样就避免了两种垃圾收集策略并行导致的问题,也消除了常见的内存泄露现象。

垃圾收集器是周期性运行的,而且如果为变量分配的内存数量很可观,那么回收工作量也是相当大的。

IE的垃圾收集器是根据内存分配量运行的,这种实现方式的问题在于,如果一个脚本包含一定个数的变量,那么该脚本很可能会在其生命周期中一直保有那么多的变量,这样一来就迫使垃圾收集器不得不频繁的运行,由此引发的严重性能问题促使IE7重写了其垃圾收集历程。

事实上,在有的浏览器中可以触发垃圾收集过程:IE中调用window.CollectGarbage()方法,Opera7及更高版本中调用window.opera.collect()方法,但是并不建议这样做。

使用具备垃圾收集机制的语言编写程序,一般不必担心内存管理的问题,但是由于分配给WEB浏览器的可用内存数量通常要比分配给桌面应用程序的少,所以对程序进行内存管理也是必不可少的。

而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据,一旦数据不再有用,最好通过将其值设置为null来释放其引用(解除引用 dereferencing),这一做法适用于大多数全局变量和全局对象的属性。

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

其中分配给WEB浏览器的可用内存数量较少的原因是要处于安全考虑,防止运行JavaScript的网页耗尽全部系统内存而导致系统崩溃。

JS基础知识回顾:变量、作用域和内存问题

时间: 2024-10-13 08:11:43

JS基础知识回顾:变量、作用域和内存问题的相关文章

JS基础知识回顾:ECMAScript的语法(一)

任何语言的核心都必然会描述这门语言最基本的工作原理,而描述的内容通常都要涉及这门语言的语法.操作符.数据类型.内置功能等用于构建复杂解决方案的基本概念. ECMAScript中的一切变量.函数名.操作符都区分大小写. ECMAScript的标识符要符合下列规则:第一个字符必须是字母.下划线或美元符号:其他字符可以是字母.下划线.美元符号或数字. 标识符中的字母也可以包含扩展的ASCII或Unicode字母字符,但是并不推荐. 按照惯例,ECMAScript标识符采用驼峰大小写的格式来书写,尽管没

JS基础知识回顾:引用类型(一)

在ECMAScript中引用类型是一种数据结构,用于将数据和功能组织在一起,而对象时引用类型的一个实例. 尽管ECMAScript从技术上讲是一门面向对象的语言,但它不具备传统的面向对象语言所支持的类和接口等基本结构,所以虽然说引用类型与类看起来想死,但他们并不是相同的概念. 不过引用类型有的时候也可以被称为对象定义,因为他们描述的是一类对象所具有的属性和方法. 新对象是使用new操作符后跟一个构造函数来实现的,构造函数本身就是一个函数,只不过该函数时处于创建新对象的目的而定义的. ECMASc

JS基础知识回顾:引用类型(四)

每个函数都是Function类型的实例,而且都与其他引用类型一样具有属性和方法. 由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定. 函数的声明有以下三种形式: function sum(num1,num2){return num1+num2;}//利用函数声明语法定义 var sum=function(num1,num2){return num1+num2;}//利用函数表达式定义 var sum=new Function("num1","nu

JS基础知识回顾:ECMAScript的语法(三)

ECMA-262描述了一组用于操作数据值的操作符,包括算术操作符.位操作符.关系操作符和相等操作符. ECMAScript操作符的与众不同之处在于,他们能够适用于很多值,例如字符串.数字值.布尔值.甚至是对象. 在将这些操作符应用于对象时,相应的操作符通常都会调用对象的valueOf()和(或)toString()方法,以便取得可以操作的值. 只能操作一个值的操作符叫做一元操作符. 递增和递减操作符直接借鉴自C,各有前置型和后置型两个版本:a++.++a.a--.--a 这四种操作符不仅适用于整

JS基础知识回顾:ECMAScript的语法(二)

ECMAScript中有五种简单数据类型(也称为基本数据类型):Undefined.Null.Boolean.Number.String ECMAScript还有一种复杂数据类型——Object,Object本质上是由一组无序的名值对组成的. ECMAScript不支持任何创建自定义类型的机制,而所有值最终都将是上述六种数据类型之一,由于ECMAScript的数据类型具有动态性,因此的确没有再定义其他数据类型的必要了. 监狱ECMAScript是松散类型的,因此需要有一种手段来检测给定变量的数据

JS基础知识回顾:引用类型(六)

ECMA-262对内置对象的定义是:由ECMAScript实现提供的.不依赖于宿主环境的对象,这些对象在ECMAScript程序执行之前就已经存在了. 开发人员不必显式的实例化内置对象,因为他们已经实例化了. 前面我们已经介绍了大多数内置对象,如Object.Array.String,ECMA-262还定义了两个单体内置对象:Global和Math. Global对象可以说是ECMAScript中最特别的一个对象了,因为不管你从什么角度上看,这个对象都是不存在的. 实际上并没有全局变量或全局属性

JS 基础知识3 变量

变量和数值相关,它储存了那个值,有了变量就可以储存操作数据了. js与其他语言不同,它是非类型的.就是变量可以存放任何类型的值,而其他语言需要存放特定类型的值. var i=5; i="fdsfad"; 这是合法的. 变量的声明一般是由VAR 关键字声明的 var i,sum; //一次声明两个变量,   若变量没有给定初始值,则值为“undefined” 在JS中多次声明同一个变量,是不会出错的,仅仅是给变量赋值的性质. 还有一种,不用var关键字声明变量,则JS会隐式的声明该变量,

JS基础知识回顾:在HTML中使用JavaScript

想HTML页面中插入JavaScript的主要方法就是使用<script>元素. HTML4.01当中为<script>元素定义了下列6个属性: language(已废弃):原来用于表示编写代码使用的脚本语言,如JavaScript.JavaScript1.2.VBScript等,由于大多数浏览器会忽略此属性,因此就没有必要再用了: type(可选):可以看成是language的替代属性,表示编写代码使用的脚本语言的内容类型,也被称作MIME类型,在未指定此属性的情况下会被默认为t

JS基础知识回顾:引用类型(二)

ECMAScript中的Date类型是在早期Java中的java.util.Date类基础上构建的. 因此,Date类型使用自UTC(Coordinated Universal Time,国际协调时间)1970年1月1日午夜零点开始经过的毫秒数来保存日期. 在使用这种数据存储格式的条件下,Date类型保存的日期能够精确到1970年1月1日或之后的285616年. 要创建一个日期对象,使用new操作符和Date构造函数即可:var now=new Date(); 在调用Date构造函数而不传递参数