变量、作用域、内存

1. 基本类型和引用类型

如前所述,JS 基本类型有6种:Undefined , Null, Bollean, Number, String, Symbol

引用类型有1种: Object

引用类型存储在内存中,由于 JS 不允许直接访问内存,所以对象的操作实际上都是通过操作地址来完成的。

1.1 动态属性

引用类型可以赋予新属性:

let person = new Object();
person.name = "Nicholas";
console.log(person.name);  // "Nicholas"

基本类型不能赋予新属性,但赋予新属性也不报错。

let name = "Nicholas";
name.age = 27;
console.log(name.age);  // undefined

同样是 String 类型,使用 new 关键字可以使其变成一个对象。

let name1 = "Nicholas";
let name2 = new String("Matt");
name1.age = 27;
name2.age = 26;
console.log(name1.age);  // undefined
console.log(name2.age);  // 26
console.log(typeof name1); // string
console.log(typeof name2); // object

1.2 函数传参

函数中传的参数都是值,函数外的参数,都会赋值一份传到函数内来。

引用类型传参:

var person = {name:'Mike'};
function foo(obj) {
  obj.name = 'Matt';
}
foo(person);
console.log(person.name); // Matt

常见问题:

function setName(obj) {  
    obj.name = "Nicholas";
    obj = new Object();
    obj.name = "Greg";
}
let person = new Object();
setName(person);
console.log(person.name);  // "Nicholas"

1.3 instanceof

判断对象是否是某个引用类型。

console.log(person instanceof Object);   // is the variable person an Object?
console.log(colors instanceof Array);    // is the variable colors an Array?
console.log(pattern instanceof RegExp);  // is the variable pattern a RegExp?

2. 执行上下文和作用域

本节部分内容参考自彻底理解作用域链.

执行上下文定义了变量或函数可以访问的数据,以及其行为。所有的执行上下文都有用一个相关的对象,这个对象上储存了所有的变量及函数。

执行上下文保存着函数执行所需要的重要信息,其中有三个属性:变量对象(variable object),作用域链(scope chain),this 指针(this value),它们影响着变量的解析、变量作用域、函数的this指向

2.1 执行上下文 — 变量对象

每次执行一个函数前,都会创建一个上下文对象。这个上下文对象有一个重要属性:变量对象。变量对象的创建过程如下:

  1. arguments 对象放入变量对象;
  2. 要执行的函数内的所有函数放入变量对象;
  3. 要执行的函数内的所有变量放入变量对象。
函数执行时,变量对象就被称作活动对象(activation object)了

变量对象使得 JS 有变量提升的特性:

  • 在函数执行前,JS 引擎会先扫一遍代码,将 arguments 、变量和函数都放入变量对象;此时变量对象中所有的变量为 undefined , 所有的函数都有一个地址。
  • 在函数执行时,遇到活动对象中相应的属性,就会直接从活动对象取出使用,而不用等该属性赋值(因为没赋值,所以是 undefined )。
function fun1(arg) {
    //执行前创建变量对象:{arg:666, fun2:fun2的地址, a:undefined}
    console.log(a); // undefined, 虽然还没碰到a,但活动对象中已经有a了(变量提升)
    var a = 123; // 活动对象中的 a 变为 123
    console.log(a); // 123
    fun2(); //  Hello
    return; // 即使是在return之后的声明,也会被放入变量对象!
    function fun2() {
        console.log('Hello');
    }
}
fun1(666);// undefined  123  Hello

在浏览器中,全局的变量对象就是 window 对象

2.2 执行上下文 — 作用域链

作用域链就是变量对象的数组。作用域链的第一个是当前函数的活动对象,第二个是当前函数父级上下文的活动对象,第三个是当前函数爷爷级上下文的活动对象……最后一个是全局上下文活动对象。当 js 执行过程中解析一个变量名时,会沿着当前执行函数的作用域链查找,如果在某个活动对象中找到了它,则使用它,找不到则报错。

例如:

