本节书摘来异步社区《D3.js数据可视化实战手册》一书中的第1章,第1.4节,作者: 【加拿大】Nick Qi Zhu,更多章节内容可以访问云栖社区“异步社区”公众号查看。
1.4 理解D3风格的JavaScript
D3.js数据可视化实战手册
对于那些习惯了过程式或者面向对象式的JavaScript风格的人来说,他们会感觉D3使用函数式的JavaScript编程风格有一些奇怪。本节会涵盖一些JavaScript中函数式编程最根本的功能性概念,以便对D3有个基本的理解,将来可以用D3的风格来编写可视化工程的代码。
1.4.1 准备阶段
在浏览器中打开下面文件的本地副本。
https://github.com/NickQiZhu/d3-cookbook/blob/master/src/ chapter1/functional-js.html
AI 代码解读
1.4.2 开始编程
现在让我们尝试更深入一点,了解一下JavaScript函数式方面的内容。请看下面的代码段,
function SimpleWidget(spec) {
var instance = {}; // <-- A
var headline, description; // <-- B
instance.render = function () {
var div = d3.select('body').append("div");
div.append("h3").text(headline); // <-- C
div.attr("class", "box")
.attr("style", "color:" + spec.color) // <-- D
.append("p")
.text(description); // <-- E
return instance; // <-- F
};
instance.headline = function (h) {
if (!arguments.length) h; // <-- G
headline = h;
return instance; // <-- H
};
instance.description = function (d) {
if (!arguments.length) d;
description = d;
return instance;
};
return instance; // <-- I
}
var widget = SimpleWidget({color: "#6495ed"})
.headline("Simple Widget")
.description("This is a simple widget demonstrating functional javascript.");
widget.render();
AI 代码解读
这段代码在页面上生成了下面图片中的示例。

