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

《JavaScript高级程序设计》学习笔记

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

1. 变量

1.1 数据类型

js中有两种数据类型的变量:

  • 基本类型:简单的数据段

Undefined、Null、Boolean、Number和String

  • 引用类型:可能由多个值构成的对象

Object、Array、Date、RegExp、Function等

1.2 基本类型和引用类型的比较

1.2.1 访问方式
  • 基本类型:按值引用

基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值。基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中;

  • 引用类型:根据情况不同

js不允许直接访问内存中的位置,所以当复制保存着对象的某个变量时,操作的是对象的引用;但是在为对象添加属性时,操作的是 是实际的对象。引用类型的值是对象,保存在堆内存中;

1.2.2 动态属性
  • 基本类型:无动态属性

不能给基本类型的值添加属性,尽管这样做不会导致任何错误

var name = "Nicholas";
name.age = 27;
alert(name.age); //undefined
复制代码
  • 引用类型:动态属性

只能给引用类型的值添加属性,也可以删除或者修改引用类型的值的属性或方法

var person = new Object();
person.name = "Nicholas";
alert(person.name); //"Nicholas"
复制代码
1.2.3 复制变量的值
  • 基本类型:会创建这个值的一个副本

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

var num1 = 5;
var num2 = num1;
num1 = 3;
console.log(num2); // 5
复制代码
  • 引用类型:只会存储引用类型的值的指针

从一个变量向另一个变量复制引用类型的值,复制的其实是指针,因此两个变量最终都指向同一个对象;

var obj1 = new Object();
var obj2 = obj1;
obj1.name = "Nicholas";
alert(obj2.name); //"Nicholas"
复制代码
1.2.4 传递参数

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

  • 基本类型
function addTen(num) {
  num += 10;
  return num;
}

var count = 20;
var result = addTen(count); alert(count); //20,没有变化 alert(result); //30
复制代码
  • 引用类型
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"
复制代码

如果 person 是按引用传递的,那么 person 就会自动被修改为指向其 name 属性值为"Greg"的新对象。但是,当接下来再访问 person.name 时,显示的值仍然是"Nicholas"。这说明即使在函数内部修改了参数的值,但原始的引用仍然保持未变。实际上,当在函数内部重写 obj 时,这个变量引用的就是一个局部对象了。而这个局部对象会在函数执行完毕后立即被销毁。

1.2.5 检测类型
  • 基本类型:typeof

typeof 操作符是确定一个变量是字符串、数值、布尔值,还是 undefined 的最佳工具。使用typeof操作符检测函数时,该操作符会返回"function"。

var s = "Nicholas";
var b = true;
var i = 22;
var u;
var n = null;
var o = new Object();
alert(typeof s); //string
alert(typeof i); //number
alert(typeof b); //boolean
alert(typeof u); //undefined
alert(typeof n); //object
alert(typeof o); //object
复制代码
  • 引用类型:instanceof

instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。如果变量是给定引用类型(根据它的原型链来识别)的实例,那么 instanceof 操作符就会返回 true。

person instanceof Object; // 变量 person 是 Object 吗?
colors instanceof Array; // 变量 colors 是 Array 吗?
pattern instanceof RegExp; //变量pattern是RegExp吗?
null instanceof Object; //false 可以区分null和对象
复制代码

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

[1, 2, 3] instanceof Object; // true
[1, 2, 3] instanceof Array; // true
复制代码

2. 作用域

2.1 执行环境(execution context:执行上下文)

2.1.1 执行环境概述

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

2.1.2 执行环境的属性

对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)

变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。

  • 作用域链(Scope chain)

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

  • this

2.2 延长作用域链

有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移 除。在两种情况下会发生这种现象。

  • try-catch 语句的 catch 块

  • with 语句

这两个语句都会在作用域链的前端添加一个变量对象。对 with 语句来说,会将指定的对象添加到作用域链中。对 catch 语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。 下面看一个例子。

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

  return url;
}
复制代码

