四、行话和俚语

Abstract

冰况有这么多术语的原因之一是观测冰况的水手经常被困在冰中,除了看冰况之外无事可做。

冰况有这么多术语的原因之一是,观察冰况的水手经常被困在冰中,除了看冰况之外无事可做——亚历克·威尔金森,《冰气球:安德烈和北极探险的英雄时代》

几个月前,我偶然看到加里·伯恩哈特的一个演讲,题目就叫“水”Wat 是互联网上的一种口语,用来描述对某个主题的困惑或有趣的怀疑,这里指的是 JavaScript。伯恩哈特的演讲采用了问答的形式。首先,他展示了一行看似合理的 JavaScript 代码,然后让观众给出结果。有一次,他问观众{}+[]会出什么。大多数观众认为结果会是某种错误,因为把文字对象和数组加在一起是没有意义的。结果反而是“0”。观众困惑地呻吟和大笑。演示就这样继续下去,问问题,然后给出似乎错得离谱的结果。

令许多 JavaScript 捍卫者懊恼的是,这个演示像病毒一样传播开来,主要是因为它有趣而轻松,给了 JavaScript 社区一个自嘲的工具。最终,就连 JavaScript 的创始人布伦丹·艾希(Brendan Eich)也加入了这场争论,在最近的一次演讲中,他半心半意地解释了他的语言在伯恩哈特的演讲中做的一些看似愚蠢的事情。

最初,我以为这一章会花在解释 JavaScript 中 Wat 的例子上。然而,随着我对 Bernhardt 演讲中使用的各种例子的深入研究,我开始意识到这些不一致之处并不是语言的缺陷,而是语言内部的秘密握手,一种编程术语。在那一点上,我对这一章的方向改变了,现在的目标是定义术语,因为它与编程有关。我将给出 JavaScript 中行话的例子,如何拥抱它或避免它,取决于你自己的风格。

行话.原型=新俚语( )

在准确定义行话之前,你必须先了解什么构成了俚语。俚语是在一种文化的正常和标准词汇之外使用的词语或表达方式。俚语传递意义的能力取决于接受者对词语或表达中高度语境化的指称进行解读的能力。在努力编纂俚语的过程中,Bethany K. Dumas 和 Jonathan Lighter (Duman & Lighter,1978 年)建议俚语的例子必须满足以下至少两个标准:

  • 它降低了“正式或严肃的演讲或写作的尊严”,即使是暂时的。换句话说,在这些情况下,这可能被认为是“对语域的明显滥用”
  • 它的使用意味着用户熟悉所指的任何事物,或者熟悉它并使用该术语的一群人。
  • "在与社会地位较高或责任较大的人的日常交谈中,这是一个禁忌词。"
  • 它取代了“一个众所周知的传统同义词。”这样做主要是为了避免由常规短语或进一步阐述引起的不适。

从这些规则中可以看出,行话只符合第二个标准。然而,即使这样,也足以开始看到可能被称为编程术语的模糊轮廓。

什么是程序化行话?

编程行话是通过使用高度特定的、通常是技术性的语言规则来压缩代码。像其他形式的行话一样,编程形式用于在社区成员之间有效地引用复杂的想法。它可以成为成员间引用复杂概念的一种速记。然而,因为行话是如此高度语境化,它经常充当社区之间的社会分割线或语言边界守卫。这可能就是为什么外行人觉得行话难以理解的原因。了解了这一点,您就可以开始确定定义编程术语的标准了:

  • 它缩短了语言的机制。
  • 社区中的普通成员很容易混淆或误解它。
  • 它破坏了服务中的视觉清晰度和其他目标(例如,更小的代码或更快的执行)。
  • 它是一种社区内部分层的手段。

行话名声不好,因为它经常被那些对术语的意思只有一点点概念的人使用。在这种情况下,行话就成了谈话中的噪音,是让说话者看起来更聪明的语言填充物。在编程俚语的情况下,它可能表现为误用或误用编程概念,希望显得聪明。当然,术语的误用会让演讲者看起来像个骗子和白痴。理查德·米切尔总结了这种情绪,他写道:

His jargon hides the hole in his heart for him, but not for us. He used scientific language instead of technology. -Richard Mitchell, "Words Can't Express"

