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.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
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