无废话JavaScript教程(全集)

  《程序员》2008.09期有一篇名为《无废话ErLang》的文章,这让我想到了许多的诸如“无废话C”、“无废话书评”这类的文章,也想到了JavaScript可没有一篇“无废话”,所以决定开个篇来写这个。与这个决定相关的,还因为另一个缘故:许多读者认为我那本《JavaScript语言精髓与编程实践》读来辛苦,所以我一直想写个简单的读本。索性,这次就写个最简单的吧。

  声明一下:如果只想看复杂的东西,不要读这篇文章了。

  一、JavaScript最初其实是过程式的

  追溯到1.0时代的JavaScript,其实是过程式的。它的基本特性有只有两项,一项是能够直接放在网页的HTML

  标签中去接管事件,例如:

  

复制代码 代码如下:

  <input type="button" onclick="alert('hello')">

  第二项则是支持一种简单的对象构造器(函数)。其实这个时代的构造器不如说是初始化函数更合适,它应当

  这样写:

  

复制代码 代码如下:

  function MyObject() {

  this.xxx = '...';

  this.yyy = '...';

  }

  obj = new MyObject();

  所以,早期的JavaScript无可置疑地背上了“基于对象的过程式脚本语言”的名头,这一点也不冤枉。除了

  上面两项特性,JavaScript有着一些一般性的脚本语言的性质,例如:

   -整个.js文件一次性装载到执行环境(例如WEB浏览器)中,在经历一次语法分析之后,开始逐行执行;

   -在上述语法分析周期,(具名的)函数和用"var"声明的变量被预先处理在一个标识符表中,以便脚本代码使用;

   -从全局的代码行或函数调用开始执行,整个过程中执行不到的代码不被查错(除第一步中的语法检错外)。

  也具有通常的过程式语言的性质,例如:

   -有if/for/while/switch等语句;

   -用function来声明函数,使用"(..)"来声明它的形式参数表,以及表示函数调用和传参;

   -类似于C语言的基本语法,包括使用"{..}"来表示代码块,以及使用"!="等运算符号;

   -一个类似于Java语言的对象操作运算符"."号,和属性、方法这样的基本概念。

  好了,现在你看到了一个基本的JavaScript语言,它的代码只有象C一样的函数与语句行,支持非常简单的面

  向对象编程。OK,这其实也差不多是JavaScript的全部……嗯……全部的语法基础观念。如果你用过一门哪怕

  稍稍入门一点的程序语言,你都会觉得JavaScript其实挺简单的。

  是啊,“写个函数,然后调用它”,就这么简单。例如:

  function hi() {

  alert('hello, world.');

  }

  hi();

  二、稍微复杂一点的是数据类型

  JavaScript有六种基本数据类型,分为两类。一类是值类型,即undefined,string, number和boolean;一类

  是引用类型,即function和object。检测数据X是何种类型,可以简单地使用"typeof X"来返回一个字符串。

  值类型与引用类型在其它高级语言中,是用“访问过程中是传值还是传引用”来区别的。简单说,在下面函数

  中:

  function foo(X) {

  }

  X传入的是值本身,还是一个指向该值的引用(你可以想象成指针),表明了X是何种类型。与其它语言不同的

  是,JavaScript不在调用入口上加指示字来说明传值的方法,例如:

  function foo(var X) {

  // 一般高级语言中,var表明总是传入变量X的引用

  }

  而是,简单的由脚本引擎根据实际传入的X的数据类型来决定如何传值。例如:

  function foo(X) {

  ...

  }

  foo('123'); // <- 字符串'123'值

  foo(aObj); // <- aObj是一个对象引用

  能这样处理的关键,在于JavaScript类型系统足够简洁。六种基本类型包括了三个哲学化的观念:能执行的

  与不能执行的;对象或非对象;有(值)或无(值)。显然,理解这种哲学性的思想并不容易,因为更复杂一层

  的、自包含的逻辑是:函数也是对象、值也是对象、无值也是值。

  这就是JavaScript类型系统的全部了。如果你想简单的用用,那么你记住下面的就够了:

   -string、number、boolean三种简单值类型是用来传给网页显示的;

   -object用来存放其它的object、funtion或上述简单值类型,并用'.'运算通过一个属性名找到它们;

   -undefined是用来检测数据有效无效的;

   -function是用来执行的。

  当然,如果你要成为思想家或者语言学的疯子,那么去思考上面的哲学化命题吧,我不拦着你。

  三、能用鼻子想通的就是直接量了

  或许很多人都搞不明白JavaScript中的直接量声明,然而它确实是非常简单的。既然我们大多数高级语言都

  支持常量声明,甚至最原始的汇编语言也支持立即值——例如:

  // in C

  #define ABYTE 256

  // in delphi

  const

  ABYTE = 256

  ; in asm

  mov ah, 256

  那么JavaScript当然……必然……可以面无愧色地支持直接量了——他们其实是一个概念。例如:

  // in javascript

  var

  aByte = 256

  只不过在理解的时候,一定要切记:所有上述的代码中,所谓直接量或立即值,是指那个'256',而不是那

  个变量或常量的标识符ABYTE/aByte。更进一步的,你要知道JavaScript支持了8种直接量声明:

  --------------------

  数值:支持整数,浮点和0x等进制前缀,以及……等等;

  布尔值:true/false;

  无值:undefined;

  函数:function() { ... },也称为匿名函数;

  字符串:使用'..'或"..",支持多行和转义符;

  正则表达式:使用/../..,支持g,i,m等正则配置;

  数组:使用[..],支持嵌套数组;

  对象:使用{...},支持嵌套对象声明;

  --------------------

  你可以把上述字符量作为一个个体,用在代码——我的意思是表达式或语句行——的任意位置。用鼻子就可以

  进行的推论是:

  //既然可以写:

  aaa = 'hello, ' + 'world';

  //那么就必然可以写:

  bbb = [1,2,3] + [4,5,6];

  //同样也就必然可以写:

  ccc = /abc/ + /cdf/

  //同样:

  // ……

  如上的,你可以把所有的直接量放在表达式或语句中间。偶尔的,因为语法解析的必要,你可能需要用一对

  括号把这个直接量括起来——否则语法上会出现歧义,例如下面这个:

  ddd = (function() {}) + (function() {})

  好了,直接量原本就这么简单,你只需要期望自己还有一个没退化的鼻子就好了。

  四、原型继承

  原型继承可能是这个世界上最简单不过的东西了。

  我们假定一个对象是一张表——伟大的Anders就支持我这个假设,他说JavaScript的对象是“属性包”——

  这样的一个表中存放的就是“name=value”这样的“名字/值”对。当我们想用下面的代码:

  aObj.name

  去找值(value)时,就在表里查一下(用delphi的人应该记得TStringList吧)。对象,哦,所谓对象——在

  我曾经的理解里——就是“带有继承关系的结构体(struct/record)”。那么,继承关系是?

  是这样,如果上面的查找不成功,对于原型继承来说,只需要在aObj这个对象的“原型”中找一下,就成了。

  这个原型也是一个对象,记录在构造器函数的prototype属性中。例如:

  function MyObject() {

  // ...

  }

  MyObject.prototype = xxx;

  var aObj = new MyObject()

  zzz = aObj.name;

  当在aObj中找不到name这个属性时,按照上面的规则,就会去xxx这个对象中找,也就是试图找找"xxx.name"。

  由于xxx本身也是一个对象,也会有一个构造器函数(例如xxxObject()),因此当xxx.name也找不到时,就会

  去xxxObject.prototype里面去找……如此……如此深层次的挖掘,直到再也找不到……就返回undefined。

  多简单啊,所谓原型继承,只不过是一个简单的检索规则。

  反过来说,你需要让aObj能访问到某个成员,也只需要修改一下它(或它们——一指类似aObj的实例)的原型

  就好了。这在JavaScript中是非常常用的。例如你想让所有的string都有某个属性:

  String.protoype.MyName = 'string'

  又例如你想让所有的对象都有某个属性(或方法,或其它什么东东),那么:

  Object.prototype.getMyName = function() {

  return this.MyName;

  }

  多么美好,现在String也能getMyName了,函数也能getMyName了,所有没有名字的也有名字了——当然,名

  字是undefined。

  没名字也是名字,我没想过你会不会变成哲学疯子,对不起。

  五、函数式

  这个可不是JavaScript的发明,它的发明人已经死了,而他的这个发明还在困扰着我们……如同爱迪生的灯

  泡还在照耀着我们。

  其实函数式语言很简单,它就是一种与命令式语言同样“完备”的语言实现方案。由于它的基础思想与命令

  式——如果你不想用这个难于理解的名词,那就把它换成C,或者Delphi好了——语言完全不同,所以大多数

  情况下,它也与这些传统的、通用的、商业化的语言格格不入。

  而事实上,你天天都在用它。

  下面这行代码,就充满了函数式语言的思想:

  a + b

  是吗?真的,如果你把那个“+”号看成一个函数,就完全一样了。事实上,所谓函数(function),就是一个

  执行过程、一段运算、一个功能入口……也就是说,代码中的某个东西,要么它是数据,要么它是运算。而如

  果它是运算,你就可以把它看成“函数”。上面这行代码——表达式中,a和b显然是数据,而+号则是对之进

  行操作的运算,所以它自然可以看成一个“功能、过程或运算”。所以……操作两个数求和的“函数”可以写

  成这样:

  func_add(a, b)

  或者这样:

  (+ a b)

  所有这些,只是文字上的标记法不同而已。就好象我说这个符号是“jia”,而你非得说它是“暗得”,另一个

  人却非要读作“a-d-d”。有什么不同吗?没有。

  所有程序员天天都在写语句,写表达式,与运算逻辑。大家都知道这些东西都可以写在一个……嗯……无比巨

  大的函数里,或者分在无数个小的、看起来很漂亮的函数里。当所有这些函数一个又一个连续起来运算时,它

  就成了“函数式语言”。所以称为“函数式语言的祖老爷爷”的LISP就是这样“把函数运算连起来”的语言:

  