在 JavaScript 中,有三种语言成分特别适合创造行话:强制、逻辑运算符和按位操作(俗称位扭曲)。)现在您已经有了识别编程术语的基础,您将在本章的剩余部分探索和理解它在 JavaScript 中是如何发生的具体例子。

Note

行话通常以贬义的方式使用,描述使用技术术语使说话者看起来聪明或专业。然而,正确使用的行话可以是一个概念的简洁指针,一个有经验的听众不需要解释。在这一章中,行话仅仅意味着高度上下文相关的代码,对于外行人来说通常是难以理解的,但不一定本质上是不好的。

强迫

与大多数其他语言一样,在 JavaScript 中,强制是将一种类型的对象或实体强制转换成另一种类型的行为。这不要与类型转换混淆,类型转换是类型之间的显式转换。在 JavaScript 中,显式类型转换如下所示:

// => "1"

var a = (1).toString();

console.log(a);

但是,也可以通过以下方式将数字隐式转换为字符串:

// => "1"

var a = 1 + "";

console.log(a);

多年来困扰我的许多最神秘的代码示例都在某种程度上涉及了强制。我的大部分困惑是由于 JavaScript 如何处理特定的多态性。如果你回想一下核心概念一章,你会记得这种形式的多态使用执行的上下文来帮助塑造结果。具体来说,JavaScript 使用重载来改变操作符的行为,这取决于它们是如何被调用的。

例如,二元运算符可用于求和或连接,但它也在这个过程中强制值。关于强制的大部分困惑在于知道它是如何或何时发生的。在 JavaScript 中,强制总是将复杂的对象简化为一种基本形式,或者在两种基本类型之间进行转换。不能将数字强制转换为数组,但可以将数组强制转换为数字。以下示例有助于解释 JavaScript 强制值的各种方式。

方法

JavaScript 使用二元运算符将两个值连接在一起。然而,为了实现这一点,JavaScript 首先将零悄悄地强制转换成一个字符串。当 JavaScript 试图将对象转换成字符串时,它首先调用toString()方法。如果toString()没有返回一个原始表示,它就遵从valueOf()函数。如果valueOf()函数也不能产生原始值,JavaScript 抛出一个TypeError异常:

// => '0'

var s = ''+0;

console.log(s);

要编号

一元运算符的工作是将后面的操作数转换成数字。像连接过程一样,它也涉及到将对象强制转换成原始形式,这次是一个数字。这相当于写1*'10'。正如在字符串转换过程中一样,JavaScript 依赖于toString()valueOf()的结果。然而,顺序是相反的:JavaScript 首先调用valueOf(),然后调用toString()。这里有一个简单的例子:

// => 10

console.log(+'10');

上下文感知强制

许多内置核心对象可以被强制,因此支持一元和二元运算。被强制的对象定制valueOf()toString()的返回值,使其在上下文中有意义。以内置的Date对象为例。当将对象转换为原始数字时,它返回自 epoch 以来的毫秒数,这对于执行计算非常有用:

// => 1373558473636

console.log(+new Date());

但是,epoch 的字符串表示形式没有那么有用,因此当日期转换为字符串时,对象返回当前日期和时间的文本表示形式:

// => Thu Jul 11 2013 11:01:13 GMT-0500 (CDT)

console.log(new Date() + '');

胁迫抓到你了

了解类型转换的操作顺序应该使您能够为自己的对象创建有意义的转换值。这样,当您的对象被强制时,就像内置的Date对象一样,它可以返回一个上下文感知的结果。然而,正如您将在下面的代码中看到的那样,这实际上比乍看起来更难做到:

var Money = function (val, sym) {

this.currencySymbol = sym;

this.cents = val;

};

var dollar = new Money(100, '$');

// Not helpful

// => NaN

console.log(+dollar);

// Not helpful

// => Total: [object Object]

console.log("Total: " + dollar);

Money.prototype.toString = function () {

return this.currencySymbol + (this.cents / 100).toFixed(2);

};

Money.prototype.valueOf = function () {

return this.cents;

};

// Helpful!

// => 100

console.log(+dollar);

// Wait what?! I wanted $1.00

// => 100

console.log(dollar + '');

// Now I am totally confused!

// => $1.00

console.log([dollar] + '');

