获取所有范围内的变量

234

有没有一种方法可以获取JavaScript中当前范围内的所有变量?


关于您对Camsoft的回答:那是完全不同的问题;我已经更新了我的答案来解决它。 - T.J. Crowder
3
这只是一个一般性的问题,更具体的细节并不能帮助太多,因为我正在使用文档不够详细的晦涩API。 - Ian
2
@Jason - 不,问题很清楚。在函数内部,作用域中的变量将包括全局变量、thisarguments、参数以及所有定义在封闭作用域中的变量。 - Tim Down
3
我想念 Perl 的符号表,所以想知道是否有计划在未来的 JavaScript 版本中添加它? - Dexygen
是的。这是我的fiddle链接:https://jsfiddle.net/mathheadinclouds/bvx1hpfn/11/ - mathheadinclouds
显示剩余2条评论
11个回答

134

虽然每个人都回答“No”,我知道“不”是正确的答案,但如果你真的需要获取一个函数的本地变量,有一种受限制的方法。

考虑这个函数:

var f = function() {
    var x = 0;
    console.log(x);
};
你可以将你的函数转换为字符串:

你可以将你的函数转换为字符串:

var s = f + '';

您将获得函数的源代码字符串

'function () {\nvar x = 0;\nconsole.log(x);\n}'

现在您可以使用类似于Esprima的解析器来解析函数代码并查找本地变量声明。

var s = 'function () {\nvar x = 0;\nconsole.log(x);\n}';
s = s.slice(12); // to remove "function () "
var esprima = require('esprima');
var result = esprima.parse(s);

并查找带有以下内容的对象:

obj.type == "VariableDeclaration"

结果如下(我已在下面删除了console.log(x)):

{
    "type": "Program",
    "body": [
        {
            "type": "VariableDeclaration",
            "declarations": [
                {
                    "type": "VariableDeclarator",
                    "id": {
                        "type": "Identifier",
                        "name": "x"
                    },
                    "init": {
                        "type": "Literal",
                        "value": 0,
                        "raw": "0"
                    }
                }
            ],
            "kind": "var"
        }
    ]
}

我已在Chrome、Firefox和Node中测试过。

但是这种方法的问题在于你只能在函数本身中定义变量。例如,对于这个:

var g = function() {
    var y = 0;
    var f = function() {
        var x = 0;
        console.log(x);
    };
}

您只能访问 x 而无法访问 y。 但是,仍然可以使用调用者的链(arguments.callee.caller.caller.caller)在循环中查找调用函数的局部变量。如果您拥有所有本地变量名称,则具有 作用域变量。使用变量名称,您可以通过简单的 eval 访问值。


14
在解决原问题方面的出色技巧。值得点赞。 - deepelement
4
调用者的变量不一定是作用域变量,而作用域变量也不一定在调用链中。闭包函数中引用的闭合变量可能在来自定义/闭合作用域之外的调用者调用它时被引用(而且这些调用者的变量根本无法从闭包体内访问)。 - DDS
@Pylipala 这里的问题是关于作用域内的变量无法从作用域外获取局部变量的值。这是不可能的,因为这就是局部变量的定义,不应该从外部访问。关于你的代码,你指的是上下文而不是作用域,但你没有将 start 放在上下文(this)中,所以你不能用 this.start 读取它。请参见此处:http://paste.ubuntu.com/11560449/ - iman
@iman 谢谢你的回答。我也猜想在Javascript方面不可能,所以我认为如果在v8方面是可能的。我发现下面的问题链接准确地描述了我的问题。Lasse Reichstein在其中说不行,但mako-taco说可以。我尝试了一些C++代码链接,但不知道如何编写它。 - Pylipala
我认为如果代码没有明确地分配本地变量的值,以便您可以轻松找到它,那么解析代码是行不通的。换句话说,除了一些微不足道的情况外,这是无用的。 - Michael
显示剩余4条评论

92

“在作用域内”的变量是由“作用域链”决定的,这些变量无法以编程方式访问。

有关详细信息(相当多),请查看ECMAScript(JavaScript)规范。这里是官方页面的链接,您可以下载规范的规范版本(PDF),这里是官方的可链接HTML版本。

