第4章 变量、作用域与内存

基本类型与引用类型

存储位置不同:基本类型(存储在栈中的实际值)引用类型(保存在内存中的对象)

复制值:

  • 把一个原始值赋值给另一个变量时,原始值会被复制到新变量的位置。

    let num1 = 5;
    let num2 = num1;
    
  • 把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置(但是这里复制的值是一个指针,它指向存储在堆内存中的对象)

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

传递参数:

ES中的所有函数的参数都是按值传递的

按值传递的特点:单向传递,只能将实参的数值传递给形参,不能将形参的值传递给实参。内置基本类型作为实参时,不能通过形参改变实参的数值,引用类型作为实参时,可以通过形参改变实参所指向空间的值。

// 使用swap函数为例
let a = 123;
let b = 234;
function swap(x,y) {
    let t;
    t = x;
    x = y;
    y = t;
}
swap(a,b);
console.log(a,b); // 123 234
// 上述是因为形参x和y与实参a和b在内存中是不同的空间,因为交换x和y不能影响a和b

let arr = [123,234];
function swap(arr1) {
    let t;
    t = arr[0];
    arr[0] = arr[1];
    arr[1] = t;
}
swap(arr);
console.log(arr[0],arr[1]); // 234 123
// 上述是因为引用类型在内存中是由两块空间构成的,参数传递的时候实参将引用类型真正保存的堆的位置传递给了形参,因此尽管实参与形参是两块不同的空间,但是它们指向的都是引用类型真实保存的地址,修改形参就会引起实参的变化

确定类型:

  • typeof判断基本数据类型,但是不能判断null(返回object)

  • instanceof操作符:

    语法:result = variable instanceof constructor

    实例:console.log( person instanceof Object/Array/RegExp );

    按照定义,所有引用值都是Object的实例,使用instanceof检测任何引用值和Object都会返回true

执行上下文与作用域

执行上下文Execution context stack,ECS:是一个评估和执行js代码的环境的抽象概念,通俗的说,每当js代码在运行的时候,它都是在执行上下文中运行。变量或者函数的上下文决定了它们可以访问哪些数据,以及它们的行为。上下文在其所有代码都执行完毕之后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或者退出浏览器)。每个上下文都有一个关联的变量对象(variable object),这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台处理数据会用到它。

上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。

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

以上代码涉及 3 个上下文:全局上下文、changeColor()的局部上下文和 swapColors()的局部 上下文。全局上下文中有一个变量 color 和一个函数 changeColor()。changeColor()的局部上下文中 有一个变量 anotherColor 和一个函数 swapColors(),但在这里可以访问全局上下文中的变量 color。 swapColors()的局部上下文中有一个变量 tempColor,只能在这个上下文中访问到。全局上下文和 changeColor()的局部上下文都无法访问到 tempColor。而在 swapColors()中则可以访问另外两个 上下文中的变量,因为它们都是父上下文。(也就是说内部上下文可以通过作用域链访问外部上下文中的一切,但是外部上下文不能访问内部上下文中的任何东西,上下文之间的连接是线性的、有序的)

作用域链:window(color、changeColor)=> changeColor(anotherColor、swapColors) => swapColors(tempColor)

  • 全局执行上下文

    默认或者基础上下文,任何不在函数内部的代码都在全局上下文中,它会执行两件事:1、创建一个全局的window对象,并且设置this的值等于这个全局对象。一个程序中只会有一个全局执行上下文。

  • 函数执行上下文

    每当一个函数被调用时,都会为该函数创建一个新的执行上下文。函数上下文可以有任意多个。

  • Eval函数执行上下文

    执行在eval函数内部的代码有属于它自己的执行上下文

执行上下文的生命周期:
  • 创建阶段

    1. this值的绑定

      在全局执行上下文中,this的值指向全局对象(window对象),在函数执行上下文中,this的值取决于该函数是如何被调用的,如果是引用对象调用,this指向该对象,否则this指向全局对象或者undefined(严格模式下)

    2. 创建词法环境组件

    3. 创建变量环境组件

  • 执行阶段

    执行变量赋值、代码执行

  • 回收阶段

    执行上下文出栈等待虚拟机回收执行上下文

执行上下文栈:也称作作用域

也叫调用栈、执行栈,它是一种拥有先进后出数据结构的栈,用来存储代码运行时创建的所有执行上下文。

