乐趣区

重学Javascript之变量作用域及内存管理

JavaScript 变量及类型检测

javascript 数据类型分为基本数据类型,引用数据类型和 Symbol。

基本数据类型

String,Number,Boolean,undefined,null

引用数据类型

Object
这里的 Object 包括带编号的有序集合 Array,包含 key/value 的无序集合和另一种特殊对象 Function。

要点:基本数据类型的值是不可变的,引用数据类型值是可变的

基本数据类型的复制

基本数据类型变量复制是分配新的地址,新值是被复制变量的一个副本,变量之间是独立的,互不影响。

var num1 = 5; 
var num2 = num1; 

引用数据类型的复制

引用数据类型复制时候也会分配新的地址,不同的是新地址中存储的是引用数据在堆内存中的指针。复制操作结束后,两者都指向同一个引用地址。因此,修改其中一个会影响另外一个。

var obj1 = new Object(); 
var obj2 = obj1; 
obj1.name = "Nicholas"; 
alert(obj2.name); //"Nicholas" 

类型检测

1、typeof

typeof 是操作符不是函数。使用 typeof 会返回如下值:

  • “undefined”———该值没有定义
  • “boolean” ———布尔值
  • “string”—- 字符串
  • “function”—- 函数
  • “number”—- 数字
  • “object”—- 对象或者 null

注意:在使用 typeof 检测数组和 null 都会返回 object,因此 typeof 并不能精确判断某一数据类型。

2、instanceof
如果某个变量是指定引用类型的实例,则 instanceof 会返回 true。当然数组的判断可以使用 Array.isArray()。

注意:instanceof 也并非完全可靠,1. 由于变量的原型并不是一层不变的,一旦原型被修改,就可能返回 false。2. 无法判断多全局对象,比如在多窗口之间进行原型判断,多窗口意味着多全局对象,拥有不同的内置构造函数,比如[] instanceof window.frames[0].Array 会返回 false

3、constructor
“javascript 中一切都是对象”,所有对象都会在其原型上继承一个 constructor 属性指向其构造函数。

注意:null 和 undefined 没有 constructor。同样,对象的 constructor 也是可以改变的,比如:

var a=[];
a.constructor=new Number();
console.log(a.constructor);//Number

4、Object.prototype.toString

Object.prototype.toString 会返回一个表示该对象的字符串

Object.prototype.toString.call()

JavaScript 执行环境与作用域

JavaScript 执行环境决定了变量或函数是否有权访问,每个执行环境都有一个变量对象(variable object), 执行环境中所有变量和函数都保存在该对象中,虽然我们无法直接访问该对象,JavaScript 解析器执行时会使用到它。某个执行环境所有代码执行完毕后随之销毁。

全局执行环境

全局执行环境是最外围的一个执行环境,在浏览器中,全局执行环境被认为是 window 对象,所有变量和函数都是 window 下的属性或者方法。

局部执行环境

每个函数都有自己的局部执行环境,当执行流进入函数时候,函数的执行环境就会被推入到执行栈中,函数执行完毕后就会被弹出执行栈。函数的参数也是局部变量,其优先级高于外部变量。

函数作用域和变量声明提升

每个函数内部变量只能在函数体内访问,函数外是无权访问的,包括函数的参数。
函数内定义的变量在整个函数体内都可访问,即使变量声明在变量使用之后,这就是变量声明提升。最典型的例子如下:

var a=1;
function test(){console.log(a);//1
    a=2;
    console.log(a);//2
}
test();

换一下

var a=1;
function test(){console.log(a);//undefined
    var a=2;
    console.log(a);//2
}
test();
for(var i=0;i<10;i++){console.log(i);// 输出 1~9
}
console.log(i);// 输出 10

ES6 之前 JavaScript 没有块级作用域

理解这句话可以看如下例子:

function test(o){if(typeof o==='object'){
    var i="test";
    for(var k=0;k<10;k++){console.log(k);
    }
    console.log(k)
  }
  console.log(i);
}

test();//undefined
test({});// 输出 0~9,10,test

可见,即使加了 if 条件判断或者循环,变量 i 和 k 都可以在大括号代码块外访问。

作用域链

每段 JavaScript 代码或者函数都有一个与之关联的作用域链(scope chain), 这个作用域链是一个对象列表或者链表,这组对象定义了这段代码“作用域”中的变量。
当 JavaScript 需要查找变量 a 的时候,它会从链中的第一个对象开始查找,如果这个对象包含变量 a 属性,则会直接使用该对象中的 a 属性,如果不存在,则继续向上查找第二个对象,如果第二个对象也没有,则继续查找下一个,以此类推。如果整个作用域链上都没有 a 属性,则会抛出异常。
注意:在 JavaScript 顶层代码中,作用域链由一个全局对象组成,在函数体内,作用域链有 2 个,一个是定义函数参数和函数局部变量的对象,一个是全局对象,如果函数内找不到某个变量,会继续在全局对象中查找。

JavaScript 内存管理

声明变量就得在内存中给它分配存储地址,基本数据类型存储在栈中,引用数据类型存储在堆中

