问题的故事:最短的JavaScript备注

图片


是在晚上,在圣彼得堡举行的年度HolyJS会议的前夕。 我们公司多年来一直是赞助商:因此,对于关心开发人员的好奇心,它也有自己的立场,并对此感兴趣。 当主课程准备好并且所有任务都由律师审查和完成时,我决定在晚上给同事们多吃一些有知识的食物:


编写一个备注器-装饰器函数,该函数保存执行包装函数的结果以防止重复计算。 您只有50个字符。

该语言当然是JavaScript 。 任务本身是经典的,但50个字符的限制变成了真正的挑战。


会议第一天的休息时间,我们讨论了实现目标,逐渐减少响应的选项。 所有炒作都以与会议的所有参与者共享任务的想法为基础,第二天我们将任务可视化(请参阅附录),并开始向想要的人分发表格。 结果,我们获得了大约40种解决方案,并再次使非凡的js开发人员社区信服,但是Dmitry Kataev的记录(SEMrush)仍然是53个字符。 让我们弄清楚!


惯常执行


function memoize(f) { let cache = {}; return function ret() { let key = JSON.stringify(arguments); if (!cache.hasOwnProperty(key)) { cache[key] = f.apply(this, arguments); } return cache[key]; } } 

结果:〜190 字符


  • 备忘录-我们的备忘录
  • f-装饰,包装函数
  • ret-结果函数

要获得答案-函数的大小-我们使用:


 memoize.toString().replace(/\s+/g, ' ').length 

在评估函数的大小时,我们要注意它的主体和参数列表。 如果函数是匿名的,则不考虑该声明。


简单的测试以测试滥用后的健康状况:


 const log = memoize(console.log); const inc = memoize(o => ox + 1); 

不行函数调用控制台结果
1。log(false)>错误
2。log('2', {x:1})>“ 2”,{x:1}
3。log(false)没什么,因为已经针对这些值执行了功能。
4。log('2', {x:1})没什么,因为已经针对这些值执行了功能。
5,inc({x:1})2
6。inc({x:2})3

接下来,将以测试结果标记每个实施的结果。


网络实施


首先,我想摆脱函数声明 ,转而支持箭头函数,因为我们对此上下文不感兴趣, 因此我们不希望使用参数,并且作为构造函数,我们不希望通过new进行调用。 同时,我们将减少使用的局部变量的名称:


 const memoize = f => { let c = {}; return function() { let k = JSON.stringify(arguments); if (!c.hasOwnProperty(k)) { c[k] = f.apply(this, arguments); } return c[k]; } } 

结果: 154 ,测试通过


然后,我们可以对结果函数执行类似的操作,但是这里需要参数 。 在这里, 散布运算符可以解决问题,使我们可以使用数组变量a替换传递的参数可迭代对象。 此外,我们将不再将此上下文传递给要修饰的函数:如有必要, Function.prototype.bind或我们的polyfil将提供帮助。


 const memoize = f => { let c = {}; return (...a) => { let k = JSON.stringify(a); if (!c.hasOwnProperty(k)) { c[k] = f(...a); } return c[k]; } } 

结果: 127 ,测试通过


现在我们转到结果函数的主体。 显然,在缓存中查找键并返回值很麻烦。 让我们尝试减少方法:


 const memoize = f => { let c = {}; return (...a) => { let k = JSON.stringify(a); return c[k] || (c[k] = f(...a)); } } 

结果: 101 ,测试3和4下降


在这里,我们放弃了hasOwnProperty方法。 我们可以负担得起,因为通过JSON.stringify序列化参数数组的结果将始终为“ [...]”,并且这样的属性不太可能出现在原型缓存( 对象 )中。


接下来,如果可以将第一个表达式转换为true ,则使用“逻辑” OR运算符的功能返回第一个表达式,否则,使用先前的函数计算返回第二个表达式。


