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

4.1 变量的原始值和引用值?

  • JavaScript 的变量是松散类型的,且变量不过就是特定时间点一个特定值的名称而已。ECMAScript变量可以包含两种类型的数据:原始值(primitive value),就是最简单的数据;引用值(reference value)则是由多个值构成的对象。

  • 原始值包含6种数据类型:Undefined、Null、Number、String、Boolean、Symbol。保存原始值的变量是按值访问的,操作的就是存储在变量中的实际值。

  • 引用值是保存在内存中的对象。JavaScript不允许直接访问内存位置,因此不能直接操作对象所在的内存空间。操作对象时实际上操作的是该对象的引用而非实际的对象本身。为此,保存引用值的变量是按引用访问的。

  • 原始值不能有属性,只有引用值(对象)可以动态添加属性。

  • 原始值复制时,原始值会被复制到新变量的位置,且两个变量可以独立使用。引用值复制时,存储在变量中的值也会被复制到新变量所在的位置,区别在于,这里存储在变量中的值实际上是一个指针,复制的也是指针,指向存储在内存中的对象,复制完成后,两个变量指向的是同一个对象,因此一个对象上的变化会在另一个对象上反映出来。

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

    function setName(obj){
        obj.name = "Nicholas";
        obj = new Object();//此处不能加let,因为obj在函数内已经是定义好的局部变量
        obj.name = "Tom";
    }
    
    let myObj = new Object();
    setName(myObj);
    console.log(myObj.name);//Nicholas,对象进入函数,被修改属性,出函数后对象保留该属性
    
  • typeof 操作符可以确定原始值(除了null)的数据类型,typeof null会返回 object。但由于对象的构造函数不同,如何确定对象的构造函数是什么呢?即确定对象是哪种类型的对象。这时需要用到instanceof操作符。

    let person = new String();
    
    console.log(person instanceof Object);//true
    console.log(person instanceof Number);//false;
    console.log(person instanceof String);//true
    

4.2 执行上下文和作用域

  • 执行上下文就是当前 JavaScript 代码被解析和执行时所在环境,每个上下文都有一个关联的变量对象(variable object),在这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台处理数据会用到它。上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数。

  • 全局上下文是最外层的上下文。在浏览器中,全局上下文就是我们常说的window 对象,因此所有通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法。

  • 每个函数都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上,在函数执行完之后,上下文栈会弹出该函数上下文,将控制器返还给之前的执行上下文。

  • 上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。每个上下文都可以到上一级上下文中去搜索变量和函数,但任何上下文都不能到下一级上下文中去搜索。

  • 重复的var声明会被忽略,重复的let声明会抛出 SyntaxError。let声明非常适合在循环中声明迭代变量,因为 var 声明的迭代变量会泄漏到循环外部。const 声明的变量必须同时初始化为某个值,一经声明,在其生命周期内的任何时候都不能再重新赋予新值。但用const声明的对象可以给其添加属性,如果想对 const 声明的对象不能修改,可以用:

    const o = Object.freeze({});
    o.name = 'Jake';
    console.log(o.name);//undefined
    
  • 标识符查找:

    var color = 'blue';
    function getColor(){
        let color = 'red';
        let color1 = 'black';
        {
            let color = 'green';
            return color;
        }
    }
    
    console.log(getColor());//'green',如果是return color1,则返回'black'
    

4.3 垃圾回收

JavaScript 是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。JavaScript通过自动内存管理实现内存分配和闲置资源回收。基本思路:确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收每隔一段时间(或者说某个预定时间)就会自动运行。垃圾回收程序必须跟踪记录哪个变量还会使用,哪个变量不会再使用,以便回收内存。如何标记未使用的变量也许会有不同的实现方式,不过在浏览器的发展史上用到过两种主要的标记策略:标记清理和引用计数。

  • 标记清理(较常用):当变量进入上下文,这个变量会被加上“存在于上下文中”的标记,当变量离开上下文时,也会被加上“离开上下文”的标记。垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后它会将所有在上下文中的变量,以及被上下中的变量引用的其他变量的标记都去掉。在此之后还存在标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不了它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。

  • 引用计数(不常用):对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为1,如果保存该值引用的变量被其他值覆盖了,那么引用数减1。当一个值的引用数为0时,就说明没办法再访问这个值了,因此可以安全地收回其内存了。

  • 内存管理:由于分配给浏览器的内存通常比分配给桌面软件的内存要少的多,分配给移动浏览器的就更少了,这更多出于安全考虑,即为了避免运行大量的JavaScript的网页耗尽了系统内存而导致操作系统崩溃。将内存占有量保持在一个较小的值可以让页面性能更好。优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据,将不必要的数据设置为null,从而释放其内存,也叫做解除引用

    function creatPerson(name){
        let localPerson = new Object();
        localPerson.name = name;
        return localPerson;
    }
    
    let globalPerson = creatPerson("Nicholas");
    globalPerson = null;//解除引用
    
    • 通过const 和 let 声明提升性能。

    • 隐藏类和删除操作。如果代码要求非常注重性能,这点非常重要。

      //隐藏类
      function article(){
          this.title = 'just test';
      }
      let a1 = new article();
      let a2 = new article();
      a2.author = 'Jake'; //此时两个article实例对应了两个不同的隐藏类,因此需要避免这种“先创建再补充”(ready-fire-aim)式的动态属性赋值,并在构造函数中一次性声明所有属性。
      
      function article(opt_author){
          this.title = 'just test';
          this.author = opt_author;
      }
      let a1 = new article();
      let a2 = new article('Jake');
      
      
      //避免动态删除属性操作
      function article(){
          this.title = 'just test';
          this.author = 'Jake';
      }
      let a1 = new article();
      let a2 = new article();
      
      delete a1.author;//动态删除属性操作和动态添加属性的操作后果一样,也需要避免
      
      function article(){
          this.title = 'just test';
          this.author = 'Jake';
      }
      let a1 = new article();
      let a2 = new article();
      
      a1.author = null;//用解除引用
      
  • 避免内存泄漏(意外声明全局变量、定时器、JavaScript闭包)

  • 静态分配与对象池。如果某个函数频繁调用,那么其内部的局部对象频繁地赋值释放,这就会使垃圾回收调度程序不停地来安排垃圾回收。为了避免这种情况,可以在初始化的某一时刻,创建一个对象池,用于管理一组可回收的对象。函数可以向这个对象池请求一个对象、设置其属性、使用它,然后操作完成后再把它还给对象池。由于没有在函数内部初始化对象,垃圾回收探测就不会发现有对象更替,因此垃圾回收程序就不会那么频繁地运行。

posted @ 2021-09-01 16:31  unuliha  阅读(54)  评论(0)    收藏  举报