转换发生的顺序似乎与您在Date示例中学到的不一致。要得到答案,您需要看看 JavaScript 在将这个对象强制转换成String时采取的步骤。这里,操作符过载再次成为问题。您可能会认为,因为您正在连接一个字符串,JavaScript 将使用toString()而不是valueOf(),就像它对Date对象所做的那样。下面是规范中关于类型转换的描述:

The abstract operation ToPrimitive accepts an input parameter and an optional parameter PreferredType. The abstract ToPrimitive operation converts its input parameters to non-object types. If an object can be converted into several basic types, it can use the optional prompt PreferredType to support that type.

在这种情况下,对象的转换遵循以下顺序:

Returns the default value of the object. You can retrieve the default value of the object by calling the [[DefaultValue]] internal method of the object and passing the optional prompt PreferredType. For all local ECMAScript objects in 8.12.8, this specification defines the behavior of [[DefaultValue]] internal methods.

所以看起来你需要明白DefaultValue是如何在对象中导出的。深入研究规范,您会发现 JavaScript 有两种确定DefaultValue的方法:一种是字符串,另一种是数字。它根据提供给DefaultValue方法的hint参数做出这个决定。如果没有提供hint,JavaScript 默认为一个Number。下面是一个假想版本的ToPrimitive()方法的样子:

var ToPrimitive;

ToPrimitive = function (obj) {

var funct, functions, val, _i, _len;

functions = ["valueOf", "toString"];

if (typeof obj === "object") {

if (obj instanceof Date) {

functions = ["toString", "valueOf"];

}

for (_i = 0, _len = functions.length; _i < _len; _i++) {

funct = functions[_i];

if (typeof obj[funct] === "function") {

val = obj[funct]();

if (typeof val === "string" || typeof val === "number" || typeof val === "boolean") {

return val;

}

}

}

throw new Error("DefaultValue is ambigious.");

}

return obj;

};

// => 1 (as string)

console.log(ToPrimitive([1]));

// => Thu Jul 11 2013 15:55:11 GMT-0500 (CDT)

console.log(ToPrimitive(new Date()));

现在您明白了为什么对象的串联不能使用自定义的toString()方法:因为没有为内部的DefaultValue函数指定提示,JavaScript 就认为您需要一个数字。这导致了对valueOf()的调用。您现在需要做的就是找出如何将提示设置为一个字符串,就像内置的Date对象一样。不幸的是,没有办法为自定义对象指定提示!在DefaultValue方法描述的底部,你会发现这个警告:

When calling the internal method of O [[DefaultValue]] without prompt, it behaves as if the prompt is a number, unless O is a date object (see 15.9.6), in which case it behaves as if the prompt is a string. The above [[DefaultValue]] specification for native objects can only return the original value. If the host object implements its own [[DefaultValue]] internal method, it must ensure that its [[DefaultValue]] internal method can only return the original value.

您现在已经发现了 JavaScript 中的一个无法回避的限制(至少不是以优雅的方式)。由于没有内置的方式来指定对DefaultValue函数的提示,对象不能像Date对象那样偏好toString()。然而,并没有失去一切;如果您参考前面的例子,您会发现您最终找到了让dollar对象以您想要的方式连接的方法。奇怪的是,如果您首先将对象包装在一个数组中,它会工作。只有这样,JavaScript 才会使用toString()方法正确地强制值,但是为什么呢?这里有一个提示:

// => object

console.log(typeof [1].valueOf());

// => string

console.log(typeof [1].toString())

你想明白了吗?记住ToPrimitive的规则说函数必须返回一个原始值。然而,数组的valueOf()方法返回一个对象,这导致ToPrimitive函数继续运行并调用toString()。对toString()的后续调用确实返回了所需的原始值。在内部,数组的toString()函数必须遍历其集合中的所有元素,并对每个元素调用toString()。这个理论很容易检验;您可以简单地将一个对象推入一个不能被强制转换为字符串的数组中:

var noConversions = [{

toString: undefined

}];

// => Uncaught TypeError: Cannot convert object to primitive value

console.log(noConversions + '');

不出所料,尝试的强制会引发错误。

通过强制进行混合类型比较

到目前为止,我一直在谈论强制,因为它适用于求和或连接的类型转换。但是,在执行相等测试之前,equals 运算符也会将操作数强制转换为原始值。考虑下面的例子:

// => true

console.log([1] == 1);

// => true

