有趣的JavaScript:无花括号

图片


首先, JavaScript总是让我感到惊讶,因为它可能不像其他广泛使用的语言同时支持两种范式:正常编程和异常编程。 而且,如果几乎所有有关适当的最佳实践和模板的内容都已被阅读,那么关于如何不编写代码而是可以编写代码的奇妙世界仍然微不足道。


在本文中,我们将分析另一个需要正常解决方案不可原谅的滥用的任务。


上一个任务:



措辞


实现一个装饰器函数,该函数计算对传递的函数的调用次数,并提供按需获取此数字的功能。 该解决方案不使用大括号和全局变量。

呼叫计数器只是一个借口,因为其中有console.count() 。 最重要的是,我们的函数在调用包装函数时会累积一些数据,并提供访问它们的特定接口。 它可以保存呼叫的所有结果,收集日志以及某种形式的备忘。 只是一个反面-每个人都可以理解和理解。


所有复杂性都处于异常限制中。 您不能使用花括号,这意味着您必须重新考虑日常做法和常规语法。


习惯性解决方案


首先,您需要选择一个起点。 通常,如果语言或其扩展名没有提供必要的修饰功能,我们将自行实现一些容器:包装函数,累积数据以及用于访问它们的接口。 这通常是一类:


class CountFunction { constructor(f) { this.calls = 0; this.f = f; } invoke() { this.calls += 1; return this.f(...arguments); } } const csum = new CountFunction((x, y) => x + y); csum.invoke(3, 7); // 10 csum.invoke(9, 6); // 15 csum.calls; // 2 

这不适合我们,因为:


  1. 在JavaScript中,您不能以这种方式实现私有属性:我们既可以读取实例的调用 (我们需要),也可以从外部写入值(我们不需要)。 当然,我们可以在构造函数中使用闭包 ,但是类的含义是什么? 而且我仍然会害怕在没有babel 7的情况下使用新鲜的私有领域
  2. 该语言支持功能范例,通过new创建实例似乎不是此处的最佳解决方案。 最好编写一个返回另一个函数的函数。 是的
  3. 最后, ClassDeclarationMethodDefinition的语法将不允许我们删除所有花括号。

但是我们有一个很棒的模块模式 ,可以使用闭包实现隐私:


 function count(f) { let calls = 0; return { invoke: function() { calls += 1; return f(...arguments); }, getCalls: function() { return calls; } }; } const csum = count((x, y) => x + y); csum.invoke(3, 7); // 10 csum.invoke(9, 6); // 15 csum.getCalls(); // 2 

您已经可以使用此功能。


有趣的决定


