毫无疑问,JavaScript 是当前最流行脚本语言,不过我认为它获此境遇是历史原因造成的,而非源自优秀的设计。相反,JavaScript 的设计过于灵活和随意,以致坊间流传着各种“杂技”,其中不乏冠之以“高性能”的技巧。而初学者和 JavaScript “熟手” 面对这样的耍杂代码无所适从,不求甚解之下就往往忽略了它。
但是,为什么这些奇技淫巧的代码没有导致语法错误?为什么它也能运行?如何举一反三?解开这些问题,需要了解被大多数人忽略的 JavaScript 基础知识。这里试图解析一些 tricks, 告诉你它们背后的秘密。
提示:
- 要尝试下面的代码,你需要使用 JavaScript 控制台,或一个交互式 JavaScript 解析器。
- 下面代码中的变量如无特别注明,均假定其已通过
var
定义。
短路语法:3 个为什么
1 || alert('ok')
先上甜点,这是大家司空见惯的短路语法。这里后面的代码不会被执行,因此它不是我要说的重点。但是,它为什么不会报错?因为它是普通的运算。
1 && foo = 1
这行代码什么会报错?经工友指出,是因为
=
运算符优先级比&&
低,它实现执行等价于(1 && foo) = 1
,显示赋值给另一个值是不允许的。
1 && (foo = 1)
这行代码为什么能运行?因为
()
运算符优先级比&&
高。
更多优先级问题
1 && 1, foo = 2
可以运行,因为,
优先级最低,表达式等级于(1 && 1), foo = 2
。1 && function() { foo = 2 }()
也没有问题,因为 前面说了()
优先级比&&
高。PS: 使用
()
求值即获得 JavaScript 内部可以接受的值。例如(2.0000000000000001)
在一些机器上可能会返回2
。感谢工友 hanyee & vapour 对运算符优先级问题的指正。
运算符技巧:把字符串转换为数值类型
先从一个单目运算符开始。什么是单目?只有一只眼睛,也就是说,运算符只接收一个参数。
转型:+'1'
我们知道
-
作为减法运算符时接收两个参数,而作为求负运算符时,接收一个参数。JavaScript 支持另一个不多见的“求正”的+
运算符。显然,+
会尝试把任何接收到的参数转换为数值型。如此,我们就有了一个廉价转型方法,考虑一下:parseInt('123')
vs+'123'
.
转型并取整:'123.4' | 0
这里使用“或运算”将左边的字符串隐式转换为数值型,再与
0
或。所有位运算都要求使用 32 位整数参与运算。所以这又是一个廉价的转型取整方法。考虑一下Math.floor('1234.5')
vs'1234.5' | 0
.但是,32 位整数表示的数据范围是有限的,因此这一招在数值超过 231 - 1 时不适用,考虑一下
'12345678912.3' ^ 0
如何举一反三?
'1234.5' ^ 0
完成转型并取整,与0
异或,得到它本身,还记得这个规则吗?~~'1234.5'
同样可以完成转型并取整,因为两次取反得到相同的值。--'1234.5'
会失败。虽然我们说负负得正,但在 C 语系中,--
运算符优先级高于单个-
。解决办法很简单,把--
写成- -
就好啦,增加一个空格避免运算符被错误地识别。同理,++
也要这样处理。
渐入佳境哈?接下来,我们把前面的知识综合运用,试图解析闭包中代码中括号的秘密。
懒人的闭包
闭包即 function() {}
代码块。通常需要控制 JavaScript 变量作用域时,我们把代码放在这个块中运行:
1 2 3 4 |
|
失败的闭包
上面的代码中要写两对括号,如果代码块太长的话,上下的括号不方便对照。于是,你可能会有意无意漏掉 function
周围的括号:
1 2 3 4 |
|
很不幸,运行这段代码会报语法错误(不同 JavaScript 执行程序抛出的异常信息可能会不一样):
Exception: SyntaxError: Unexpected token '('
背景知识:()
只能用来求值、定义参数列表或调用函数表达式(expression)。本来,function() {}
定义的一个函数字面量(如同数组字面量 []
)表达式是可以拿来调用的,但是由于设计上的原因,function
有两种表达形式:
function fn() {};
: 这是函数声明(declaration)的语句(statement);var foo = function() {}
: 这是函数字面量(literal)表达式。与上面雷同的写法var foo = function fn() {}
也是合法的表达式,不过有一点小区别。
即是说,JavaScript 需要有足够的上下文(context)才能判断 function
的使用属于语句还是表达式。
所以,加上一对括号,就排除了 function
作为语句声明的目的,既然不是语句,那就是函数表达式咯。
对于单独(在语句前后加上分号将其表达为独立语句)的 ;function() {}();
来说,JavaScript 无法区分其中的 function
是表达式还是语句。此时,JavaScript 选择了传统的语句识别,于是它被识别为两个语句————两个有问题的语句:前者缺少函数名称声明,后者不允许使用空的 ()
进行求值。
于是懒人们行动了,网上流传了一些不写第一组括号也能正确运行的闭包。
懒人的解决方案
这是比较常见的懒人闭包:
1 2 3 4 |
|
为什么它能执行?因为它很好地“营造”了一个让 JavaScript 将其中的 function
识别为表达式的环境————通过单目求正运算符,让 JavaScript 知道这里的 function
不可能是语句。哪有对语句进行运算的啊。
开始举一反三吧
既然写个 +
运算符就行,那就好玩了:
-function() { return -1; }()
求负也行哈。PS: 如果你运行这个语句,会得到1
, 因为函数返回的-1
被你求负了一次。!function() { return -1; }()
取非也没问题。提问:运行它会返回什么?1 + function() { return -1; }()
非常标准的表达式,当然 OK 啦…void function() { return -1; }()
亲,void
也是运算符哦,delete
能用吗?当然可以!1, function() { return -1; }()
别把逗号不当运算符!~function() { return -1; }()
位运算符也来凑热闹了哈…- 还可以写很多,随便怎么玩,只要组成表达式就行,自由发挥吧…
嗯,先解析到这里吧。后续还有新的 trick, 我也会在这里继续更新。
最后,我想说的是,上面这些 tricks 大多都是 JavaScript 不好的设计导致的滥用,一些变种的代码让团队协作更困难。学会运用这些并不会显著增长你的技能,但了解背后的原理则会让你更深入地理解 JavaScript.