根据您对Camsoft的评论更新

您的事件函数中的“作用域内”变量是由定义事件函数的位置而不是调用它的方式确定的。但是,您可能会发现通过执行KennyTM指出的类似于for (var propName in ____)的操作,了解有关this和参数提供的有用信息,因为这将告诉您在提供给您的各种对象上可用的内容(this和参数;如果您不确定它们给您什么参数,则可以通过每个函数隐式定义的arguments变量找到)。

因此,除了由于定义函数的位置而在范围内的任何内容之外,您还可以通过其他方法找出其他内容:

var n, arg, name;
alert("typeof this = " + typeof this);
for (name in this) {
    alert("this[" + name + "]=" + this[name]);
}
for (n = 0; n < arguments.length; ++n) {
    arg = arguments[n];
    alert("typeof arguments[" + n + "] = " + typeof arg);
    for (name in arg) {
        alert("arguments[" + n + "][" + name + "]=" + arg[name]);
    }
}

你可以扩展使用调试器,以获取更有用的信息。

不过,我可能会使用像Chrome的dev tools(即使你平时不使用Chrome进行开发),或者Firebug(即使你平时不使用Firefox进行开发),或者在Opera上使用Dragonfly,或者在IE上使用"F12 Developer Tools"。并且阅读他们提供给你的任何JavaScript文件。并且要用正确的文档敲打它们头部。


顺便提一下,Google Chrome有一个很棒的调试器,可以与Firebug相媲美(而且还有更多的功能)。 - Swivel
4
@Swivelgames说:哦,我更喜欢Chrome的开发者工具,而不是Firefox和Firebug。它们在2010年还没有太多用处。 :-) - T.J. Crowder
2
@tjcrowder 确实!我注意到答案上的时间戳有一段时间了 :) - Swivel

49
在ECMAScript 6中,可以通过将代码包裹在具有代理对象的with语句中来实现。请注意,这需要非严格模式,并且这是一种不良习惯。

function storeVars(target) {
  return new Proxy(target, {
    has(target, prop) { return true; },
    get(target, prop) { return (prop in target ? target : window)[prop]; }
  });
}
var vars = {}; // Outer variable, not stored.
with(storeVars(vars)) {
  var a = 1;   // Stored in vars
  var b = 2;   // Stored in vars
  (function() {
    var c = 3; // Inner variable, not stored.
  })();
}
console.log(vars);

代理声明拥有with内引用的所有标识符,因此变量赋值存储在目标中。对于查找,代理从代理目标或全局对象检索值(而不是父作用域)。letconst变量不包括在内。

Bergi此答案启发。


3
这种简单的技术取得了出乎意料的成功。 - Jonathan Eunice
3
这是什么巫术魔法? - T Tse
函数声明也不包括在内。 - Binary

34

是和不是。在几乎所有情况下都是“不”。但是,如果您想要检查全局范围,那么只有在有限的情况下才会是“是”。以以下示例为例:

var a = 1, b = 2, c = 3;

for ( var i in window ) {
    console.log(i, typeof window[i], window[i]);
}

在150多个输出中,其中包括以下内容:

getInterface function getInterface()
i string i // <- there it is!
c number 3
b number 2
a number 1 // <- and another
_firebug object Object firebug=1.4.5 element=div#_firebugConsole
"Firebug command line does not support '$0'"
"Firebug command line does not support '$1'"
_FirebugCommandLine object Object
hasDuplicate boolean false

因此,可以列出当前作用域中的一些变量,但是这种方式不可靠,不简洁,效率低且不易访问。

一个更好的问题是为什么你想知道哪些变量在作用域内?


2
我认为这个问题更加普遍,不仅限于在Web浏览器中使用JavaScript。 - Radu Simionescu
如果你在使用Node,只需将单词“window”更改为“global”即可...或者你可以使用单词“this”,但在“use strict”下是被禁止的。 - Ivan Castellanos
9
为什么在调试和/或开发过程中,至少不应该想要检查哪些变量在作用域内呢? - Dexygen
@Justin,更好的问题是为什么计算机不允许我们知道哪些变量在作用域内? - Pacerier
1
谢谢,如果您需要检查某个变量是否存在以及它在非常复杂或由其他人设计的环境中如何定义,这可能会很有用。 - jcf
显示剩余3条评论

