资讯专栏INFORMATION COLUMN

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

JohnLui / 1397人阅读

摘要:保存在堆内存中。因此改变一方,另一方也会发生相应的改变。作用域链当代码在一个环境中执行时,会创建变量对象的一个作用域链,以保证对执行环境有权访问的所有变量和函数的有序访问。

基本类型和引用类型的值

基本类型值:简单的数据段 ,五种基本类型(Number Boolean String Null Undefined)的值都是基本类型值,基本类型的值在内存中大小固定,因此保存在栈内存中。
引用类型值:可能由多个值构成的对象。不能操作引用类型的内存空间。保存在堆内存中。

动态的属性
引用类型值

我们可以为引用类型的值添加、修改、删除属性和方法,比如:

var cat = new Animal();
cat.name = "cat";
cat.speak = function()  {
  alert(this.name);
};
cat.speak(); // cat
基本类型值

然而为基本类型的值添加属性和方法是无效的。

var name = "Sue";
name.age = 18;
alert(name.age); //undefined
复制变量值
基本类型值
var num1 = 5;
var num2 = 5;

num1与num2的内存空间是完全独立的,对一方的改变不会影响到另一方。

引用类型值
var obj1 = new Object();
var obj2 = obj1;
obj2.name = "Sue";
alert(obj1.name); // Sue

当我们将对象obj1复制给obj2时,只是创建了一个指针副本,这个指针副本与obj1指向同一个保存在堆内存中的对象。因此改变一方,另一方也会发生相应的改变。

传递参数
实参与形参
var num = 2
function add(num1, num2) {
  return num1 + num2;
}
add(1, num);

在上述代码中,add(1, num)传入的参数是实参,而arguments[]总是获取由实参串起来的参数值,在函数体中的num1 num2是形参,相当于声明了两个局部变量,指向arguments[0] arguments[1]

按值传递

ECMAScript 中所有函数的参数都是按值传递的,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样(无论是基本类型还是引用类型)。

function changeStuff(num, obj1, obj2)
{
    num = num * 10;
    obj1.item = "changed";
    obj2 = {item: "changed"};
}
var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};
changeStuff(num, obj1, obj2);
console.log(num);   // 10
console.log(obj1.item);    // changed
console.log(obj2.item);    // unchanged

以上的例子是怎么说明ECMAScript中函数的参数都是按值传递的呢?
首先基本数据类型,全局变量num复制自身给参数num,二者是完全独立的,改动不会相互影响。
关于对象,我们似乎看到了函数内部对obj1对象属性的改动反应到了函数外部,而obj2重新赋值为另一个对象却没有反应到外部,这是为什么呢?书中解释得有点简单,笔者找了一下资料,原来传入对象的时候,其实传入的是对象在内存中的地址,当在函数中改变对象的属性时,是在同一个区域进行操作,所以会在函数外反映出来,然而,如果对这个局部变量重新赋值,内存中的地址改变,就不会对函数外的对象产生影响了,这种思想称为 call by sharing。

 However, since the function has access to the same object as the caller (no copy is made), mutations to those objects, if the objects are mutable, within the function are visible to the caller, which may appear to differ from call by value semantics. Mutations of a mutable object within the function are visible to the caller because the object is not copied or cloned — it is shared. Wikipedia
检测类型
哪种基本数据类型

使用typeof可以辨认String Number Undefined Boolean Object还有函数。

typeof("name"); //string
typeof(18); //number
typeof(undefined); //undefined
typeof(null); //object
typeof(true); //boolean
typeof(new Array()); //object
typeof(Array); //function

正则表达式在某些浏览器中typeof返回结果为object,某些返回function

什么类型的对象

instanceof可以判断是否是给定类型的实例

var a = new Array;
a instanceof Array; //true

使用instanceof测试基本数据类型时,用于返回false

执行环境和作用域 执行环境(execution context)

定义了变量或函数有权访问的其他数据。每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。

全局执行环境

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

函数执行环境

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

作用域链(scope chain)

当代码在一个环境中执行时,会创建变量对象的一个作用域链,以保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象,对于全局执行环境,就是window对象,对于函数执行环境,就是该函数的活动对象。作用域链的后续,是该函数对象的[[scope]]属性(全局执行环境没有后续)。

函数对象

