Javascript高级程序设计 读书笔记

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

2018-02-02  本文已影响84人  Sue1024

基本类型和引用类型的值

基本类型值:简单的数据段 ,五种基本类型(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
上一篇下一篇

猜你喜欢

热点阅读