剑客
关注科技互联网

JavaScript 高级程序设计笔记(四)

第四章 变量、作用域和内存问题

4.1 基本类型和引用类型的值

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

  • 基本数据类型值:简单的数据段,包括 UndefinedNullBooleanNumberString 这五种基本数据类型值
  • 引用类型值:多个值构成的对象,按引用访问,JS 不允许直接访问内存中的对象本体的真实位置

4.1.1 动态的属性

定义基本类型值和引用类型值的方法类似,但区别是:

  • 引用类型值:可以动态地添加属性和方法,删除也一样

    var person = new Object(); 
    person.name = "Nicholas"; 
    alert(person.name); //"Nicholas"
  • 基本数据类型:赋值后就不能再添加了,虽然不报错但访问时会显示 undefined

4.1.2 复制变量值

从一个变量向另一个变量复制基本类型值和引用类型值时,也不相同

  • 基本数据类型值:值拷贝
  • 引用类型值:引用拷贝(浅拷贝)

4.1.3 传递参数

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

在向参数传递值时(基本类型和引用类型)时,被传递的值会被复制给一个局部变量(命名参数,arguments 对象中的一个元素)

虽然引用复制的是指针,但它并不是引用传递,还是值复制,虽然这个值是指针,看下面的例子:

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

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

调用 setName(person) 时会把 person 对象的引用拷贝一份给 obj,接着执行 obj.name = "Nicholas" 因为 obj 和 person 指向内存同一个对象,因此其实是在 person 对象上加了个 name 属性。马上我们把 obj 变量指向一个局部对象,再赋值 name 属性 "Greg",这个局部对象在退出函数时就被销毁了。它和 person.name 并没有什么关系,也不会影响 person.name

可以把 ECMAScript 函数的参数想象成局部变量

4.1.4 检测类型

要检测一个变量是不是基本数据类型, typeof 操作符是个好工具,但它面对 null 和对象时, typeof 会返回 "object"

检测引用类型(对象)我们通常使用 instanceof 操作符,它内部原理是通过原型链来识别的

alert(person instanceof Object); // 变量 person 是 Object 吗?  
alert(colors instanceof Array); // 变量 colors 是 Array 吗?  
alert(pattern instanceof RegExp); // 变量 pattern 是 RegExp 吗?

所有引用类型都是 Object 的实例,但如果用 instanceof 检测基本数据类型就会失败,因为基本数据类型不是对象。

4.2 执行环境及作用域

执行环境是 JavaScript 中最重要的概念,它定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的 变量对象 ,环境中定义的所有变量和函数都保持在这个对象中。

我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它

全局执行环境通常是最外围的一个执行环境,由 ECMAScript 实现所在的宿主环境决定。在 Web 浏览器中,全局执行环境是 window 对象,此时所有全局变量和函数都是作为 window 对象的属性和方法创建的。执行环境中的所有代码执行完毕后,环境也会跟着销毁。(全局执行环境直到应用程序退出、关闭网页或浏览器时才会被销毁)

每个函数都有自己的 执行环境 。当执行流进入一个函数时,函数的环境也会被推入一个环境栈中。函数执行完毕后,栈将其环境弹出,把控制权交给之前的执行环境。(压栈和出栈)

代码在一个环境中执行时,会创建变量对象的一个 作用域链 ,作用是:『保证对执行环境有权访问的变量和函数有序访问』。 作用域链 可以看做是从当前作用域向外延伸到『包含自己的下一个作用域』,直到最外层全局执行环境的链条。

前面提到每个作用域(执行环境)下都有对应的 变量对象 ,而这个变量对象始终都在作用域链的前端。比如在函数的作用域中,则将其 活动对象 作为变量对象,即一开始的 arguments 对象(此对象在全局环境中是不存在的),作用域链中的下一个变量对象来自于包含自己的外部环境,依次向外,全局执行环境中的变量对象始终是作用域链中的最后一个对象。

var color = "blue";

function changeColor(){  
    var anotherColor = "red";

    function swapColors(){
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
        // 这里可以访问 color、anotherColor 和 tempColor 
    }
    // 这里可以访问 color 和 anotherColor、但不能访问 tempColor
    swapColors();
}
// 这里只能访问 color 
changeColor();