为什么在这里使用牙套? 这是4种不同的情况:


  1. 定义计数函数的主体( FunctionDeclaration
  2. 初始化返回的对象
  3. 具有两个表达式的调用函数体( FunctionExpression )的定义
  4. 使用单个表达式定义getCalls( FunctionExpression函数的主体

让我们从第二段开始。 实际上,在通过invoke使最终函数的调用复杂化的同时,我们无需返回新对象。 我们可以利用JavaScript中的函数是对象这一事实,这意味着它可以包含自己的字段和方法。 让我们创建return df函数并向其中添加getCalls方法,该方法通过闭包可以像以前一样访问调用


 function count(f) { let calls = 0; function df() { calls += 1; return f(...arguments); } df.getCalls = function() { return calls; } return df; } 

使用它会更令人愉快:


 const csum = count((x, y) => x + y); csum(3, 7); // 10 csum(9, 6); // 15 csum.getCalls(); // 2 

第四点很清楚:我们只是用ArrowFunction代替FunctionExpression 。 如果主体中只有一个表达式,则不使用花括号将为我们提供箭头功能的简短记录:


 function count(f) { let calls = 0; function df() { calls += 1; return f(...arguments); } df.getCalls = () => calls; return df; } 

对于第三个 -一切都更加复杂。 请记住,我们所做的第一件事是使用FunctionDeclaration df将Invoke函数替换为ExpressExpression 。 要将其重写为ArrowFunction,必须解决两个问题:不失去对参数的访问权限(现在它是arguments的伪数组),以及避免两个表达式的函数体。


第一个问题将帮助我们解决使用spread operator为函数参数args显式指定的问题。 要将两个表达式组合为一个,可以使用逻辑AND 。 与返回布尔值的经典逻辑合取运算符不同,它从左到右计算第一个“ false”的操作数并返回它,如果全部为“ true”,则返回最后一个值。 计数器的第一个递增将使我们为1,这意味着此子表达式将始终转换为true。 我们对第二个子表达式中的函数调用结果的“真实性”的可约性不感兴趣:无论如何,计算器都将停止在该位置。 现在我们可以使用ArrowFunction


 function count(f) { let calls = 0; let df = (...args) => (calls += 1) && f(...args); df.getCalls = () => calls; return df; } 

您可以使用前缀增量来修饰记录:


 function count(f) { let calls = 0; let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; } 

解决第一个也是最困难的一点的方法是, ArrowFunction代替FunctionDeclaration 。 但是我们的身体仍然大括号:


 const count = f => { let calls = 0; let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; }; 

如果我们想摆脱框架主体的花括号,我们将不得不避免通过let声明和初始化变量。 我们有两个整体变量: calldf


首先,让我们处理柜台。 我们可以通过在函数参数列表中定义局部变量来创建局部变量,并通过使用IIFE(立即调用函数表达式)调用它来传递初始值:


 const count = f => (calls => { let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; })(0); 

仍然需要将三个表达式连接为一个。 由于我们拥有所有三个表示函数的表达式,这些表达式始终可以简化为true,因此我们也可以使用逻辑AND


 const count = f => (calls => (df = (...args) => ++calls && f(...args)) && (df.getCalls = () => calls) && df)(0); 

但是,还有另一个连接表达式的选项:使用逗号运算符 。 最好是这样,因为它不会处理不必要的逻辑转换并且需要较少的括号。 从左到右评估操作数,结果是后者的值:


 const count = f => (calls => (df = (...args) => ++calls && f(...args), df.getCalls = () => calls, df))(0); 

我想我设法骗了你? 我们大胆地摆脱了df变量的声明,只保留了箭头函数的赋值。 在这种情况下,该变量将被全局声明,这是不可接受的! 对于df ,我们在IIFE函数的参数中重复局部变量的初始化,但不会传递任何初始值:


 const count = f => ((calls, df) => (df = (...args) => ++calls && f(...args), df.getCalls = () => calls, df))(0); 

这样就达到了目标。


主题变化


有趣的是,我们能够避免创建和初始化局部变量,功能块中的多个表达式以及创建对象文字。 同时,原始解决方案保持简洁:没有全局变量,没有计数器隐私,可以访问要包装的函数的参数。


通常,您可以采用任何实现并尝试执行类似的操作。 例如,在这方面, bind函数的polyfill非常简单:


 const bind = (f, ctx, ...a) => (...args) => f.apply(ctx, a.concat(args)); 

但是,如果参数f不是函数,则应以一种很好的方式抛出异常。 并且throw异常不能在表达式的上下文中抛出。 您可以等待throw表达式(阶段2),然后重试。 还是现在有人已经有了想法?


或者考虑一个描述点坐标的类:


 class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `(${this.x}, ${this.y})`; } } 

可以用一个函数来表示:


 const point = (x, y) => (p => (px = x, py = y, p.toString = () => ['(', x, ', ', y, ')'].join(''), p))(new Object); 

仅在这里,我们失去了原型继承: toStringPoint原型对象的属性,而不是单独创建的对象。 如果您努力尝试,可以避免这种情况吗?


在转换的结果中,我们得到了功能编程与命令性hack和语言本身的某些功能的不健康混合。 如果您考虑一下,这可能会成为源代码的有趣(但不实际)混淆器。 您可以提出自己的“括弧式混淆器”任务版本,并在闲暇时利用JavaScript工作的同事和朋友娱乐。


结论


问题是,它对谁有用,为什么需要它? 这对于初学者是完全有害的,因为它对语言的过度复杂性和偏差形成了错误的观念。 但这对于从业者很有用,因为它使您可以从另一端看待语言的功能:调用不是要避免,调用是将来要避免。

Source: https://habr.com/ru/post/zh-CN428337/


All Articles