在javascript无处不在的今天,我们每天都能很容易地学习到新的东西。一旦你掌握了这门语言的基本知识,就可以随处找到蕴含着丰富知识的代码。Bookmarklets 是一个完美的多种功能组合起来的工具,每当我发现一个有用的功能,我都会开心地去学习其源码,从中探索出怎么实现这样强大的功能。另外,如Google Analytics Code,或是Facebook Likebox的一些调用外部服务的小代码段,能教给我们的比一些书还多。
今天我想逐条跟大家分析Addy Osmani 几天前分享的一个单行代码 。这行代码可以帮你debug你的各层CSS。为了方便观看,我将其分为三行显示:
1 | [].forEach.call($$("*"), function(a){ |
将它放在你的浏览器控制台中运行,页面中各层的HTML就会被不同的颜色标记出来。很酷对不对?基本上,这行代码获取了页面中所有元素,然后给它们加上1px,颜色随机的边框。虽然它的原理很简单,但是想要自己写出这样的代码,你得熟练掌握Web开发的方方面面。下面让我们一起学习它们。
选取一个页面上所有的元素
首先需要做的是选取所有的元素,Addy用了只能在浏览器控制台中使用的 $$
。你可以在自己的浏览器的javascript控制台中输入 $$('a')
,然后你会得到一个含有当前页面所有锚元素的列表。
$$
函数是现代浏览器命令行的API的一部分,它等同于使用 document.querySelectorAll
方法。你可以将一个CSS选择器作为参数传入 document.querySelectorAll
去选取当前页面的元素。所以如果你想在浏览器的控制台以外使用那个单行代码,你可以用 document.querySelectorAll('*')
来替代 $$('*')
。点击这个stackoverflow问答可以进一步了解**$$**。
非常好!对于我来说,能从这行代码中学习到 $$函数 就已经很满足了。然而在一个页面上选取所有元素的方法还有很多。如果你有看过gist上面的评论,你会发现有人在讨论这个话题。其中一个人就是Mathias Bynens(那里有很多大神!),他告诉我们可以用document.all去实现这个功能,并且能在所有的浏览器上运行。
遍历元素
于是我们得到了存储所有的元素的NodeList,然后想给它们一个一个加上彩色的边框。不过等等,我们的代码里到底是怎么写的呢?
1 | [].forEach.call($$('*'), function(element) { /* And the modification code here */ }); |
NodeList看起来像Array,你可以使用方括号去访问它的节点,还可以访问length属性去了解它包含了多少元素,但是它并没有实现Array的所有接口,因此 $$('*').forEach
会失效。在Javascript里,有很多看起来像但不是数组的对象,例如函数里的arguments变量。我们有一个很好用的方法去处理这些对象:通过 call
和 apply
去实现像NodeList一样的非array对象有机会去调用array的函数。我几个月前谈过这些函数,它们调用另外一个函数,并将第一个参数作为该函数里面的this对象。
1 | function say(name) { |
单行代码用 [].forEach.call
来替代 Array.prototype.forEach.call
。通过Array对象[]去调用Array的函数这样的方式(哈,又是一个给力的小技巧),节省了一些字节。这相当于在 $$('*').forEach
中,把 $$('*')
当成一个Array来使用。
如果你再去看看评论区,你会发现有些人为了让代码更短些而使用 for(i=0;A=$$('*');)
。这确实可行,但是它会泄漏全局变量,所以如果你想要在控制台以外的地方使用这种方法,你最好在一个干净的环境中使用。
1 | for(var i=0,B=document.querySelectorAll('*');A=B[i++];){ /* your code here */ } |
如果你在浏览器的控制台中使用就无所谓了,从你在里面定义它们开始,i和A变量将会一直在里面。
给元素上色
为了使这些元素都有漂亮的边框,代码使用了CSS的outline属性。如果你还不知道的话,显示的边框是在CSS区块模型外的,它并不对元素本身在布局中的大小和位置产生任何影响,因此该属性非常适合用来实现我们的需求。 它的语法就像border的一样,所以理解下面的部分应该不难:
1 | a.style.outline="1px solid #" + color |
有意思的是这里定义颜色的方式:
1 | (~~(Math.random() * (1 << 24))).toString(16) |
被吓到了吗?当然,我不是一个位操作的专家,因此这是我最喜欢的部分,因为我从中学到了很多新东西。
我们想得到的是十六进制表示的颜色,如白色 #FFFFFF
, 或是蓝色 #0000FF
,亦或是…谁知道呢… #37f9ac
吧。像我一样的普通人都习惯了使用十进制数字,而我们心爱的代码对十六进制非常地了解。
首先我们可以从中学到用 toString
方法把十进制整数转换成十六进制整数。该方法以接收的参数作为其进制基数,将一个数转换为一个字符串表示的数。如果没有传入参数,将会默认进行十进制转换,但其实你可以使用其他基数。
1 | (30).toString(); // "30" |
反过来,你可以使用 parseInt
方法的第二个参数将十六进制数的字符串形式转换为十进制数。
1 | parseInt("30"); // "30" |
我们需要一个介于0和十六进制数 ffffff之间的随机数,那么就是 parseInt("ffffff", 16) == 16777215
。16777215正好是2^24 - 1。
你喜欢二进制算术吗?不喜欢的话,你只需要知道 1 << 24 == 16777216
就好了(建议在控制台里试试)。
如果喜欢二进制算术,你得知道每次你在1的右侧加一个0,就相当于做了一次2^n操作,其中n为你加0的次数。
1 | 1 // 1 == 2^0 |
左移运算 x << n
是向x的二进制表示加入n个0,因此 1 << 24
是16777216的简写版,进而通过 Math.random() * (1 << 4)
我们可以得到一个介于0和16777216之间的随机数。
这还没完,因为 Math.random
返回的值是浮点数,而我们只需要整数部分。我们这里使用了波浪符号(~)去实现。波浪符号用于对一个变量按位取反。如果你不明白我在说什么,这里有个很好的Javascript波浪符号讲解。
但是这些代码重点不在于按位取反,而在于利用位操作符会丢弃浮点数中的小数部分的特性,因此连续两次按位取反是一个替代 parseInt
的便捷方法:
1 | var a = 12.34, // ~~a = 12 |
再提醒大家一遍,如果你去gist中看看评论区,你会发现大家在用更简短的代码去获取 parseInt
的结果。使用 OR
位操作符你可以去掉我们随机数中的小数部分。
1 | ~~a == 0 | a == parseInt(a, 10) |
OR
操作符的优先级在最后,所以使用时可以省掉括号。这里是Javascript操作符的优先级介绍,感兴趣的话可以看看。
终于我们有了介于0和16777216之间的随机数,即我们的随机颜色。为了使用它,我们现在只需要用 toString(16)
将其转换成字符串形式的十六进制数即可。
一些感想
当一名程序员并不容易。我们疯狂地写代码,有时却没有意识到我们需要多少知识去做我们做的事情。而消化掉所有我们在工作中用到的概念,需要很长的时间。
我想突出强调的是我们工作的复杂度,因为我知道程序员通常都会被低估(尤其在我的国家,西班牙)。在大部分公司里我们扮演着重要角色,并且我们的工作非常有价值——能经常这样说,是一件很好的事情。
如果你第一眼看到这单行代码,你就能理解它,你可以为你感到骄傲。
如果不能,但是你还是把文章看到这里,不要担心,你很快就有能力写出这样的代码,因为你是一个学习者!