var global_var = -1;
function outter() {
    var outter_var = 111;
    console.log(`globle_var = ${global_var}, outer_var = ${outter_var}`);
    
    function inner() {
        var inner_var = 222;
        console.log(`global_var = ${global_var}, outer_var = ${outter_var}, inner_var = ${inner_var}`)
    }
}
outer();
// in outter, global_var = -1, outter_var = 111
// in inner, global_var = -1, outter_var = 111, inner_var = 222

用上述代码解释作用域链:

  1. 开始执行 outter 时,作用域链为:[ outter 的变量对象,全局的变量对象]。
  • outter作用域中找不到 globle_var 变量,于是沿着作用域链去父级变量对象中找。
  • 在父级变量对象中找到了 globle_var,能正确输出。
  1. 开始执行 inner 时,作用域链为: [ inner 的变量对象, outter 的变量对象, 全局变量对象]

    执行过程和上面类似。

用作用域链可以更好地理解闭包的思想。

3. 垃圾回收

JS 具有自动垃圾回收机制,会定期对不再使用的变量、对象所占的内存进行释放。局部变量一般在函数执行完成后会被回收,但局部变量在函数执行完仍被使用时,局部变量不会被回收。

function fun1() {
    const obj = {};
}

function fun2() {
    const obj2 = {};
    return obj2;
}
const a = fun1();
const b = fun2();

上述代码中,obj1 对象在函数执行完后被回收,obj2由于函数执行完后仍在使用(b 指向了 obj2 的地址),则不会被回收

JS 有两种垃圾回收策略:标记清除引用计数

3.1 标记清除

本节部分内容参考自javascript 垃圾回收机制.

当变量进入执行上下文时被标记为“进入环境”(in-context),当变量离开执行上下文时被标记为“离开环境”(out-of-context);前者不能被回收,后者可以被回收。

function fun() {
    const a = 1; // a 被标记为 进入环境
}
fun(); // 函数执行完毕 a 被标记为 离开环境

3.2 引用计数

统计引用类型变量声明后被引用的次数,当次数为 0 时,该变量将被回收。

function func4 () {
      const c = {}; // 引用类型变量 c的引用计数为 0
      let d = c; // c 被 d 引用 c的引用计数为 1
      let e = c; // c 被 e 引用 c的引用计数为 2
      d = {}; // d 不再引用c c的引用计数减为 1
      e = null; // e 不再引用 c c的引用计数减为 0 将被回收
}

但是引用计数的方式,有一个相对明显的缺点——循环引用

function func5 () {
      let f = {};
      let g = {};
      f.prop = g;
      g.prop = f;
      // 由于 f 和 g 互相引用,计数永远不可能为 0
}

像上面这种情况就需要手动将变量的内存释放

f.prop = null;
g.prop = null;

3.3 管理内存

内存占用越小,页面性能越高。当变量不再使用时,最好将其赋值为 null .

function createPerson(name) {  
    let localPerson = new Object();  
    localPerson.name = name;  
    return localPerson;
}
let globalPerson = createPerson("Nicholas");// do something with globalPersonglobalPerson = null;

注意:将不再使用的变量赋值为 null 并不能自动回收与其关联的内存,这种做法的目的是确保下次发生垃圾回收时,将其移出上下文

使用 const 和 let 能够提高性能。因为二者是块级作用域,当块级作用域执行完毕后,会提醒GC回收垃圾。

3.4 内存泄漏

  • 不必要的的引用会导致内存泄漏
function setName() {
    name = 'Sara'
}

上述代码产生了一个全局变量name ,当函数执行完后,name仍占用内存,这就导致了内存泄漏。所以函数内的变量一定要加上varletcost

  • 定时器会导致内存泄漏
let name = 'Jake';
setInterval(() => {  
    console.log(name);
}, 100);

在定时器执行过程中,GC无法清除正在使用的外部变量的内存。

  • 闭包导致内存泄漏
let outer = function() {  
    let name = 'Jake';  
    return function() {    
        return name;  
    };
};

outer 函数执行完后,name的内存不会清空。


芒果香蕉
4 声望1 粉丝