《JavaScript高级程序设计》Chapter04 变量,作用域,内存

原始值&引用值

  • 原始值(primitive value):Undefined, Null, Boolean, Number, String, Symbol

    • 按值访问,直接操作存储在变量中的实际值
  • 引用值(reference value):Object

    • 按引用访问,操作的是对该对象的引用,而非直接操作对象本身

动态属性

  • 对引用值可以随时添加、修改和删除属性与方法;原始值不能添加属性,尽管这样做不会报错(只有引用值可以动态添加后面可以使用的属性)

  • 如果使用new关键字对原始类型进行初始化,则会创建一个Object类型实例,但行为与原始值是类似的

    let name1 = 'Leo';
    let name2 = new String('David');
    name1.age = 19;
    name2.age = 21;
    console.log(name1.age); // undefined
    console.log(name2.age); // 21
    console.log(typeof name1); // string
    console.log(typeof name2); // object
    

复制值

  • 原始值的浅拷贝(值的副本)和引用值的深拷贝(引用的拷贝)

参数传递

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

    • 对于引用值,传递的是堆内存上的对象的引用(即将引用拷贝给函数的参数),函数内部的改变也会影响到外部的对象

      function setName(obj){
          obj.name = 'Leo';
      }
      
      let person = {};
      setName(person);
      console.log(person.name); // Leo
      -------------------------------------
      function setName(obj){
          obj.name = 'Leo';
          obj = {}; // obj由指向堆内存上person的指针,变成了指向新建的本地对象的指针,函数退出后即被销毁
          obj.name = 'David'; // 对函数外的person对象没有影响
      }
      
      let person = {};
      setName(person);
      console.log(person.name); // Leo
      