console.log([1] == "1");

// => true

console.log([{

toString: function () {

return 1;

}

}] == "1");

// => false

console.log([1] === 1);

// => false

console.log([1] === "1");

// => false

console.log([{

toString: function () {

return 1;

}

}] === "1");

令人担忧的是,通过强制,一个对象本质上可以等于一个原始值,但至少现在您知道这种情况何时发生。此外,您可以看到为什么在 JavaScript 最佳实践中如此大力提倡使用严格等于运算符来比较值。

复杂胁迫

既然你已经掌握了强制的基础知识,让我们来试试一个高级的例子(我说的高级,是指让人麻木的迟钝)。想想这块宝石:

// => '10'

++[[]][+[]]+[+[]]

理解正在发生的事情的最好方法是首先打开内部包装,然后向外工作。首先,从左到右看内部数组:

// => [Array[0]]

[[]]

// An array which contains a single value, a coerced zero thanks to the unary operation.

// => [0]

[+[]]

// A second array also containing a coerced zero.

// => [0]

[+[]]

接下来,思考二元运算符两边的两个操作数。从左边开始:

// => 1

++[[]]['0']

这个声明有点棘手。实际上,内部数组在索引“0”处被访问并被返回。在返回点,左边的一元运算符将它递增,这也将它变成一个数字。然后将这两个值合并。由于左操作数是一个数字,而右操作数是一个数组,所以组合将通过串联进行,而不是求和。因此,最终的序列如下所示:

// => '10'

1 + ['0']

现在您已经理解了为什么这是行话——因为它通过对内部强制机制的深入理解来执行任务。让我们继续讨论逻辑运算符的话题,以理解它们在编程术语中的作用。

逻辑运算符

逻辑运算符用于返回布尔值,但在某些情况下,它们可用于缩短语句中的控制流。这种短路通常会缩短代码,但会牺牲表达能力。这样,逻辑运算符非常适合创建编程术语。下一节将逐步介绍各种逻辑运算符,解释如何使用它们来产生术语。

逻辑与(&&)

逻辑 OR 和逻辑 and 都用于链接返回布尔值的比较。在逻辑 AND 的情况下,所有条件求值必须为真;否则,返回 false。

通过比较或隐式回退的赋值

了解了&&的行为,就有可能在一条语句中同时利用链接和返回值:

var car = {

hasWheels: function () {

return true;

}

engineRunning: function () {

return true;

}

wheelsTurning: function () {

return true;

}

};

if (car.inMotion = car.hasWheels() && car.engineRunning() && car.wheelsTurning()) {

console.log('vrrrrooooommmm');

}

虽然上面的代码在技术上是正确的,但在条件表达式中使用赋值语句并不是一种好的做法,因为人们经常将赋值语句误解为相等比较,这可能会导致混淆。

逻辑或(||)

与逻辑 AND 运算符非常相似,逻辑 or 运算符可以用作控制流机制,它从左到右比较操作数,寻找第一个真值。与 AND 运算符不同,or 运算符只需要一个操作数为真就能成功。

默认值

使用逻辑“或”的一种常见方式是将默认值赋给在方法签名中被认为是可选的变量。OR 操作符测试左操作数,寻找一个undefined将会寻找一个可以被强制为布尔值的值。一旦找到,该值就被赋给变量。

var Car = function(){

var args = Array.prototype.slice.call(arguments);

this.name = args[0] || 'tesla'

this.mpg = args[1] || 100

this.mph = args[2] || 80

// => Volt

console.log(this.name);

// => 90

console.log(this.mpg);

// => 80

console.log(this.mph);

}

new Car('Volt',90);

逻辑 NOT(!)

逻辑 NOT 运算符需要一个右操作数,即布尔值或可以强制为一个布尔值。只有当操作数为假时,它才返回真。

速记布尔型

正如您在强制一节中看到的,隐式类型转换很难通过阅读代码来理解。我在 JavaScript 中看到的最普遍的约定之一是使用逻辑 NOT 作为布尔值的快捷方式。考虑 NOT 运算符可以强制然后表达布尔值的以下方式:

// number is coerced to a Boolean false

// NOT inverts it to true

// => true

console.log(!0);

// number is coerced to a Boolean true

// NOT inverts it to false

// => false

console.log(!1);

// number is coerced to a Boolean true

