• Smallfly
    2019-12-30
    看了几遍,也参考了规范文档,还是有不少疑问点,希望老师能指点一二。

    1. 自 ES5 开始引入词法环境,但还没有 let 关键字,只能用 var 关键字声明变量,而 var 声明的变量又是属于变量环境的(Variable Environment),那 ES5 是出于什么原因考虑引入词法环境呢?

    2. 词法变量和 var 变量共用一个名字表,因此不能用 let 和 var 声明同名变量,为什么用 var 声明同名变量却可以?

    3. 「环境的本质是”作用域的映射“」 ,这句话应该怎么理解呢。我现有的理解是,环境类似于一个链表形式的作用域,变量的查找就是从当前的作用域逐级向上查找,环境不应该是作用域的集合么?

    这篇文章的标题看都看不懂,期待老师的下一讲……
    展开

    作者回复: 1. 这个问题我之前没细想过。但是ES5本来也不是一个突然提出来的东西,它其实是在ES4的基础上的一次“缩减”,并为以后的ES6之类做一个奠基性的事情。所以他们应该是考虑到了今后对词法环境的需求,并且事实上严格模式的提出,也是这样的目的,都是为后面的语言设计做一些基础性的工作。此外,事实上在当时他们已经有意地在设计和使用词法环境了,例如try...catch(x)中的这个x,在IE和Firefox里面它的作用域一直就不同,IE是用变量环境的,而Firefox是用词法环境的。这个争端早于ES5之前就有的,所以问题出在哪里,以及将来需要做成怎样,这些在当时应该已经有明确的选择了,而ES5只不过把其中的冰山一角显露出来了而已。

    2. var的多次声明只算第一次,其它的都是按赋值处理的(准确地说,是按多次绑定值处理的)。

    3. 环境跟作用域的关系,浅层地说,它是后者的一个扩展版本的实现。——这样理解并没有问题。但是从概念层面来说,作用域在文本解析和引擎执行中的语义并不相同,而环境更是清晰地区分了这种不同。最后,环境与作用域的关系,有点类似于“类与对象实例”之间的关系。总之,他们有差异,但不明显。关于这个概念的进化演变过程,我在给D2的演讲《 JS 语言在引擎级别的执行过程》中有讲过,只是目前D2组委会那边还没有视频放出来,回头你可以找来看看。

    
     3
  • 行问
    2019-12-30
    其实函数中也存在一个类似的例外。但这个处理过程是在函数的环境创建之后,在函数声明实例化阶段来完成的,……

     据我的理解,函数在 JavaScript 中是一等公民,函数提升,但我不懂的是“函数每调用一次,是否函数声明的实例化一次吗”。一直以来就是“定义(声明)一个函数”,再调用,但不知道函数实际背后的逻辑是怎样。

     不知道我的问题,周大看懂了没。感谢
    展开

    作者回复: 是的。函数每调用一次,“函数(的声明)”就会实例化一次。

    不过,这个“函数声明的实例化”与你所理解的并不同,它不是说“function f() { ... }”这个函数要被实例化,而是“函数内的那些声明(包括形式参数名和var/let/const声明名字等)”——这些东西需要被实例化。

    也就是说,之所以每次调用一下这个函数f,函数内的那些变量都被初始化为undfefined,且形式参数要跟调用时的实际参数绑定一下,这些都是“函数(的声明)实例化”阶段帮你做掉的。

    还有,这与“函数是一等公民”并没有什么关系,以及与“函数提升”也没有关系。“函数是一等公民”与“函数提升”之间,也没有关系。这些不要混着讲,他们是不同领域或不同层面的概念。——在第22讲的时候,我会略略地提及到“一等公民”的意义的。

    最后,函数(作为声明文本),与函数的引用(作为一个变量名),以及函数的实例(作为一个对象),还有作为函数的闭包(是一个环境/作用域的映射/上下文的参考),这些概念之间有相关性,但并不是一一对应的。例如我常常说的,一个函数的实例,可以有多个闭包。——关于这个示例,你想一下下面这种调用就明白了:

    ```
    function f() {
      // 当前闭包,return当前函数实例,而在外部又调用该函数实例再创建了一个闭包
      // 因此一个函数实例就有了两个闭包(递归就是更多个了)
      return arguments.callee;
    }

    // (类似递归,)与递归的效果是相同的
    f()();
    ```

    
     3
  • qqq
    2019-12-30
    eval 的间接调用会使用全局环境的对象环境,所有绕过了严格模式,是不是呀

    作者回复: :) 对的。

    但还可以有更深入的分析,把这些东西讲透,也是下一讲的主要目的。

    
     2
  • 晓小东
    2020-01-03
    老师我对环境理解如下您看是否正确:
    环境的创建发生在代码文本的解析阶段,生成可执行代码前, 引擎就创建好了。
    比如: 我在代码文本中, 声明了同名var与let ,就会发生语法错误。

    问题1: 环境应该也有类似(执行上下文)作用域链类似链式的结构,可以查抄上一级环境及下一级环境。(老师我现在可以理解作用域理解上为什么会有歧义了,作用域有静态环境下,还有动态执行环境下,所谓作用域就是为了管理某个东西而存在,只是不同环境下所管理对象不同而已。不知是否可以这样理解)。

    2. 对于执行上文中包含词法环境与变量环境的理解:
    既然环境创建发生于代码文本解析阶段, 引擎执行这段文本之前,是否就意味着,这段文本所创建的环境,执行阶段是不能发生变更的,(应该可以增加,比如动态脚本载入,会扩展全局环境节点)
    而eval中文本是不能解析的,所以只能在执行阶段去动态解析。
    所以既然环境无法变更,就只能在当前可执行上下文中来维护登记这么一个动态生成变量环境,与词法环境,相对应还有一个变量列表和词法列表(不知道是否属于同一个东西,相对于变量环境与词法环境, delete x 可以被清除那些, 还有全局上下文变量溢出的x)

    3. 引擎在编译阶段确定标识符的位置,优化标识符查找性能,是否跟环境创建有关, 或者环境创建就是一个独立目的,本质就是一个名字表的影射。

    4. with跟eval类似, 因为with 和eval 本质都是接收一个参数, 变量, 所以只能动态的去创建所对应环境,引擎无法前期做标识符查找的优化, 大量eval和with会拖拽引擎的执行速率。

    展开

    作者回复: 哈哈。问题的密度好大。^^.

    首先,环境的确是在执行之前创建的,但比语法分析阶段要略晚。环境是“因为要执行,所以才创建的”。而语法分析,与执不执行并没有关系。当一个东西(例如全局的代码块)需要执行时,引擎才会创建它对应的环境。

    当然除了4个与执行直接相关的环境之外,其它的环境是“不用于执行的”。——这个在文章中一再区分它们,就是因为历史中它们混淆得非常厉害。不用于执行的环境,只是说它们与“某个执行上下文不直接关联,不是用来作为context.VariableEnvironment或context.LexicalEnvironment的。但是,它们仍然也是“因为某个东西要被执行,才会被创建出来的”,只是“不直接关联给context”而已。

    关于问题1,环境确实也是链式的,它的创建接口类似于:
    ```
    aEnv = new DeclarativeEnvironment ( outerEnv )
    ````
    所以你在“问题2”中的推论也就正确:缺省情况下,环境内的内容是不应该改变的。但JavaScript又允许“eval()”来动态地添加(以及使用var来动态删除)变量,所以才会有了“词法环境和变量环境(VariableEnvironment and LexicalEnvironment)”双设计,因为其中的VariableEnvironment就是交由eval()来动态操作的。——所以“Eval环境”才与众不同。
    所以总的来说,你的“问题2”中的理解是对的,方向也没错。只是你不敢大胆确认而已。^^.

    关于问题3,是独立目的。本质就是名字表的影射。事实上在引擎内部,从parser阶段得来的语法分析树是另外的一个结构,那个分析过程也“可以有(不一定会有)”标识符冲突之类的判断,与环境中的检测无关。环境,其实是基于这个语法分析结果的一个映射,是为执行服务的。

    关于问题4,确实很类似。它们都创建了新的环境。不过with(x)创建的环境不用于执行,它的context(用于管理一个独立的代码片断)仍然是当前函数或全局的;而“Eval环境”是用于执行代码的,它有自己的context。eval和with都会降低效率。

     1
    
  • 许童童
    2020-01-01
    老师的文章确实是非常有深度的。和其它的文章是完全不一样的。

    作者回复: 多谢多谢。^^.

    
    
  • 晓小东
    2019-12-31
    老师还是接上一个问题如下(我试着在之前打印下变量b):
    const a = 100;

    console.log(b);

    let b = 200;
    var a1 = 300;

    为什么console.log(b); 不是出现暂时性死区报错 ReferenceError: Cannot access 'b' before initialization
    而是 Uncaught ReferenceError: b is not defined
    执行脚本前貌似只处理varDecl(变量声明) lexicalDecls(词法声明)没有处理, 与函数环境有所不同,global全局环境的这种处理方式,有什么好处呢(或者出于什么目的,而这样设计,或者就是对象环境与声明环境这种复合环境的特性)。
    展开

    作者回复: 'b is not defined'应该是一个不太友好的错误提示。但是ECMAScript只规范了错误的类型,并不规范错误的提示,所以……不同的引擎在这上面的表现确实不一样哦,也没有什么道理可言哦~

    “执行脚本前貌似只处理varDecl(变量声明)lexicalDecls(词法声明)没有处理”,这样的理解不太正确。因为引擎对这两种都是处理的,只不过var变量是绑定了初值的,而let/const不绑定初值(直到执行到它们声明并赋值的那行语句为止)。由于后者不绑定初值,所以出错提示“Cannot access 'b' before initialization”才是正确的意思,并且你按照这个来理解varDecl(变量声明)lexicalDecls(词法声明)就好了。

    至于“b is not defined”,老实说我之前也发现这个东东,我觉得这样提示不对,但没可奈何不是么。:(~

    
    
  • 晓小东
    2019-12-31
    老师我遇一个问题,我在刷一个面试题,说全局环境下let,const 没有在window属性下面, 我用chrome Source面版创建一个test.js, 测试了下, 代码如下:

    const a = 100;
    let b = 200;
    var a1 = 300;

    在右侧scope中发现两个scope
    Script:
       a:100
       b: 200
    Global:
        a1: 300

    非严格模式Script貌似跟eval执行有些类似
    > ScriptCtx.VariableEnvironment === globalCtx.LexicalEnvironment
    true
    > ScriptCtx.VariableEnvironment === ScriptCtx.LexicalEnvironment
    false

    但在严格模式下却又与eval不同 与非严格模式貌似没有区别

    可不可以介绍下浏览器执行环境下Script相关执行环境与上下文知识点。

    chrome-devtool > source > Snippets> test.js
    展开

    作者回复: 是这样的,全局环境GlobalEnv是一个复合环境,包括一个由global构成的对象环境(objEnv)和一个一般的声明环境(declsEnv),它是双环境组成的,统一交付一个环境存取界面。这个是它的结构(objEnv/declsEnv很类似或者是直接对应你在DevTools's scope中看到的Global/Script)。

    let/const声明会放在declsEnv里面,而var的变量名会通过objEnv来声明,这个之前也讲过了。它们组合而成的,整个的环境叫GlobalEnv,并且用来作为ScriptCtx中的变量环境和词法环境,并且后面的两种环境都指向“同一个”GlobalEnv,没有差别,也不会分开。——再次强调,ScriptCtx的“变量环境和词法环境”是相同的。

    但是你在使用eval的时候,对“变量环境和词法环境”的使用却是有所不同的。eval会自己创建一个执行上下文,称为evalCtx。那么这个上下文中也有“变量环境和词法环境”,它们指向却是不同的。evalCtx.LexicalEnvironment将是新创建的、独立的,用来存放eval(x)的`x`中声明的const/let,并且当eval结束时,就销毁了。而evalCtx.VariableEnvironment将指向全局的ScriptCtx.VariableEnvironment,这样也就指向了GlobalEnv。所以在`x`中声明的var,就丢到了全局环境中,也就写到了global这个对象上面,不会受eval结束的影响。这就是它的完整机制了。

    你可以写一个test.js用node运行起来看看:
    ```
    // 运行中x, y有值
    eval('var x = 100; let y = 200; console.log(x, y)');

    // eval结束后x留在了全局,而y没有了
    console.log(typeof x, typeof y)
    ```

    关于在浏览器环境中的情况,也与这个逻辑没差。只不过DevTools展示那些内部组件(例如Script/Globa/Scope)的时候,使用的术语或者展示方式跟ECMAScript约定不一致罢了。

    
    
  • Astrogladiator-埃蒂...
    2019-12-30
    (0, eval)("x = 100")
    我用typeof (0,eval) 显示这个是函数类型,应该是一个立即执行的函数
    类似 (function(params){})(params);
    "use strict" , eval)("x = 100")
    我用typeof (0,eval) 显示这个是函数类型,应该是一个立即执行的函数
    类似 (function(params){})(params);
    "use strict" 是不是说只是限制了当前上下文的声明环境,但在这个闭包构造的声明环境中并不受此限制?
    展开

    作者回复: 不是的,“use strict”其实并没有这样的作用。关于这些,请期待下一讲。^^.

    
    
我们在线,来聊聊吧