在javascript无处不在的今天,我们每天都能很容易地学习到新的东西。一旦你掌握了这门语言的基本知识,就可以随处找到蕴含着丰富知识的代码。Bookmarklets是一个完美的多种功能组合起来的工具,每当我发现一个有用的功能,我都会开心地去学习其源码,从中探索出怎么实现这样强大的功能。另外,如Google Analytics Code,或是Facebook Likebox的一些调用外部服务的小代码段,能教给我们的比一些书还多。
今天我想逐条跟大家分析Addy Osmani 几天前分享的一个单行代码 。这行代码可以帮你debug你的各层CSS。为了方便观看,我将其分为三行显示:
1 2 3 |
[].forEach.call($ $(""),function(a){ a.style.outline="1px solid #"+(~~(Math.random()*(1<<24))).toString(16) }) |
将它放在你的浏览器控制台中运行,页面中各层的HTML就会被不同的颜色标记出来。很酷对不对?基本上,这行代码获取了页面中所有元素,然后给它们加上1px,颜色随机的边框。虽然它的原理很简单,但是想要自己写出这样的代码,你得熟练掌握Web开发的方方面面。下面让我们一起学习它们。
选取一个页面上所有的元素
首先需要做的是选取所有的元素,Addy用了只能在浏览器控制台中使用的$ $
。你可以在自己的浏览器的javascript控制台中输入$ $(‘a’)
,然后你会得到一个含有当前页面所有锚元素的列表。
$ $
函数是现代浏览器命令行的API的一部分,它等同于使用document.querySelectorAll
方法。你可以将一个CSS选择器作为参数传入document.querySelectorAll去选取当前页面的元素。所以如果你想在浏览器的控制台以外使用那个单行代码,你可以用document.querySelectorAll('*')
来替代$ $('*')
。点击这个stackoverflow问答可以进一步了解$ $
。
非常好!对于我来说,能从这行代码中学习到$ $
函数就已经很满足了。然而在一个页面上选取所有元素的方法还有很多。如果你有看过gist上面的评论,你会发现有人在讨论这个话题。其中一个人就是Mathias Bynens(那里有很多大神!),他告诉我们可以用document.all
去实现这个功能,并且能在所有的浏览器上运行。
遍历元素
于是我们得到了存储所有的元素的NodeList
,然后想给它们一个一个加上彩色的边框。不过等等,我们的代码里到底是怎么写的呢?
1 2 |
[].forEach.call( $ $('*'), function( element ) { /* And the modification code here */ }); [].forEach.call( $ $('*'), function( element ) { /* 对元素作出改变的代码 */ }); |
NodeList
看起来像Array
,你可以使用方括号去访问它的节点,还可以访问length属性去了解它包含了多少元素,但是它并没有实现Array
的所有接口,因此$ $(‘*’).forEach
会失效。在Javascript里,有很多看起来像但不是数组的对象,例如函数里的arguments变量。我们有一个很好用的方法去处理这些对象:通过call和apply去实现像NodeList
一样的非array对象有机会去调用array的函数。我几个月前谈过这些函数,它们调用另外一个函数,并将第一个参数作为该函数里面的this对象。
1 2 3 4 5 6 7 8 9 10 11 12 |
function say(name) { console.log( this + ' ' + name ); } say.call( 'hola', 'Mike' ); // Prints out 'hola Mike' in the console say.call( 'hola', 'Mike' ); // 在console中输出’hola Mike’ // Also you can use it on the arguments object //你也可以在arguments对象上使用它 function example( arg1, arg2, arg3 ) { return Array.prototype.slice.call(arguments, 1); // Returns [arg2, arg3] } |
单行代码用[].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 2 3 4 5 6 7 8 9 10 11 |
(30).toString(); // "30" (30).toString(10); // "30" (30).toString(16); // "1e" Hexadecimal (30).toString(2); // "11110" Binary (30).toString(36); // "u" 36 is the maximum base allowed (30).toString(); // "30" (30).toString(10); // "30" (30).toString(16); // "1e" 十六进制 (30).toString(2); // "11110" 二进制 (30).toString(36); // "u" 36 是所允许的最大基数 |
反过来,你可以使用parseInt
方法的第二个参数将十六进制数的字符串形式转换为十进制数。
1 2 3 4 5 |
parseInt("30"); // "30" parseInt("30", 10); // "30" parseInt("1e", 16); // "30" parseInt("11110", 2); // "30" parseInt("u", 36); // "30" |
我们需要一个介于0
和十六进制数 ffffff
之间的随机数,那么就是parseInt("ffffff", 16) == 16777215
。16777215正好是2^24 - 1
。
你喜欢二进制算术吗?不喜欢的话,你只需要知道1<<24 == 16777216
就好了(建议在控制台里试试)。
如果喜欢二进制算术,你得知道每次你在1的右侧加一个0,就相当于做了一次2^n
操作,其中n
为你加0的次数。
1 2 3 4 |
1 // 1 == 2^0 100 // 4 == 2^2 10000 // 16 == 2^4 1000000000000000000000000 // 16777216 == 2^24 |
左移运算x<<n
是向x
的二进制表示加入n
个0
,因此1<<24
是16777216的简写版,进而通过Math.random()*(1<<4)
我们可以得到一个介于0
和16777216
之间的随机数。
这还没完,因为Math.random
返回的值是浮点数,而我们只需要整数部分。我们这里使用了波浪符号(~)去实现。波浪符号用于对一个变量按位取反。如果你不明白我在说什么,这里有个很好的Javascript波浪符号讲解。
但是这些代码重点不在于按位取反,而在于利用位操作符会丢弃浮点数中的小数部分的特性,因此连续两次按位取反是一个替代parseInt
的便捷方法:
1 2 3 4 5 6 7 |
var a = 12.34, // ~~a = 12 b = -1231.8754, // ~~b = -1231 c = 3213.000001; // ~~c = 3213 ~~a == parseInt(a, 10); // true ~~b == parseInt(b, 10); // true ~~c == parseInt(c, 10); // true |
再提醒大家一遍,如果你去gist中看看评论区,你会发现大家在用更简短的代码去获取parseInt
的结果。使用OR
位操作符你可以去掉我们随机数中的小数部分。
1 2 3 |
~~a == 0|a == parseInt(a, 10) ~~b == 0|b == parseInt(b, 10) ~~c == 0|c == parseInt(c, 10) |
Or
操作符的优先级在最后,所以使用时可以省掉括号。这里是Javascript操作符的优先级介绍,感兴趣的话可以看看。
终于我们有了介于0
和16777216
之间的随机数,即我们的随机颜色。为了使用它,我们现在只需要用toString(16)
将其转换成字符串形式的十六进制数即可。
一些感想
当一名程序员并不容易。我们疯狂地写代码,有时却没有意识到我们需要多少知识去做我们做的事情。而消化掉所有我们在工作中用到的概念,需要很长的时间。
我想突出强调的是我们工作的复杂度,因为我知道程序员通常都会被低估(尤其在我的国家,西班牙)。在大部分公司里我们扮演着重要角色,并且我们的工作非常有价值——能经常这样说,是一件很好的事情。
如果你第一眼看到这单行代码,你就能理解它,你可以为你感到骄傲。
如果不能,但是你还是把文章看到这里,不要担心,你很快就有能力写出这样的代码,因为你是一个学习者!
如果你看到文章的第二行就在想太长不看,但你还是看到这里了,你真是个奇葩,但我仍欢迎你在评论区写下你的意见:)