// NOT inverts it to false

// => false

console.log(!-1);

// string is coerced to a Boolean truthy *something*

// NOT inverts it to false

// => false

console.log(!'0');

// string is coerced to a Boolean truthy *something*

// NOT inverts it to false

// => false

console.log(!'1');

// this is coerced to a Boolean falsey *nothing*

// NOT inverts it to true

// => true

console.log(!undefined);

// this is coerced to a Boolean truthy *something*

// NOT inverts it to true

// => false

console.log(!this);

// unary operator coerces empty array into zero

// zero is coerced into Boolean false

// NOT inverts it to true

// => true

console.log(!+[]);

// inner NOT coerces the empty array to false

// false is not a valid array index so undefined is returned

// undefined is coerced into Boolean false

// NOT inverts it to true

// => true

console.log(![][![]]);

双音符

正如您在上一个示例中看到的,逻辑 NOT 运算符可以将多种实体转换为变量,包括未定义的变量。知道了这一点,你就可以把缺少一个变量当作事实上的假变量。在下面的例子中,您可以看到双 NOTs 的使用如何允许代码以相同的方式处理未定义的和显式的 false 布尔值。然而,这个代码非常不透明;它在视觉空间中节省的东西,在概念的清晰性上失去了。

var user = {

isAdmin: function () {

return !!this.admin;

}

};

// undefined this.admin is coerced to false

// then inverted to true

// then inverted again to false

// => false

console.log(user.isAdmin());

user.admin = true;

// this.admin is true without coercion

// inverted to false

// inverted back to true

// => true

console.log(user.isAdmin());

user.admin = false;

// => false

console.log(user.isAdmin());

立即调用函数表达式

使用逻辑 NOT 运算符,可以编写更简洁的立即调用函数表达式。在这种情况下,逻辑 NOT 运算符告诉解析器不要将函数视为函数声明,而是提供新执行上下文的表达式:

// Uncaught SyntaxError: Unexpected token (

function(){console.log('foo');}();

// => foo

!function(){console.log('foo');}();

既然您已经对这一部分进行了逻辑总结,那么您可以过渡到 JavaScript 的一些真正的后路,更好的说法是位操作。

钻头旋转

顾名思义,位运算是在比特级别处理数据的过程。一般来说,这对于要求快速执行和/或具有有限资源的算法是有用的。具体来说,这些操作必须只需要对数据进行原始转换,才能从这种操作中获益。位运算是许多低级任务的标准,包括通过套接字通信、压缩或加密信息,或者处理位图图形。使用位操作来实现基于角色的访问控制(RBAC)系统也很常见,因为它们的访问权限可以只用一个位字段来描述,但在数据库中仍然是一个单一的数字。

按位运算符有四种不同的风格:分别是NOTANDORXOR。除了逻辑操作符之外,JavaScript 还有左右移位操作符。如你所料,正确解释这些操作符的方法和原因是相当复杂的,还必须包括理解比特移位一般是如何工作的。因此,它超出了本章的范围。相反,您将继续关注行话表达式,但现在重点放在按位运算的使用上。接下来的例子和解释有点无聊的行话。

按位与(&)

按位OR函数在每个位位置返回 1,其中两个操作数在指定位置都为 1。

将十六进制转换为 RGB

有时,将十六进制数转换成 RGB 值很有用;例如,在 CSS 类的服务中:

// my favorite hex color

var color = 0xC0FFEE;

// Red

// => 192

console.log((color>>16) & 0xFF);

// Green

// => 255

console.log((color>>8) & 0xFF);

// Blue

// => 238

console.log(color & 0xFF);

您可以进一步扩展这个函数,创建一个渐变工厂 1 来返回颜色渐变:当给定一个开始和结束颜色以及若干停止点时。

