Replies: 7 comments 15 replies
-
1, 2 隔靴搔痒,3 以及原 proposal 要面对的非正交的东西太多…… |
Beta Was this translation helpful? Give feedback.
-
@Huxpro 我们先要对do expression有个预期,这个东西到底是为了解决什么问题?其实如果考虑语言能力的话,它本来就是没有很大必要的,它的存在只是为了开发者体验——所以它本来的目标就是「搔痒」而已。那么我们的问题就是别越搔越痒甚至挠破就好了。比如我认为从表达式里进行流程跳转就是「挠破」了。😂 |
Beta Was this translation helpful? Give feedback.
-
“我们先要对do expression有个预期,这个东西到底是为了解决什么问题?” 我觉得这个问题非常根本,我尝试探讨一下,这个根本需求似乎是随着块级作用域(其次是 TypeScript)的诞生而衍生出来的。换言之,该提案所试图解决的问题,本质上是新出现的块级作用域变量,和旧的JS语法中大量未考虑这一问题的历史 比如这两个例子是以前常见的代码: // x, y, z 都仅用于计算 value1 和 value2,后续步骤不再用到,属于临时变量
var x = 1;
var y = 2;
var z = 3;
var value1 = x + y + z;
var value2 = x * y * z;
value1;
value2 += 1; try { var value = fn(); }
catch { return; }
value; 而有了块级作用域后,我们自然希望这样写: import { value1, value2 } from {
const x = 1;
const y = 2;
const z = 3;
export const value1 = x + y + z;
export let value2 = x * y * z;
}
value1;// read-only
value2 += 1; import { value } from {
try { export const value = fn(); }
catch { return; }
}
value;// read-only 但是由于不能如此,因此只能用 let value1, value2;
{
const x = 1;
const y = 2;
const z = 3;
value1 = x + y + z;
value2 = x * y * z;
}
value1;
value2 += 1; let value;
try { value = fn(); }
catch { return; }
value; 但这会产生一些问题:
理论上,使用函数(无论外置还是IIFE)也能部分解决这个问题,但本质上这就等于倒退回了没块级作用域的时期,块级作用域的好处如下:
因此函数方案是显然不行的。而同样显然的是,do expression 从根本上有两大问题,意味着它好不了太多:
当然,解构赋值的问题并不属于该提案本身,是本就存在的问题,如果大家都不在乎……那 do expression 提案挺好的,只要增加一个语法关键词用于明确返回值即可……(只是 |
Beta Was this translation helpful? Give feedback.
-
跟本讨论主题无关,只是看到这个之后 import { value } from {
try { export const value = fn(); }
catch { return; }
} 想说一下,这代码其实有了 module blocks 之后,似乎是可能的: const { value } = await import(module {
try { export const value = fn(); }
catch { return; }
}) 不过我们当然不能用module block来干这个事情,因为那是杀鸡用牛刀,而且module block不允许访问环境变量。 |
Beta Was this translation helpful? Give feedback.
-
关于IIFE的问题,@LongTengDao 列了5点,其中关于性能开销的部分,可能并不完全成立,因为如果引擎能够识别IIFE模式,则可以做对应的优化,性能问题可能并不是最关键的。 心智负担部分,@LongTengDao 是否可以展开再分析一下? 另外第5点「函数会使得 break return 等语法作用目标被破坏」实际上是辨证的。考虑目前此类需求就是用IIFE写的,那么当do expression加入语言后,就可能会有许多从IIFE重构为do expression的情况。此时如果不能有等价于IIFE的return的能力(比如常见的guard模式下的early return用法),则反而使得重构很困难,而增加了开发者的困扰。 |
Beta Was this translation helpful? Give feedback.
-
好的,谢谢贺老鼓励引导!那我尝试抛砖引玉,阐述一下。 (一)关于性能
我确实完全忽略了 IIFE 理论上的性能优化上限,是相当于函数从未存在过! 像 但 4 是依然存在的,由于函数只能有一个返回值,所以多值时需要解构赋值,这个开销不省了它我实在是浑身难受(主要是写框架或库时,总忍不住作为挖井人,想替使用侧多做一点儿优化)…… (二)关于心智负担2、3 都是因为性能问题衍生出来的,既然那不再是问题,这也就成了脱裤子放屁的负优化了。简单来说,像这种代码: export default function () {
( function a () {} )();
( function b () {} )();
( function c () {} )();
} 我以前为了避免每次运行临时创建一个函数的开销,会外置: export default function () {
a();
b();
c();
}
function a () {}
function b () {}
function c () {} 可读性令人窒息(有时候这是解耦最佳实践,但强关联的局部代码就恰恰相反了),而且用不了上下文变量,要作为参数传递,就更窒息了。 (三)关于辩证首先澄清一点,我是明确坚持 do expression 应当有显式退出语句的(只是我觉得它不应该是 当然要支持类似 early return 的功能,否则绝对毁誉参半。我只不过认为通过别的关键字,可以达到二者并存的目的。(既拥有超越 IIFE 的外层逻辑流 early return 的能力,又有和 IIFE 一样自身 early return 的能力。) 小结既然 IIFE 的性能代价可以无视,那么 do expression 如果还有必要存在,它存在的意义无非就是这两点额外优势了(如果没有就成鸡肋了,总不能只是为了作为比 IIFE 省几个字符的语法糖存在吧,在这个意义上,我认为 iife-based 可行,但没有意义):
至于万物皆表达式,这完全颠覆了 JS 这门语言的语法基础(语法结构体>语句>表达式,并在函数表达式(现在要加上do表达式)内轮回)。如果这样做,第一,认知成本可能比学新语言还高;第二,如果改革到这种程度,那一切语法的实用性和最佳实践都能且要重估了,就别提什么 do expression 这种小补丁了。 综上,IIFE-based 路向的非 return 关键字的变种,可能是有意义且可行的发展方向。 |
Beta Was this translation helpful? Give feedback.
-
前文贺老提到 do 表达式要阅读到最后才能知道是不是一个 do while 的问题。仔细想想,不换关键字也行,其实 |
Beta Was this translation helpful? Give feedback.
-
do expression 这个提案在stage 1上也停滞很久了(3年),在下次的会议上,有人想推它到 stage 2。见 slide:https://docs.google.com/presentation/d/14UYf30NeOd5TFZ4QJFigwBLZVotOwuQq3E-BCMIhGgk/
我认为这个slide很好地展示了 do expression 面临的核心问题,就是隐式的返回值在各种奇怪的情况下不符合程序员的预期,尤其是循环和变量声明,还有流程跳转(return/break/continue等)。这个更新提案给的解决方案就是把所有这些奇怪的情况都 ban 掉(syntax error),作为一个 Maximally minimal 的解决方案,这个方法是可行的,因为可以留待以后再讨论。不过还有另一个大问题是语法,这个slide的方案并没有涉及(仍然延续了
do { ... }
)。我一直认为,语义和语法是要相适配的。所以我把两个问题一起讨论。
do expression 的语义,本质上有两种解法。
eval
-based一种是基于现有定义的隐式值。spec已经对每个js语句的隐式值有明确定义。这个语义目前可以通过 eval 观察到。即
eval(code)
所返回的就是代码code
执行后得到的隐式值。因此我们有方案一,直接用eval { ... }
语法和基于eval(code)
的语义。也即
等价于
如果是这样的方案,那么我们可能不需要ban掉任何东西。其中,return/break/continue等在原本提案中有争议的跳出到scope以外的行为,本来就是不允许出现在 eval 中的,所以已经自然地被 ban 掉了。剩下的循环和声明的行为,虽然可能奇怪,但是因为
eval(code)
是已经存在的东西,我们只不过提供了一个更加静态化的eval(从而有更好的浏览器优化,也不受到CSP禁用eval的影响)。如果说一定要考虑程序员预期,得ban掉它们,那么可以留给linter去ban。iife-based
另一种是基于IIFE。即把
let result = do { ... }
理解为let result = (() => { ... })()
(这里用arrow function而不是普通函数,是因为arrow functions中this
是lexical的,更符合do expression场景的预期)。这种语义的好处就是所有js程序员都已经掌握,没有任何难理解之处。本质上,这种方式就是一个语法糖而已。
如果按照这种语义,我们的语法设计最好也能至少有一点能暗示 arrow function。以下使用
{> ... }
语法作为示例(以>
暗示arrow function)。其他语法,只要能让程序员联想到函数,也都是可以考虑的。也即
等价于
注意我们需要显式使用
return
关键字,而不再隐式使用最后一个值。(我个人不认为节约一个return
关键字有特别巨大的价值,尤其是,如果我们使用基于iife的语义,保持和iife的语法对应是更有价值的。如果我们就是不想要return
关键字,我们最好是回到eval
-based方案。)使用显式
return
,我们也不再需要ban掉任何东西(包括跳出scope的break/continue,本来就是不允许的)。采用这种语义,也使得未来可能的扩展更容易理解。比如
程序员很容易预期result应该是一个promise,因为本质上这就是一个async函数的iife结果。
第三种解法:一切皆表达式
最后,其实还有第三种解法。
如前所述,所有statement已经有可观测的值(通过
eval
),那么从某种程度上说,它们已经是表达式,只不过在语法上需要被进一步放宽,允许出现在所有表达式可以出现的地方。比方说,对于if
来讲,有人就提出,可以直接支持let x = if (x) 1; else 2
。这当然是可以的。不过我们有两个问题,第一,这只适用于if
,没有普遍性,更多语句怎么办(比如let x = do { let tmp = Math.random(); tmp * tmp }
这样简单的例子)?第二,当我们看到分号的时候,我们觉得语句应该结束了,但后面跟着的else
打破了这一点。其实这两点本质上都是一个事情,就是
;
是一个优先级更高的东西(实质上可认为是优先级最高的),表达式没法(不应)超出分号的界限。但我们本来就有一个改变优先级的东西,那就是括号。所以可以简单地允许括号适用于所有表达式(包含语句——因为语句现在也成了表达式)。于是:
本质上,这是把
;
变成了分号表达式——增强版的逗号表达式(区别是在优先级列表上,逗号表达式的优先级最低,而分号表达式的优先级几乎最高——仅次于括号)。采用这种方案,其语义应该和
eval
-based 是一样的,但没有eval
的前例之后,是否要ban掉某些东西就都要费神去考虑。当然这也带来了最大的语义可能性,因为eval
-based和iife-based都不应超出原本的语义限制。这种方案的另一个问题是将来若要支持async,就不太能直接用
let x = async (expression)
(因为现在会被解析为对一个名为async的函数的调用),而需要特殊的语法,比如(async: expression)
。另外的语法方案是,仍然使用基于花括号的语法。之所以不能
let v = { if (x) 1; else 2 }
是因为当parser看到{
的时候已经认为这是一个 object literal 了。注意 parser 是无法根据后续的token来主动区分 block 和 object literal 的,因为{a: 1, b: 2}
也可以被解释为两个带有label的语句所构成的block。所以需要在{
前后加入额外符号以区分。可能的方案比如使用双花括号let v = {{ if (x) 1; else 2 }}
或转义let v = \{ if (x) 1; else 2 }
,虽然从个人角度,这样的语法似乎还不如用括号。当前的提案语法do { ... }
可被视为类似的(不使用额外符号,而是使用关键字的)变种。do {}
语法的问题主要是当和while
循环混用时会有一些诡异的结果。对于parser来说是没有问题的,但是对于人来说,我们希望在do {
开始的地方就知道这是一个 do expression 还是一个do ... while
。一句话总结:do expression 的问题主要是语义预期和适配于语义的语法设计上。Maximally minimal 的方案可以暂时规避这些问题(选择了最小的语义子集),但即使可以由此进入 stage 2,在 stage 3 之前仍然需要厘清关键的方向性问题。
Beta Was this translation helpful? Give feedback.
All reactions