内存的生命周期

  1. 分配你所需要的内存
  2. 使用分配到的内存(读、写)
  3. 不需要时将其释放 \ 归还

内存的分配

let myNumber = 23

JavaScript 在执行上面代码时候流程如下:

  1. 为 myNumber 定义唯一标识符(identifier);
  2. 在内存中分配一个地址(运行时候分配);
  3. 将值 23 存储到分配的地址中。

基本数据类型的复制是分配新的地址,是被复制对象的副本,因此会互不干扰;

引用数据类型的复制是堆地址指针的复制,复制后它们指向同一地址,因此修改其中一个会影响另外一个。

内存的使用
在 JavaScript 中使用分配的内存意味着在其中读写,这可以通过读取或写入变量或对象属性的值,或者将参数传递给函数来实现。

内存释放
这里最困难的地方是确定何时不再需要分配的内存,它通常要求开发人员确定程序中哪些地方不再需要内存的并释放它。

垃圾收集

引用计数

这是最简单的垃圾收集器算法。如果没有引用指向这个对象的时候,这个对象就被认为是“可以作为垃圾收集”。

var o = { 
 a: {b:2}
}; 
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量 o
// 很显然,没有一个可以被垃圾收集


var o2 = o; // o2 变量是第二个对“这个对象”的引用
o = 1;      // 现在,“这个对象”的原始引用 o 被 o2 替换了

var oa = o2.a; // 引用“这个对象”的 a 属性
// 现在,“这个对象”有两个引用了,一个是 o2,一个是 oa

o2 = "yo"; // 最初的对象现在已经是零引用了
       // 他可以被垃圾回收了
       // 然而它的属性 a 的对象还在被 oa 引用,所以还不能回收

oa = null; // a 属性的那个对象现在也是零引用了
       // 它可以被垃圾回收了

循环引用的问题
当遇到循环的时候就会有一个限制。在下面的实例之中,创建两个对象,并且互相引用,因此就会产生一个循环。当函数调用结束之后它们会走出作用域之外,因此它们就没什么用并且可以被释放。但是,基于引用计数的算法认为这两个对象都会被至少引用一次,所以它俩都不会被垃圾收集器收集。

function f(){var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();

标记清除

JavaScript 中最常用的垃圾收集方式是标记清除(mark-and-sweep)。当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。这个算法由以下步骤组成:

  1. 这个垃圾收集器构建一个“roots”列表。Root 是全局变量,被代码中的引用所保存。在 JavaScript 中,“window”就是这样的作为 root 的全局变量的例子。
  2. 所有的 root 都会被监测并且被标志成活跃的(比如不是垃圾)。所有的子代也会递归地被监测。所有能够由 root 访问的一切都不会被认为是垃圾。
  3. 所有不再被标志成活跃的内存块都被认为是垃圾。这个收集器现在就可以释放这些内存并将它们返还给操作系统。

四种常见的 JavaScript 泄露

1. 全局变量
一个未声明变量的引用会在全局对象内部产生一个新的变量。在浏览器的情况,这个全局变量就会是 window。

function foo(arg) {bar = "some text";}
等同于:function foo(arg) {window.bar = "some text";}

2. 被遗忘的计时器和回调
setInterval 在 JavaScript 中是经常被使用的。大多数提供观察者和其他模式的回调函数库都会在调用自己的实例变得无法访问之后对其任何引用也设置为不可访问。但是在 setInterval 的情况下,这样的代码很常见

var serverData = loadData();
setInterval(function() {var renderer = document.getElementById('renderer');
   if(renderer) {renderer.innerHTML = JSON.stringify(serverData);
   }
}, 5000); // 每 5000ms 执行一次

renderer 所代表的对象在未来可能被移除,让部分 interval 处理器中代码变得不再被需要。然而,这个处理器不能够被收集因为 interval 依然活跃的(这个 interval 需要被停止从而表面这种情况)。如果这个 interval 处理器不能够被收集,那么它的依赖也不能够被收集。这意味这存储大量数据的 severData 也不能够被收集。

3. 闭包
闭包的特性是内部函数能够访问外部函数的作用域。

var sayName = function(){
  var name = 'jozo';
  return function(){alert(name);
  }
};
var say = sayName(); 
say();

sayName 返回了一个匿名函数,该函数又引用了 sayName 的局部变量 name,sayName 调用后变量 name 应该被回收,但是由于 say 继续引用,导致无法回收。

小结:
1、JavaScript 基本数据类型:string,number,boolean,null,undefined,引用类型,包括 Object,Array,function,ES6 新增的 symbol。
2、判断数据类型的方法有 typeof,instanceof,constructor,Object.prototype.toString。
3、JavaScript 分为全局作用域和局部作用域,作用域链向上层层查找。
4、基本数据类型占据固定大小空间,因此存储在栈内存中,引用类型占据空间不确定,存储在堆内存中。复制基本数据类型会分配新地址,新旧互不影响,引用类型复制是复制指针,新旧变量会互相影响。
5、JavaScript 垃圾回收方法包括引用计数和标记清除。
6、JavaScript 常见的内存泄漏包括全局变量,循环引用,计时器,闭包。

退出移动版