var GradientFactory = (function () {

var _beginColor = {

red: 0

green: 0

blue: 0

};

var _endColor = {

red: 255

green: 255

blue: 255

};

var _colorStops = 24;

var _colors = [];

var _colorKeys = ['red', 'green', 'blue'];

var _rgbToHex = function (r, g, b) {

return '#' + _byteToHex(r) + _byteToHex(g) + _byteToHex(b);

};

var _byteToHex = function (n) {

var hexVals = "0123456789ABCDEF";

return String(hexVals.substr((n >> 4) & 0x0F, 1)) + hexVals.substr(n & 0x0F, 1);

};

var _parseColor = function (color) {

if ((color).toString() === "[object Object]") {

return color;

} else {

color = (color.charAt(0) == "#") ? color.substring(1, 7) : color;

return {

red: parseInt((color).substring(0, 2), 16)

green: parseInt((color).substring(2, 4), 16)

blue: parseInt((color).substring(4, 6), 16)

};

}

};

var _generate = function (opts) {

var _colors = [];

var options = opts || {};

var diff = {

red: 0

green: 0

blue: 0

};

var len = _colorKeys.length;

var pOffset = 0;

if (typeof (options.from) !== 'undefined') {

_beginColor = _parseColor(options.from);

}

if (typeof (options.to) !== 'undefined') {

_endColor = _parseColor(options.to);

}

if (typeof (options.stops) !== 'undefined') {

_colorStops = options.stops;

}

_colorStops = Math.max(1, _colorStops - 1);

for (var x = 0; x < _colorStops; x++) {

pOffset = parseFloat(x, 10) / _colorStops;

for (var y = 0; y < len; y++) {

diff[_colorKeys[y]] = _endColor[_colorKeys[y]] - _beginColor[_colorKeys[y]];

diff[_colorKeys[y]] = (diff[_colorKeys[y]] * pOffset) + _beginColor[_colorKeys[y]];

}

_colors.push(_rgbToHex(diff.red, diff.green, diff.blue));

}

_colors.push(_rgbToHex(_endColor.red, _endColor.green, _endColor.blue));

return _colors;

};

return {

generate: _generate

};

}).call(this);

// From hex to hex

// => ["#000000", "#262626", "#4C4C4C", "#727272", "#999999"]

console.log(GradientFactory.generate({

from: '#000000'

to: '#999999'

stops: 5

}));

// From color object to hex

// => ["#C0FFEE", "#CFFFF2", "#DFFFF6", "#EFFFFA", "#FFFFFF"]

console.log(GradientFactory.generate({

from: {

red: 192

green: 255

blue: 238

}

to: {

red: 255

green: 255

blue: 255

}

stops: 5

}));

按位或(|)

按位OR函数在每个位位置返回 1,其中两个操作数中的任何一个在指定位置为 1。

截断数字

正如您在上一节中了解到的,这个函数对一对位执行逐位OR运算。它也可以用来向下舍入数字。

// => 30

var x = (30.9 | 0);

console.log(x);

按位异或(^)

下面的例子利用了这样一个事实:在 2 位模式的特定位不匹配的地方,按位XOR运算符返回 1。

确定符号相等

这个表达式是确定两个操作数是否有相反符号的简单方法。它之所以有效,是因为 JavaScript 使用二的补码来表示负数,这使得XOR成为可能。

var signsMatch = function (x, y) {

return !((x ^ y) < 0);

};

// => false

console.log(signsMatch(10, -10));

// => true

console.log(signsMatch(0, 0));

// => true

console.log(signsMatch(0, -0));

// => true

console.log(signsMatch(-10, -10));

// => true

console.log(signsMatch(1, 1e0));

// => false

console.log(signsMatch(-1, 1e0));

切换位

偶尔,您会看到用于切换位的XOR操作符,这有助于切换对象的状态。这里有一个例子:

var light = {

on: 1

toggle: function () {

return this.on ^= 1;

}

};

// => 0

console.log(light.toggle());

// => 1

console.log(light.toggle());

// => 0

console.log(light.toggle());

位元 not(;)

按位NOT函数实质上是交换一个数字的符号,然后从中减去 1。在幕后,JavaScript 将操作数转换为二进制表示,然后通过将所有位从 1 交换到 0 来计算新的数字,反之亦然。这个新数叫做原数的一补数。最后,一的补码被转换回十进制数。了解了NOT的行为,您就可以用一些聪明的方法来利用它。

逐位算术

偶尔,您会看到开发人员使用按位NOT对变量执行算术运算。这里有一个例子:

// => 9

∼-10

// => 11

-∼10

// => 18

2*∼-10

将字符串解析为数字

按位NOT操作符返回操作数的取反值,字符串作为这个过程的一部分被强制。因此,提供一个 double NOT会将数字返回到其原始符号。

var num = "100.7"

