【问题标题】:Where are JavaScript vars stored?JavaScript 变量存储在哪里?
【发布时间】:2017-03-26 14:43:25
【问题描述】:

我玩了一下 common lisp 并意识到与 Lisp 不同,所有局部变量都是参数(通过 lambda)lambda 或通过参数(通过 let)。

换句话说,他们总是遵循 IIFE 习语:

((x, y, z) => {
   /* I has variables */
})(1, 2, 3);

例如

((lambda (x y z)
    ; I has variables
) 1 2 3)

(let ((x 1)(y 2)(z 3))
    ; I has variables
)

在 JavaScript 中,vars “感觉”类似于 setq,但如果局部变量没有声明为遮蔽它,setq 会改变全局范围,在 JavaScript 中,无论如何 vars 都不会改变全局范围。

假设我想在 Lisp 中执行此操作:

(function() {
    var x = 1;
    var y = 2;
    var z = 3;

    /* woo, I has 3 vars */
})();
/*note that the vars no longer exist */

如果我尝试这样做:

; progn is like lambda but never has arguments and automatically iifes itself.
; eg (progn (setq x 1)) is ((lambda () (setq x 1))
(progn
    (setq x 1)
    (setq y 2)
    (setq z 3)
    ; Woo I has 3 vars
)
; oops, I polluted global scope :(

为了获得这种类似 JavaScript 的感觉,我最终做了类似的事情

; wait, we're writing smalltalk now? 
; [ 
;     | x y z | 
;     x := 1.
;     y := 2.
;     z := 3.
;     "I has three vars..."
; ] value.
(let
    ((x)(y)(z))

    (setq x 1)
    (setq x 2)
    (setq x 3)
)

奇怪的是,Lisp 似乎没有与 JavaScript 的 var/let/const 平行;在上面的例子的意义上(是吗?我对 Lisp 不是很熟悉......)。

我的问题是; vars 实际存储在哪里?它们没有作为参数传递,也没有在参数中显式声明......但是它们必须存储在某个地方,并且某个地方不是全局范围...

【问题讨论】:

  • 你问的是“功能范围”吗?我实际上并不清楚这里的问题是什么,但简而言之 - 函数中 declared 的任何参数都保留在函数中。您也可以拥有全局变量,并且函数也可以访问它们。
  • 这个问题可能是正确的,但是,JS 与 Lisp 的比较非常奇怪。有很多语言引入了具有块和函数作用域的局部变量,JS 在这方面一点也不陌生,它只是众多语言中的一种。如果 'where' 解决了 JS 引擎管理内存的方式,那么问题应该更具体。
  • 这个想法带有多种语言。实际上,我对 Perl 词法范围更感兴趣(my $x,而在 my 成为事物之前,一切都是 setq,因为 $x 总是以与 setq 相同的方式暴露给全局)并且 Lisp 缺乏类似的“我的“ 范围。那就是(sub {my $foo = 3;})->(); 不会污染全局范围,而(sub {$foo = 3;})->(); 会运行并且不会运行,如果你有严格的 subs(strict bans setq?) 但是“my $”的行为与 var 类似,而 JavaScript 则更多目前比 Perl 更受欢迎。 Smalltalk 没有这个,但是 Ruby 有。
  • 每个人都喜欢“var/my/our”之类的行为,而当我与校园周围的人交谈时,却被 lambda/let/iife 吓坏了,这真是令人惊讶。特别是因为闭包在逻辑上看起来更自然,而 var/my/our 使用魔法来处理我们的变量而不破坏全局范围。 “var/my/our”在实现细节方面似乎比“iife”更复杂,但我们发现它们更自然;甚至在一定程度上使用“让 x = 2。y = x + 3”的学校。在数学中,而不是“(let ((x 2)) (let ((y (+ 3 x))) ...))”,这在范围界定方面更“诚实”。
  • “更难”的东西对我们来说似乎“更容易”理解,尽管显式范围应该更容易,因为它使依赖关系显式;通过将 y = x + 3 嵌套在 x 的定义中,可以避免很多歧义。非常奇怪。

标签: javascript lambda ecmascript-6 lisp


【解决方案1】:

JavaScript 变量是称为LexicalEnvironment 对象的对象上的“绑定”。当执行进入一个可以有自己的绑定的作用域时(例如进入一个函数时),一个新的 LexicalEnvironment 会被创建并填充参数、本地变量和本地声明的函数。

这是一个规范结构; JavaScript 引擎如何实际实现它取决于引擎,前提是它忠实地复制了规范的语义。规范中没有任何内容可以让我们直接访问该对象。 (特别是:如果没有关闭变量,它们可以很高兴地在堆栈上实现并通过在退出时重置堆栈指针来清理。)

请注意,这就是 闭包 在 JavaScript 中的工作方式,因此阅读闭包可以让您更深入地了解变量的存储位置。简而言之:当您创建一个函数时,该函数在创建时具有对活动 LexicalEnvironment 对象的(有些间接的)引用。由于对象包含变量的绑定,因此函数可以通过对象访问它们。

一个具体的例子可能会有所帮助;见 cmets:

// A function that returns a function that closes over its
// local variable
function f() {
  // When this function is called, a *LexicalEnvironment* object
  // is created and populated with an `a` variable
  // (and a few other things)
  var a = Math.random();
  
  // If we create a function, it gets a reference to the object,
  // and so it can access that variable
  return function() {
    return a;
  };
}

// Create a lexical environment containing a variable, get back
// a function with access to it
var f1 = f();

// Do it again
var f2 = f();

// Now we have *two* separate lexical environment objects (well,
// more, but two related to `f`). They both continue to exist
// as long as there's something referring to them (like all other
// objects). Our `f1` and `f2` each refer to one of them, so they
// still exist and `f1` and `f2` can use the `a` on each of them:
console.log(f1());
console.log(f2());

// Now we release the functions, which release the lexical
// environment object they had references to
f1 = f2 = undefined;

【讨论】:

  • LocalEnvironment 会有点像每个 javascript 范围的隐式参数吗?例如((lambda ((localEnvironment)) "dummy text") environmentInfo),其中每个作用域都将其环境隐式注入主体,并且所有变量都隐式绑定到该环境,有点像将环境用作哈希表。
  • @Dmitry:(LexicalEnvironment,不是 LocalEnvironment。)规范进入血淋淋的细节,但是当你进入一个范围时,引擎会创建一个新的环境对象并添加它到它们的当前链;在作用域内运行的代码使用链顶部的“活动”代码。
  • 是否可以直接从 javascript 范围访问词法环境对象?我找不到它
  • @Dmitry:不,正如我在回答中所说,规范没有提供任何直接访问该对象的方法。
  • @Dmitry:是的,它由引擎自己的标识符解析处理,它使用一系列词法环境对象(从最里面开始,然后是下一个,等等)。
【解决方案2】:

在 Common Lisp 中,使用 setq 之类的变量分配变量并不会像这样声明变量。这种块作用域没有机制,提及变量将在最内部的块作用域中创建它。

您已经提到了letlambda

注意lambda&aux 变量:

(lambda (&aux (x 1) (y 2) (z 3))

  ; x and y and z are variables here...

  )

这些辅助变量需要在lambda参数列表中声明,但不是调用函数的参数的一部分。

例子:

CL-USER 61 > ((lambda (x &aux (y (* x x)) (z 12))
                (+ y x z))
              5)
42

你的例子

CL-USER 63 > (let (x y z)

               (setq x 1)
               (setq y 2)
               (setq z 3)

               (+ x y z))
6

是一种流行的选择,尤其是因为多年前它通常被写成:

CL-USER 72 > (prog (x y z)

               (setq x 1)
               (setq y 2)
               (setq z 3)

               (return (+ x y z)))
6

prog 提供了一个block、局部变量和一个tagbody。在 tagbody 中,我们可以有本地 tag 并通过 go to 结构跳转。

总结:这意味着你可以有块局部或函数局部变量,但你必须先在 Common Lisp 中声明它们。在其他一些语言中,您可以在某个范围内的任何位置执行此操作。

当您输入一个块时,通过这种方式,词法变量列表是已知的并且是固定的。语言实现不需要扫描正文中的新变量,也不需要提供某种方法来使用新变量扩展当前的词法环境。

【讨论】:

  • 与此问题/答案无关且与此meta post 相关的评论/讨论已 movd to chat
  • In Common Lisp assigning a variable with something like setq does not declare the variable as such. There is no mechanism for such block scope, where mentioning a variable will create it in the inner most block scope.。这种机制叫什么? Lexical Scope 包括隐式和显式声明,但是具体的“var”或 perl 的“my/our”行为呢?($ 行为与 setq 非常相似,前面没有任何词)
【解决方案3】:

这不是答案,但评论太长了:抱歉。

创建似乎很常见的变量绑定有三种可能性:

  1. 变量被显式绑定在作用域的开头;
  2. 有一个构造可以创建一个绑定,该绑定可以出现在范围内的任何位置,但不仅仅是赋值;
  3. 变量在第一次赋值时被隐式绑定在周围的范围内。

(3) 的一个例子是 Python。这样做的语言最终需要显式的范围解析运算符(global 和现在 Python 中的 nonlocal)来处理他们所做的愚蠢、懒惰、双关语,不值得进一步讨论:你需要不同的绑定结构完成任务,否则您将永远生活在痛苦中。

Common Lisp 是 (1) 的一个示例,而 C 曾经是:它们都是 (C was) 语言示例,其中所有绑定都需要在作用域构造的开头创建。

在 (1) 中,有些语言具有某种块构造(C 中的{ ... }),可用于对 进行分组以用于范围,以及有一个分组结构和一个或多个单独的作用域结构:CL 属于后者。

所以在(旧)C 中你会说

{
    int i = 3;
    ...
}

在 CL 中你说

(let ((i 3)) ...)

-- 这里的作用域结构是let(当然还有其他的)。

当然,作为 Lisp 的 CL 可以完美地发明一个看起来像 C 块的结构:

(defmacro scope (&body vars/body)
  (loop for (var? . body?) on vars/body
        while (and (listp var?)
                   (<= 2 (length var?) 3)
                   (eql (first var?) 'var)
                   (symbolp (second var?)))
        collect (case (length var?)
                  (2 (second var?))
                  (3 (rest var?))) into bindings
        finally (return `(let* ,bindings ,var? ,@body?))))

现在

(scope
  (var a)
  (var b 2)
  ...)

是语言中的一个结构。

因此,实际上,(1) 中的这两个变体更相似,而不是不同:这只是你如何在语言中切割你想要的底层结构的问题:C 合并了范围块和分组,而 CL 没有。

(2) 然而不同。在这里,您可以在范围构造中混合绑定构造和其他事物。 C现在是这样,JavaScript也是。 CL 本身不是这样(显然,作为一个 Lisp,它可以变成在一些宏观上像这样,尽管那个宏观会比我上面写的要复杂得多) .

在这样的语言中,您确实需要一个单独的绑定创建构造,因为它不能成为范围构造的一部分。像 C 这样的语言已经将绑定创建构造视为独立的,因此对它们来说,只需放宽规则,即绑定必须全部在作用域构造的开头创建。

这样的语言有一个重要的问题需要回答:绑定的范围是什么?做类似的事情

{
    ...
    int x = 3;
    ...
}

真心的

{
    ...
    {
        int x = 3;
        ...
    }
}

或者是什么意思

{
    int x;
    ...
    x = 3;
    ...
}

后一种情况更容易,但意味着存在一个尴尬的区域,其中对x 的引用是合法的,但它的值可能没有明确定义。 C 采用我认为的前一种解释。

这对于像 C 这样的分组结构与作用域结构相同的语言来说非常适用。但是对于像 JavaScript 这样分组构造不是作用域构造的语言,更糟糕的是,除了函数之外没有作用域构造(或者过去没有),这并不好。事实上,像这样的语言并不能真正解决问题,因为没有有用的范围构造。好吧,当然,他们通过扩展范围构造来解决它。

请注意,尽管 CL 是一种 (1) 语言,但 Lisp 家族的语言却是 (2) 种语言:尤其是 Racket。在 Racket 中(但不是,我认为,在 Scheme 中)你可以说

(define (foo)
  (define bar 1)
  ;; is baz bound here?
  (display bar)
  (define baz 3)
  (list bar baz))

答案是肯定的,baz 绑定在那里,但它绑定到一个未定义的对象,如果你尝试使用它,你会得到一个错误(在运行时,而不是编译时)。所以这是一个运行时错误:

(定义 (foo) (定义栏 1) (显示 baz) (定义 baz 3) (列表栏 baz))

此时

(define (foo)
  (define bar 1)
  (display baz)
  (let ()
    (define baz 3)
    (list bar baz)))

是编译时错误。 (实际上,我对 Racket 中的哪些构造创建范围有点困惑。)

【讨论】:

  • 允许不同类型的赋值是不是很自然,因为它消除了赋值的工作的歧义?(如果一个赋值负责所有类型的赋值,它必须与 if 语句混淆,因为它不知道它是否必须执行廉价的全局符号分配或遍历环境链,只有在没有名为 foo 的符号时,才将该符号添加到符号表中。
  • 另外,这三个类别中的哪个类别确实为作用域闭包创建作用域字典,其中创建的每个作用域都会创建一个作用域字典,其中父作用域字典为空,而如果有环境字典,则新字典的父键被设置为它所包含的范围的字典,如果有的话,它已经具有指向其父级的指针。
  • @Dmitry:词法范围语言中的赋值永远不必做任何事情:在编译时总是知道变量绑定的位置(这几乎是词法范围的定义)。所有这些关于字典等的东西都是一些由它们的(基本)实现定义的语言选择公开的hack,根本没有充分的理由。
【解决方案4】:

因此,如果我正确理解了答案,则行为如下所示:

var env_get = function(env, varname) {
    var tenv = env;

    while (true) {    
        if (tenv.hasOwnProperty(varname)) {
            return tenv[varname];
        } else {
            if (tenv.superenv == null) {
                throw "no such variable" + tenv;
            } else {
                tenv = tenv.superenv;
            }
        }
    }
};

(function(superenv) {
    var env = {
        'superenv': superenv,
        'one': 1
    };

    console.log(env_get(env, "one"));

    (function(superenv) {
        var env = {
            'superenv': superenv,
            'two': 2
        };
        console.log(env_get(env, "one"));
        console.log(env_get(env, "two"));

        (function(superenv) {
            var env = {
                'superenv': superenv,
                'three': 3
            }                        
            console.log(env_get(env, "one"));
            console.log(env_get(env, "two"));                
            console.log(env_get(env, "three"));
        })(env);
    })(env);
})(null);

或在 Lisp 中:

(defun env_get (env varname) (let
    ((tenv))
    (setq tenv env)

    (loop (let
        ((val))

        (setq val (gethash varname tenv))
        (if val
            (return val)
            (let
                ((superenv))
                (setq superenv (gethash 'superenv tenv))

                (if superenv
                    (setq tenv superenv)
                    (return 'undefined)
                )
            )
        )
    ))
))

((lambda (superenv) (let 
    ((env))

    (setq env (make-hash-table))
    (setf (gethash 'superenv env) nil)
    (setf (gethash 'one env) 1)

    (print (env_get env 'one))
    (print (env_get env 'two))
    (print (env_get env 'three))     

    ((lambda (superenv) (let        
        ((env))

        (setq env (make-hash-table))
        (setf (gethash 'superenv env) superenv)
        (setf (gethash 'two env) 2)

        (print (env_get env 'one))
        (print (env_get env 'two))
        (print (env_get env 'three))  

        ((lambda (superenv) (let        
            ((env))

            (setq env (make-hash-table))
            (setf (gethash 'superenv env) superenv)
            (setf (gethash 'two env) 2)
            (setf (gethash 'three env) 3)

            (print (env_get env 'one))
            (print (env_get env 'two))
            (print (env_get env 'three))                    
        )) env)                    
    )) env)    
)) nil)

【讨论】:

    猜你喜欢
    • 2010-10-02
    • 2018-02-01
    • 2011-04-11
    • 2010-09-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多