JavaScript开发人员经常抱怨他们的编程语言因拥有太多过于复杂,令人困惑的功能而受到不公平的指责。 许多人对这种对JS的态度感到挣扎,谈论为什么批评这种语言的错误之处。 该材料的作者(我们今天出版的翻译)决定不为JS辩护,而是转向该语言的阴暗面。 但是,在这里他不想谈论例如JavaScript为经验不足的程序员设置的陷阱。 他对如果您尝试使用完全不关心他人的人编写的代码来确认该语言的不良声誉会产生什么问题感兴趣。

在本材料的示例中,将使用许多语言机制。 顺便说一下,您在这里看到的大部分内容都可以使用其他语言,因此,通过尽职调查,您还可以找到它们的阴暗面。 但是,JavaScript无疑是各种欺凌行为的真正礼物,并且在这一领域很难与其他语言竞争。 如果您编写其他人需要使用的代码,则JS为您提供了无穷无尽的机会来惹恼,迷惑,骚扰和欺骗这些人。 实际上,在这里我们仅考虑此类技术的一小部分。
吸气剂
JavaScript支持getter-使您可以像使用常规属性一样使用它们返回的函数。 在正常使用下,它看起来像这样:
let greeter = { name: 'Bob', get hello() { return `Hello ${this.name}`} } console.log(greeter.hello) // Hello Bob greeter.name = 'World'; console.log(greeter.hello) // Hello World
例如,如果使用吸气剂,密谋邪恶,则可以创建自毁对象:
let obj = { foo: 1, bar: 2, baz: 3, get evil() { let keys = Object.keys(this); if(keys) { delete this[keys[0]] } return 'Nothing to see here'; } }
在这里,每次调用
obj.evil
,都会删除该对象的其他属性之一。 同时,使用
obj.evil
的代码不会知道在他的鼻子底下发生了非常奇怪的事情。 但是,这仅仅是关于使用JavaScript机制可以实现的有害副作用的讨论的开始。
意外的代理
吸气剂很棒,但是它们已经存在很多年了,许多开发人员都知道它们。 现在,借助代理,我们可以使用一种功能更强大的工具来娱乐对象。 代理是一种ES6功能,可让您围绕对象创建包装器。 在他们的帮助下,您可以控制用户尝试读取或写入代理对象的属性时发生的情况。 例如,这允许创建一个对象,在尝试访问该对象的某个键的三分之一中,该对象将通过随机选择的键返回一个值。
let obj = {a: 1, b: 2, c: 3}; let handler = { get: function(obj, prop) { if (Math.random() > 0.33) { return obj[prop]; } else { let keys = Object.keys(obj); let key = keys[Math.floor(Math.random()*keys.length)] return obj[key]; } } }; let evilObj = new Proxy(obj, handler);
不幸的是,开发人员工具将
evilObj
标识为
Proxy
类型的对象已部分揭露了我们的
evilObj
。 但是,上述构造在揭示其实质之前,能够为将要使用它的人们提供许多愉快的时光。
传染性功能
到目前为止,我们已经讨论了对象如何修改自身。 但是,此外,我们可以创建外观纯真的函数来感染传递给它们的对象,从而改变其行为。 例如,假设我们有一个简单的
get()
函数,该函数允许您安全地在传递给它的对象中搜索属性,同时考虑到这样的对象可能不存在的事实:
let get = (obj, property, default) => { if(!obj) { return default; } return obj[property]; }
重写这样的函数很容易,这样它就可以感染传输给它的对象,并对其稍加更改。 例如,可以确保在尝试遍历对象的键时不再显示其帮助访问的属性:
let get = (obj, property, defaultValue) => { if(!obj || !property in obj) { return defaultValue; } let value = obj[property]; delete obj[property]; Object.defineProperty(obj, property, { value, enumerable: false }) return obj[property]; } let x = {a: 1, b:2 }; console.log(Object.keys(x)); // ['a', 'b'] console.log(get(x, 'a')); console.log(Object.keys(x)); // ['b']
这是对对象行为进行非常细微干预的示例。 枚举对象的键并不是最引人注目的操作,因为它不是很稀有,但使用得并不多。 由于此类对象修改可能导致的错误无法与它们的代码联系在一起,因此它们可以在某个项目中存在相当长的时间。
原型混乱
我们在上面讨论了JS的各种功能,包括一些相当近期的功能。 但是,有时没有什么比经过时间考验的旧技术更好。 JS的最受批评的特性之一就是能够修改内置原型。 此功能在JS的早期使用,用于扩展嵌入式对象,例如数组。 例如,通过将
contains
方法添加到原型
Array
对象中来扩展标准阵列功能的方法:
Array.prototype.contains = function(item) { return this.indexOf(item) !== -1; }
事实证明,如果您在真正使用的库中执行此类操作,则可能
会破坏使用该库的整个应用程序中该语言的基本机制
的工作。 因此,对于想要做其他令人讨厌的事情的患者开发人员,在标准对象的原型中包含其他有用的方法可以认为是非常成功的举动。 但是,如果我们谈论的是不耐烦的社交病,可以快速获得一些,但同样有趣的是。 原型的修改具有一个非常有用的属性,其特征在于,修改会影响在特定环境中执行的所有代码,甚至是从模块加载或在闭包中执行的代码。 因此,如果您以第三方脚本的形式设计以下代码(例如,它可能是来自广告网络或分析服务的脚本),则使用此脚本的整个网站都容易出现小错误。
Array.prototype.map = function(fn) { let arr = this; let arr2 = arr.reduce((acc, val, idx) => { if (Math.random() > 0.95) { idx = idx + 1 } let index = acc.length - 1 === idx ? (idx - 1 ) : idx acc[index] = fn(val, index, arr); return acc; },[]); return arr2; }
在这里,我们重新定义了标准方法
Array.prototype.map
以便通常可以正常工作,但是在5%的情况下,它会交换数组的两个元素。 多次调用此方法后,您将获得以下内容:
let arr = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]; let square = x => x * x; console.log(arr.map(square)); // [1,4,9,16,25,36,49,64,100,81,121,144,169,196,225 console.log(arr.map(square)); // [1,4,9,16,25,36,49,64,81,100,121,144,169,196,225] console.log(arr.map(square)); // [1,4,9,16,25,36,49,64,81,100,121,144,169,196,225]
在这里,我们启动了三次。 首次使用它时发生的情况与以下两个调用它的结果略有不同。 这是一个较小的更改,它并不总是会导致某种故障。 最好的部分是,如果不阅读源代码,就不可能理解由该方法引起的很少发生的错误的原因,而这正是这些错误的原因。 我们的功能在使用开发人员工具时不会引起注意,在严格模式下工作时不会产生错误。 通常,借助这样的帮助,完全有可能使某人发疯。
复杂的名字
众所周知,命名实体是计算机科学中最困难的两个任务之一。 因此,恶名不仅是由有意识地试图伤害他人的人发明的。 当然,很难相信经验丰富的Linux用户。 他们花了很多年的时间将最糟糕的命名IT入侵者(Microsoft)与最深层次的邪恶联系在一起。 但是不成功的名称不会直接损害程序。 我们不会谈论诸如误导性名称和评论失去相关性之类的小事情。 例如,关于:
// let arrayOfNumbers = { userid: 1, name: 'Darth Vader'};
为了深入研究并理解注释和变量名称有问题,那些阅读发生这种情况的代码的人将不得不放慢脚步并思考一下。 但这是胡说八道。 让我们谈谈真正有趣的事情。 您是否知道大多数Unicode字符都可以用来命名JavaScript中的变量? 如果您在分配变量名称方面是肯定的,那么您会喜欢使用图标形式的名称的想法(
Habr切割emoji表情,尽管在此之后的原始名称为emoji kakahi ):
let = { postid: 123, postName: 'Evil JavaScript'}
虽然,我们在这里谈论的是真正的令人讨厌的事情,所以我们最好转向与通常用于命名变量的字符相似的字符,但并非如此。 例如,让我们这样做:
let obj = {}; console.log(obj);
obj
名称中的字母
b
看起来几乎是正常的,但它不是小写的拉丁字母b。 这就是所谓的全角小写拉丁字母b。 符号是不同的,因此任何尝试手动输入此类变量名称的人都将很困惑。
总结
尽管有使用JavaScript可以完成各种令人讨厌的事情的故事,但本材料旨在警告程序员不要使用所描述的技巧,并向他们传达可能造成真正危害的事实。 该材料的作者说,了解编写不良的代码中可能出现的问题总是很有用的。 他认为在实际项目中可以找到类似的东西,但希望它以破坏性较小的形式存在。 但是,编写此类代码的程序员并未试图伤害他人的事实并不能使使用此类代码并对其进行调试变得更加容易。 同时,了解有目的的伤害尝试会带来什么,可以扩大程序员的视野,并帮助他找到类似错误的根源。 没有人可以完全确定使用它的代码中没有错误。 也许有人知道他们过于怀疑的倾向,就会设法使自己放心,对这样的错误的焦虑只是他想象力的虚构。 但是,这并不能防止此类错误(可能有意引入某些代码中以证明自己)。
亲爱的读者们! 您是否在实践中遇到过与本文讨论的内容类似的内容?