复制代码 代码如下:

  (defun subst (x y z)

  (cond ((atom z)

  (cond ((eq z y) x)

  ('t z)))

  ('t (cons (subst x y (car z))

  (subst x y (cdr z))))))

  有人认为它是丑陋的意大利面条,也有人认为它是最简洁的语言。Oh,随你,了解了就好了,管它象什么尼。

  由于函数式语言只有连续执行的函数——所以它没有了“语句”而又必须实现逻辑上的完整性。简单地说,他

  需要在连续执行的过程中实现“顺序、分支与循环”三个基本逻辑。函数式的解决之道,是用三元条件运算来

  代替if/then/else,也就下面的代码:

  if a then b else c

  // 相当于

  a ? b : c

  另外,就是用递归(函数调函数)来实现循环。考虑到递归函数调用中会导致栈溢出(Stack overflow at ...),

  所以函数式语言就提出了“尾递归”,也就是在书写代码是“确保”仅只在函数内的最后一个运算中递归调用。

  这样一来,这个递归调用就不需要保留栈了——因为再没有后续运算了,因此就能被优化成一行不需要返回的

  jmp汇编指令。

  世界真美好,所谓函数式不过是一堆运算入口,以及jmp、jmp、jmp。但,但是,难道不是吗——这块CPU?

  六、更高级的函数式(1)

  其实世界并不美好。

  如果一块CPU死躺在那里,它也就只有顺序呀、分支啦、循环之类的指令在里头。但是当它运行起来,就得有一

  个时钟在滴嗒…滴嗒…如果是多核的,还会同时有好几个这样的东东在滴嗒着。

  这就是问题,世界原本不是单一时序,也不是守序的。所以会有并发,会有中断,也会有异常。函数式语言应

  该自己应该像意大利面条一样无限延展开去,那么是不是有多个滴嗒时就该有多根意大利面条呢?但,这种情

  况下还叫意大利面条吗?

  函数式里的解决方案叫延续(Continuation)和结点。延续是停止当前转到另一处、然后再返回来;而结点则确

  保一个位置上的代码是独立完备的,与另一个结点无关。在函数式语言中,多个结点就象平行宇宙,大多数情

  况下它们是互相透明的,如果它们要发生联系,得需要极其巨大的能量,以及……一个虫洞。

  多个结点的问题我们不需要深究,多个空间下的访问带来的时空问题,是天体学家以及时空多维理论家们研究

  的问题,我们只需要知道:如果你希望多个结点之间存在交叉的访问关系,那么世界/宇宙会因此毁灭。这样类

  似的说明写在ErLang这类天生支持多个宇宙的语言的白皮书、参考手册以及宇航员日常指南之中。如果你要穷

  究其根源,并且认为自己已经能明确了解300个以上的高等数字、物理学和天体学术语,地么下面有一份面向程

  序员的入口指引,不妨从这里开始:

  http://www.blogjava.net/canonical/archive/2007/12/05/165664.html

  延续是解决状态问题的方法之一。简单说来,有时间就有状态:有过去,现在和将来。对于过去,我们有历史,也就会因为过去发生了什么而决定现在发生什么,例如因为昨天喝高了小酒,所以今天只能吃稀粥;而现在也就是明天的昨天,所以要为了明天能做什么而记录下今天的状态,例如今天已经吃了稀粥;至于明天,当然,将要发生的事情很多,我们得一件一件地做好准备。

  Oh,重要的就是这个“做好准备”了。这在计算机系统里叫:事件。或者叫计划,或者叫突发。知道为什么我们的PC机可以运行吗?En...是因为有一个指令流水线在按照一个足够微小的时间片去执行指令。对于运算系统来说,这个定时发生的指令访问行为,就是中断。所以有一天一个朋友问我:如果执行逻辑只有顺序、分支与循环,那么流程系统中的触发器算什么?我想了很久,无解。而现在来回答这个问题,就得把“时间”这个维度加上,所谓突发、并发以及类似的东西,是时序概念下的逻辑。

  好了,当我们“做好准备”,为了将来要触发某个东西做准备的时候——简单的说,就当是个时钟处理程序好了(在windows中它叫OnTimer,在浏览器中它叫setTimeout/setInterval),为了这个时候我们能够在函数式语言中做好一顿意大利面条,我们说明:“我们已经做好了准备”,以及“我们做好了怎样的准备”。而按照函数式的基本原则,一个函数是与状态无关的,它与“将来”这样一个时序无关。这,变成了一个矛盾。

  事实上开始我们已经与这个矛盾正面冲突了一次,这就是“循环”。因为循环需要一个状态量来指示循环进度,

  这个“进度”就是时序相关的。而函数式使用了“递归”来解决它,也就是通过递归函数的参数来传递这个进度。在“递归参数”——这个界面的两边,函数式都是时序无关的。由此带来的问题就是栈溢出,解决方法则是尾递归;又由此带来的问题就是编程复杂性,以及能否证明尾递归能替代所有递归;解决方案是……温伯格说得没错,所有解决问题的方法都会带来新的问题。Oh...又是温伯格。 现在,我们明确的需要“更多的状态”了,因为我们已经将系统运行一个或是多个的时序里——也就CPU。即使我们有结点,而且保证“没有虫洞”,那么我们也需要解决一个CPU中的过去、现在与将来的问题。

  函数式的仙家们说了两个字:持续。简单啊,就是把为过去、现在的状态,和为将来的准备作为函数的参数传过去。看起来,“现在”立即就要爆炸了,因为既要包括过去的、现在的状态以及变化,还要包括将来的运算。于是新的解决方案是:若要现在不爆炸,"持续"的界面上不要发生运算就好了。

  运算由“将来”根据“过去”的数据做决策,这就是持续。支持它的函数式特性,就是在惰性求值。简单的说,

  function f(x, c) {

  ...

  c(x);

  }

  f(data, f);

  由于在传送界面f()上,c本身是不求值的,所以爆炸会发生在/不会发生都是在将来c(x)进行运算的时候。如果

  宇宙的熵有极限,那么这个极限也是在末可知的将来。而且,在末可知的将来,也有可能回扭曲回现在。这就是

  持续的两个原则:

  --------------------

  一个成功持续之后,是一个新的持续(或过程)

  一个失败持续将回到上一个选择点

  --------------------

  正是因为有持续的状态,且这个状态及持续本身都是通过函数参数传递的,所以,“回到选择点”只是将来自身

  的一个决择,与现在的状态是无关的。

  七、更高级的函数式(2)

  无论如何,种种函数式的复杂性,确实是在编程范型中保持一种自我的纯粹性而存在的。例如生成器(产生器)

  这个问题,由于一批数据的产生之间是有关系的(例如增量),而产生过程是时序相关的(不总是在一次调用中

  得到全部的数据),因此函数式语言定义了一个“生成器”这样的函数概念。我们可以在生成器中定义一个yield,

  这个返回就是“将来”回到这个“现在”的一个虫洞。通过这个虫洞,我们可以拿到一个时序相关的数据。下面

  就是这样的一个例子(mozilla上的斐波那契数列示例):

  <!-- for firefox 3 -->

  <script type="application/javascript;version=1.7">

  function fib() {

  var i = 0, j = 1;

  while (true) {

  yield i; //<-虫洞在这里

  var t = i;

  i = j;

  j += t;

  }

  }

  var g = fib();

  a = g.next();

  b = g.next();

  c = g.next();

  // ...

  // .. 三天以后(或一个循环以后),某时空旅行家想回到fib里去看看

  z = g.next();

  // 呵,原来是值“2”

  alert(z);

  </script>

  八、结语

  好象,好象这篇《无废话》有很多废话……哈哈,玩笑啦,真的全无废话,还是活人能看的木?

  真的那么想读无废话,你应该去读大学教材了。或者,这里还有一本《JavaScirpt语言精髓与编程实践》。

  看见木,语言……精髓哦……,哈哈。