在一个函数被定义时,会创建这个函数对象的[[scope]]属性,指向这个函数的外围。

活动对象

在一个函数被调用时,会创建一个活动对象,首先将该函数的形参和实参(arguments)添加进该活动对象,然后添加函数体内声明的变量和函数(提前声明,在刚进入该函数执行环境时,值为undefined),这个活动对象将作为该函数执行环境作用域链的最前端。
关于JS的提前声明机制,我们举个例子证明一下:

function add (num1){
    console.log(num2);
    var num3 = 4;
    return num1 + num2;
}
add(1); //undefined 5

上述代码中,我们在变量声明前使用它,却没有跑出ReferenceError,说明函数执行时,一开始,num2就已经声明了。

内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。这些环境之间的联系是线性、有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。
我们举一个例子,顺便理解一下前面的概念:

var num = 10;
function add (num1, num2) {
  function preAdd(num) {
    var pre = 1;
    return pre  + num;
  num1 = preAdd(num1);
  var result = num1 + num2 + num;
  return result;
}
add(10, 20);

开始执行add()时,首先创建一个执行上下文,然后创建一个活动对象,将arguments num1 num2 pre preAdd() result保存在活动对象中,并将活动对象放在作用域链的前端,执行上下文取得add保存的[[scope]],并将其放入作用域链的后端,然后执行到preAddpreAdd创建一个执行上下文,并压入栈顶,创建一个活动对象,保存arguments num pre ,放在作用域链的前端,取得preAdd[[scope]],放入作用域链的后端。当编译器开始解析pre时,首先从preAdd作用域链的前端开始找,找到了立刻停止。当编译器开始解析result = num1 + num2 + num,由于在add的作用域链前端(局部变量)中没有该变量,因此继续在作用域后端中寻找,并最终在全局变量中找到了num

延长作用域链
with

在块作用域内,将指定变量放在作用域链的前端

try-catch

创建一个新的变量对象,其中包含的是被抛出的错误对象的声明,将这个对象放在作用域链的最前端,catch执行结束后,作用域链恢复。

没有块级作用域

ECMAScript中没有块级作用域,因此块的执行环境与其外部的执行环境相同。

声明变量

使用var 声明的变量会自动被添加到最接近的环境中。如果初始化变量时没有使用var 声明,该变量会自动被添加到全局环境(严格模式下,这样写会抛错)。

查询标识符

当对一个变量进行读取或修改操作时,我们首先要搜索到它,搜索的顺序如图:
标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。

性能 垃圾收集

Javascript具有自动垃圾收集机制,周期性地回收那些不再使用的变量,并释放其占用的内存。

标记清除(mark-and-sweep)

这是Javascript中最常用的垃圾收集方式,当变量进入环境时,将其标记为“进入环境”,离开环境时,标记为“离开环境”。理论上,不可以回收标记为“进入环境”的变量。

可以使用任何方式来标记变量。比如,可以通过翻转某个特殊的位来记录一个变量何时进入环境,或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪个变量发生了变化。说到底,如何标记变量其实并不重要,关键在于采取什么策略。
引用计数(reference counting)

不太常见,跟踪记录每个值被引用的次数。
当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值或当它们的生命期结束的时候,要给它们所指向的对象的引用计数减1。当这个值的引用次数变成0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。

var a = new Cat(); // 1
var b = a; // 2
var c = b; // 3
b = new Dog(); // 2
c = new Fox(); // 1
a = new Object(); // 0

这样看起来,引用计数法似乎没什么问题,然而,当遇到循环引用时,就跪了。。。

var a = new Object(); //a指向的Object的引用次数+1
var b = new Object(); //b指向的Object的引用次数+1
a.another = b; //b指向的Object的引用次数+1
b.another = a; //a指向的Object的引用次数+1

此时,两个对象的引用次数都为2,用于都不会变为0,永远都不会被GC,浪费内存。
由于引用计数存在上述问题,因此早在Navigator 4.0就放弃了这一策略,但循环引用带来的麻烦却依然存在。
IE 中有一部分对象并不是原生JavaScript 对象。例如,BOM 和DOM 中的对象就是使用C++以COM(Component Object Model,组件对象模型)对象的形式实现的,COM的垃圾回收策略是引用计数法,因此只要涉及到COM对象,就会存在循环引用的问题,举一个例子:

var element = document.getElementById("some_element");
var myObject = new Object();
myObject.element = element;
element.someObject = myObject;

IE9 把BOM 和DOM 对象都转换成了真正的JavaScript 对象。这样,就避免了
两种垃圾收集算法并存导致的问题。

管理内存

由于系统分配给浏览器的内存比较小(比桌面应用小),而内存限制势必会影响网页性能,因此Javascript中,优化内存占用是一个必要的问题,最佳方式就是只保留必要的数据。局部变量会在离开执行环境后自动解除引用,而后被GC,因此我们只需在不再需要某个全局变量时,将其设为null,来解除它对内存的引用(即解除引用dereferencing),适用于大多数全局变量和全局对象的属性。
针对上一节的例子,我们可以使用同样的方法:

myObject.element = null;
element.someObject = null

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/94769.html

相关文章

  • JavaScript高级程序设计(第3版)》——变量作用域和内存问题(四)

    摘要:执行环境的类型有两种全局全局执行环境局部函数执行环境每个环境都可以向上搜索作用域链,以查询变量和函数名但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。内部可通过作用域链访问外部,外部不能访问内部。 变量、作用域和内存问题 ECMAScript 数据类型 基本类型(5种): Undefined,Null,Boolean,Number,String typeof() 检测...

    YacaToy 评论0 收藏0
  • JavaScript红宝书笔记(四)---变量作用域和内存问题

    摘要:在操作对象时,实际上是在操作对象的引用而不是实际的对象。为此,引用类型的值是按引用访问的。标记清除是目前主流的垃圾收集算法,这种算法的思想是给当前不使用的值加上标记,然后再回收其内存 1.在操作对象时,实际上是在操作对象的引用而不是实际的对象。为此,引用类型的值是按引用访问的。 2.当从一个变量向另一个变量复制引用类型的值时,两个变量实际上将引用同一个对象,因此,改变其中一个变量,就会...

    imtianx 评论0 收藏0
  • JavaScript变量作用域和内存问题

    摘要:全局变量是最外围的一个执行环境,代码在环境中执行,会创建一个作用域链,用途是保证对执行环境有权访问所有变量和函数的有序访问。作用域链中最后一个对象始终是全局执行环境。内部环境可以通过作用域链访问所有的外部环境,外部则不能访问内部。 1、基本类型和引用类型的值 * 基本类型 : 指的是简单的数据段,五种基本类型是按值访问的,可以直接操作保存在变量中实际的值。 * 引用类型 : 指那些可能...

    Dr_Noooo 评论0 收藏0
  • JS学习笔记(第4章)(变量作用域和内存问题

    摘要:具体来说就是当执行流进入下列任何一个语句时,作用域链就会得到加长语句的块和语句。这两个语句都会在作用域链的前端添加一个变量对象。对来说,会将指定的对象添加到作用域链中。 1. 基本类型和引用类型的值 JavaScript变量可以用来保存两种类型的值:基本类性值和引用类性值。基本类型值源自以下5种基本数据类型:Undefined、Null、Boolean、Number和String。基本...

    linkin 评论0 收藏0
  • 深入javascript——作用域和闭包

    摘要:注意由于闭包会额外的附带函数的作用域内部匿名函数携带外部函数的作用域,因此,闭包会比其它函数多占用些内存空间,过度的使用可能会导致内存占用的增加。 作用域和作用域链是javascript中非常重要的特性,对于他们的理解直接关系到对于整个javascript体系的理解,而闭包又是对作用域的延伸,也是在实际开发中经常使用的一个特性,实际上,不仅仅是javascript,在很多语言中都...

    oogh 评论0 收藏0
  • 变量作用域和内存问题

    摘要:变量作用域和内存问题基本类型和引用类型的值基本类型就是简单的数据段种值类型,而引用类型就是对象操控对象的引用。但是不但能访问自己的变量,也能访问和全局作用域下的变量。延长作用域链相当于创造了一个新的变量对象在当前作用域的上方。 变量作用域和内存问题 1.基本类型和引用类型的值 基本类型就是简单的数据段(5种值类型),而引用类型就是对象(操控对象的引用)。 1.1复制变量值 引用类型实际...

    wuyangchun 评论0 收藏0

发表评论

0条评论

JohnLui

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<