// => true

console.log(parseInt(num,10) === ∼∼num);

按位移位(<>,>>>)

比特移位是使用按位运算符,通过将整数的二进制表示在比特域中向左或向右移动任意数量的比特位置来操作整数。移位的过程导致一个新数的形成。在与硬件设备交互时,移位是很常见的,因为它们通常缺乏浮点数的支持。位偏移在图像处理中也非常有用,例如,当位偏移用于处理颜色配置文件之间的转换,或处理像素域的位图操作时。

比特移位在 JavaScript 中不太常用,但是对于对一个数字执行简单的算术移位或者作为一个更大函数的一部分仍然非常有用,您将在接下来的 signum 函数示例中看到。

希格诺函数

signum(也称为 sign)函数的目的是确定一个数是小于、等于还是大于零;因此可以返回-101作为结果。

var sign = function(x) {

return (x >> 31) | ((-x) >>> 31);

};

// => -1

console.log(sign(-100));

// => 0

console.log(sign(0));

// => 1

console.log(sign(100));

虽然您使用了移位来计算数字的符号,但是您也可以使用两个普通的旧三进制表达式组合在一起:

// => 1

console.log(100 ? 100 < 0 ? -1 : 1 : 0);

现在你已经知道这个函数是有效的,让我们来看看为什么。首先,考虑右移位运算符。该运算符的作用是将操作数移位指定的位数,在本例中为 31 位。因为使用的是位域的结尾,正数总是返回0,负数总是返回-1。这里有几个例子:

// => -1

console.log(-200 >> 31);

// => -1

console.log(-100 >> 31);

// => 0

console.log(0 >> 31);

// => 0

console.log(100 >> 31);

// => 0

console.log(200 >> 31);

接下来,使用补零右移操作符>>>将 31 位向右移动,并从左侧移动任何需要的零。同样,您可以在下面的代码中看到这种情况:

// => 1

console.log(-200 >>> 31);

// => 1

console.log(-100 >>> 31);

// => 0

console.log(0 >>> 31);

// => 0

console.log(100 >>> 31);

// => 0

console.log(200 >>> 31);

最后,为了获得返回值,您使用了按位OR操作符。然而,除非您反转OR操作数右边的数字符号,否则您不会得到预期的结果。简化的函数如下所示:

// => -1

console.log(-200 >> 31 | 200 >>> 31);

// => -1

console.log(-100 >> 31 | 100 >>> 31);

// => 0

console.log(0 >> 31 | 0 >>> 31);

// => 1

console.log(100 >> 31 | -100 >>> 31);

// => 1

console.log(200 >> 31 | -200 >>> 31);

不透明代码

用任何语言都有可能写出晦涩或混乱的代码。有整个社区致力于这些追求。例如,黑帽编码者使用难以阅读的代码作为对抗白帽的一层防御。其他人从编写神秘代码中找到乐趣。有一种叫做编程高尔夫的消遣方式,玩家试图用最少的字符数(击球数)返回一个函数(球洞)的预期结果。以下是纯粹为了游戏而故意使用模糊语法的例子。这些例子中有许多可能被认为是 JavaScript 中真正的 WAT 例子。许多例子都是从网站wtfjs.com得到的灵感。

偷偷摸摸的评估

顾名思义,这个函数为执行代码提供了一个访问 eval 的后门。一些网站试图给用户提供一个经过净化的 JavaScript 子集来使用。如这段代码所示,用动态语言(如 JavaScript)很难做到这一点。这个脚本通过访问String.sub方法的constructor函数来工作。JavaScript constructor方法接受一个字符串,然后就地进行计算。

// => foo

""["sub"]["constructor"]("console.log('foo')")()

你所有的基地

比较不同碱基的数目时要小心。例如,在这里你比较一个八进制数和一个使用科学记数法的十进制数。除非你仔细阅读,否则你可能会对结果感到困惑。

// comparing against octals

// => false

1 + 064 == 65

// => false

064 > 60

// comparing against scientific notation

// => false

3000000000 > 4e9

变量的 Unicode

JavaScript 允许将 Unicode 用作属性描述符和变量名,这可能会导致一些非常不可读的代码。请考虑以下几点:

var \u1000 = {\u1001: function () {

return 'Unicode';

}

};

// => 'Unicode'

