JavaScript 基础、进阶以及 Ubuntu 系统中的 JavaScript 开发调

阅读目录

  • 前言
  • 对象和原型链
  • 我使用的画图工具Graphviz
  • 作用域链、上下文环境和闭包
  • 函数和this
  • 用JavaScript模拟经典的面向对象编程
  • JavaScript的模块化写法
  • 总结

前言

JavaScript 是我接触到的第二门编程语言,第一门是 C 语言。然后才是 C++、Java 还有其它一些什么。所以我对 JavaScript 是非常有感情的,毕竟使用它有十多年了。早就想写一篇关于 JavaScript 方面的东西,从入门的学习笔记到高手的心得体会一应俱全,不管我怎么写,都难免落入俗套,所以迟迟没有动笔。另外一个原因,也是因为在 Ubuntu 环境中一直没有找到很好的 JavaScript 开发工具,这种困境直到 Node.js 和 Visual Studio Code 的出现才完全解除。

十年前,对 JavaScript 的介绍都是说他是基于对象的编程语言,而从没有哪本书会说 JavaScript 是一门面向对象的编程语言。基于对象很好理解,毕竟在 JavaScript 中一切都是对象,我们随时可以使用点号操作符来调用某个对象的方法。但是十年前,我们编写 JavaScript 程序时,都是像 C 语言那样使用函数来组织我们的程序的,只有在论坛的某个角落中,有少数的高手会偶尔提到你可以通过修改某个对象的prototype来让你的函数达到更高层次的复用,直到 Flash 的 ActionScript 出现时,才有人系统介绍基于原型的继承。十年后的现在,使用 JavaScript 的原型链和闭包来模拟经典的面向对象程序设计已经是广为流传的方案,所以,说 JavaScript 是一门面向对象的编程语言也丝毫不为过。

我喜欢 JavaScript,是因为它非常具有表现力,你可以在其中发挥你的想象力来组织各种不可思议的程序写法。也许 JavaScript 语言并不完美,它有很多坑和陷阱,而正是这些很有特色的语言特性,让 JavaScript 的世界出现了很多奇技淫巧。

对象和原型链

JavaScript 是一门基于对象的编程语言,在 JavaScript 中一切都是对象,包括函数,也是被当成一等一的对象对待,这正是 JavaScript 极其富有表现力的原因。在 JavaScript 中,创建一个对象可以这么写:

var someThing = new Object();

这和其它面向对象语言的使用某个类的构造函数创建一个对象是一样一样的。但是在 JavaScript 中,这不是最推荐的写法,使用对象字面量来定义一个对象更简洁,如下:

var anotherThing = {};

这两个语句其本质是一样的,都是生成一个空对象。对象字面量也可以用来写数组以及更加复杂的对象,这样:

var weekDays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];

这样:

var person = {
    name : "youxia",
    age : 30,
    gender : "male",
    sayHello : function(){ return "Hello, my name is " + this.name; }
}

甚至这样数组和对象互相嵌套:

var workers = [{name : "somebody", speciality : "Java"}, {name : "another", speciality : ["HTML", "CSS", "JavaScript"]}];

需要注意的是,对象字面量中的分隔符都是逗号而不是分号,而且即使 JavaScript 对象字面量的写法和 JSON 的格式相似度很高,但是它们还是有本质的区别的。

在我们捣鼓 JavaScript 的过程中,工具是非常重要的。我这里介绍的第一个工具就是 Chromium 浏览器中自带的 JavaScript 控制台。在 Ubuntu 中安装 Chromium 浏览器只需要一个命令就可以搞定,如下:

sudo apt-get install chromium

启动 Chromium 浏览器后,只需要按 F12 就可以调出 JavaScript 控制台。当然,在菜单中找出来也可以。下面,让我把上面的示例代码输入到 JavaScript 控制台中,一是可以看看我们写的代码是否有语法错误,二是可以看看 JavaScript 对象的真面目。如下图:
JavaScript 基础、进阶以及 Ubuntu 系统中的 JavaScript 开发调

