[BUG] GM_getValue 返回的是可变对象引用,而不是拷贝 / GM_getValue returns mutable object references instead of copies
@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