JS变量、作用域和内存问题

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

ECMAScript包含两个不同数据类型的值:基本类型和引用类型。基本类型指的是简单的数据段,按值访问;而引用数据类型指那些可能由多个值构成的对象,它的值是保存在内存中的对象,在JS中,不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间,在操作对象时,实际上是操作对象的引用而不是实际的值,按引用访问的

动态的属性

定义基本类型和引用类型值的方法是一样的:创建一个变量并为该变量赋值。但两种类型的值操作却不一样

  • 基本类型的值
    不能为基本类型添加属性和方法,添加了也不会报错,但再次访问时结果为undefined
  • 引用类型的值
    可以为其添加属性和方法,也可以改变和删除其属性和方法
var person=new Object();
person.name="ly";
console.log(person.name);    //"ly"

var name="sw";
name.age=18;
console.log(name.age);      //undefined
复制变量值

除了保存方式不同,在我们复制基本类型和引用类型时也存在不同。

  • 基本类型
    在变量对象上创建一个新值 ,然后把该值复制到为新变量分配的位置上
var num1=5;
var num2=num1;   //5

num1和num2相等,值都是5,但他们是相互独立的,num2的值只是num1的副本。此后,两个变量的任何操作都不会相互影响

  • 引用类型
    复制引用类型时,同样也会将存储在变量的值赋值一份放到为新变量分配的空间中,不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作之后,两个变量实际上将引用同一个对象,改变其中一个变量,也会影响另一个变量。
var obj1=new Object();
var obj2=obj1;
obj1.name="ly";
console.log(obj2.name);     //"ly"

在这里插入图片描述

传递参数

ECMAScript中所有参数都是按值传递的。也就是将外部的值复制给函数内部的局部变量(参数)。
在传递基本类型时,就是将基本类型的值赋值给一个局部变量;在传递引用类型时 ,就是将这个值在内存中的地址复制给一个局部变量。

function fn(num){
	num+=10;
	return num;
}
var count=20;
var result=fn(count);
console.log(count);      //20
console.log(result);     //30

参数num和变量count互不相识,仅仅是具有同样的值。但是要是引用传递的话,外部变量的值也会改变。

function fn(obj){
	obj.name="ly";
}
var person=new Object();
fn(person);
console.log(person.name);    //"ly"

obj和person指向同一个对象,当函数内部给对象添加一个属性时,person也会有所反映。
为了证明参数是按值传递的,再看一下下面的例子:

function fn(obj){
	obj.name="ly";
	obj=new Object();
	obj.name="sw";
}
var person=new Object();
fn(person);
console.log(person.name);    //"ly"

这个例子在函数内部为obj重新定义了一个对象,并给了一个值。如果person是按引用传递的,那么person就会自动东被修改为指向其name属性值为"sw"的新对象,但是,person的输出结果依旧是"ly",这说明,即使在函数内部改变了参数的值,但原始的引用仍然保持不变。实际上,在函数内部重写obj时,这个变量的引用就是一个局部对象了,而这个局部对象在函数执行完之后就会被立即销毁。

检测类型

要检测一个变量是不是基本类型,我们之前有学到的typeof操作符是最佳工具。
typeof可以确定一个变量是字符串、数值、布尔值、,还是undefined。如果变量的值是null或一个对象,typeof会返回"object"。

var u;
var obj=new Object();

typeof "ly"    //"string"
typeof true    //"boolean"
typeof 22      //"number"
typeof u       //"undefined"
typeof null    //"object"
typeof obj    //"object"

typeof可以很容易的检测出基本类型,但是引用类型其作用不大。
instanceof是ECMAScript提供的判别某个值是什么类型的对象的操作符(根据原型链来识别)。

person innstanceof Object    //变量person是Object吗?
color innstanceof Array      //变量color是Array吗?
pattern innstanceof RegExp    //变量pattern是RegExp吗?

根据规定,所有引用类型都是Object的实例,所以在检测一个引用类型的值和Object构造函数时,instanceof始终返回true。
如果使用instanceof检测基本类型,始终会返回false,因为基本类型不是对象。

使用typeof操作符检测函数时,会返回“function"。

2.执行环境及作用域

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

每个执行环境中都有个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。(解析器在处理数据时会在后台使用它)

全局执行环境是最外围的一个执行环境。所在的宿主环境不同,表示执行环境的对象也不同。在Web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出——例如关闭浏览器或网页——时才会被销毁)。

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

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

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

var color="blue";
function changeColor(){
	if(color=="blue"){
		color="red";
	}else{
		color="blue";
	}
}
changeColor();
console.log(color);

在上面这个例子中,函数changeColor()的作用域链包含两个对象,一个是他自己的变量对象,一个是全局环境的变量对象。可以在函数内部访问color变量,是因为我们可以在函数的作用域链中找到他。

==内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。==这些环境之间的联系是线性的、有序的。

延长作用域链

有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。也就是说,当执行流进入下列任何一个语句时,作用域链就会延长

  • try-catch语句的catch
    会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明
  • with语句
    将指定的对象添加到作用域链中

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

function fn(){
	var qs="?debug=true";
	with(location){
		var url=href+qs;
	}
	return url;
}

在上面的例子中,with接收的是location对象,这个变量对象就被添加到作用域链的前端。函数中定义了一个变量qs。当在with语句中引用变量href时,可以在当前执行环境的变量对象中找到。当引用变量qs时,引用的则是在函数中定义的那个变量,而该变量位于函数环境的变量对象中。至于with内部,则定义了一个url变量,因而url就成了函数执行环境的一部分,所以可以作为函数的值被访问。

没有块级作用域

块级作用域:以花括号封闭的代码块都有自己的作用域。

if(true){
	var color="blue";
}
alert(color);

以上代码,在有块级作用域的语言中,color会在if语句执行完毕后被销毁;但在JS中,if语句中的变量声明会将变量添加到当前的执行环境中。

①声明变量
使用var声明的变量会自动被添加到最接近的环境中。如果初始化变量时没有加var声明,该变量会自动被添加到全局环境中。

function fn(n1,n2){
	var sum=n1+n2;
	return sum;
}
var result=fn(2,3);
alert(sum);

以上代码中,函数定义了一个局部变量sum,虽然结果值从函数中返回了,但变量sum在函数外部是访问不到的,因此最后一句代码回导致错误。如果省略函数中sum的var,那么在执行完函数后,sum也可以访问得到,因为他被添加到全局环境中。

②查询标识符

挡在某个环境中为了读取或写入而引用一个标识符时,必须通过搜索来确定该标识符代表什么。

  • 搜索过程:
    从作用域链的前端开始,向上逐级查询与给定的名字匹配的标识符。
    如果在局部环境中找到了该标识符,搜索过程停止,变量就绪。
    如果在局部环境中没有找到该变量名,则继续沿作用域链向上搜索,搜索过程将一直追溯到全局环境的变量对象。
    如果在全局环境中也没有找到这个标识符,则意味着这个变量未声明。
    如果局部变量和全局变量中含有同名的标识符,会使用局部变量中的标识符
var color="blue";
function fn(){
	var color="red";
	return color;
}
alert(fn());   //"red"

3.垃圾收集

JS具有垃圾收集机制,会管理代码执行过程中使用的内存,所需内存的分配以及无用内存的回收完全实现了自动管理。

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

垃圾收集器必须跟踪哪个变量有用,哪个变量无用,对于不再有用的变量打上标记,以备将来收回其占用的内存。

通常有两种策略:标记清除、引用计数

①标记清除
JS最常用的垃圾收集方式。

当变量进入环境时,就将这个变量标记为 “进入环境”,从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入到相应的环境,就有可能会用到他们;而当变量离开环境时,将其标记为 “离开环境”
可以使用任何方式来标记变量。比如,可以通过翻转某个特殊的位来记录一个变量何时进入环境,或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪个变量发生了变化。

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

②引用计数

含义:跟踪记录每个值被引用的次数。

当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数是1,如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值的引用的变量又取得了另外一个值,则这个值的引用次数减1.当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次在运行时,他就会释放那些引用次数为0的值所占的内存空间。

引用计数策略有个严重的问题——循环引用
循环引用是指对象A中包含一个指向对象B的指针,对象B中也包含了指向对象A的引用。

function fn(){
	var objA=new Object();
	var objB=new Object();
	objA.some=objB;
	objB.another=objA;
}

在上面的例子中,objA和objB通过各自的属性相互引用,这两个对象的引用次数都是2.当函数执行完毕后,objA和objB还将继续存在,因为他们的引用次数永远不会是0,假设这个函数被多次调用,就会导致大量的内存得不到回收。所以很多浏览器都采用标记清除的方式实现垃圾回收,但在IE中还存在问题,IE中JS访问的COM(组件对象模型,是C++的)对象依然是基于引用计数的,JS中的BOM和DOM就是由COM对象的形式实现的,所以,只要在IE中涉及到COM对象,就会存在循环引用的问题:

var ele=document.getElementById("name");
var obj=new Object();
obj.ele=ele;
ele.some=obj;

以上循环引用,即使DOM从页面移除,他也永远不会被回收。为了避免类似这样的循环引用问题,最好在不使用他们的时候手动断开它们之间的连接:

obj.ele=null;
ele.some=null;

这样当垃圾收集器下次运作时,就会删除这些值并回收他们的内存空间。

IE9将BOM和DOM对象都转换成了真正的JS对象,这样就避免了问题,也消除了常见的内存泄漏现象

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值