对于广大的前端攻城狮来讲,Chromium 的 JavaScript 控制台已经是一个烂大街的工具了,在控制台中写console.log("Hello, World!");就像是在 C 语言中写printf("Hello, World!");一样成为了入门标配。在控制台中输入 JavaScript 语句后,一按 Enter 该行代码就立即执行,如果要输入多行代码怎么办呢?一个办法就是按 Shift+Enter 进行换行,另外一个办法就是在别的编辑器中写好然后复制粘贴。不过在 Chromium 的 JavaScript 控制台中还有一些不那么广泛流传的小技巧,比如使用console.dir()函数输出 JavaScript 对象的内部结构,如下图:
JavaScript 基础、进阶以及 Ubuntu 系统中的 JavaScript 开发调

从图中,可以很容易看出每一个对象的属性、方法和原型链。

和其它的面向对象编程语言不同, JavaScript 不是基于类的代码复用体系,它选择了一种很奇特的基于原型的代码复用机制。通俗点说,如果你想创建很多对象,而这些对象有某些相同的属性和行为,你为每一个对象编写单独的代码肯定是不合算的。在其它的面向对象编程语言中,你可以先设计一个类,然后再以这个类为模板来创建对象。我这里称这种方式为经典的面向对象体系。而在 JavaScript 中,解决这个问题的方式是把一个对象作为另外一个对象的原型,拥有相同原型的对象自然拥有了相同的属性和行为。对象拥有原型,原型又有原型的原型,最终构成一个原型链。在现代 JavaScript 模式中,硬是用函数、闭包和原型链模拟了经典的面向对象体系。

原型这个概念本身并不复杂,复杂的是 JavaScript 中的隐式原型和函数对象。什么是隐式原型,就是说在 JavaScript 中不管你以什么方式创建一个对象,它都会自动给你生成一个原型对象,我们的对象中,有一个隐藏的__proto__属性,它指向这个自动生成的原型对象;并且在 JavaScript 中不管你以什么方式创建一个对象,它最终都是从构造函数生成的,以对象字面量构造的对象也有构造函数,它们分别是Object()Array(),每一个构造函数都有一个自动生成的prototype属性,它也指向那个自动生成的原型对象。而且在 JavaScript 中一切都是对象,构造函数也不例外,所以构造函数既有prototype属性,又有__proto__属性。再而且,自动生成的原型对象也是对象,所以它也应该有自己的原型对象。你看,说起来都这么拗口,理解就更加不容易了,更何况 JavaScript 中还内置了Object()Array()String()Number()Boolean()Function()这一系列的构造函数。看来不画个图是真的理不顺了。下面我们来抽丝剥茧。

先考察空对象someThing,哪怕它是以对象字面量的方式创建的,它也是从构造函数Object()构造出来的。这时,JavaScript 会自动创建一个原型对象,我们称这个原型对象为Object.prototype,构造函数Object()prototype属性指向这个对象,对象someThing__proto__属性也指向这个对象。也就是说,构造函数Object()prototype属性和对象someThing__proto__属性指向的是同一个原型对象。而且,这个原型对象中有一个constructor属性,它又指回了构造函数Object(),这样形成了一个环形的连接。如下图:
JavaScript 基础、进阶以及 Ubuntu 系统中的 JavaScript 开发调

要注意的是,这个图中所显示的关系是对象刚创建出来的时候的情况,这些属性的指向都是可以随意修改的,改了就不是这个样子了。下面在 JavaScript 控制台中验证一下上图中的关系:
JavaScript 基础、进阶以及 Ubuntu 系统中的 JavaScript 开发调

请注意,构造函数Object()prototype属性和__proto__属性是不同的,只有函数对象才同时具有这两个属性,普通对象只有__proto__属性,而且这个__proto__属性是隐藏属性,不是每个浏览器都允许访问的,比如 IE 浏览器。下面,我们来看看 IE 浏览器的开发者工具:
JavaScript 基础、进阶以及 Ubuntu 系统中的 JavaScript 开发调

这是一个反面教材,它既不支持console.dir()来查看对象,也不允许访问__proto__内部属性。所以,在后面我讲到继承时,需要使用特殊的技巧来避免在我们的代码中使用__proto__内部属性。上面的例子和示意图中,都只说构造函数Object()prototype属性指向原型对象,没有说构造函数Object()__proto__属性指向哪里,那么它究竟指向哪里呢?这里先留一点悬念。