17

你有多少时间?

如果你讨厌你的 CPU,你可以通过暴力破解每个有效的变量名,并对每个变量名进行 eval 操作来查看其结果!

以下代码片段尝试了前1000个暴力破解字符串,足以找到作用域中编造的变量名:

let alpha = 'abcdefghijklmnopqrstuvwxyz';
let everyPossibleString = function*() {
  yield '';
  for (let prefix of everyPossibleString()) for (let char of alpha) yield `${prefix}${char}`;
};
let allVarsInScope = (iterations=1000) => {  
  let results = {};
  let count = 0;
  for (let bruteforceString of everyPossibleString()) {
    if (!bruteforceString) continue; // Skip the first empty string
    try { results[bruteforceString] = eval(bruteforceString); } catch(err) {}
    if (count++ > iterations) break;
  }
  return results;
};

let myScope = (() => {
    
  let dd = 'ddd';
  let ee = 'eee';
  let ff = 'fff';
  
  ((gg, hh) => {
    
    // We can't call a separate function, since that function would be outside our
    // scope and wouldn't be able to see any variables - but we can define the
    // function in place (using `eval(allVarsInScope.toString())`), and then call
    // that defined-in-place function
    console.log(eval(allVarsInScope.toString())());
    
  })('ggg', 'hhh');
  
})();

这个脚本最终(经过很长时间)会找到所有作用域变量的名称,以及我创建的一些示例变量abc niftyswell。请注意,它只会找到由字母组成的变量名。

let preElem = document.getElementsByClassName('display')[0];
let statusElem = document.getElementsByClassName('status')[0];
let alpha = 'abcdefghijklmnopqrstuvwxyz';
alpha += alpha.toUpperCase();
let everyPossibleString = function*() {
  yield '';
  for (let prefix of everyPossibleString()) for (let char of alpha) yield `${prefix}${char}`;
};

(async () => {
  
  let abc = 'This is the ABC variable :-|';
  let neato = 'This is the NEATO variable :-)';
  let swell = 'This is the SWELL variable :-D';
  
  let ignoreRegex = /^(?:t(?:his|op)|self|window)$/;
  let results = {};
  let batch = 100;
  let waitMs = 25;
  let count = 0;
  let startStr = null;
  let t = null;
  
  for (let bruteStr of everyPossibleString()) {
    
    // Some property accesses cause problems within stackoverflow's iframe snippet
    // system - we'll bypass these problematic properties
    if (ignoreRegex.test(bruteStr)) continue;
    
    if (count === 0) {
      t = Date.now();
      startStr = bruteStr;
    }
    
    try { results[bruteStr] = eval(bruteStr); } catch(err) {}
    
    if (count++ >= batch) {
      
      count = 0;
      
      // Update the html
      statusElem.innerHTML = `Did batch of ${batch} from ${startStr} -> ${bruteStr} in ${waitMs.toFixed(0)}ms`;
      preElem.innerHTML = JSON.stringify(results, null, 2);
      
      // Optimize `batch` based on how well the CPU is holding up
      let dur = (Date.now() - t) + 2; // +2ms for breathing room
      let estimatedMaxBatch = batch * (waitMs / dur);
      batch = Math.round(batch * 0.8 + estimatedMaxBatch * 0.2);
      
      // 
      await new Promise(r => setTimeout(r, waitMs));
      
    }
            
  }
  
  console.log('Done...'); // Will literally never happen
  
})();
html, body { position: fixed; left: 0; top: 0; right: 0; bottom: 0; margin: 0; padding: 0; overflow: hidden }
.display {
  position: fixed;
  box-sizing: border-box;
  left: 0; top: 0;
  bottom: 30px; right: 0;
  overflow-y: scroll;
  white-space: pre;
  font-family: monospace;
  padding: 10px;
  box-shadow: inset 0 0 10px 1px rgba(0, 0, 0, 0.3);
}
.status {
  position: fixed;
  box-sizing: border-box;
  left: 0; bottom: 0px; right: 0; height: 30px; line-height: 30px;
  padding: 0 10px;
  background-color: rgba(0, 0, 0, 1);
  color: rgba(255, 255, 255, 1);
  font-family: monospace;
}
<div class="display"></div>
<div class="status"></div>