1.4.3 工作原理
尽管非常简单,但是不可否认,这段示例中的代码跟D3风格的JavaScript非常相似。这不是巧合,在JavaScript编程范型中,这叫做函数对象。跟很多有趣的话题一样,这个话题也能写一本书。不过在本节中,我会尝试尽量多讲一些这种特殊范型的东西,好让不理解D3语法的读者也能创建这种风格的库文件。正如D3的维基页面上所讲的那样,这种函数式编程风格给D3带来了很大的便利性。
D3的函数风格,使得多种组件插件之间的代码重用成为现实。
D3维基(2013年8月)
函数即对象
JavaScript中的函数是对象。跟其他的对象一样,函数只是键值对的一个集合。函数对象跟普通对象的区别就是,函数可以执行,函数还带有两个隐藏的属性,即函数上下文和函数代码。这两个隐藏属性有时候会给你一个大大的“意外惊喜”,如果你有着很深的过程式编程背景,这点可能更明显。不过这也是我们最需要注意的关键点了,要了解一下D3使用函数的奇怪方式。
图像说明文字JavaScript的大部分特性都显得有些不够“面向对象”,不过在函数对象这方面,JavaScript跟其他语言相比较应该更胜一筹。
现在我们心里有了这样的概念,那就再看一遍这段代码。
var instance = {}; // <-- A
var headline, description; // <-- B
instance.render = function () {
var div = d3.select('body').append("div");
div.append("h3").text(headline); // <-- C
div.attr("class", "box")
.attr("style", "color:" + spec.color) // <-- D
.append("p")
.text(description); // <-- E
return instance; // <-- F
};
AI 代码解读
在A、B和C行,我们可以看到instance、headline和description都是SimpleWidget这个函数对象的内部私有变量。可是render函数却是instance对象的一个方法,并且被定义为对象字面量。函数本身也是对象,它可以存储在对象/函数、其他变量、数组里,也可以作为函数参数。运行SimpleWidget的结果就是I行所写的,返回一个instance对象。
function SimpleWidget(spec) {
...
return instance; // <-- I
}```
图像说明文字render函数中用到了一些我们还没讲过的D3中的函数,不过我们现在先不管它们,后面的章节中我们会详细讲解的。它们也只是渲染了一些可视化的东西,跟我们目前的话题没有太多的关系。
静态变量作用域
好奇的读者可能会问,这个示例中的变量作用域到底是怎样的啊?看上去好奇怪,render函数不仅访问了instance、headline和description,还访问了从SimpleWidget传进来的spec变量。这个怪异的变量作用域其实是由一个简单的静态作用域规则来决定的。可以把这个规则想象成这样:当查找一个变量引用时,该变量先被当成是一个本地变量。如果没有找到变量声明(比如C行中的headline),就继续在父对象里找(本例中,SimpleWidget函数就是静态的父对象,headline变量的声明在B行)。如果还是没有找到,就不断地重复这个过程,递归地去父对象里查找,一直到全局变量的定义那层。如果最后还是没有找到,就针对该变量生成一个引用错误。这样的作用域行为与大多数流行语言(诸如Java、C#)中的变量处理方式大不相同,可能需要一段时间适应,要是你觉得不习惯的话,也不用担心,练得多了,就习惯了。
图像说明文字对于有Java和C#背景的人,需要再提醒一下,JavaScript没有实现块作用域(block scoping)。我们这里描述的静态作用域规则,仅仅适用于函数/对象级别,不适用于块级别。
AI 代码解读
for(var i = 0; i < 10; i++){
for(var i = 0; i < 2; i++){
console.log(i);
AI 代码解读
}
}`
图像说明文字对于上面这段代码,你可能觉得它会打印20个数字。其实在JavaScript里,这段代码会陷入到无限循环。因为JavaScript没有实现块级别的作用域,所以里面那层循环的i跟外面那层循环的i是同一个变量。于是里面的循环改变了i的值,导致外面的循环永远不会结束。
跟流行的原型编程中的伪类模式相比,这样的模式通常被称作函数模式。函数模式的优点是提供了更好的信息隐藏和封装。因为只能通过静态作用域规则里限定的那些嵌套定义的函数来访问私有变量(我们示例中的headline和description),所以SimpleWidget函数返回的对象就更加灵活,也更加健壮。
如果我们用函数式风格创建对象,并且该对象所有的方法都没有用this,那这个对象就是持久(durable)的 1。持久对象就是许多功能行为的集合了。
(Crockfort D. 2008年)```
可变参数函数
在G行有些奇怪的东西。
AI 代码解读
instance.headline = function (h) {
if (!arguments.length) h; // <-- G
headline = h;
return instance; // <-- H
};`
你可能会问,G行的这个arguments从哪里来的?这个例子中从来没有定义过它。其实这个arguments是内建的隐藏参数,并可在函数执行时直接使用。arguments是一个数组,它包含了所在函数的全部参数。
图像说明文字实际上,arguments本身并不是JavaScript的数组对象。虽然它有length 属性,并可以用索引下标访问每个元素,但是它没有JavaScript中数组对象的那么多方法(比如slice、concat)。如果你想在arguments上使用JavaScript数组对象的方法,需要这样:
var newArgs = Array.prototype.slice.apply (arguments);```
把这个隐藏的参数和JavaScript可以在函数声明时省略参数的功能结合起来,就可以写出instance.headline这种不需要指定参数个数的函数。在本例中,我们可以传一个参数h,也可以不传。因为如果没有传进来参数,arguments.length就返回0,headline函数就返回h,如果h有值,就变成了一个赋值操作。为了说清楚,我们看看下面这段代码。
AI 代码解读
var widget = SimpleWidget({color: "#6495ed"})
.headline("Simple Widget"); // 给headline赋值
AI 代码解读
console.log(widget.headline()); // 输出"Simple Widget"`
这里你可以看到headline在参数不同的情况下,可以分别作为setter和getter(赋值操作和取值操作)。
函数级联调用
这个例子另一个有趣的地方是函数的级联调用。这也是D3库提供的一个主要的函数调用方式,因为D3库中的大多数函数都设计成了这种链式的结构,以便能提供简洁的、上下文连贯的编程接口。如果你理解可变参数函数的概念,就很好理解这个了。可变参数函数(比如headline函数)能同时作为setter和getter,当其作为setter时,返回instance对象,这就使得你可以在返回的instance上立即执行另一个函数,此即链式调用。
看下面这段代码。
var widget = SimpleWidget({color: "#6495ed"})
.headline("Simple Widget")
.description("This is ...")
.render();```
这个例子中,SimpleWidget函数返回了instance对象(如I行所示)。那么,headline函数在这里是一个setter,同时也返回一个instance对象(如H行所示)。description函数执行后也返回instance对象。最后调用了render函数。
现在我们已经大概了解了JavaScript的函数式风格,有了一个可工作的D3开发环境,也准备好了使用D3提供的丰富功能来一试身手。在开始前,我还想讲几个比较重点的事情,即如何寻找、分享代码以及有困难时如何获取帮助。
AI 代码解读