-
Notifications
You must be signed in to change notification settings - Fork 310
Description
@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