执行上下文&作用域

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

    • 代码正在执行的上下文的变量对象始终位于作用域链的最前端。如果上下文是函数,则其活动对象( activation object)用作变量对象。活动对象最初只有一个定义变量: arguments。(全局上下文中没有这个变量。) 作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;全局上下文的变量对象始终是作用域链的最后一个变量对象。

    • 代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用城链的最前端开始,然后逐级往后,直到找到标识符。(如果没有找到标识符,那么通常会报错

    • 上下文之间线性、有序连接,每个上下文都可以到上级上下文中搜索变量与函数,但不能到下一级上下文中搜索

变量声明

  • var:变量会被自动添加到最近的上下文。若未声明就被初始化,则会被自动添加到全局上下文
    • 由于hoisting的存在,var声明的变量可以在声明前被使用(不推荐这样做)
  • let:块级作用域
    • 块内用let声明的变量,在块外不能被访问
    • 严格讲let也存在hoisting,但是由于“暂时性死区”,在声明前不能使用let变量
  • const: 块级作用域
    • const变量声明的同时必须初始化,且在生命周期内不能被再赋值
    • 对于声明为const的对象。不能被赋以其他的引用值,但是对象内部的键是可以改变的
      • 使用 Object.freeze({...}) 声明一个对象,则整个对象都不可以改变

垃圾回收

  • 通过自动内存管理实现内存分配和闲置资源回收:释放不会再被使用的变量占用的内存

标记清理

  • 变量进入上下文中时会被加上“存在于上下文中”的标记;变量离开上下文时,会被加上“离开上下文的标记”
    • 垃圾回收程序运行时,为内存中所有变量加标记,再将位于上下文中的变量和被上下文中的变量引用的变量的标记去掉,此时做内存清理:销毁带标记的所有值并收回他们占据的内存资源

引用计数

  • 对每个值都记录它被引用的次数,每被赋给一个变量就对其引用数+1,保存对该值引用的变量被其他值覆盖时就对其引用数-1;一个值引用数为0时说明该值已无法访问,即可安全的回收其内存资源
    • 问题:循环引用(对象A、B通过各自的属性相互引用时,引用数永远不会变成0,导致内存资源无法回收)

内存管理

  • 将内存占用量保持在较小水平可以让页面性能更好,应当保证在执行代码时只保存必要的数据

    • 如果数据不再必要,就将其置为null,从而释放引用(解除引用)(适用于全局变量与全局对象的属性),在下次垃圾回收时这些被解引用的资源将被回收

      function createPerson(name){
          let localPerson = new Object();
          localPerson.name = name;
          return localPerson;
      }
      
      let globalPerson = createPerson('Leo');
      globalPerson = null; // 解除全局变量globalPerson对值的引用,这份资源将被回收
      
  • 使用let与const声明提升性能:以块为作用域,可以让垃圾回收程序更早介入,更早的收回内存资源

  • 隐藏类与删除操作

    • JavaScript引擎会将创建的对象与隐藏类关联起来,跟踪它们的属性特征;能共享相同隐藏类的对象会带来潜在的性能提升

    • 如果两个类实例共享同一个构造函数与原型,这两个类实例就会共享相同的隐藏类

      function Article() {
          this.title = "Hello world";
      }
      
      let a1 = new Article();
      let a2 = new Article(); // 此时两个Article实例共享一个相同的隐藏类
      
      a2.author = "Leo"; // 此时两个Article实例对应两个不同的隐藏类
      
      • 应当尽量避免JavaScript “先创建再补充”的动态属性赋值,在构造函数中一次性声明所有属性
    • 使用delete关键字,会导致生成相同的隐藏类片段

      function Article() {
          this.title = "Hello world";
          this.author = "Leo";
      }
      
      let a1 = new Article();
      let a2 = new Article();
      
      // delete a1.author; // 此时两个Article实例对应两个不同的隐藏类
      
      a1.author = null; // 最佳实践
      
      • 使用delete动态删除属性与动态添加属性导致的后果相同
      • 应当将不想要的属性设置为null,既保持隐藏类不变与继续共享,同时也达到删除引用值供垃圾回收程序回收的效果

内存泄漏

  • 意外声明全局变量

    function setName() {
        name = "Leo";
    }
    
    • 此时的name属性被当做window的属性创建,window本身不被清理name就不会消失
    • 解决:在name前加上var,let,const即可
  • 定时器

    let name = "Leo";
    setInterval(() => {
        console.log(name);
    }, 100);
    
    • 定时器的回调引用了外部变量,只要定时器一直运行,回调引用的name就会一直占用内存,垃圾回收程序就不会清理此外部变量
  • 闭包

    let outer = function() {
        let name = "Leo";
        return function() {
            return name;
        };
    };
    
    • 代码执行后创建了一个内部闭包,只要返回的函数一直存在就不能清理name,因为闭包一直在引用它

静态分配&对象池

理论上,如果能合理使用分配的内存,同时避免多余的垃圾回收,那就可以保住因释放内存而损失的性能

  • 当一些对象更替速度非常快时,垃圾回收机制会被更加频繁的调用,造成不必要的开销,降低性能

  • 使用对象池:创建对象池管理一组可回收的对象,在需要时应用程序可以向对象池请求一个对象而不是再创建一个对象,使用完后再把这个对象归还给对象池,以供后续需要时拿出来重复使用

    • 此时,由于没有发生对象的初始化,垃圾回收探测就不会发现有对象的更替,垃圾回收机制就不会那么频繁的被调用
    • 并非所有对象都适合拿来“池化”――因为维护对象池也要造成一定开销。对生成时开销不大的对象进行池化,反而可能会出现“维护对象池的开销”大于“生成新对象的开销”,从而使性能降低的情况。但是对于生成时开销可观的对象,池化技术就是提高性能的有效策略了。
  • 如果对象池按需分配(对象不存在则创建新的,对象存在时复用存在的),则对象池有单调增长但为静态的内存——可选择数组进行维护

    • JavaScript数组大小可变,一旦申请空间已满且继续添加,就会删除已有数组而申请一个更大的数组,这个动态分配操作会引起垃圾回收机制的调用:应当在初始化时就创建好一个大小足够的数组,从而避免先删除再创建的操作
posted @   CodingSaltFish  阅读(32)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示