在这里我们进行了测试3和4。发生这种情况是因为修饰后的功能console.log没有返回值:结果将是undefined 。 我们将其放在缓存中,当我们再次调用它时尝试检查Disjunctor功能时,我们在第一个操作数中隐式显示为false ,因此进入第二个操作数,从而导致函数调用。 对于所有减少为false的结果: 0,“”,null,NaN都会发生这种效果。


可以使用条件三元运算符代替OR和if语句


 const memoize = f => { let c = {}; return (...a) => { let k = JSON.stringify(a); return c.hasOwnProperty(k) ?c[k] :c[k] = f(...a); } } 

结果: 118 ,测试通过


减少非常轻微。 但是,如果将Map用作存储而不是简单对象,该怎么办? 还有一个简短的has方法:


 const memoize = f => { let c = new Map; return (...a) => { let k = JSON.stringify(a); return (c.has(k) ?c :c.set(k, f(...a))).get(k); } } 

结果: 121 ,测试通过


减少完全失败。 但是立即丢弃Map是不值得的。 键值存储的这种实现允许您将对象用作键。 这就是说,我们应该完全放弃JSON.stringify吗?


 const memoize = f => { let c = new Map; return (...a) => (c.has(a) ?c :c.set(a, f(...a))).get(a); } 

结果: 83 ,测试3和4下降


看起来很有前途! 但是,测试3和4再次开始下降,这是因为Map对象中键的比较是使用SameValueZero算法实现的。 如果省略NaN,-00的详细信息,则它的作用类似于严格比较运算符=== )。 对于包装函数的每次调用,即使具有相同的值,我们都有一个新的参数数组(因此有一个对象)。 比较是根据对象的引用进行的,因此Map.prototype.has方法永远找不到任何内容。


因此,使用Map并没有减少hasOwnPropertyJSON.stringify


In 操作员进行救援,该救援人员检查对象或其原​​型链中是否存在属性。 上面已经解释了为什么我们不怕原型搜索。


 const memoize = f => { let c = {}; return (...a) => { let k = JSON.stringify(a); return k in c ?c[k] :c[k] = f(...a); } } 

结果: 105 ,测试通过


备注器和结果函数的主体由两个表达式组成,需要在return语句中的逻辑之前声明和初始化一个局部变量。 这里是否可以将箭头函数的主体简化为一个表达式? 当然,使用IIFE立即调用函数表达式 )模式:


 const memoize = f => (c => (...a) => (k => k in c ?c[k] : c[k] = f(...a))(JSON.stringify(a)) )({}); 

结果: 82 ,测试通过


现在是时候摆脱多余的空间了:


 f=>(c=>(...a)=>(k=>k in c?c[k]:c[k]=f(...a))(JSON.stringify(a)))({}); 

结果: 68 ,测试通过


显然,瓶颈是长的JSON.stringify方法,该方法将对象递归序列化为JSON字符串,我们将其用作密钥。 实际上,我们不需要序列化函数,而需要一个哈希函数,通过它可以检查对象的相等性,因为它可以在其他语言中工作。 但是,不幸的是,JavaScript中没有本机解决方案,并且Object原型中的hashCode多义性显然超出了范围。


嗯,为什么我们还要序列化自己? 通过键将元素添加到对象时,将隐式调用其toString。 由于我们拒绝通过spread运算符使用可迭代参数对象来支持数组,因此toString的调用不会来自Object.prototype ,而不会来自Array.prototype ,在其中将其重新定义并以逗号分隔其元素。 因此,对于不同的参数集,我们获得了不同的键。


 f=>(c=>(...a)=>a in c?c[a]:c[a]=f(...a))({}); 

结果: 44 ,测试6掉落


测试6才刚刚开始下降,似乎返回值是测试5中上一个函数调用的结果。为什么会这样? 是的,我们绕过了对arguments对象的调用toString ,但是我们没有考虑到任何参数也可以是一个复杂的对象,因此调用toString可以使每个人都喜欢它[object Object] 。 这意味着参数{x:1}和{x:2}将在哈希中使用相同的键。


