V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
James369
V2EX  ›  程序员

js 的类有没有析构函数,如何进行资源释放??

  •  1
     
  •   James369 · 50 天前 · 4337 次点击
    这是一个创建于 50 天前的主题,其中的信息可能已经有所发展或是发生改变。
    查了半天没看到 js 类有析构函数,那么类对象在销毁的时候,如何把申请的资源释放呢,否则可能内存泄漏。

    比如,考虑以下场景,有一些学生类 Student,以及一个图书馆类 Library 。
    1. 开始创建了若干学生 Student 对象,然后做了一些操作,向 Library 借了几本书。
    2. 过了一会,有些学生对象做了一些其它操作(比如上课、睡觉,但就是没有还书),然后自动释放了(离开了变量作用域,生存周期结束了)。
    3. 此时虽然说 Student 自动释放了,但是还书操作没有显式调用,造成书籍未还。

    所以,如果 Student 类有析构函数,我就可以在析构函数中进行还书等的资源释放。正规语言都有析构函数,现在怎么处理,?
    第 1 条附言  ·  50 天前
    补充一下,严谨来讲不是内存泄漏,应该是资源释放。
    第 2 条附言  ·  50 天前
    再补充一下,不是正规语言有析构函数,是面向对象的非脚本语言。
    第 3 条附言  ·  50 天前
    也不仅仅说业务逻辑,资源释放,比如:打开的文件句柄、网络连接、加的锁、等等。
    78 条回复    2021-09-05 20:48:14 +08:00
    learningman
        1
    learningman   50 天前 via Android
    这类语言都会自己 GC 的,放心吧。引用计数之类的
    James369
        2
    James369   50 天前
    @learningman 自己会释放没错,但是它操作的其它资源不会自动进行反操作(这不是内存引用,这是业务操作)。
    我再举个例子,比如 Student 在 Canvas 上画了一个圆,但是 Student 释放的时候,它不会做擦除圆的操作。
    cxe2v
        3
    cxe2v   50 天前
    把对应的资源变量置空,等引擎 GC 的时候自己释放
    yxwzaxns
        4
    yxwzaxns   50 天前
    v8 会去做这些事情,讲道理,一个成熟的语言应该不用让使用者去关心这种问题,他应该会自己学会 gc...吧
    g1f9
        5
    g1f9   50 天前
    什么叫正规语言🤔
    NewConn
        6
    NewConn   50 天前   ❤️ 1
    不知道楼上是没看懂楼主问题,还是不知道析构函数是什么
    楼主显然问的是一个业务问题,不是 js 或者 v8 的 GC 问题

    楼主问的是,对象销毁时,业务上需要做一个业务操作;类推到构造函数就是,对象创建时,业务上做一些业务操作
    James369
        8
    James369   50 天前
    #6 是的,很多人还是没明白我的意思,唉
    gzf6
        9
    gzf6   50 天前
    试试 WeakMap
    ahhui
        10
    ahhui   50 天前
    有的,参考这里
    https://stackoverflow.com/questions/22566667/javascript-destructor-or-something-like-that
    引用:
    this.destroy = function(){
    this.stop();
    }
    misdake
        11
    misdake   50 天前
    正常不应该依赖析构函数来做这些事情吧。
    借书的时候 library 应该要记录书借给谁了,留着 student 的引用,student 不会被自动释放。
    darknoll
        12
    darknoll   50 天前
    是 gc 语言
    pinkSlime
        13
    pinkSlime   50 天前
    好家伙,javascript 一下被归类为不正经,不,不正规的语言了
    私以为 构析函数仅仅与内存相关,关闭句柄啊之类的。“书”这个对象自有 GC 来释放,而“还书”这明明是个逻辑操作,应该显式调用更好吧。
    darknoll
        14
    darknoll   50 天前
    现在除了 c/c++,基本上都是 gc 语言
    des
        15
    des   50 天前 via iPhone   ❤️ 4
    业务层面的东西就该放在业务层面考虑
    “ 有些学生对象做了一些其它操作,然后自动释放了”
    你既然知道什么时候释放,就该手动归还啊。
    microchang
        16
    microchang   50 天前
    是类似于 react 里面的 componentwillunmount 吧?这个不知道原生怎么写,不过应该可以在业务逻辑上绕开。或者看楼上的那个
    Vegetable
        17
    Vegetable   50 天前
    正常涉及到业务的资源应该手动释放,而不是依赖对象的生命周期吧。destructor 这种功能应该很少用到才对,本来有 GC 机制的编程语言,普遍没有析构函数一说。这和业务应该是两个层面的东西。
    pkoukk
        18
    pkoukk   50 天前
    析构函数是用来干这个的么?
    机器断电重启,或者进程被 kill 掉怎么办啊,你这些没还书的不还是没还书嘛...
    业务逻辑就业务处理..别依赖语言机制
    wszgrcy
        19
    wszgrcy   50 天前
    要不然考虑静态依赖注入?在静态依赖注入中销毁的类会自动调用销毁函数(前提是你要定义)
    IsaacYoung
        20
    IsaacYoung   50 天前
    业务资源的释放 需要 业务代码处理
    janus77
        21
    janus77   50 天前
    业务问题就用业务办法,自己手动释放。你说的“离开作用域会自己释放”某种程度上就是不对的一个观念。
    kop1989
        22
    kop1989   50 天前   ❤️ 8
    第一段说“内存泄漏”。
    第二段转头就说“书未还”(业务问题)了。

    楼主真的想明白你到底要干什么了么。

    1 、析构函数你用来处理业务?
    2 、有回收机制的语言怎么会有狭义的、原教旨主义的析构函数?
    3 、建议你去了解一下带有 GC 机制语言的“销毁事件”的特点与注意事项。
    Chingim
        23
    Chingim   50 天前
    析构函数式用来处理系统资源的, 不是用来处理业务逻辑的

    这个例子, library 应该保留 student 的引用, 不要让他们跑了
    aguesuka
        24
    aguesuka   50 天前
    这是语言糟粕, 连 Java 的析构函数 finalize 从 9 也开始过时了.
    runze
        25
    runze   50 天前   ❤️ 1
    ss99604
        26
    ss99604   50 天前
    WeakRef 、FinalizationRegistry
    agagega
        27
    agagega   50 天前 via iPhone   ❤️ 1
    楼上真有人觉得析构函数这个 C++/Rust/Swift 里重中之重的概念是没有意义的糟粕吗.. RAII 不是管理内存这么简单。
    hjahgdthab750
        28
    hjahgdthab750   50 天前
    我原来一直以为析构=untuple
    jadehare
        29
    jadehare   50 天前
    你可以上课,睡觉的时候让他们还书,或者删除引用的时候直接让 library 去找每个学生要书
    shawckzh
        30
    shawckzh   50 天前   ❤️ 1
    GC 语言不应该 RAII,不然 go 为啥会有 defer

    LZ 要是觉得显式调用麻烦,建议自己写个简单的 defer,就是函数退出时隐式调用呗,wrap 一下就行
    qq73666
        31
    qq73666   50 天前
    @darknoll oc,swift 不服
    cyberscorpio
        32
    cyberscorpio   50 天前
    不用的时候将其置为 null 即可
    runze
        33
    runze   50 天前
    @agagega #27 但这是 JS 呀!
    一般都是对标 Py 、PHP,跟 Java 、Go 对比已经算是离谱了,为什么要跟 C++、Rust 对比?
    yannxia
        34
    yannxia   50 天前
    @agagega 对于带 GC 的语言,业务逻辑的释放,是应该在自己的代码设计里面,至于 C++ 里面也不太建议析构逻辑上的东西。
    推荐都是 close hook
    chenmobuys
        35
    chenmobuys   50 天前
    @runze 实际上 PHP 也有析构函数
    chairuosen
        36
    chairuosen   50 天前
    系统内置的没有,如果硬要用,只能把对象用 defineProperty 或者 Proxy 存在另一个对象上,然后监听置空时主动调一下旧数据的 destroy 方法
    Pythoner666666
        37
    Pythoner666666   50 天前
    我都写 JS 了 还要我手动还回收内存?
    avastms
        38
    avastms   50 天前   ❤️ 1
    我现在的项目里就包含这种代码,
    目前在 js 环境里需要用到 WeakRef 和相关的 FinalizationRegistry,详情

    https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry
    jrtzxh020
        39
    jrtzxh020   50 天前
    感觉是你的业务逻辑设计有问题
    ysc3839
        40
    ysc3839   50 天前
    有 GC 的语言不能用 RAII,比如 Python, Java, JavaScript 都不可以用 RAII,因为离开作用域后对象不一定立即释放,析构函数不能及时执行,可能会出现预料之外的情况。
    qrobot
        41
    qrobot   50 天前
    @yxwzaxns #4L 我想知道 rust 算成熟的语言嘛
    akira
        42
    akira   50 天前
    现实社会也不会这么魔幻的啊, 我借书卡丢了 ,书也不会自动 回退到图书馆 去的呀
    7gugu
        43
    7gugu   50 天前
    楼主应该是想要类似 React 的 componentWillUnmount 的生命周期函数吧,FinalizationRegistry 可能是你想要的东西?不过业务层的东西,在业务层上处理会不会更好🤔?
    James369
        44
    James369   50 天前
    @chairuosen #36 我 js 比较菜,具体怎么做,愿闻其详
    qrobot
        45
    qrobot   50 天前
    @James369 楼主首先你理解错误了。 或则说你不应该在 Student 释放的时候去做擦除圆的操作。

    例如, 我在 Student 释放的同时,去创建 Student 。 那么这个对象将永远无法真正的释放。 例如在释放的时候进行执行大量的逻辑。 显然这是非常影响 gc 的性能行为。

    不过你可以在要销毁的时候。 执行其他情况, 你不应该依赖 gc


    例如我记得 java 的 gc, 只是做一个垃圾标识, 至于什么时候进行清理,要看 gc 的心情。

    例如在 JS 中, 你应该这样

    ```js
    let student = new Student()

    // 标记需要回收
    delete student;

    // free 是你自己自定义的方法,用来编写逻辑,释放你的图片信息,以及擦除圆的操作。
    free(student)
    ```
    renmu123
        46
    renmu123   50 天前 via Android
    js 既非面向对象,也是脚本语言,所以没有析构函数很正常
    qrobot
        47
    qrobot   50 天前
    @yxwzaxns 回答 4L

    所有的语言,无论高低都需要关心内存问题

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

    有一些语言是需要开发者手动明确进行处理的,例如 rust c/c++ , 而有一些是隐含帮你处理例如 Java Go 等等。 但是无论那种你都需要关注内存分配,以及销毁的问题。

    例如在 Java 中 常见的内存泄漏


    ```
    Static Vector v = new Vector(10);

    for (int i = 0; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;
    }
    ```

    无论是那种语言,开发都需要关注内存回收的机制,以及内存回收的问题。如果不去关注这些。 很容易出一些内存泄漏的问题。
    silk
        48
    silk   50 天前
    你拿明朝的剑来斩清朝的官?
    2i2Re2PLMaDnghL
        49
    2i2Re2PLMaDnghL   50 天前
    重新设计业务逻辑
    GC 本来发生时间都不确定,JS 某些实现根本不进行任何 GC,快速 fork 一份运行完整个进程一并销毁,不需要任何 GC
    本来语言层面上就没保证的事情。

    听说,Scheme 语言设计上没有保证函数的参数是从左到右顺次求值的,所以存在一些 Scheme 实现特意不按这个顺序求值,并以取笑搞错了的人为乐
    hjdtl
        50
    hjdtl   50 天前
    楼主在实际开发环境中遇到什么问题了吗?可以分享出来,大家帮你解决。

    不要制造虚无问题了
    zjsxwc
        51
    zjsxwc   50 天前
    虽然 swift 没有 gc,也没有析构函数,但 swift 有 deinit 函数

    In Swift, destructors are not required, as the memory deallocation is abstracted away and done automatically. However, they are available and known as “deinitializers”, to perform any cleanup that needs to be done just prior to actual deallocation of the object. Deinitializers are optional, and there can be one at most in a class.

    In our car example, before we send it to the junkyard, we might want to un-register the vehicle’s license and cancel the insurance:

    class Car {
    //properties
    init(model:String, color:String, vin:Int) {
    // init code
    }

    deinit {
    unRegisterLicense() // some function that un-registers the license
    cancelInsurance() // some function that cancels the insurance policy
    }
    }
    aneostart173
        52
    aneostart173   50 天前
    自己写一个类,自定义生命周期,所有其他类都继承这个类。
    chairuosen
        53
    chairuosen   50 天前   ❤️ 1
    @James369 #44


    var Obj = function(name){
    this.name = name;
    }
    Obj.prototype.destroy = function(){
    console.log('destroy', this.name);
    }
    var store = {};
    var myObj = new Obj('123');
    Object.defineProperty(store, 'myObj', {
    get: function(){
    return myObj;
    },
    set: function(newVal){
    if(!newVal){
    myObj && myObj.destroy();
    }
    myObj = newVal;
    }
    });

    setTimeout(function(){
    store.myObj = null;
    },100)
    libook
        54
    libook   50 天前   ❤️ 5
    一时间不知道怎么回复……

    关于内存泄漏:

    要用内存的时候需要申请,用完了要释放,这些操作仅存在于可以直接操作内存的语言,如 C 、C++,申请了内存忘了释放会导致内存泄漏,在一些重业务轻资源管理的场景下,人们发明了一些自动释放内存的语言,如 Java 、C#、JS 、Go,大多是采用 GC 方案,也有采用其他方案的如 Rust 的 safe 的代码采用 Ownership 方案。

    但不是说有了自动释放内存的方案就可以完全避免“内存泄漏”了,没有遵照 GC 、Ownership 的规则来使用同样会导致内存泄漏,比如持续创建对象,但又让对象持续被引用,GC 检测到对象被引用则不释放内存,可能业务上已经用不到这些对象了,但对象依然越来越多,直到用满内存。

    所以对于 JS 来说,绝大多数时候都不需要考虑内存相关的问题,特殊情况下要释放内存也不需要借助析构函数之类的特性,而是随时都可以直接为 GC 创造条件诱导其释放内存,比如打破引用关系,比如使用 WakeMap 。

    关于析构函数:

    C 语言也没有析构函数(非 OO 连构造函数都没有)。
    析构函数( destructor )是面向对象编程思想里的一个概念,存在于 RAII 方式的场景,RAII 方式的特点是资源的分配和释放的时机是被精准控制的。JS 是使用 GC 来释放内存,GC 的释放时机是不确定的,而且假如对象不满足 GC 释放要求的时候强行释放可能会导致有些资源被提前释放了,从而使得引用关系树紊乱,这也是 GC 要避免的问题之一。你可以使用 FinalizationRegistry 来监听对象被 GC 回收的事件,某种意义上来说可以算是一种“析构函数”,但这个与其他很多语言的方式有很大区别,开发者无法预料这个事件什么时候发生,但如果契合你的需求场景,也是可以用的。同时不同 JS 引擎对于 GC 的实现也可能有差异,过度关心内存可能会导致程序兼容性下降,特别是适配问题很烦人的浏览器端开发场景。

    关于释放文件句柄、网络连接、锁:

    这些资源跟内存是完全不同的,确实是需要手动释放的,但不是说必须用析构函数,C 语言没有析构函数,除了要手动释放内存以外,也要释放这些非内存资源。
    JS 也一样,相关 API 也都提供了诸如 filehandle.close()、writableStream.end()之类的方法,但并不一定需要在对象被回收的时候释放。用 C++之类的语言的时候,有析构函数可以用来释放内存,于是当其他资源生命周期和对象一样的时候,可以一起放在析构函数里释放;但并不是说释放资源一定要在析构函数里做,C++也可以不在析构函数里释放这些资源。人们在用 JS 的时候,压根不会去考虑内存释放的问题,所以也压根不会考虑使用析构函数,那么只要是符合业务流程定义的位置,都可以显式调用方法释放相应的资源,你可以直接在一个代码流程中释放,也可以监听相应事件来释放。
    如果程序中没有释放资源,当进程退出的时候( Node.js 支持多进程编程),操作系统也会回收所有资源,包括内存、文件句柄、网络连接等等。

    最后:
    1. 不知道题主是否明确清楚自己是否真的需要析构函数来释放这些资源,如果只是从 C++之类的开发精力带过来的习惯,那大可不必,JS 和其他语言有很多不同点,很多时候在一个语言上硬套另一个语言的思路会步履维艰,最终会觉得这个语言很难用。不如拥抱这个语言自己的开发习惯,可以参照一些主流项目的代码,看看大家一般会怎么做。
    2. 语言表达的最终目标是让别人准确理解自己的意思,所以使用概念的时候还是要多加斟酌的,看是不是自己想表达的那个意思,造成误解可能会有其他连带的麻烦。
    GeruzoniAnsasu
        55
    GeruzoniAnsasu   50 天前   ❤️ 4
    不提 RAII 的回复其实都可以不用看了

    是的大量带 GC 的语言都无法 RAII,他们都必须手动管理资源释放。虽然 golang 之类的语言有 defer,但逻辑跟 RAII 完全不同,是两套思维方式。

    他们这些“脚本语言”的做法:



    池化(本质上是由分派器管理资源)

    依赖注入(本质上是剥离资源申请释放的依赖,放到第三方)

    使用 promise/future (本质上是模仿 RAII )

    完全抛弃状态,使用函数式



    是不是很熟悉,「设计模式」跟语言特性是有很大相关性的
    agagega
        56
    agagega   50 天前
    @libook
    这个让我想起来,很多人所说的动态语言里面的「内存泄漏」,实际上是引用还在但业务上不需要了,其实不是真正的内存泄漏。跟「粘包」有点像。
    James369
        57
    James369   50 天前
    @libook #54 让你说对了,确实从后端转过来,确实很多不习惯。
    thtznet
        58
    thtznet   50 天前
    学生借了书回家,放在家里的书桌上,还没来得及翻页,学生因为吃错药病发去世从这个世界消失了,过了 1 个月,书还在桌上。这是符合世界运行的客观规律的。
    zhw2590582
        59
    zhw2590582   50 天前
    Student 在 Canvas 上画了一个圆,但是 Student 释放的时候,它不会做擦除圆的操作
    这好像是业务的销毁逻辑,不是 GC 做的事
    xarthur
        60
    xarthur   50 天前
    你需要的不是析构函数,你需要的是生命周期函数,这个你自己定义就行了。
    只是之前把要在生命周期函数里做的工作放到析构函数里了……
    fyxtc
        61
    fyxtc   50 天前
    楼主应该是学生吧?析构和 GC 保证的应该是 Student 销毁的时候,再次引用实例 stu 能保证不可能取到曾经存在的任何信息(当然包括借书信息),反之就是内存泄露(所以这就是为什么 C/C++有内存泄露问题,因为需要你自己去手动控制)。而既然你是手动调用让 stu 去借了,自然应用由你来手动归还,这不是语言层面的问题,所以不应该依赖任何语言设计的语法,换言之,就算有析构,你肯定也需要在特定的生命周期内去显式调用析构(通过 delete 之类)而不应该依赖自动调用。

    就拿你举的例子来说,如果不是通过 new 创建的,那么离开作用域自动析构,那么你就应该在离开作用域的时候调用还书,很直接的线性关系。如果是 new 出来或者有存在其他地方的引用的,那么肯定是在之后某个通知下触发学生销毁了,那么就应该在那里调用还书,而不是依赖析构。
    ragnaroks
        62
    ragnaroks   50 天前
    这个事情让我想到 blazor 官方示例最开始在 page 里面有个 timer 更新页面时间(表现双向更新效果),但是因为 blazor 没有“主动释放”导致大量访问后就会塞爆内存。

    btw,现在也没有 OnDestory 的事件,但是可以实现 IDispose 接口
    namelosw
        63
    namelosw   50 天前
    > 正规语言都有析构函数

    你这说得义正辞严的。我猜你用过的正规语言只有一个哈哈哈哈。

    连 C 都没有不用析构函数,Rust 都不怎么用析构函数,更别提 Java 和 JavaScript 了。
    aloxaf
        64
    aloxaf   50 天前
    @namelosw Rust 哪有「不怎么用」,明明到处都是
    zxCoder
        65
    zxCoder   50 天前
    这个跟析构函数没有关系吧,这个是业务逻辑的问题
    WilliamYang
        66
    WilliamYang   50 天前   ❤️ 1
    又一个 XY 问题,你需要的是业务怎么处理,而不是问有没有析构函数
    ipwx
        67
    ipwx   50 天前
    gc 语言应该要显式 close,因为你无法控制 gc 什么时候调用析构函数,是巨大的 bug 隐患。
    TypeError
        68
    TypeError   50 天前 via Android
    内存释放靠 gc
    资源释放靠手动或者语法保证( Python with/Golang defer )

    https://stackoverflow.com/questions/62879698/any-tips-on-context-manager-similar-to-python-in-javascript
    liberize
        69
    liberize   49 天前
    说白了,这些没析构函数的语言就是得提供一个 close 接口,手动调用 close
    zjsxwc
        70
    zjsxwc   49 天前
    GC 语言( PHP\JAVA )的析构函数(__destruct/finalize ) != RAII 语言( C++)的析构函数 (~xx ) != ARC 语言( oc/swift )的析构函数( dealloc/deinit ) != 所有权语言( Rust )的析构函数( drop )
    zjsxwc
        71
    zjsxwc   49 天前
    上面 4 类语言中,只有 GC 语言存在 STW 卡顿现象。
    Building
        72
    Building   49 天前
    在释放函数执行还书操作本身就不合理吧。
    1. 书和学生互相引用,这样根本走不到释放函数。
    2. 书和学生无互相引用,学生释放了并不会影响还书。
    3. 如果必需要执行还书操作,书和学生不相互引用,却能提前释放学生,这个在设计上没有问腿吗
    wangxin13g
        73
    wangxin13g   49 天前
    析构函数不应该有任何业务层面上的操作。
    qq1009479218
        74
    qq1009479218   49 天前
    手动析构
    aleen42
        75
    aleen42   49 天前
    lbyo
        76
    lbyo   48 天前
    @qrobot #47

    > 有一些语言是需要开发者手动明确进行处理的,例如 rust c/c++

    Ownership is Rust’s most unique feature, and it enables Rust to make memory safety guarantees without needing a garbage collector.
    KuroNekoFan
        77
    KuroNekoFan   48 天前
    楼主是把 OO 和编程语言能力搞混了吗
    liuhan907
        78
    liuhan907   48 天前 via Android
    @GeruzoniAnsasu
    一圈回复看下来,raii 都已经是时代的眼泪了。
    关于   ·   帮助文档   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1395 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 00:32 · PVG 08:32 · LAX 17:32 · JFK 20:32
    ♥ Do have faith in what you're doing.