2作者: EGreg6 个月前
我曾经很难理解内存泄漏是从哪里来的,尤其是在 iOS Safari 上。我进入开发者工具 &gt; 时间线标签,看到内存一直在增加,但不知道怎么增加的,也不知道在哪里。所以我写了这个函数来遍历各种软件添加的所有全局对象,避免重复访问同一个对象。这个函数是异步的,这样可以避免占用太多的用户体验。你可以运行它来开始查看引用在哪里泄漏了。 ```javascript Q = {}; Q.globalNames = Object.keys(window); // 快照基线 Q.globalNamesAdded = function () { const current = Object.keys(window); const baseline = Q.globalNames; const added = []; for (let i = 0; i < current.length; i++) { if (!baseline.includes(current[i])) { added.push(current[i]); } } return added; }; Q.walkGlobalsAsync = function (filterFn, options = {}) { const seen = new WeakSet(); const found = new Set(); const pathMap = new WeakMap(); const maxDepth = options.maxDepth || 5; const includeStack = options.includeStack || false; const logEvery = options.logEvery || 100; const startingKeys = Q.globalNamesAdded ? Q.globalNamesAdded() : Object.keys(window); let totalChecked = 0; let matchesFound = 0; function walk(obj, path = 'window', depth = 0) { if (!obj || typeof obj !== 'object') return; if (seen.has(obj)) return; seen.add(obj); totalChecked++; if (totalChecked % logEvery === 0) { console.log(`已检查 ${totalChecked} 个对象,找到 ${matchesFound}`); } if (filterFn(obj)) { found.add(obj); matchesFound++; if (includeStack) { pathMap.set(obj, path); console.log(`[找到] ${path}`, obj); } else { console.log(`[找到]`, obj); } } if (depth >= maxDepth) return; const skipKeys = obj instanceof HTMLElement ? new Set([ 'parentNode', 'parentElement', 'nextSibling', 'previousSibling', 'firstChild', 'lastChild', 'children', 'childNodes', 'ownerDocument', 'style', 'classList', 'dataset', 'attributes', 'innerHTML', 'outerHTML', 'nextElementSibling', 'previousElementSibling' ]) : null; for (const key in obj) { if (skipKeys && skipKeys.has(key)) continue; try { walk(obj[key], path + '.' + key, depth + 1); } catch (e) {} } } let i = 0; function nextBatch() { const batchSize = 10; const end = Math.min(i + batchSize, startingKeys.length); for (; i < end; i++) { try { walk(window[startingKeys[i]], 'window.' + startingKeys[i], 0); } catch (e) {} } if (i < startingKeys.length) { setTimeout(nextBatch, 0); // 安排下一个批次 } else { console.log(`完成。找到 ${matchesFound} 个被保留的对象。`); if (includeStack) { console.log([...found].map(obj => ({ object: obj, path: pathMap.get(obj) }))); } else { console.log([...found]); } } } nextBatch(); }; ``` 下面是如何使用它: ```javascript Q.walkGlobalsAsync( obj => obj instanceof HTMLElement && !document.contains(obj), { includeStack: true, maxDepth: 4, logEvery: 50 } ); ``` 但是——请注意,即使你能找到闭包本身,这个方法也**不会**找到被闭包保留的对象,你仍然需要手动检查它们的代码。