console.log(\u1000.\u1001());

确实是水

尽管 Unicode 示例可能有点难以理解,但它无法与后面的内容相比。这段代码以某种方式产生了单词'secret'这一事实看起来几乎是不可思议的。这段代码是由一个名为 jsfuck、 2 的程序生成的,从它的名字来看,这个程序的灵感来自于同样令人不快却恰如其分的标题 Brainfuck 语言。这是真正的代码,即使是最有经验的开发人员也会说哇!?

// => 'secret'

console.log((![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+([][(![]+[])[+[[+[]]]]+([][[]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]+(![]+[])[+[[!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+[]]]])

为了理解这段代码最终是如何产生字符串'secret'的,您需要简单地弄清楚它进行转换的机制。实际上,这比它第一次出现要容易。为了简洁起见,让我们只弄清楚如何再现隐藏单词中的字母 s。在代码中,s 可以在这个表达式中找到:(![]+[])[+[[!+[]+!+[]+!+[]]]]。了解了强制和一元运算之后,您就可以开始一步步地执行这段代码了。首先看看内部数组:

// => [3]

[!+[]+!+[]+!+[]]

您知道一元运算符将空数组转换为数字;在这种情况下,一个零。接下来你会看到逻辑NOT操作符,你知道它给出了操作数相反的布尔值。在这种情况下,操作数被强制转换为false,NOT操作符忠实地将其转换为true。这就剩下方程式true + true + true。接下来,二元运算符将true值相加,这首先需要将它们强制转换为数字。这意味着true + true + true现在是1 + 1 + 1。将它们相加得到3。下面的代码证明了刚才的步骤:

// => true

+[[!+[]+!+[]+!+[]]] == [3]

要继续,您需要了解括号内发生了什么。同样,一旦你把它分解开来,这是很容易弄清楚的。首先考虑这个:

// => true

!+[]

好了,很清楚了;你之前看到了同样的序列。然而,在这个版本中,布尔值false与空数组连接在一起。这意味着布尔值false变成了字符串"false"。本质上,我们的代码已经被简化为一个字符串,就像一个数组一样被访问,以获得第四个项目,也就是您要寻找的字母 s。成功!

// => 's'

("false")[3]

// => true

"s" == (![]+[])[+[[!+[]+!+[]+!+[]]]]

我鼓励你看看 jsfuck 4 项目的源代码,因为有一些有趣的瓶子里的船式扭曲,用来获得完全编码任何东西所需的所有字符。有些编码相当史诗。这里有一个例子:

// => true

'(' == ([][(![]+[])[+[[+[]]]]+([][[]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]+(![]+[])[+[[!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]]+[])[+[[+!+[]]]+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]

用 209 个字符来编码一个右括号。确实是水!

摘要

您刚刚花了整整一章的时间学习强制、位运算和逻辑运算符。现在,您更好地理解了为什么在使用 JavaScript 的这些特性时,实际结果和预期结果之间经常会出现脱节。擅长利用这些细微差别的程序员通常可以将非常复杂的行为打包到几个字符中。正如我在介绍中提到的,这种高度上下文相关的代码我称之为编程术语,但其他人贬损地称之为 WAT 风格的编程。在尝试使用或阅读 JavaScript 行话时,需要记住以下几点。

  • 编程行话是通过使用高度具体且通常是技术性的语言规则对代码进行压缩。
  • 行话不好也不坏;这取决于演讲者和听众是否理解表达的上下文和参考点。
  • 强制是将一种类型的对象或实体强制为另一种类型的行为。
  • 在 JavaScript 中,强制要么是为了将复杂对象简化为基本形式,要么是为了在两种基本类型之间进行转换。
  • 逻辑运算符用于返回布尔值,但在某些情况下,它们可用于缩短语句中的控制流。
  • 只能对整数执行位运算。
  • 位运算对于需要快速执行和/或运算资源有限的算法非常有用。
  • 位运算通常可以用来代替其他与数学相关的函数——例如,用∼∼'10'代替parseInt('10',10).

附加参考

Footnotes 1

T2http://markdaggett.com/blog/2012/03/23/generate-beautiful-gradients-using-javascript/

2

T2http://www.jsfuck.com/

3

T2http://en.wikipedia.org/wiki/Brainfuck

4

T2https://github.com/aemkei/jsfuck