我非常清楚,在实际情况中几乎没有任何场合是可行的。

5
我很高兴这个答案出现了,即使它不是一个让 @Gershom 点赞的答案。 - James Wilson

16

1
是的,但是在范围内的变量不仅限于当前变量对象。在范围内的变量由作用域链确定,这是一个链,通常由一系列变量对象组成,并以全局对象终止(尽管with语句可以用于插入链中的其他对象)。 - T.J. Crowder
在 bclary.com 上提供链接 +1。我很高兴地说,ECMA 网站上将在接下来的几个月内发布最新规范的 HTML 版本。 - T.J. Crowder
3
注意,两个链接都失效了。 - 0xc0de
你可以使用静态分析 https://jsfiddle.net/mathheadinclouds/bvx1hpfn/11/ - mathheadinclouds

11

我制作了一个 fiddle,基本上实现了 iman 提出的上述想法。当您在return ipsum*ipsum - ... 中悬停在第二个 ipsum 上时,它是什么样子。

enter image description here

高亮显示具有不同范围的变量声明(使用不同颜色进行区分)。有红色边框的 lorem 是一个被屏蔽的变量(未在范围内,但如果树中下面的另一个 lorem 不在那里,则在范围内)。

我正在使用 esprima 库解析 JavaScript,并使用 estraverse、escodegen 和 escope(是 esprima 之上的实用程序库)。所有“重活”都由这些库完成(当然,最复杂的是 esprima 自身)。

工作原理

ast = esprima.parse(sourceString, {range: true, sourceType: 'script'});

生成抽象语法树。然后,
analysis = escope.analyze(ast);

生成包含程序中所有作用域信息的复杂数据结构。其余部分则是将编码在该分析对象(以及抽象语法树本身)中的信息汇集起来,并将其转化为交互式着色方案。

因此,正确答案实际上不是“否”,而是“是,但是”。其中的“但是”非常重要:你基本上需要使用JavaScript重新编写Chrome浏览器(和它的开发工具)。JavaScript是一个图灵完备的语言,所以原则上这是可能的。不可能的是在不使用源代码(作为字符串)的全部内容并且执行高度复杂的操作情况下完成整个过程。


9

获取特定作用域变量的最简单方法

  1. 打开开发者工具 > 资源(在Chrome中)
  2. 打开包含访问该作用域的函数的文件(使用命令/ctrl+p查找文件)
  3. 在该函数内设置断点并运行代码
  4. 当程序停在断点处时,您可以通过控制台(或作用域变量窗口)访问作用域变量

注意:您需要使用未压缩的JS来执行此操作。

显示所有非私有变量的最简单方法

  1. 打开控制台(在Chrome中)
  2. 输入:this.window
  3. 按回车键

现在,您将看到一个对象树,您可以展开其中包含的所有已声明对象。


7

正如大家所观察到的那样:你不能这样做。 但是,你可以创建一个对象,并将声明的每个变量都分配给该对象。 这样,你就可以轻松地检查你的变量:

var v = {}; //put everything here

var f = function(a, b){//do something
}; v.f = f; //make's easy to debug
var a = [1,2,3];
v.a = a;
var x = 'x';
v.x = x;  //so on...

console.log(v); //it's all there

1
+1 感谢分享这个很棒的例子,如果为了完整性需要可以通过 console.log(window); 查看。http://jsfiddle.net/4x3Tx/ - Stano
1
值得一提的是,这里有一个简写方式:let a = v.a = [ 1, 2, 3 ]; - Gershom Maes

4

如果你只是想手动检查变量以帮助调试,只需启动调试器:

debugger;

直接进入浏览器控制台。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接