下一步,我们自己创建一个构造函数,然后使用这个构造函数创建一个对象,看看它们之间原型的关系,代码是这样的:

functionPerson(name, age, gender){
    this.name = name;
    this.age = age;
    this.gender = gender;
}
Person.prototype.sayHello = function(){ return "Hello, my name is " + this.name; };
var somebody = new Person("youxia", 30, "male");

输入到 Chromium 的 JavaScript 控制台中,然后使用console.dir()分别查看构造函数Person()和对象somebody,如下两图:
JavaScript 基础、进阶以及 Ubuntu 系统中的 JavaScript 开发调

JavaScript 基础、进阶以及 Ubuntu 系统中的 JavaScript 开发调

用图片来表示它们之间的关系,应该是这样的:
JavaScript 基础、进阶以及 Ubuntu 系统中的 JavaScript 开发调

我使用蓝色表示构造函数,黄色表示对象,如果是 JavaScript 自带的构造函数和 prototype 对象,则颜色深一些。从上图中可以看出,构造函数Person()有一个prototype属性和一个__proto__属性,__proto__属性的指向依然留悬念,prototype属性指向Person.prototype对象,这是系统在我们定义构造函数Person()的时候,自动创建的一个和构造函数Person()相关联的原型对象,请注意,这个原型对象是和构造函数Person()相关联的原型对象,而不是构造函数Person()的原型对象。当我们使用构造函数Person()创建对象somebody时,somebody的原型就是这个系统自动创建的原型对象Person.prototype,就是说对象somebody__proto__属性指向原型对象Person.prototype。而这个原型对象中有一个constructor属性,又指回构造函数Person(),形成一个环。这和空对象和构造函数Object()是一样的。而且原型对象Person.prototype__proto__属性指向Object.prototype。如果在这个图中把空对象和构造函数Object()加进去的话,看起来是这样的:
JavaScript 基础、进阶以及 Ubuntu 系统中的 JavaScript 开发调

有点复杂了,是吗?不过这还不算最复杂的,想想看,如果把JavaScript 内置的Object()Array()String()Number()Boolean()Function()这一系列的构造函数以及与它们相关联的原型对象都加进去,会是什么情况?每一个构造函数都有一个和它相关联的原型对象,Object()Object.prototypeArray()Array.prototype,依此类推。其中最特殊的是Function()Function.prototype,因为所有的函数和构造函数都是对象,所以所有的函数和构造函数都有构造函数,而这个构造函数就是Function()。也就是说,所有的函数和构造函数都是由Function()生成,包括Function()本身。所以,所有的构造函数的__proto__属性都应该指向Function.prototype,前面留的悬念终于有答案了。如果只考虑构造函数Person()Object()Function()及其关联的原型对象,在不解决悬念的情况下,图形是这样的:
JavaScript 基础、进阶以及 Ubuntu 系统中的 JavaScript 开发调

可以看到,每一个构造函数和它关联的原型对象构成一个环,而且每一个构造函数的__proto__属性无所指。通过前面的分析我们知道,每一个函数和构造函数的__proto__属性应该都指向Function.prototype。我用红线标出这个关系,结果应该如下图:
JavaScript 基础、进阶以及 Ubuntu 系统中的 JavaScript 开发调

如果我们画出前面提到过的所有构造函数、对象、原型对象的全家福,会是个什么样子呢?请看下图:
JavaScript 基础、进阶以及 Ubuntu 系统中的 JavaScript 开发调

晕菜了没?欢迎指出错误。把图一画,就发现其实 JavaScript 中的原型链没有那么复杂,有几个内置构造函数就有几个配套的原型对象而已。我这里只画了六个内置构造函数和一个自定义构造函数,还有几个内置构造函数没有画,比如Date()Math()Error()RegExp(),但是这不影响我们理解。写到这里,是不是应该介绍一下我使用的画图工具了?

我使用的画图工具Graphviz

