• blacknhole
    2019-11-15
    从内容上其实已经说清楚了,不过在内容表达上还是会让人产生困惑,我觉得问题是出在“当前上下文中的那个变量a”和“原始的变量a”这样的表述方式上。或许如下表述在语意上会更加清晰:

    1,这里其实只有一个变量,就是a,不存在那个变量a和这个变量a之分,有分别的其实是变量a的值,即“变量a过去的值”和“变量a现在的值”。

    2,当发生第一次赋值时,“左操作数a作为一个引用被覆盖”,此时变量a产生了新的值。

    3,第二次赋值时,“整个语句行的最左侧‘空悬’了一个已经求值过的‘a.x’”,这是一个表达式结果,这个结果以及其中保留的“a”(即“变量a过去的值”)与变量a已经没有关系了,因为变量a已经有了新的值,即“变量a现在的值”。

    4,第二次赋值其实是,在“变量a过去的值”那个对象上,创建一个新属性x,x的值为变量a的值,即“变量a现在的值”。

    5,在第二次赋值后,因为“变量a过去的值”那个对象已经不再被任何变量持有,所以它已经无法被访问到了,它“跑丢了”。

    是这样吧?
    展开

    作者回复: 赞的!就是这个意思。呵呵~

     5
     34
  • 蓝配鸡
    2019-11-18
    不明白为什么a.x 这个表达式的result是一个a的引用呢?

    不应该是 undefined吗?

    没明白...

    作者回复: Result是引用。
    value是undefined。

    value = GetValue(Result)

     1
     4
  • sprinty
    2019-11-15
    老是您好:我理解的指针和引用是,指针是存储的地址,引用是存储的别名。

    在 js 中的“引用”与传统语言中的“指针”有哪些根本性的区别。

    作者回复: 其实我早期也是这么理解的。好象大家理解事物的方式都差不多,就是从相似性出发,从差异性辨别。

    但是我后来发现,与其如此,不如为新东西建个体系,然后在新体系中来看待这个新事物。这一下子就不同了。

    以至于我现在对引用的认识,就不太依赖与比较或比拟。引用就是引用,它就是一个计算的结果,它存放结果中包括的那几个东西。它是一个数据结构,用在引擎层面来存储计算过程的中间信息,以及在连续计算中传递这些信息。

     1
     4
  • 天方夜
    2019-11-18
    1. with ({x:100}) delete x 中 delete 删除的是对象的成员,即 property x;
    2. (0, eval) 之中有一步逗号运算;
    3. 表达式 (a.x) 的计算结果是 a 对象的属性 x 这个引用,所以可行;
    4. with 只指定属性查找的优先级,所以 with 里面 x = 100 还是会泄漏到全局。

    作者回复: 第2个不太完整。不过总体满分😃
    第二个涉及的问题到20讲才开讲呢^_^

    
     3
  • 青史成灰
    2019-11-16
    老师上面引用《JavaScript权威指南》中说“JavaScript总是严格按照从左到右的顺序计算表达式”,那为什么下文的2次赋值操作`a.x = a = {n:2}`,是先赋值`a={n:2}`,然后才是`a.x = a`呢

    作者回复: 这个顺序是这样来读的(你仔细看看顺序是不是从左至右):


    第一次
    ======
    a.x = a = {n:2}
    ^1 ^2

    第二次
    ======
    a = { n: 2 }
    ^3 ^4

    第三次
    ======
    { n: 2 }
    ^5 ^6

    第四次(以下求值然后回传)
    ======
    求值传回(4)
    @4 <= ^5, ^6

    第五次
    ======
    求值回传(3)
    @3 = (^4 <= ^5, ^6)

    第六次
    ======
    求值回传(2)
    a = @3 = (^4 <= ^5, ^6)

    第七次
    ======
    求值回传(1)
    a.x = a = @3 = ...

     1
     3
  • Lambert
    2019-11-15
    “a.x”这个表达式的语义是:
    计算单值表达式a,得到a的引用;
    将右侧的名字x理解为一个标识符,并作为“.”运算的右操作数;
    计算“a.x”表达式的结果(Result)。
    老师请问一下 这个时候 的 Result 是 undefined吗? 因为还没有进行赋值

    作者回复: 这个时候的Result是一个“引用(Reference)”。

    如果它在后续运算中被作为lhs,例如 a.x = ...,那么它就是作为“引用”来使用,这样就可以访问到`x`这个属性,并置值;如果它在后续运算中被作为rhs,例如console.log(a.x),那么它就会被GetValue()取值(并作为值来使用),于是console.log()就能打印出它的值来。

    a.x整体被作为“一个操作数”,它的用法与它被使用的位置是有关的。但是“得到它(亦即是对a.x这个表达式求Result)”的过程并没有什么不同。

    你可以读一下这个“.”操作在ECMAScript中的规范:
    https://tc39.es/ecma262/#sec-property-accessors-runtime-semantics-evaluation

     2
     3
  • 陈伟
    2019-11-15
    有的地方描述有点晕,看了好几遍才明白表述的意思,要是有一些动态的图演示的话可能效果更好点

    作者回复: 这个……确实实现起来有难度。我通常在做讲演稿的时候才会用这种方式,但讲演稿的讲法,跟这里的课程的讲课方法区别还是很大的。

    当然,即使不用动态的图,使用流程图或框线图其实也挺好的。不过,总之,以极客时间的“语音课程”来说,很难讲。——话说回来,如果是需要更深的阅读,以及更丰富的图例,以及表格等表现形式,那么可以看我的书哦。《JavaScript语言精髓与编程实践》这本书的第三版……快要出版了吔~ ^^.

     2
     2
  • GitHubGanKai
    2020-01-11
    老师你好,有个问题想要请教一下你,就是MDN中:typeof 操作符返回一个字符串,表示未经计算的操作数的类型。那么这句话中的 ‘未经计算的操作数’是什么意思呢?这个‘未经计算的操作数’有哪些类型呢?而且这个typeof的返回值,返回的应该不是一种类型吧!因为用typeof检测类型的时候可能返回 'function',但是function又不属于数据类型,是不是有点矛盾呢?

    作者回复: 我之前并没有听过关于这个 ‘未经计算的操作数’的说法。因此我特地地看了一下MDN中的相关说明。

     ‘未经计算的操作数(unevaluated operand)’这个,其实也并没有特别的难解。例如有一个值是2的常量x,对于这个`x`,如果它“计算了”,那结果当然就是2,对吧。那么这种情况下,“未经计算时的x”是什么呢?

    这个其实还是我们在文章中说的“引用(规范类型)”。“引用(规范类型)”作为左手端时,只是引用,并不求值,这种情况下它就是`unevaluated operand`。所以,一个错误的、根本不存在的引用也可以被typeof操作,因为这个“错误的、根本不存在的”并没有被“计算”,所以也就不会抛出错误。例如你试试:
    ```
    typeof adfasdljkfla; // <- 随便一个变量名
    ```
    之所以没有异常发生,就是后面的`adfasdljkfla`这个东东“未经计算”。同样的,如果我们尝试下面的代码:
    ```
    typeof(adfasdljkfla); // 同上例
    ```
    这里其实多了一个操作符,就是一对括号表示的“分组运算符(grouping)”,这个运算符也是“返回未经运算的结果”。所以同样,不会出错。——在我们这个系列的文章中,这种情况称为“引用(规范类型)”,或者一个“(未决定操作手性rhs/lhs的)结果Result”。

    还有你的另一个问题:
    > 而且这个typeof的返回值,返回的应该不是一种类型吧!
    这个是其它类型的语言来理解函数式语言的一个常见误区。尤其是,如果你以传统的(经典的)数据结构的知识为基础,那么更是会有误解。

    在JavaScript中,以及在函数式语言中 ,“函数”的确是一种数据类型。它可以作为值(在函数界面上)传递,也可以作为结果(在函数返回中)传出,还可以查看类型,还可以与其它数据进行运算(例如 1 + (function(){})),那么它为什么不是“一种数据类型”呢?

    函数既是数据,也是运算,这个是函数式语言的核心概念。

    
     1
  • Ronnie
    2019-11-26
    a.x= ...., 这个是不是可以这样理解, 在求值a.x=..., 时候,由于x 在原来变量里不存在,所以js engine,首先会create 一个新的属性x,然后把这个x 的引用作为赋值的左操作数,然后依据右边的操作数完成赋值,但右边的a={n: 2}是一个表达式,赋值完成后,返回的是a 的新值{n: 2}, 这样赋值后 x 就等于{n: 2}, 由于x所在的对象{n: 1, x: {n: 2}} 现在没有引用(很快会被回收)也就没法访问了。

    作者回复: “所以js engine,首先会create 一个新的属性x”。——这个操作是不会发生的。如果这样做,那么意味着“访问不存在的属性”是一个“创建属性”的行为,这样一来,系统的负担/开销就大得不得了了。

    所以我在讲述这里的时候,说的是“空悬”了一个引用。就是这个意思。除了没有“创建属性”这样的隐式行为之外,你其它的描述是没什么问题的。

     1
     1
  • 18625322963
    2019-11-19
    按照权威指南那句话,是不是可以这么理解,先执行a.x = {},此时a指向内存的值为{x: x}.然后执行a = {},因为原a指向的内存是对象,可以理解为此时的a是被重新定义了,而原a引用为0,销毁了,如果有一个b曾经指向原a,那么此时b 引用 原a,原a指向的内存没有被销毁, 而a等于新a ,相当于重新创建了一个值保存

    作者回复: 请参见对“青史成灰”的留言的回复。^^.

    
     1
  • 铭
    2019-11-16
    反反复复看了几遍,留言区里帮我屡清了思路。

    第一句:
    var a = {n : 1};
    // 变量声明,变量a作为引用,最终指向了等号右侧表达式的计算结果,即一个对象{n : 1}

    第二句:
    a.x = a = {m : 2};
    // 两个等号划分了3个表达式(宏观上);
    // a.x... 要为a添加x属性的蠢蠢欲动,缓存a,a = {n : 1};
    // a.x = a... 没有做赋值操作!如果代码写到这截止,事实上会报一种错,叫Error: Maximum call stack size exceeded
    // a.x = a = {m : 2}; 做了两次赋值操作,首先后半段先做赋值操作,a的引用指向了新的对象{m : 2},第二次赋值操作完成了为之前缓存的a添加x属性的如愿已久,x的引用指向后面的这个完成了初始化的a。现在,我们去使用a,实际上使用的是后面的这个a,a = {m : 2},那之前缓存的那个a呢?被引擎吃掉了,无法访问到。那它指向哪个对象呢?{n : 1, x : {m : 2}},理由是一次初始化和一次属性拓展。
    展开

    作者回复: 除了“a.x = a”导致栈异常之外,这个好象不太对。其它应该没什么问题了。

    
     1
  • 旺旺
    2019-11-15
    JavaScript果然太灵活,然后感觉好难啊
    
     1
  • 许童童
    2019-11-15
    老师讲得真细啊,学到了很多,谢谢老师。
    
     1
  • Wiggle Wiggle
    2019-11-15
    那么“引用”这个数据结构究竟是什么样子呢?在引擎内部是如何实现的呢?老师可否讲一下或者给个链接?

    作者回复: https://tc39.es/ecma262/#sec-reference-specification-type

    ^^.

    
     1
  • Smallfly
    2019-11-15
    文章读起来挺吃力的,可能是 JS 很多设计跟固有思维不一致,也可能是对 EMACScript 规范不了解,老师能否考虑下放文章中涉及到的规范地址?

    作者回复: 好主意!我问问编辑能怎么改。
    后面的内容我尽量都加上。多谢提议!

     1
     1
  • 夜行观星
    2020-01-10
    看到留言反馈,跟我预想的一样,讲语言绕过编译相关的知识是在难讲,第二节和第三节不理解,就容易混淆。其实上面的内容拿JS的 EBNF 语法,对应着生成AST,逐步讲解会不会更好呢?突然冒出来一两句EBNF让人很突兀。
    
    
  • wDaLian
    2020-01-05
    我是将整个代码分成三个案例来看
       1. 首先案例一中先执行了c.a 因为这是一个表达式,然后紧接着执行了b(这个时候b也是表单式吧个人不确定希望老师告诉一下),然后执行了{'a':1},接着从右往左一次赋值就得到案例一的结果
        2.案例二也是一样先执行表达式d.a ,然后依次 d ,{'a':1},然后又依次从右往左赋值,但是d先变成了
    {‘a’:1},此时d的指向已经变了不是之前的指向了,然后该给d.a赋值,但是d.a在表达式执行的时候d是上一次
    的内存指向,赋值发现之前的d已经没有所以不能给d.a赋值,但打印的时候打印的其实是现在新的指向d因此是1
        3.案例三就是老是说的案例,我理解就是还是一样依次执行e.a,e,{‘ee’:1}然后又从右往左赋值,又到了e.a的时候发现e的指向已经变了,原来的e没有了因此不能赋值了,但是下面打印的e.a其实新的指向也就是{'ee':1},因为这里确实没有'a'这个key 所以打印就是undefined

        // 案例一
        var c = {};
        var b= {}
        c.a = b = {'a':1};
        console.log(c.a); // {a: 1}
        
        // 案例二
        var d = {};
        d.a = d = {'a':1};
        console.log(d.a); // 1

        // 案例三
        var e = {};
        e.a = e = {'ee':1};
        console.log(e.a); // undefined

    不知道说的对不对希望老师 帮看一下我的理解
    展开

    作者回复: 你的整个理解过程是对的。

    不过在案例3里面,“原来的e没有了因此不能赋值了”这个说法不对的。原来的e还是在的,在这个赋值操作中它也成功赋值了,只是赋在“原来的e”上面(所以我称为左侧“空悬”了一个e),而你又没有办法找到“原来的e”,所以看不见这个结果罢了。

    你只需要为“原来的e”添加一个引用,就能观察到它了。比如声明为:
    ```
    var x;
    var e = x = {};
    ```
    然后你能从x.a观察到“原来的e”的赋值效果。

    这也是在这一讲里面讲到过的方法。^^.

    
    
  • 情诗和你
    2019-12-27
    python 也是类似的诶,这个很有趣
    
    
  • AIMD
    2019-12-26
    我觉的可以这样理解,
    var a = {n:1}, ref = a;// 第一行
    a.x = a = {n:2};// 第二行
    console.log(a.x); // --> undefined
    console.log(ref.x); // {n:2}

    1.首先声明了a变量,里面放了一个对象(即a容器里面放了{n:1})
    2.成员访问运输符的优先级高于赋值操作符,所以a.x = a = {n:2};中的a.x会被运算求值为一个引用结果( a = { n:1
               x: undefined
    })即这个对象的地址,此时a.x已经不在是容器,就是一个Result
    3.变量容器同步,此时a对象存放着( a = { n:1
               x: undefined
    })
    4.接着从右自左执行赋值操作 ,此时a对象变成了
    a = { n: 1
             x: {n:2}
    })

    5.因为a.x已经不是容器,无法赋值为最新的状态,所以保留原来的Result,在访问a.x就是undefined ;
    6.因为ref和a是同一个引用,所以ref.x就是第4步骤最新状态的a.x的值
    展开

    作者回复: 对的。确实是这样。^^.

    
    
  • Geek_8b9ecb
    2019-12-18
    老师好,为什么root会是{index:'NONE',next:{index:10,next:{index:9,...}}}

    作者回复: 这个……这一节讲的不就是为什么么?这个例子就是一个应用了(当然我其实不建议这么用),所以答案还是得从这一课的内容中找。。。

    
    
我们在线,来聊聊吧