当js第一次遇到我们写的脚本时,它会创建一个全局的执行上下文并且压入当前调用栈,每当引擎遇到一个函数调用,它会为该函数创建一个新的函数执行上下文并压入栈的顶部。引擎执行栈顶的函数,执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

let a = 'Hello World!';

function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}

function second() {
  console.log('Inside second function');
}

first();
console.log('Inside Global Execution Context');

上述过程加载时,js创建了一个全局执行上下文并把它压入当前执行栈,当遇到first函数调用时,js为该函数创建了一个函数执行上下文并把它压入执行栈,当遇到first函数内部调用second函数时,js引擎为second创建了新的函数执行上下文并入栈,second执行完毕,弹出,first执行完毕弹出,一旦所有代码执行完毕,js从当前栈中移除全局执行上下文。

词法环境:

由环境记录器(存储变量和函数声明的实际位置)和一个可能的引用外部词法环境(意味着它可以访问其父级的词法环境)的空值组成。

  • 全局环境

    外部环境引用为null,环境记录器内有原型函数,用户定义的全局变量,this的值指向全局对象

  • 函数环境

    外部环境可能是全局环境或者任何包含此内部函数的外部函数,环境记录器有函数内部用户定义的变量

变量环境:

它同样是一个词法环境,环境记录器拥有变量声明语句在上行上下文中创建的绑定关系。ES6提出了变量环境,它只用来存储var变量绑定,词法环境被用来存储函数声明和变量(let 和const)绑定。

作用域链增强:

下述两种情况,都会在作用域链前端添加一个变量对象。

  • try/catch语句的catch块

​ 会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。

  • with语句

​ 会向作用域链前端添加指定的对象

function buildUrl() { 
 let qs = "?debug=true"; 
 // with语句将location对象作为上下文,因此location会被添加到作用域链前端
 // with语句中的代码引用变量href时,实际上引用的是location.href,也就是自己变量对象的属性,在引用qs时,引用的则是定义在buildUrl()中的那个变量,它定义在函数上下文的变量对象上。
 with(location){ 
 let url = href + qs; 
 } 
 // 在with语句中使用var声明的变量url会成为函数上下文的一部分,可以作为函数的值被返回,但是let声明的url,因为被限制在块级作用域中,所以在with块之外没有定义
 return url; 
} 
变量声明:
  • 使用var的函数作用域声明