在此,with 语句接收的是 location 对象,因此其变量对象中就包含了 location 对象的所有属性和方法,而这个变量对象被添加到了作用域链的前端。buildUrl()函数中定义了一个变量 qs。当在 with 语句中引用变量 href 时(实际引用的是 location.href),可以在当前执行环境的变量对象中找到。当引用变量 qs 时,引用的则是在 buildUrl()中定义的那个变量,而该变量位于函数环境的变量对象中。至于 with 语句内部,则定义了一个名为 url 的变量,因而 url 就成了函数执行环境的一部分,所以可以作为函数的值被返回。

2.2.3 没有块级作用域

JavaScript 没有块级作用域,但是 JavaScript 有函数作用域的概念,变量在声明它的函数体以及这个函数体嵌套的任意函数体内都是有定义的。

if (true) {
  var color = "blue";
} else {  
  var size = "medium";
}

alert(color); //"blue"
alert(size); //"undefined"
复制代码

对于有块级作用域的语言来说,for 语句初始化变量的表达式所定义的变量,只会存在于循环的环 境之中。而对于 JavaScript 来说,由 for 语句创建的变量 i 即使在 for 循环执行结束后,也依旧会存在 于循环外部的执行环境中。

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

alert(i); //10
复制代码

使用 var 声明的变量会自动被添加到最接近的环境中。在函数内部,最接近的环境就是函数的局部环境;在 with 语句中,最接近的环境是函数环境。如果初始化变量时没有使用 var 声明,该变量会自动被添加到全局环境。

function add(num1, num2) {
  sum = num1 + num2;
  return sum;
}

var result = add(10, 20); //30
alert(sum); //30
复制代码

3. 内存(垃圾回收)

JavaScript 具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。这种垃圾收集机制的原理其实很简单:找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性地执行这一操作。

函数中局部变量的正常生命周期:局部变量只在函数执行的过程中存在。而在 这个过程中,会为局部变量在栈(或堆)内存上分配相应的空间,以便存储它们的值。然后在函数中使 用这些变量,直至函数执行结束。此时,局部变量就没有存在的必要了,因此可以释放它们的内存以供 将来使用。在这种情况下,很容易判断变量是否还有存在的必要;但并非所有情况下都这么容易就能得 出结论。垃圾收集器必须跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以备将来收 回其占用的内存。

通常有两个策略来标识无用变量。

3.1 标记清除

JavaScript 中最常用的垃圾收集方式是标记清除(mark-and-sweep)。这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。定期的,垃圾回收器将从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和所有不能获得的对象。这个算法遵循“有零引用的对象”总是不可获得的,但是相反却不一定。

3.2 引用计数

另一种不太常见的垃圾收集策略叫做*引用计数(reference counting)。“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。

引用计数的问题:循环引用

循环引用指的是对象 A 中包含一个指向对象 B 的指针,而对象 B 中也包含一个指向对象 A 的引用

function problem(){
  var objectA = new Object();
  var objectB = new Object();
  objectA.someOtherObject = objectB;
  objectB.anotherObject = objectA;
}
复制代码

在这个例子中,objectA和objectB通过各自的属性相互引用;也就是说,这两个对象的引用次 数都是 2。在采用标记清除策略的实现中,由于函数执行之后,这两个对象都离开了作用域,因此这种相互引用不是个问题。但在采用引用计数策略的实现中,当函数执行完毕后,objectA和objectB还将继续存在,因为它们的引用次数永远不会是 0。假如这个函数被重复多次调用,就会导致大量内存得不到回收。

objectA.someOtherObject = null;
objectB.anotherObject = null;
复制代码

为了避免类似这样的循环引用问题,最好是在不使用它们的时候手工断开原生 JavaScript 对象与 DOM 元素之间的连接。例如,可以使用下面的代码消除前面例子创建的循环引用。

3.3 性能问题

垃圾收集器是周期性运行的,而且如果为变量分配的内存数量很可观,那么回收工作量也是相当大 的。在这种情况下,确定垃圾收集的时间间隔是一个非常重要的问题。

3.4 管理内存

确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为 null 来释放其引用——这个做法叫做解除引用(dereferencing)。这一做法适用于大多数全局变量和全局对象的属性。局部变量会在 它们离开执行环境时自动被解除引用。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值