在我的 Linux 系列中,有一篇介绍画图工具的文章,不过我这次使用的工具是另辟蹊径的 Graphviz,据说这是一个由贝尔实验室的几个牛人开发和使用的画流程图的工具,它使用一种脚本语言定义图形元素,然后自动进行布局和生成图片。首先,在 Ubuntu 中安装 Graphiz 非常简单,一个命令的事儿:

sudo apt-get install graphviz

然后,创建一个文本文件,我这里把它命名为sample.gv,其内容如下:

digraph GraphvizDemo{

    Alone_Node;
    
    Node1 -> Node2 -> Node3;
    
}

这是一个最简单的图形定义文件了,在 Graphviz 中图形仅仅由三个元素组成,它们分别是:1、Graph,代表整个图形,上面源代码中的digraph GraphvizDemo{}就定义了一个 Graph,我们还可以定义 SubGraph,代表子图形,可以用 SubGraph 将图形中的元素分组;2、Node,代表图形中的一个节点,可以看到 Node 的定义非常简单,上面源码中的Alone_Node;就是定义了一个节点;3、Edge,代表连接 Node 的边,上面源码中的Node1 -> Node2 -> Node3;就是定义了三个节点和两条边,可以先定义节点再定义边,也可以直接在定义边的同时定义节点。然后,调用 Graphviz 中的dot命令,就可以生成图形了:

dot -Tpng sample.gv > sample.png

生成的图形如下:
JavaScript 基础、进阶以及 Ubuntu 系统中的 JavaScript 开发调

上面的图形中都是用的默认属性,所以看起来效果不咋地。我们可以为其中的元素定义属性,包括定义节点的形状、边的形状、节点之间的距离、字体的大小和颜色等等。比如下面是一个稍微复杂点的例子:

digraph GraphvizDemo{
    
    nodesep=0.5;
    ranksep=0.5;
        
    node [shape="record",style="filled",color="black",fillcolor="#f4a582",fontname="consolas",fontsize=15];
    edge [style="solid",color="#053061"];
        
    root  [label="<l>left|<r>right"];
    left  [label="<l>left|<r>right"];
    right [label="<l>left|<r>right"];
    leaf1 [label="<l>left|<r>right"];
    leaf2 [label="<l>left|<r>right"];
    leaf3 [label="<l>left|<r>right"];
    leaf4 [label="<l>left|<r>right"];
    
    root:l:s -> left:n;
    root:r:s -> right:n;
    left:l:s -> leaf1:n;
    left:r:s -> leaf2:n;
    right:l:s -> leaf3:n;
    right:r:s -> leaf4:n;
}

在这个例子中,我们使用了nodesep=0.5;ranksep=0.5设置了 Graph 的全局属性,使用了node [shape=...];[edge [style=...];这样的语句设置了 Node 和 Edge 的全局属性,并且在每一个 Node 和 Edge 后面分别设置了它们自己的属性。在这些属性中,比较特别的是 Node 的shape属性,我将它设置为record,这样就可以很方便地利用 Node 的label属性来绘制出类似表格的效果了。同时,在定义 Edge 的时候还可以指定箭头的起始点。

执行dot命令,可以得到这样的图形:
JavaScript 基础、进阶以及 Ubuntu 系统中的 JavaScript 开发调

是不是漂亮了很多?虽然以上工作使用任何文本编辑器都可以完成,但是为了提高工作效率,我当然要祭出我的神器 Eclipse 了。在 Eclipse 中可以定义外部工具,所以我写一个 shell 脚本,将它定义为一个外部工具,这样,每次编写完图形定义文件,点一下鼠标,就可以自动生成图片了。使用 Eclipse 还可以解决预览的问题,只需要编写一个 html 页面,该页面中只包含生成的图片,就可以利用 Eclipse 自带的 Web 浏览器预览图片了。这样,每次改动图形定义文件后,只需要点一下鼠标生成图片,再点一下鼠标刷新浏览器就可以实时预览图片了。虽然不是所见即所得,但是工作效率已经很高了。请看动画:
JavaScript 基础、进阶以及 Ubuntu 系统中的 JavaScript 开发调

Graphviz 中可以设置的属性很多,具体内容可以查看 Graphviz官网 上的文档。

相关推荐