var声明会被拿到函数或者全局作用域的顶部,位于作用域中所有代码之前,这个现象叫做“提升”hoisting。

  • 使用let的块级作用域声明

    块级作用域由最近的一对包含花括号{}界定。换句话说,if 块、while 块、function 块,甚至连单独的块也是 let 声明变量的作用域。

    let在同一作用域内不能声明两次,重复的var声明会被忽略,但是重复的let声明会抛出SyntaxError

    { 
     let d; 
    } 
    console.log(d); // ReferenceError: d 没有定义
    

    let 的行为非常适合在循环中声明迭代变量。使用 var 声明的迭代变量会泄漏到循环外部,这种情况应该避免。

    for (var i = 0; i < 10; ++i) &#123;&#125; 
    console.log(i); // 10 
    for (let j = 0; j < 10; ++j) &#123;&#125; 
    console.log(j); // ReferenceError: j 没有定义
    
  • 使用const的常量声明

    使用 const 声明的变量必须同时初始化为某个值。 一经声明,在其生命周期的任何时候都不能再重新赋予新值。除了上述规则,其他方面与let声明一致

    const 声明只应用到顶级原语或者对象。换句话说,赋值为对象的 const 变量不能再被重新赋值 为其他引用值,但对象的键则不受限制。

    const o1 = &#123;&#125;; 
    o1 = &#123;&#125;; // TypeError: 给常量赋值
    const o2 = &#123;&#125;; 
    o2.name = 'Jake'; 
    console.log(o2.name); // 'Jake' 
    // 如果想要整个对象都不能修改,可以使用Object.freeze(),这样再给属性赋值时虽然不会报错,但会静默失败。
    const o3 = Object.freeze(&#123;&#125;); 
    o3.name = 'Jake'; 
    console.log(o3.name); // undefined 
    
  • 标识符查找

    当在特定上下文中读取或者写入而引用一个标识符时,必须通过搜索确定这个标识符表示什么。搜索开始于作用域链前端,以给定的名称搜索对应的标识符。如果在局部上下文中找到该标识符,则搜索 停止,变量确定;如果没有找到变量名,则继续沿作用域链搜索。(注意,作用域链中的对象也有一个 原型链,因此搜索可能涉及每个对象的原型链。)这个过程一直持续到搜索至全局上下文的变量对象。 如果仍然没有找到标识符,则说明其未声明。

    var color = 'blue'; 
    function getColor() &#123; 
     let color = 'red'; 
     // 块级作用域
     &#123; 
     let color = 'green'; 
     // 此时的color能够从块级作用域中找到,就停止查找,否则他还会搜索函数作用域、全局作用域
     return color; 
     &#125; 
    &#125; 
    console.log(getColor()); // 'green'
    

    标识符查找并非没有代价。访问局部变量比访问全局变量要快,因为不用切换作用 域。

垃圾回收

垃圾回收算法:就是垃圾收集器按照固定的时间间隔,周期性地寻找那些不再使用的变量,然后将其清除或者释放内存。垃圾回收算法是个不完美的算法,因为某块内存是否可用,属于不可预判的问题,也就意味着单纯依靠算法是解决不了的。主要有两种主要的标记策略:标记清理和引用计数。

  • 标记清理 mark and sweep

    常用,总共分为标记阶段和清除阶段。首先遍历堆内存上所有的对象,分别给它们打上标记,然后在代码执行过程结束之后,对所使用过的变量取消标记。在清除阶段把具有标记的内存对象进行整体清除,从而释放内存空间。

    优点:简单

    缺点:通过标记清除之后,剩余对象内存位置不变,空闲内存空间是不连续的,造成 内存碎片问题。内存碎片多了之后,要存储一个新的需要占据较大内存空间的对象会有影响。

    改进:标记整理mark-compact算法,在标记结束之后,会将不需要清理的对象向内存的一端进行移动,最后清理掉边界的内存。

  • 引用计数

    不常用,思路:对每个值都记录它的引用次数。

    1. 声明变量并给它赋一个引用值时,这个值的引用数为 1

    2. 如果同一个值又被赋给另一个变 量,那么引用数加 1。

    3. 如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。

    4. 当一 个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。

      let a = new Object() 	// 此对象的引用计数为 1(a引用)
      let b = a 		// 此对象的引用计数是 2(a,b引用)
      a = null  		// 此对象的引用计数为 1(b引用)
      b = null 	 	// 此对象的引用计数为 0(无引用)
      ...			// GC 回收此对象
      

      问题:循环引用

      function problem() &#123; 
       let objectA = new Object(); 
       let objectB = new Object(); 
       objectA.someOtherObject = objectB; 
       objectB.anotherObject = objectA; 
      &#125;
      

      对象 A 有一个指针指向对象 B,而对象 B 也引用了对象 A。objectA 和 objectB 通过各自的属性相互引用,意味着它们的引用数都是 2。在标记清理策略下,这不是问题,因为在函数结束后,这两个对象都不在作用域中。而在引用计数策略下,objectA 和 objectB 在函数结束后还会存在 (为啥???),因为它们的引用数永远不会变成 0。

内存管理

1、通过const和let声明提升性能

2、隐藏类和删除操作

3、内存泄漏

4、静态分配与对象池

​ 减少浏览器执行垃圾回收的次数(间接控制触发垃圾回收的条件),合理使用分配的内存,同时避免多余的垃圾回收,那就可以保住因释放内存而损失的性能。

小结
  • js变量的两种类型的值:原始值(基本数据类型)、引用值(引用数据类型)

  • 作用域(执行上下文)

  • js的垃圾回收机制

    离开作用域的值会被自动标记为可回收,然后在垃圾回收期间被删除

    主流的垃圾回收算法是标记清理,即先给当前不使用的值加上标记,再回来回收它们的内存

    引用计数是另一种垃圾回收策略,需要记录值被引用了多少次。JavaScript 引擎不再使用这种算 法,但某些旧版本的 IE 仍然会受这种算法的影响,原因是 JavaScript 会访问非原生 JavaScript 对 象(如 DOM 元素)

    引用计数在代码中存在循环引用时会出现问题。

    解除变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助。为促进内存回收,全局对 象、全局对象的属性和循环引用都应该在不需要时解除引用。