用于转换为base64的btoa似乎是序列化功能的良好竞争者。 但是他首先导致弦,所以没有机会。 我们考虑了生成URI并形成ArrayBuffer的方向 ,可以使用任何函数来获取哈希值或序列化的值。 但是它们仍然存在。


顺便说一下, JSON.stringify具有其自身的特性: Infinity,NaN,undefined,Symbol将被强制转换null 。 功能也是如此。 如果可能,会从该对象隐式调用JSON ,并且MapSet将由简单枚举的元素表示。 给定最终格式:JSON,这是可以理解的。


接下来呢?


有毒修饰


我们当然都喜欢纯函数,但是面对问题,这样的要求是不值得的。 这意味着是时候添加一些副作用了。


首先,为什么不按以下方式启动缓存:


 (f,c={})=>(...a)=>(k=>k in c?c[k]:c[k]=f(...a))(JSON.stringify(a)); 

结果: 66 ,测试通过


在这里,我们在arrow函数中使用默认参数 。 当然,我们为客户提供了设置缓存的机会,那又如何呢? 但是我们减少了2个字符。


我还能如何为要装饰的功能启动缓存? 正确答案:为什么我们需要启动它? 为什么不在要包装的函数上下文中使用现成的东西。 但是,如果函数本身呢? 我们都知道JavaScript中的函数也是对象:


 f=>(...a)=>(k=>k in f?f[k]:f[k]=f(...a))(JSON.stringify(a)); 

结果: 59 ,测试通过


在这里, JSON.stringify将保护我们免于与对象(函数)的其他属性和方法相交,将参数包装在“ [...]”中。


此时此刻,以前应用的IIFE模式不再是合理的。 但是为避免return语句,迫切需要为arrow函数保留单个表达式:


 f=>(...a)=>(k=JSON.stringify(a),k in f?f[k]:f[k]=f(...a)); 

结果: 57 ,测试通过


由于我们未在arrow函数中使用block语句 ,因此无法声明变量( varlet ),但可以使用全局上下文-副作用! 在这里,冲突已经有可能出现。


使用逗号运算符,我们将两个表达式连接为一个表达式:操作数从左到右求值,结果是后者的值。


 f=>(...a)=>(k=JSON.stringify(a))in f?f[k]:f[k]=f(...a); 

结果: 54 ,测试通过


因此,只需重新排列一个括号,就可以一次删除三个字符。 计算键的分组运算符允许我们将表达式的两个操作数组合为一个表达式,并且右括号删除了in运算符之前的空格。


最后:


 f=>(...a)=>f[k=JSON.stringify(a)]=k in f?f[k]:f(...a); 

结果: 53 ,测试通过


为什么在访问值时不计算密钥。 然后-相同的三元运算符和赋值。 总计:53个字符!


是否可以删除其余3个字符?


理解力


为什么要这样? 这个简单的任务以及随后的从习惯到不雅的转换链证明了JavaScript语言的许多功能。 在讨论中,我们谈到了以下内容:


  • 箭头函数表达式
  • 词汇范围和IIFE
  • 类数组参数对象
  • 点差,逗号或运算符
  • 严格的比较运算符
  • JSON.stringify和toString
  • 在运算符和hasOwnProperty中
  • 分组运算符和阻止语句
  • 地图对象
  • 还有其他

这样的故事是让您沉浸于语言细节研究中的一个好理由,有助于更好地理解它(反之亦然)。 当然,只是为了好玩!


应用程式


图片


在冒险中,里克经常必须校准他的门炮。 该过程需要时间,但通常会重复输入。 这位科学家试图记住一次已经获得的结果,以免重复进行计算,但是酗酒和老年衰老强烈地影响了他的记忆力。 他要求Morty改进枪支设置模块,并添加了备忘录功能。 此函数应保存装饰后的结果,以防止重复计算。 只有莫蒂害怕长功能。 帮助他尽可能紧凑地解决问题。 被修饰的函数可以将整数,字符串,布尔值和对象作为参数。

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


All Articles