以上代码涉及 3 个执行环境:全局、 changeColor() 的局部环境、 swapColors() 的局部环境。 JavaScript 高级程序设计笔记(四)

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

函数参数也被当做变量对待

4.2.1 延长作用域链

当执行流进入下列任何一个语句,作用域链会得到加长,这两个语句都会在作用域链的前端添加一个变量对象:

  • try-catch 语句和 catch 块:会创建一个新的变量对象,包含被抛出的错误对象的声明
  • with 语句会将指定的对象添加到作用域链中
function buildUrl() {  
    var qs = "?debug=true";
    with(location){
        var url = href + qs;
    }
    return url; 
}

with 语句将 location 对象的所有属性和方法都添加到了当前变量对象中,这个变量对象又被添加到作用域链的前端。(实际类似于把 location 对象 import 到我们当前的作用域中),这样在碰到原执行环境中没有的变量 href 时,会在 location.href 里找到。

4.2.2 没有块级作用域

JavaScript 没有块级作用域,其他类 C 语言封闭的花括号代码块就是一段作用域(ECMAScript 的执行环境),JavaScript 只有函数有执行环境(作用域),花括号并没有

特别注意 if 和 for 语句

if 代码块中的 color 在 if 执行完毕后并没有被销毁,而是添加到了当前执行环境中(全局执行环境)

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

for 循环也是一样,for 语句创建的变量 i 存在于全局环境中,所以循环完毕后依然存在

for (var i=0; i < 10; i++){  
    doSomething(i);
}
alert(i);      //10

4.2.2.1 声明变量

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

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

function add(num1, num2) {  
    sum = num1 + num2;  // 这里的 sum 是全局变量,如果加 var 就成局部变量了
    return sum; 
}
var result = add(10, 20); //30  
alert(sum); //30 因为是全局变量,所以可以在函数外部访问

我们建议在初始化变量前还是要声明比较好,可以规避很多作用域的问题,严格模式下,不允许上面这种写法

4.2.2.2 查询标识符

在某个环境中为了读取或写入而引用一个标识符时,必须通过搜索来确定该标识符实际代表什么。其实就是沿着作用域链一直向上查找,直到追溯到全局环境的变量对象。

var color = "blue";  
function getColor(){  
    return color;
}
alert(getColor());  //"blue"

下面是查找 color 标识符的过程 JavaScript 高级程序设计笔记(四)

整个查找过程是一旦找到就不继续找了,如果首先在局部环境中找到,即使上一层外部环境中也有,就不继续找了。比如

var color = "blue";  
    function getColor(){
        var color = "red";
        return color;
}
alert(getColor());  //"red"

想在函数内部访问全局 color,只能使用 window.color

4.3 垃圾收集

JavaScript 具有自动垃圾收集机制,也就是说执行环境会负责管理代码执行过程中使用的内存。垃圾回收期会定期来扫荡,他会跟踪哪个变量有用和没用,对于没用的变量打上标记,以便将来回收。具体到浏览器的标记策略主要有两个:

  • 标记清除
  • 引用计数

4.3.1 标记清除

JavaScript 最常用的垃圾收集方式,变量进入环境时,就将这个变量标记为『进入环境』,而变量离开环境就标记为『离开环境』。

垃圾收集器在运行时会给存储在内存中的所有变量都加上标记。然后,它会去掉当前环境中的变量以及被引用的变量的标记。剩下的还有标记的变量将被视为准备删除的变量。因为环境中的变量已经无法访问到这些变量了。

4.3.2 引用计数

另一种是 JS 不太常用的引用计数机制,不过 ARC 在苹果那里可是大方异彩,缺点就是循环引用。

4.3.3 性能问题

垃圾收集器是周期运行的,缺点就是会产生性能问题。IE 7 开始采用『动态调整触发垃圾回收临界值』的方式来优化。

4.3.4 管理内存

分配给 Web 的内存通常比桌面要小。我们可以将那些不再使用的变量设置为 null(解除引用),以便垃圾收集器下次工作时将其回收。

function createPerson(name){  
    var localPerson = new Object();
    localPerson.name = name;
    return localPerson; 
}
var globalPerson = createPerson("Nicholas");  
//手工解除 globalPerson 的引用   
globalPerson = null;


-EOF-

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址