Skip to content

[BUG] GM_getValue 返回的是可变对象引用,而不是拷贝 / GM_getValue returns mutable object references instead of copies #1141

@cyfung1031

Description

@cyfung1031

@CodFrm https://bbs.tampermonkey.net.cn/thread-9929-1-1.html related

https://greasyfork.org/scripts/529453-twitter-x-media-downloader

问题描述

Summary

在使用 GM_getValue 读取数组或对象时,返回的并不是数据的拷贝,而是对已存储数据的直接引用。因此,对返回对象进行修改(例如对数组调用 push)时,即使没有再次调用 GM_setValue,也会直接影响存储中的值。这种行为很不直观,容易导致开发者误以为修改仅作用于本地变量,从而产生难以排查的隐性 Bug。除非显式对返回值进行深拷贝或将 GM 存储视为不可变数据,否则很容易踩坑。

When using GM_getValue to retrieve arrays or objects, the returned value is a direct reference to the stored data rather than a cloned copy. As a result, mutating the returned object (e.g., calling push on an array) silently modifies the stored value without calling GM_setValue again. This can lead to unexpected side effects where subsequent GM_getValue calls reflect mutations that were assumed to be local. This behavior is non-obvious and can cause hard-to-track bugs unless developers explicitly clone the value or treat GM storage as immutable.

Example

// ==UserScript==
// @name         New Userscript ZLEF-1
// @namespace    https://docs.scriptcat.org/
// @version      0.1.0
// @description  GM_getValue 回传的是可变引用(reference),不是拷贝(copy);直接 mutate 会改到存储 / GM_getValue returns mutable references; mutating it changes storage
// @author       You
// @match        https://example.com/*?zlef*
// @grant        GM_setValue
// @grant        GM_getValue
// @noframes
// ==/UserScript==

function test() {
  // 1) 第一次读取 GM storage 的值
  // 1) First read from GM storage
  let ret1 = GM_getValue("abc");
  console.log(123, ret1);

  // ⚠️ 核心陷阱 / Core pitfall:
  // ret1 不是「拷贝」,而是「指向存储内部资料的引用」
  // ret1 is NOT a cloned copy; it's a reference to the stored object.
  //
  // ✅ 这代表:你对 ret1 做任何「原地修改」(mutate),
  // 即使你没有再呼叫 GM_setValue,也会直接改到 GM storage 里的值。
  // ✅ That means: any in-place mutation on ret1 will silently update GM storage,
  // even without calling GM_setValue again.
  //
  // 例如:push / pop / splice / sort / reverse / obj.xxx = ...
  // e.g. push/pop/splice/sort/reverse / obj.xxx = ...

  ret1.push("test"); // ❗看似改本地,实际上在改存储 / Looks local, actually mutates storage

  // 2) 再次读取 GM storage
  // 2) Read again from GM storage
  let ret2 = GM_getValue("abc");
  console.log(456, ret1, ret2);

  // 3) 第三次读取:用来检查「是不是同一个引用」
  // 3) Third read: check whether it's the same reference
  let ret3 = GM_getValue("abc");

  // 在 ScriptCat 的实作中:通常会得到 true(同一个引用)
  // In ScriptCat implementation: typically true (same reference)
  // 在 Tampermonkey:通常是 false(回传拷贝或不同实例)
  // In Tampermonkey: typically false (copy or different instance)
  console.log(789, ret2 === ret3); // TM: false

  // 结论 / Conclusion:
  // 你 mutate GM_getValue 回传的物件 = mutate 存储本身
  // Mutating objects returned by GM_getValue mutates the stored value itself.
  //
  // 安全做法 / Safe approaches:
  // - 把 GM storage 当成不可变资料(immutable),读出来先 clone 再改
  // - 或者:改完一定要 GM_setValue 回写(并且避免共享引用)
  // - Treat GM storage as immutable: clone before mutate
  // - Or: always GM_setValue after modifications (avoid shared references)
}

// -------------------------------------------------------------------------

(function () {
  "use strict";

  // 当 URL 以 zlef 结尾:仅读取并输出存储值(观察是否被「偷改」)
  // If URL ends with "zlef": just read & log stored value (observe silent mutation)
  if (location.href.endsWith("zlef")) {
    console.log(GM_getValue("abc"));
  } else {
    // 从 URL 取出 zlef 后面的字串
    // Extract substring after "zlef" from URL
    let p = location.href.split("zlef")[1];

    // 建立新阵列并撷取 a-z 片段
    // Create a new array and extract alphabetic substrings
    const arr = [];
    p.replace(/[a-z]+/g, (w) => arr.push(w));

    // 将阵列存入 GM storage
    // Store the array into GM storage
    GM_setValue("abc", arr);

    // 进入测试:会在没有 GM_setValue 的情况下,直接把存储内容改掉
    // Run test: it will mutate the stored value without calling GM_setValue
    test();
  }
})();

// -------------------------------------------------------------------------

Reproduction 重现步骤

脚本猫版本

Both 1.2.x and 1.3.x

操作系统以及浏览器信息

补充信息 (选填)

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0🚑 需要紧急处理的内容bugSomething isn't workinghotfix需要尽快更新到扩展商店

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions