2007年3月30日

不要把AJAX用烂

如今AJAX已经火的真是不得了,幸运地是AJAX得到了推广,催生了大量的JavaScript脚本库,并把B/S的体验推向了极致(当然FlashWPF什么的也很好,但它们都不是实实在在的完全基于Web的,从Google的产品线上也说明了,还是标准好用一些...);不幸地是当一门技术被在不该用的时候使用了,就是确凿的滥用了,这在国内尤其突出。

什么时候使用AJAX

这里我列了一个表格,或许可以作为你来使用它的“黄金定律”。这个表格每一行记录都是独立成立的,不妨看看你正在做的是否满足这个定律。
使用AJAX 不使用AJAX
非互联网 互联网
应用级的 基于内容的
范围内 整个网页
  • 大多数互联网的应用是不需要AJAX的,因为AJAX对于搜索引擎不够“友好”。
  • 如果你想做的是一个应用(这里的“应用”是一个狭隘的名词,尤指互联网应用),比如一个在线图片处理程序,那么使用AJAX是非常可行的方案,就像snipshot一样。当你的网站是基于内容的时候,就不该使用它了, 这条定律的反面教材就是网易搜孤新浪的博客,那里好像都是AJAX的吧,blog应该是基于内容的。当然,基于RSS等的供给方式会使内容提供完整,但发现它却不是那么容易。
  • 范围内使用AJAX是最好的办法。当你网页中分页的时候,不妨考虑使用AJAX;当需要进行一个评分操作的时候,使用AJAX就太棒了。

2007年3月14日

在JavaScript中实现基于类模式的继承

JavaScript 是一个和类无关的(class-free),面向对象的语言,它使用了原型继承的方式代替了类的(classical,传统的)继承方式,这会使那些用惯了像C++和Java的程序员感到困惑。过一会我们会发现,JavaScript的原型继承方式比传统的继承方式更据表现力。

Java JavaScript
强类型(Strongly-typed) 弱类型(Loosely-typed)
静态(Static) 动态(Dynamic)
基于类的(Classical) 基于原型的(Prototypal)
类(Classes) 函数(Functions)
构造器(Constructors) 函数(Functions)
方法(Methods) 函数(Functions)

首先,我们为什么要关心继承机制呢?有2个理由,首先,这有利于类型,我们想要Javascript自动建造相似类的引用关系,安全类型(type-safety)是从一个需要程序直接构造对象引用类型的系统获取,这是鉴别强类型语言一个关键点,但对于像JavaScript这样的弱类型语言是不相关的,因为它的对象的引用关系根本不需要建造(casting)。

第2个理由是代码的重用,让一系列对象实现一些相同的方法是相当平常的事情,传统的类通过一套单独的定义实现了这一点。让对象与一些其它对象类似也是很平常的,但是不同点是需要增加或者修改一小撮方法。类的继承方式对这些是非常有用,但是原型继承甚至是更有用的。

为了证明这点,我们将引入一个甜点(sugar),它将给我们提供一种别具风格的像传统的基于类的方式书写代码。我们稍后也会介绍一种基于类的语言不可做的,非常有用的模式。最后我们将解释这甜点(sugar)

类模式的继承

首先,我们创建一个Parenizor类,它有设置和获取属性value值的方法,toString方法将用括号包裹住value值。

function Parenizor(value) {
    this.setValue(value);
}

Parenizor.method('setValue', function (value) {
    this.value = value;
    return this;
});

Parenizor.method('getValue', function () {
    return this.value;
});

Parenizor.method('toString', function () {
    return '(' + this.getValue() + ')';
});

这个语法有一点不寻常,但很容易看到“类模式”的痕迹。method方法生成一个方法名和一个函数,并把他们增加到类的公有方法中。

现在我们可以写了。

myParenizor = new Parenizor(0);
myString = myParenizor.toString();

正像你所期待的一样,myString的值是"(0)"

现在我们将创建另一个类,它继承于Parenizor,功能上相同除了当value为0或空的时候,方法toString将返回"-0-"

function ZParenizor(value) {
    this.setValue(value);
}

ZParenizor.inherits(Parenizor);

ZParenizor.method('toString', function () {
    if (this.getValue()) {
        return this.uber('toString');
    }
    return "-0-";
});

inherits方法与Java的extends方法类似。uber方法与Java的super方法类似,它允许一个方法调用父类的的方法。(名称的改变是为了避免与系统关键字重复。)

我们可以这样写了

myZParenizor = new ZParenizor(0);
myString = myZParenizor.toString();

这时, myString 的值是 "-0-" 了。

JavaScript没有类,但是我们可以编排类做的事情。

多重继承

基于原型模式,我们可以实现多重继承,这是允许我们创建一个继承多个类不同方法的新类。复杂的多重继承难以实现,因为方法名称的重复带来了潜在的问题。我们可以在JavaScript中实现复杂的多重继承,但是对于这个例子来说我们使用了一种更遵守纪律的方式,我们叫它Swiss Inheritance

设想有一个叫做NumberValue的类,它有一个setValue来检查value属性在一个确切的范围内是否是数字,如果有必要就抛出异常。我们仅仅想要ZParenizor类拥有setValuesetRange方法,所以我们这样写:

ZParenizor.swiss(NumberValue, 'setValue', 'setRange');

这在类上只增加了我们需要的方法。

寄生继承(Parasitic Inheritance)

有另一种方式写ZParenizor,代替从Parenizor继承,我们写一个构造器,然后调用Parenizor的构造器,停止它本身的返回值,并且不使用增加公有方法的方式,为构造器增加一个特权方法

function ZParenizor2(value) {
    var that = new Parenizor(value);
    that.toString = function () {
        if (this.getValue()) {
            return this.uber('toString');
        }
        return "-0-"
    };
    return that;
}

传统的“类模式”的继承是关于“是一个(is-a)”的关系,而寄生继承是关于“过去是一个现在是一个(was-a-but-now's-a)”的关系。在创建一个对象的时候,构造器起着更大的作用。注意那些uber方法(super方法),仍然可以在特权方法里访问。

类的扩充(Class Augmentation)

JavaScript的动态的特性允许我们对已存在的类增加或替换他们的方法。我们可以在任何时候调用method 方法,不管是现在还是将来的类的实例都将拥有新增加的方法,我们差不多可以在任何时候扩充类,继承方式则需要倒转过来,我们称这种方式为类的扩充(Class Augmentation)以避免与Java的意味着别的含义的extends相混淆。

对象扩充(Object Augmentation)

在静态的面向对象的语言中,如果你想要一个对象与另一个对象有着细微的不同,你需要定义一个新类;在JavaScript中,你可以为实例增加方法而不是类。这是一个极强的能力,因为你可以写更好的类了,并且类也可以写的更简单了。回想起JavaScript对象就像哈希表一样,你可以在任何时候增加新值,如果这个值是一个函数,那么它就成为了一个方法。

所以在上面的实例中,我根本就不需要ZParenizor这个类了,我只要简单地修改一下我的实例就行了。

myParenizor = new Parenizor(0);
myParenizor.toString = function () {
    if (this.getValue()) {
        return this.uber('toString');
    }
    return "-0-";
};
myString = myParenizor.toString();

我们在myParenizor实例上直接地增加了toString,而没有使用任何形式的继承。我们可以发展个别的实例,因为JavaScript是与类型无关的(class-free)。

甜点(Sugar)

为了使上面的实例可以运行,我写了4个sugar方法,首先method是为类增加一个实例方法的方法。

Function.prototype.method = function (name, func) {
    this.prototype[name] = func;
    return this;
};

这为Function.prototype增加一个公有方法。所有由类的扩充的函数都可以使用它。它有一个名称和一个函数,并把它们增加到了函数的prototype上。(译者注:就是为所有function语句的作为类的原型提供了一个公有方法。)

它返回this,当我写一个不需要返回值的方法时,我通常让它返回this,这顾虑到了串联样式( cascade-style)的程序。

接下来是inherits方法,它的作用是让一个类从另一个上继承。它应当在那两个类都以声明完之后调用,在继承之前,类的方法已经增加了。

Function.method('inherits', function (parent) {
    var d = {}, p = (this.prototype = new parent());
    this.method('uber', function uber(name) {
        if (!(name in d)) {
            d[name] = 0;
        }        
        var f, r, t = d[name], v = parent.prototype;
        if (t) {
            while (t) {
                v = v.constructor.prototype;
                t -= 1;
            }
            f = v[name];
        } else {
            f = p[name];
            if (f == this[name]) {
                f = v[name];
            }
        }
        d[name] += 1;
        r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
        d[name] -= 1;
        return r;
    });
    return this;
});

我们又一次扩充了Function,我们创建了一个父类的实例,并把它作为新的原型。我们也纠正了constructor字段,并且我们在这个原型增加了uber方法。

uber方法在他自己的原型里查找指定的方法,这就是在寄生继承或多项扩充的时候为了调用的函数。如果我们使用传统的继承方式,那么我们需要找到在其父类里的原型中找到这个函数。return语句使用了函数的apply方法来调用这个函数。参数(如果有的话)被获从arguments的数组里,不幸的是,arguments不是一个真正的数组,所以我们必须再一次使用apply来调用数组的slice的方法。

最后是swiss方法。

Function.method('swiss', function (parent) {
    for (var i = 1; i < arguments.length; i += 1) {
        var name = arguments[i];
        this.prototype[name] = parent.prototype[name];
    }
    return this;
});

swiss方法自始自终循环arguments参数,对于每一个名称,它都复制出一个成员从父类的原型上到新类的原型上。

结束语

JavaScript可以用传统语言的方式使用,但是它有个层次上表现就是它非常的独特。我们已经观察过了类模式的继承、Swiss Inheritance、寄生继承、类的扩充和对象的扩充,这一套代码重用模式来自于被人为要比Java渺小的和简陋的多的JavaScript。

传统语言是硬梆梆的,为它增加一个新的成员的方式只能是就是创建一个新类。在JavaScript中,对象是软绵绵的,一个新的成员可以被增加到这个对象上使用简单的赋值即可。

因为在JavaScript中的对象是如此的灵活,你将可以考虑不同的关于类的继承方式,不要太深的层次,浅浅的是有效率的和富于表现力的。

特别声明,文章翻译于 Classical Inheritance in JavaScript
版权由原作者所有。

在JavaScript实现基于原型的继承

5年前我写了在JavaScript中实现基于类模式的继承。它显示了JavaScript是基于像类一样,原型的语言(class-free, prototypal)。并且它有足够的能力去模仿一个“类”似的结构,我的程序样式在那之后进行了发展,就像是任何好的程序员应当的那样。我已经完全拥护“原型主义”,并且把我从“类”的模式中解放出来。

我的旅程是迂回的,因为JavaScript它本身对于它原型的天性也相抵触。在一个原型的系统里,对象继承于对象。JavaScript尽管它缺乏一个完成那些运算的操作符,但是它有new操作符,比如:

new f()

产生一个新的对象,它继承于

f.prototype

这间接并有意地使这门语言看似可以与那些古典的训练程序员的方式一样,但是未必如此,就像我们可以看到Java程序员一直持有JavaScript是非常浅显的观点一样。JavaScript的构造器模式没有出现古典派的一伙当中,这使JavaScript是真正的天性的原型语言变得模糊不清,结果是,几乎没有知道如何有效地使用这门语言的程序员了。

幸运的是,很容易创建一个实现原型继承的操作符工具,它在我的工具包里有标准的特点,我也很高兴推荐你使用它。

    function object(o) {
        function F() {}
        F.prototype = o;
        return new F();
    }

object函数分解了JavaScript的构造器模式,真正地达到了原型继承。它把一个旧的对象作为参数,并且返回了新的空的并继承了旧的对象的对象。当我们试图获取这个新对象的成员的时候,当它本身没有这个成员的时候,旧的对象将会提供这个成员。对象从对象继承了,很多对象如此导向会如何呢?

代替创建类,你该使用原型对象,并且使用object函数创建新的实例。在JavaScript中对象是可变的,所以我们可以扩充新的实例,给他们新的属性和方法,这些新实例又可以担当更新的实例的原型。我们不需要类去做一些相似的对象。

便于使用,我们可以创建一个叫做object函数,并且提供其它的用户化定制,比如扩充对象使用特权方法。我不时地叫他们为缔造函数,如果我们要一个缔造函数来调用另一个缔造函数来代替调用object函数,那样我们就有了parasitic继承的模式了。

我发现用这些工具,外加上JavaScript的lambdas和对象的quasi-literals,我可以写一个很好的结构化的程序,它可以是庞大的、复杂的和高效率的。古典的对象模型今天最流行的方式渐行渐远,但我认为基于原型的对象模型是更有前途和提供更丰富的表现力。

学习这些新的模式也更使我成为一个更好的古典式的程序员,以不变应万变。

特别声明,文章翻译于 Prototypal Inheritance in JavaScript
版权由原作者所有。

2007年3月13日

JavaScript中的私有成员

JavaScriptJavaScript 世界上最被误解的程序设计语言。一些人认为它缺乏信息隐藏的特性,因为对象不能有私有的实例变量和方法,但实际上这是一种误会,JavaScript对象可以有私有成员,这篇文章将描述这样的情形。

对象

对象objects是JavaScript的根基,数组是对象,函数是对象,对象是对象。但是对象是什么呢?对象是键/值对的集合,键是字符串,值是字符串、数字、布尔值和对象(包括数组和函数)。对象以哈希表的方式实现,所以值容易被迅速地找到。

如果一个值是一个函数,我们可以认为它是一个方法,当一个对象的方法被调用的时候,this变量指向了该对象,方法可以通过this变量存取该对象的实例变量。

对象可以由构造器来产生,也就是那些初始化对象的函数们。构造器提供了像其他语言类提供的特性,包括静态的变量和方法。

公有成员

一个对象的所有成员都是公有成员,任何函数都可以存取、修改或删除这些成员,又或者增加新的成员。把成员放入一个对象中主要有有2种方式:

在构造器中

这种方式通常是被用于初始化公共实例变量,构造器的this变量被用于增加对象的成员。

function Container(param) {
    this.member = param;
}

这么说来,如果我们构造一个新对象

var myContainer = new Container('abc');

那么myContainer.member就包含'abc'了。

在原型中

这种方式经常用来增加公有方法,当一个成员被找到,但不是在对象本身上找到的,那么它是从对象构造器的原型成员上带来的。原型机制被用于继承,这也避免浪费内存。使用构造器为所有的对象增加方法,只要为构造器的原型增加一个函数就可以了:

Container.prototype.stamp = function (string) {
    return this.member + string;
}

所以我们可以这样地触发此方法

myContainer.stamp('def')

其结果为'abcdef'

私有成员

私有成员由构造器创建,普通的var声明和参数构成了私有成员

function Container(param) {
    this.member = param;
    var secret = 3;
    var that = this;
}

这个构造器有3个私有的实例变量:paramsecretthat。它们被附着在对象上,但是他们与外界不直接相互存取,他们也不能与对象本身的公有方法相互存取(译者按:这里的公有方法应该是指那些通过原型实现的方法),他们可以与私有方法相互存取。私有方法指的是那些构造器内的内部方法。

function Container(param) {

    function dec() {
        if (secret > 0) {
            secret -= 1;
            return true;
        } else {
            return false;
        }
    }

    this.member = param;
    var secret = 3;
    var that = this;
}

私有方法dec间车secret实例变量,如果它比0大,它就减1并且返回true,否则返回false这可以用于保证对象3次以内的调用。

根据习惯,我们创建了一个私有的that成员,这使对象的私有方法可以使用对象实例了,这是对ECMAScript语言规范对于内部方法把this设置为一个不正确的值的一种回旋。

私有方法不能被公有方法(译者按:这里的公有方法应该是指那些通过原型实现的方法)直接调用,为了使私有方法变的有用,我们需要引入一个特权方法

特权方法

一个特权方法可以存取私有变量和私有方法,并且它本身与公有方法及外部可以相互存取,删除和替换一个特权方法是可行的,但是改变它特性或者强迫放弃它的秘密都是不可能的。

特权方法是指那些在构造器中与this关联的方法。

function Container(param) {

    function dec() {
        if (secret > 0) {
            secret -= 1;
            return true;
        } else {
            return false;
        }
    }

    this.member = param;
    var secret = 3;
    var that = this;

    this.service = function () {
        if (dec()) {
            return that.member;
        } else {
            return null;
        }
    };
}

service就是一个特权方法。在前三次的调用myContainer.service()中,将会返回'abc',之后将返回nullservice调用了私有方法dec,就是那个可以存取私有变量secret的方法。service对于其他的对象和方法是可用的,但是它不允许直接存取私有成员。

闭包

这种公有,私有和特权成员的方式是可行的,因为JavaScript拥有闭包。这句话的意思是说一个内部函数总是有权访问它外部函数变量和参数,甚至是在外部函数已经返回之后。这是该语言一个非常强大的特性,没有一本关于JavaScript书谈论了如何“开采”它,大多数书根本没有提及到它。

私有和特权成员只有在对象被创建的时候产生,公有成员可以在任何时间增加。

套路

公有方法

function Constructor(...) { 
    this.membername = value;
}
Constructor.prototype.membername = value;

私有方法

function Constructor(...) { 
    var that = this;
    var membername = value;
    function membername(...) {...}
}

注意这个函数语句

function membername(...) {...}

是对如下的简写

var membername = function membername(...) {...};

特权方法

function Constructor(...) {
    this.membername = function (...) {...};
}
特别声明,文章翻译于Private Members in JavaScript
版权由原作者所有。

JavaScript中的编码惯例

这篇文章描述的是在JavaScript程序中的一些编码的惯例和规则,是从Sun 公司的文档Java的编码惯例得到灵感的,它经过了很大的修改因为JavaScript不是Java

对于一个软件来说,它的长期价值是与代码的编写质量成正比的,在其整个生命周期,一个程序将经历过许多人的眼睛和双手。如果一个程序能够清晰地沟通它的结构和特性,在不久的将来它被修改的时候就不会那么容易的崩溃。

编码惯例有助于减少编码的的脆弱性。

所有的JavaScript代码会直接暴露给公众,所以它应当具有出版那样的品质。

整洁的代码是有价值的!

JavaScript文件

JavaScript程序应当被存储和发表在.js 文件中。

JavaScript不应当被内含在HTML文件中,除非代码清晰地处理一次会话,在HTML里的JavaScript代码增加了页的体积并也不利于缓存和压缩。

<script src=filename.js>尽可能地不要放在body标签之内,这能减少脚本造成的对其它的页面组件延迟的现象。没必要使用languagetype属性,他们属于服务器而不是script标签的,他们用来决定MIME(多用途的网际邮件扩充协议)类型。译者按:对于xHTML必须要加上type属性。

代码缩进

缩进的尺度是4个空格,避免使用Tab,因为(到写这篇文章的21世纪为止)没有一个关于Tab使用的规范。使用空格会造成文件变大,但是对于本地网络来说,大小无关紧要,并且这个差异已经被minification消除了。

行的长度

避免一行代码超过80个字符,如果一条语句不适合单行的时候,有必要切断它。

  • 切断换行在逗号之后
  • 切断换行在任何操作符之后
  • 切断的下一行要缩进8个空格

在操作符之后切断换行,减少了那些像被分号的插入带来的复制粘贴错误。

注释

多用注释吧!对于那些过了段时间想了解你写代码的含义的人(甚至包括你自己),这非常有用。注释必须是“恰当的”和“清晰的”,就像代码他们在做注释一样。一个意外的金砖可能被感激,挫折和愤怒就不会。(An occasional nugget of humor might be appreciated. Frustrations and resentments will not)

保持注释和修改一致是很重要的,错误的注释难以让人理解和阅读。

让注释变得意味深长,把精力放到那些不是马上可见的事情上,不要浪费读者的时间像这样:

i = 0; // Set i to zero.

一般使用行注释,块注释保持统一匀称的格式。

变量声明

所有的变量在使用之前必须被声明,JavaScript本身不需要这样,但是这样做使程序更如果读懂,也有助于发现那些可能被暗暗地变成“全局的”的没有被声明的变量。

一个函数应当有一个单独的var语句来声明所有属与它的局部变量,这个var语句也应该被放置在这个函数最开始的位置。

每个变量和关于它的注释使用单独的一行是首选的,它们应该依据字母顺序排列。

     var currentEntry, // currently selected table entry
        level,        // indentation level
        size;         // size of table

JavaScript没有块域(block scope),所以在块里面定义变量搞糊涂那些对C家族语言富有经验的程序员。

不要给一个变量和参数与外部区域的变量以相同的名称,JavaScript可以重新定义变量名称,但这也增加了程序被看错的可能性。

尽量不要使用全局变量。

函数声明

所有的函数在使用之前必须被声明,内部函数要在局部变量声明之后,这样能使代码更清晰地显示处于闭包之间。

在函数名称和((左括弧)之间不应该有空格,在) (右括弧){ (左大括弧)之间应该有一个空格。函数内容应该缩进4个空格,} (右大括弧)应与函数头对齐。

    function outer(c, d) {
        var e = c * d;

        function inner(a, b) {
            return (e * a) + b;
        }

        return inner(0, 1);
    }

全局的方法应该被最小化。

在JavaScript,函数和对象可以放到任何表达式允许的地方,所以K+R的大括弧形式被使用了。这提供了最复杂的结构的最好的可读性。如果一个方法是一个匿名函数,那么function( (左括弧)之间应该放置一个空格,如果空格被忽略了,看起来就好像函数的名字是function一样,引起错误的阅读。

    function getElementsByClassName(className) {
        var results = [];
        walkTheDOM(document.body, function (node) {
            var a,                  // array of class names
                c = node.className, // the node's classname
                i;                  // loop counter

// If the node has a class name, then split it into a list of simple names.
// If any of them match the requested name, then append the node to the set of results.

            if (c) {
                a = c.split(' ');
                for (i = 0; i < a.length; i += 1) {
                    if (a[i] === className) {
                        results.push(node);
                        break;
                    }
                }
            }
        });
        return results;
    }

    div.onclick = function (e) {
        return false;
    };

    that = {
        method: function () {
            return this.datum;
        },
        datum: 0;
    };

名称

名称应当由26个大小写字母(A .. Z, a .. z),10个阿拉伯数字 (0 .. 9), 及_ (下划线)组成。避免使用国际性的字符,因为它们不是在哪都可以执行的很好。不要在名称中使用$符号(美元符号)或者\符号(反斜线符号)

不要使用_ (下划线)作为名称的第一个字符,因为这一般被习惯于用于声明“私有变量”但实际上它并不是私有的。如果“私有”是非常重要的,请使用私有成员提供的方式。避免使用任何缺乏论证的方式。

大多数变量和函数应该以小写字母开头。

那些将用于new 前缀的构造函数应当使用大写字母开头,如果不使用new会发生糟糕的事情,所以大写给了使用上提供了一个重要的提示。

全局变量应该全部被大写。(JavaScript没有宏或常量,这不是为了显示那些JavaScript没有的的特性。)

语句

简单的语句

每行至少包含一条语句,每个简单语句最后要插入一个分号“;”,注意那些要分配一个函数或对象的语句也必须以分号结尾。

JavaScript允许任何的表达式作为语句使用,这可能会遮蔽一些错误,尤其是在分号之前插入的时候。最恰当的表达式被用于语句应该是在赋值和调用中。

复合语句

复合语句是那些被圈起在{ } (大括弧)中的一系列语句。

  • 被圈起的语句前要缩进4个空格。
  • { (左大括弧)要在开始符合语句这一行的结尾。
  • } (右大括弧)应该新起一行,并和前面左大括弧相对齐。
  • 大括弧应当圈住所有语句,甚至一个单行的语句,在当他们是控制结构的一部分的时候,比如if语句或for语句的时候。这有助于增加新的语句在出现bug的时候。

标签(Labels)

语句的标签是可选的,只有以下这些语句需要标签:whiledoforswitch

return语句

一个带值return语句不要在值的左右使用( )(小括弧)。返回值表达式必须与return关键字同行以避免分号的插入。

if语句

if形式的语句应该以下面的方式出现:

    if (condition) {
        statements;
    }
    
    if (condition) {
        statements;
    } else {
        statements;
    }
    
    if (condition) {
        statements;
    } else if (condition) {
        statements;
    } else {
        statements;
    } 

注意if statements总是要使用大括弧的。

for语句

for 形式的语句应该以下面的方式出现:

    for (initialization; condition; update) {
        statements;
    }

    for (variable in object) {
        statements;
    } 

第一种形式用于数组。

第二种形式用于对象。这可以知道被加入到对象原型Object.prototype的成员,被枚举出来。这可以知道程序的结构:

    for (variable in object) {
        if (object.hasOwnProperty(variable)) {
            statements;
        }
    } 

注意for statements总是要使用大括弧的。

while语句

while 形式的语句应该以下面的方式出现:

    while (condition) {
        statements;
    } 

注意while statements总是要使用大括弧的。

do语句

do形式的语句应该以下面的方式出现:

    do {
        statements;
    } while (condition); 

注意do statements总是要使用大括弧的。与其他的符合语句不同,do语句总是以分号结尾。

switch语句

switch形式的语句应该以下面的方式出现:

    switch (expression) {
    case expression:
        statements;
    default:
        statements;
    }

每个case与switch对齐,避免过度缩进。

除了default的每组语句应当以breakreturnthrow结尾,不要遍历。

try语句

try形式的语句应该以下面的方式出现:

    try {
        statements;
    } catch (variable) {
        statements;
    }

    try {
        statements;
    } catch (variable) {
        statements;
    } finally {
        statements;
    } 

continue语句

避免使用continue语句,它使函数的控制流变的不清晰。

with语句

with语句不应当被使用

空白区域

在逻辑不同块上插入空白可以提高程序的可读性。

在如下情况下空白应当被使用。

  • 一个关键字后跟着一个左括弧,他们应该由一个空格分开。
    while (true) {
  • 在一个函数值和他们的左括弧之间应该插入一个空格,这有助于区别关键字和函数符号。(译者注:这是说在function之后直接有左括弧的情况。)
  • 所有的二元算子除了. (句点)( (左括弧) and [ (左中括弧)之外都要与他们操作者隔开一段距离。
  • 一元算子没必要和它的操作者用空格隔开。
  • 每个在for语句之内的语句后面的分号之后都放置一个空格。
  • 每个逗号后要放置一个空格。

额外的建议

{}[]

使用{}来代替new Object(),使用[] 代替new Array()

当成员的名字是连续整型时请使用数组,当成员的名字是任意的字符和名称的时候请使用对象。

, (逗号) 操作符

避免使用逗号,除了非常规则地在for语句中使用。(这不包括那些使用在对象形式,数组形式,var语句中和参数形式中。)

块区域

在JavaScript中没有块区域,只有方法有区域。不要使用块,除了使用复合语句的时候。

赋值表达式

避免在ifwhile语句的条件中使用赋值表达式。

    if (a = b) {

是正确声明?或者是

    if (a == b) {

才是呢?避免构造那些不容易确定是否正确的形式。

===!== 操作符

大多数情况下,使用===!==是更好的选择,==!=会进行类型的强制转换。确切一步说就是不要使用==来比较同为假的值。

捣乱的加减号

小心点,不要在+符号后用一个+++符号,这样的写法会引起混淆,在他们之间插入空格会使你意图更加清晰。

    total = subtotal + +myInput.value;

更好的写法是

    total = subtotal + (+myInput.value);

这样+ +不至于被看成++符号了。

eval是魔鬼

eval函数是JavaScript中最被滥用的特征,避免使用它。

eval类似的,不要使用Function构造器,不要把字符串传给setTimeoutsetInterval语句。

特别声明,文章翻译于 Code Conventions for the JavaScript Programming Language
版权由原作者所有。

2007年3月9日

纵览 JavaScript语言

简介

这篇文章是为专业程序员介绍的JavaScript语言的,它是一种小巧的语言,如果你熟悉其他的编程语言,那么这篇文章对你来讲不是那么难以理解。

JavaScript不是Java,他们是两门完全不同的语言,JavaScript不是Java的子集,JavaScript不能被认为是Java(Java就是Java)。JavaScript分享了像Java一样分享C语言的语法,但从更深角度讲JavaScript更与SchemeSelf有相似之处。它是一门小巧的语言,但是它确实强大的和丰富的语言。你应该好好观察一下它,你会发现它不是一个玩具语言,而是一个拥有许多与众不同特性的、完整的语言。

JavaScript是一门不用花太多时间学习的正规语言。它能更好地适合一些任务,比如与Java相比它更适合客户端编程。以我的实践经验,我发现用JavaScript工作使我成为一个更好的Java程序员,因为它给我带来一套丰富的技巧。

当我最初接触JavaScript时,我没有理他以为它不值得我的注意。后来,我对它刮目相看,因为我发现隐藏在浏览器中的它是一个如此有效的程序语言。我最初是在JavaScript在Sun和Netscape公司的初始陈述注意到它的。他们对JavaScript做了许多错误陈述,以免使它和Java处于竞争的位置。这些错误陈述继续充斥这那些只为傀儡和业余爱好者提供的、不好的JavaScript书中。

历史

JavaScript是被Netscape的Brendan Eich,作为一个页面脚本语言,在Navigator 2中开发的。它是一个非常具有表现力的动态程序语言,因为和浏览器的关系,它立刻变的大红大紫了。它从来没有得到一个可以校正它的问题的和基于真正使用目的测试周期,这一切导致它很强大却也有缺陷。

这篇文章描述了ECMAScript 版本3 (也叫 JavaScript 1.5)。 Microsoft和Netscape一起开发了一个没有改正缺陷的呆滞版本。新版本的语言也许不能叫JavaScript,也不在这篇文章的讨论范围内。

数据类型

JavaScript包含一小套数据类型,它有3种简单的类型:boolean, number, 和 string;特殊的值:null,undefined;其他的所有基于object类型的变化。

布尔类型有2个值:truefalse

数字是64位的浮点值,与Java的Double类似。它没有整型。除法运算可能带来小数位的结果。数字包括特殊值NaN(不是一个数字)和Infinity

字符串(string)是从零到多个Unicode字符组成的。没有单独的字节类型。一个字节被描绘成一个长度为1的字符串。字符串被'符号或"符号引用到一起,单引号和双引号可以替换使用,但必须前后匹配使用。

'This is a string.'
"Isn't this a string? Yes!"
'A' // The character A
"" // An empty string

转义用\字符,就像Java一样。字符串是不可变的。字符串有一个length属性类查看字符串的用字符的个数。

var s = "Hello World!";
s.length == 12

可以给这些简单类型增加函数,你可以为所有数字类型增加一个int()的函数,当你调用Math.PI.int()的时候就会得到3这个结果。

一个实现可能提供其它类型,比如日期(Dates)类型,正则表达式类型,但是他们真正的类型是Object类型,所有其它类型都是Object类型。

对象

JavaScript拥有非常漂亮的符号便利以对付那些键值集合(哈希表)(hashtables)。

var myHashtable = {};

这条语句了声明了一个键值集合,并把它分配到一个局部变量里。JavaScript是弱类型的,所以我们不能用类型的名称来进行声明。我们用索引的方式从键值结合中增加、替换和获取对象。

myHashtable["name"] = "Carl Hollywood";

也可以使用“.”的方式带来一点便利。

myHashtable.city = "Anytown";

当索引是一个合法的系统保留字时,点符号就变得没用了。这是因为一个语言上的定义错误,保留字不能被用于点符号的形势下,但是可以用于索引的形式下。

你会发现JavaScript的哈希表符号与Java的对象和数组符号非常地相似。JavaScript把这更发扬广大了:对象和哈希表是一个回事,所以我可以这样写

var myHashtable = new Object();

这个结果确实是一样的。

for语句可以实现对象一个枚举的能力。

for (var n in myHashtable) {
    if (myHashtable.hasOwnProperty(n)) {
        document.writeln("<p>" + n + ": " + myHashtable[n] + "</p>");
    }
}

这个结果应该是

<p>name: Carl Hollywood</p>
<p>city: Anytown</p>

一个对象就是一个涉及到键/值对的集合,键是字符串(或者其他的元素比如那些被转化为字符串的数字),值可以是任何数据类型,包括其他对象。对象经常以哈希表的方式实现,但是没有任何哈希表(比如哈希函数或重写的函数)天性的可见。

对象很容易被嵌套到别的对象当中,并且对象的表达式可以内容对象里使用。

this.div = document.body.children[document.body.children.length - 1];

对象的文字表达中,一个对象描述了包含在大括弧中的一组用逗点隔开的键/值对。键可以是在其后用冒号的标识符或字符串。由于语言定义的错误,系统预留字不能以标识符方式出现,但可以用字符串方式出现。值可以是任何类型的文字或表达式。

var myObject = {name: "Jack B. Nimble", 'goto': 'Jail', grade: 'A', level: 3};
return {
    event: event,
    op: event.type,
    to: event.srcElement,
    x: event.clientX + document.body.scrollLeft,
    y: event.clientY + document.body.scrollTop};
emptyObject = {};

JavaScript的对象模式是JSON的数据交换格式的基础。

新的成员可以在任何时候被附着增加到任何对象上。

myObject.nickname = 'Jackie the Bee';

数组和函数以对象方式实现。

数组

JavaScript中数组也是哈希表对象,这使它非常好地与稀少的数组程序相称。当你声明了一个数组,你不需要声明它的大小,数组会自增长,这很像Java的向量(vectors)。数组的值根据键来定位,而不是根据偏移量,这使JavaScript数组非常方便地使用,但不适合那些数字分析应用。

对象和数组最主要的不同点是length属性。length属性总是比最大的整数键大1。有2种方式创建一个新数组。

var myArray = [];
var myArray = new Array();

数组不是强类型,它可以包括数字、字符串、布尔值、对象、函数和数组,你可以在一个数组中混合使用字符串、数字和对象。你以嵌套序列的方式使用数组,这和s-expressions的方式非常地类似。数组的第一个索引通常用0。

当数组增加一个新元素的时候,它的索引是比数组长度。然后数组的长度变成在索引基础上加1。这个便利的特性使数组很容易在for循环中增加元素的数量。

数组有个与对象类似的符号标记。

myList = ['oats', 'peas', 'beans', 'barley'];

emptyArray = [];

month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

slides = [
    {url: 'slide0001.html', title: 'Looking&nbsp;Ahead'},
    {url: 'slide0008.html', title: 'Forecast'},
    {url: 'slide0021.html', title: 'Summary'}
];

新元素以分配的方式增加到数组中。

a[i + j] = f(a[i], a[j]);

函数

JavaScript的函数看起来想C语言的函数一样,但JavaScript用function声明而不是类型关键字。当调用一个函数时,传递指定数目的参数不是必须的,过度的参数会被忽略,丢失的参数被赋值为undefined,这使写一个处理可选参数的函数变的很容易。

一个函数有权使用一个参数数组,这个数组包含了所有的实际上被调用传递进来的参数。这使处理一个带可变数目的参数变的很容易,例如:

function sum() {  // Take any number of parameters and return the sum
    var total = 0;
    for (var i = 0; i < arguments.length; ++i) {
        total += arguments[i];
    }
    return total;
}

JavaScript有内部函数,这与Java的内部类起了相同的作用,但是不是那么地明显。JavaScript也有也有扮演着lambda表达式的匿名函数。函数有词法上的作用域。

函数是对象的第一课,这意味着函数可以被存储在对象中,并且作为参数传递给其它函数。

定义函数

有3种方式定义一个函数:function声明,function操作符和function构造器。

function声明

function声明在当前的作用域内创建一个被命名的函数。

function name(argumentlist) block

函数可以嵌套,必须闭合,有0个或多个以逗号隔开的参数名称。block是由0个或多个{ }附着着。

function声明以function操作符形式速记:

var name = function name (argumentlist) block ;

function操作符

function操作符是一个产生一个对象的前缀操作符,它看起来和function声明很类似。

function name(argumentlist) block

名称是可选的,如果提供了,那么可以用于函数的递归调用。这也用于访问函数的对象成员(除了IE以外)。如果名称被省略了,那么它就是一个匿名函数。

函数的操作符经常用于把它分配给一个原型(prototype)。

函数的操作符也经常用于在适当的地方定义函数,比迅速地得到当写回调的时候。( The function operator can also be used to define functions in-place, which is handy when writing callbacks.)

function构造器

function构造器携带字符包含在参数和内部,从而产生一个function对象。

new Function(strings...)

不要使用这种方式,由于语言引用的惯例导致很难恰当的表达一个函数身体作为一个字符串。在字符形式中,早期的错误检查不起作用。这也是很慢的,因为编译器必须被触发每当构造器被调用的时候,并且这样浪费内存,因为每个函数需要它自己独立的实现。

对象和this

一个函数是一个对象,它可以包含成员就像其它对象一样。这就允许一个函数包含它自己的数据表,这也允许一个对象扮演一个“类”,包含构造函数和一套相关的函数。

一个方式可以是一个对象的成员,当一个函数是一个对象的成员的时候,被成为“方法”。有一个特殊的变量,叫做“this”,当一个对象的方式被调用时指向了对象。

例如:在表达式foo.bar()中,this变量就指向了对象foo,它作为了函数bar的一个额外的参数。函数bar可以使用this进入对象内部以找到有用的项目。

一个深一点的表示式do.re.mi.fa()this变量指向了对象do.re.mi,而不是对象do。在一个简单的函数调用中,this指向了没有太大用的Global Object (也叫 window)。当调用一个内部函数时,正确的行为是保护好当前this的值。

构造器

当函数被用于初始化对象的时候被叫做构造器,调用一个构造器的次序与调用一个普通的函数有一些细微的不同。一个构造器带着new前缀被调用:

new Constructor(parameters...)

根据习惯,构造器的名字的首字母使用大写的。

new前缀改变了this 的含义,不是平常的值,而是代表了“新对象”。构造器的身体将会初始化对象的成员。构造起将会返回一个新对象,除非明确的使用了return语句进行替换。

被构造的对象将会包含一个秘密的指向了构造器的prototype的成员的原型链接域。

原型(Prototype)

对象包含一个隐藏的连接属性,这个链接指向了prototype,就是构造器的实例。

当条目以点形式或索引形式从一个对象被访问的时候,如果条目没有在对象中被发现那么就检查他的链接对象,如果链接对象中也没发现,那么如果链接对象本身有一个链接对象,那么就继续检查这个链接对象,如果链接对象所有环节都检查完毕,那么就返回undefined

原型用于链接住一系列的继承

成员可以用分配的方式增加到prototype中来,这我们定义一个类Demo,它从Ancestor继承,并增加了它独有的方法foo

function Demo() {}
Demo.prototype = new Ancestor();
Demo.prototype.foo = function () {};

变量

定义变量使用var语句。当变量在函数内部定义时,var有着函数范围(function-scope)级别,这个变量不能在函数外被访问。JavaScript中没有其他的范围域粒度了,更具体的说,JavaScript中没有块域(block-scope)

任何在函数中使用但没有用var明确定义的变量,都会被假定属于全局对象。

任何没有被明确初始化的变量都会被赋值于undefined

变量不是强类型的,一个变量引用一个对象、字符串、数字、布尔值、null值或者是undefined

当函数被调用的时候会产生一套新的变量,这允许递归的调用。

闭合

函数可以被定义到其他函数中,内部函数有权使用在外部函数定义的变量,如果一个引用内部函数继续存在(例如一个回调函数),那么外部函数的变量也将继续存在。

返回值

JavaScript没有void类型,所以每个函数都必须返回一个值,除了构造器之外默认值是undefined,构造器的默认返回值是this

语句

语句包括 var, if, switch, for, while, do, break, continue, return, try, throw, and with,它们大多数与C类的语言相同。

var语句是一个或多个变量名称的列表,它们以逗号隔开,带着可选的初始化表达式。

var a, b = window.document.body;

如果var语句出现在所有的函数之外那么就声明了一个全局的变量。如果出现在一个函数内,就声明了一个局部的变量。

if语句,while语句,do语句, 和符合逻辑的操作符中,JavaScript视为falsenullundefined""(空字符串),和数字0作为假(false)。其它所有值被视为真(true)。

switch语句中的case标记可以是表达式,它们不必非得是常数,也可以是字符串。

有2种形式的for语句,第一种像这样 (init; test; inc)形式。第二种是对象重复器。

for (name in object) {
    if (object.hasOwnProperty(name)) {
        value = object[name];
    }
}

上面执行在object中的每一个name,名字产生的次序是不确定的。

语句可以有个前缀,就是一个标识符跟着一个冒号。

with语句不应该被使用

操作符

JavaScript有还算完整的操作符,大多数他们都与C类的语言工作的方式相同,但请注意一些细微的差异

+操作符用于加法和字符串联,如果任意操作数是字符串,那么进行串联,这可能会引起错误,比如说'$' + 3 + 4 会得到'$34',而不是'$7'

+可以作为一个前缀操作符把字符操作数转化为数字。

!!可以作为一个前缀操作符,转化操作数为布尔值。

&&操作符,一般被称为“逻辑与”(logical and),它也被称为guard,如果第一个操作数是falsenullundefined""(空字符串)或者数字0那么将返回第一个操作数,否则将返回第二个操作数。这提供了一个便利地测试null值的方法:

var value = p && p.name; /* The name value will
only be retrieved from p if p has a value, avoiding an error. */

||操作符一般被称为“逻辑或”(logical or),它也被称为default,如果第一个操作数是falsenullundefined""(空字符串)或者数字0, 那么将返回第二个操作数,否则返回第一个操作数。这提供了一个便利地设定默认值的方式:

value = v || 10; /* Use the value of v, but if v
doesn't have a value, use 10 instead. */

JavaScript支持一套逐位和移位的操作运算符,但却没有偶提供整型将他们应用。当一个数字操作数(一个64位浮点数)转化成32位整型的在运算之前,然后在转化回浮点数在操作在运算之后会发生什么呢?

在JavaScript中,void是一个前缀操作符,而不是一种类型,它总是返回undefined值。这仅仅有一点点价值,我提到它的原因是万一你不小心以习惯打出来它,而不会被它怪异的行为所迷惑。

typeof操作符返回一个基于它的操作数的字符串

错误产生了。

Object 'object'
Array 'object'
Function 'function'
String 'string'
Number 'number'
Boolean 'boolean'
null 'object'
undefined 'undefined'

杂目

全局对象

全局对象包含着所有的函数和所有的那些不在任何函数内定义的变量和对象,令人惊讶的是,全局变量在语言中并没有一个确切的名字。一些时候,this指向它,但大多数情况并不是的。在浏览器中windowself都是指向全局对象的全局对象成员,因此给了一个间接的对它的寻址方法。

如果一个变量可以被访问,但是没有在当前域下被找到,就会在全局对象范围查找,如果仍没有,就会返回错误。

ECMAScript规范没有谈论关于多个全局对象的可行性,或者上下文关系,但是浏览器支持这个,每个窗口都有它自己的全局对象。

分号的插入时机

这个语言的一个错误及时分号的插入,有一个技巧是用分号来终止语句。用各种开发工具去进行插入工作是合理的,没有必要为了语言的定义而让编译器去去做,请使用分号。

预留字

JavaScript对系统预留字的支持严重的过了头,这些预留字是:

abstract
boolean break byte
case catch char class const continue
debugger default delete do double
else enum export extends
false final finally float for function
goto
if implements import in instanceof int interface
long
native new null
package private protected public
return
short static super switch synchronized
this throw throws transient true try typeof
var volatile void
while with

很大一部分这些关键字并没有在JavaScript中使用,一个预留字不可以在如下的方式下被使用:

  1. 作为一个对象名字标记
  2. 作为点形式的的成员
  3. 作为函数的参数
  4. 作为一个var
  5. 作为一个绝对的全局变量
  6. 作为一个语句标号(As a statement label)

前2个限制是不可原谅的,绝对不要使用。接下来的2个的使用可以被接受,但是这样使用是非常不牢靠的。

特别声明,文章翻译于 A Survey of the JavaScript Programming Language
版权由原作者所有。

2007年3月8日

JavaScript 世界上最被误解的程序设计语言

JavaScript, 也被称为Mocha, LiveScript, JScript或是ECMAScript,是世界上最为流行的编程语言之一。事实上世界上每一台个人电脑都至少安装了一个JavaScript解释程序,并且它处于使用之中。JavaScript的流行完全取决于它作为万维网(WWW)脚本语言的角色。

尽管它很流行,却很少有人知道它是一个非常精致动态的基于对象的程序语言(dynamic object-oriented general-purpose programming language)。这为何会成为一个奥秘呢?又为什么这个语言如此被误解呢?

名称

Java-前缀暗示着不是为何涉及到了Java,就像Java的一个子集或是Java的某个版本。看起来名称的选择引起了混淆,并且这种混淆引起了误解。JavaScript不能被认为是Java,Java就是Java,JavaScript是完全不同的另一门语言。

JavaScript在语法与Java有类似之处,非常像Java与C语言一样。但是JavaScript不是Java的子集就像Java不是C语言的子集一样。Java(最初叫Oak)在应用程序中要比最初设想的好的多。

JavaScript不是开发Java的Sun公司开发的。JavaScript是由网景公司(Netscape)开发的,它最初被叫做LiveScript,这个旧名字并没有把一切搞乱。

-Script后缀暗示着它不是一个真正的程序语言,一个脚本语言要比程序语言少很多东西,但却真正地限定了它的作用。与C语言相比,JavaScript用表现力和活力交换了性能。

披着C语言外衣的LISP语言

JavaScript的像C语言的语法,包含了大括弧方式,隐式的声明,使它呈现出一个普通的过程语言(procedural language)的模样,这是被误解的。因为JavaScript更多地与功能语言相似就像LISP或Scheme与C语言和Java相比。它用数组代替了表(List),对象代替了属性表,函数是第一级的,闭包(closures)的。你可以使用微升而不必平衡所有的parens。(You get lambdas without having to balance all those parens.)

思维定势

JavaScript被设计在网景浏览器上运行,它成功地成为了所有浏览器的标准装备。这导致了思维定势,JavaScript是George Reeves似的语言,它也非常适合于非Web相关的应用程序。

发展的目标

JavaScript的一个版本是相当弱,它缺乏异常处理,内部方法和继承。以如今的形式来看,它是一个完整的面向对象的程序设计语言。但是许多看法都是基于它不成熟的形式的。

ECMA的指导委员会用心良苦地推动了JavaScript的发展,但是却带来了一个语言最大的问题:已经有太多的版本了。这无疑增加了混淆。

设计问题

没有一种编程语言是完美的,JavaScript也有它的部分设计问题,比如说:+可以对不同的类型进行加法和连接字符串的作用;具有错误倾向的with语句应该被避免的;保留字的方针过于严格了;分号(<;)插入了大量的错误,如正则表达式符号。这些问题导致了程序错误,使这个语言的整体陷于这些问题之中。幸运地是可以通过lint程序减轻这些问题。

整个语言的设计是大体上是合理的。令人惊讶的是,ECMAScript的指导委员没有意愿要纠正这些问题。肯能他们对制作一个全新的脚本语言更感兴趣。

糟糕的实现

JavaScript的早期的实现充满了bug,这些问题严重地反映在语言上。混合起这些,那些实现被嵌入到那些可怕的糟糕的web浏览器中。

劣质的书籍

几乎所有的关于JavaScript的书都是非常糟糕的,里面充斥着错误,低劣的例子,并且提升了不好的习惯。语言的重要特性往往被解释不好,或者干脆没有。我回顾许多关于JavaScript的书籍,我只推荐一本由David Flanagan著的JavaScript: The Definitive Guide (5th Edition)(译者注:《Javascript通用指南(第5版)》,也就是传说中的“犀牛书”)。(提醒作者们:如果你写了一本好的,请发给我一本赠阅本)

低于标准的标准

ECMA出版的JavaScript语言官方规范手册,这个手册是非常地劣等,它非常难以阅读和理解,这就是那些作者写出劣质书籍的原因把,因为那些作者不能用这个手册来提升他们对JavaScrpt的理解。 ECMA和TC39应该为此感到羞愧。

业余程序员

大多数写JavaScript程序并不是真正的程序员,他们缺乏训练来写好程序。尽管JavaScript拥有如此丰富的表现力,但是他们却不把它用到有用的地方去,这使JavaScript落下了一个“业余者的语言”的称谓,并且不适合专业的程序。这显然不是事实。

面向对象(Object-Oriented)

JavaScript是面向对象的吗?它可以拥有数据和遵照数据的方法,对象可以包含其他对象。它没有类,但是它有像类一样的构造器,来担当类的方法和变量的容器。它没有面向类的接口,但是它确实有面向原型(prototype-oriented)的接口.

建造对象系统的两个主要方式是继承(is-a) 和聚集体(has-a),JavaScript都拥有,但是它动态的天性使它更擅长于聚集体。

一些观点认为JavaScript不是真正地面向对象因为它不能提供信息的隐藏。的确如此,对象不能有私有变量和私有方法:所有的成员都是公开的。但是这个观点切断了JavaScript的对象可以拥有私有的变量和私有方法(点这里看看如何实现) 当然很少有人能理解这些,因为呢,JavaScript 世界上最被误解的程序设计语言

一些观点认为JavaScript不是真正地面向对象因为它不能提供继承,但是这也切断了JavaScript不仅支持类样式的继承,而且代码重用模式也是非常好的

特别声明,文章翻译于JavaScript: The World's Most Misunderstood Programming Language
版权由原作者所有。

2007年3月6日

如何在Windows Workflow Foundation(WF)选择工作流类型

Windows Workflow Foundation 支持三种基本的工作流模式:顺序工作流(Sequential), 状态机工作流(State Machine)和数据岛工作流(Data-Driven). 许多人都问我如何选择他们的问题,所以关于这点我愿意与大家分享一下我的看法。我们首先从一个简单的问题开始。有一篇文稿,我让弗雷德(Fled)来检查, 让乔尼(Joe)来审核,最后由我来把它发给我的客户。

这是一个显而易见的顺序工作流。为了实现我们问题,我创建了一个顺序工作流工程,分别增加了弗雷德检查,乔尼审核,最后我来把它发送到用户并完成工作流的顺序活动(Sequence Activity)。 一个顺序工作流的特点在于“工作流总是在控制之中”的事实。当弗雷德,乔尼和我被告知要做那些我们先前定义好的事情时,我们就开始做。我们做我们该做的事情,当我们已经做完了便通知工作流,然后由工作流来决定接下来发生什么。 当然顺序工作流并不意味着所有的事情都像上面我们描述的那样只在一条直线上发生,我们也同样可以有条件分支、循环等等。换句话说,就是

一个顺序工作流的特点在于“工作流总是在控制之中”的事实。当弗雷德,乔尼和我被告知要做那些我们先前定义好的事情时,我们就开始做。我们做我们该做的事情,当我们已经做完了便通知工作流,然后由工作流来决定接下来发生什么。

当然顺序工作流并不意味着所有的事情都像上面我们描述的那样只在一条直线上发生,我们也同样可以有条件分支、循环等等。换句话说,就是工作流控制着“顺序”。顺序工作流模式作为一个古典的工作流模式被许多工作流产品所支持和基于已经有很多年了。 依我看,给工作流一个不好的名字也是需要相当注意的。倒不是告诉人们该做什么有任何错误(有时候我也以自己的习惯行事),当有些时候这样确实是无法工作的。 让我们看一个另外一个例子。比如说我正在测试一个正在处于开发阶段的产品。当我发现一个问题,于是我新建了一个bug,并把它指派相应的开发人员,然后“安心地"等待程序员修复它。我将要写一个工作流来管理这个过程。到现在来说,一切听起来都很正常,步骤应该是:测试人员新建bug,开发人员修复bug,测试人员关闭bug。这就像我们刚才那个关于文稿的例子。 但这个方案是不牢靠的,测试的例子里真正的发生了什么呢?一个测试人员新建了一个bug,并且将它指派了比尔(Bill)。但是比尔说这不是我的问题,这应该是克莱夫(Clive)的问题,并且重新指派给了他;或者比尔说这根本就不是一个bug(或者类似说法),将其标识为不做处理;或者他要求测试人员给于更详细的信息;更甚者,他的心情很好,修复了bug并反馈给了测试人员;或者最初的测试人员有事不在,只有其他测试人员;或者测试人员撤销了这个错误的bug(不安全地)。每一个参与者都能做出一套不同选择在给定的位置上。 如果我以顺序工作流模式写这个例子会发生什么?可能会像这样(如果你能宽恕我这段伪代码): 。顺序工作流模式作为一个古典的工作流模式被许多工作流产品所支持和基于已经有很多年了。

依我看,给工作流一个不好的名字也是需要相当注意的。倒不是告诉人们该做什么有任何错误(有时候我也以自己的习惯行事),当有些时候这样确实是无法工作的。

让我们看一个另外一个例子。比如说我正在测试一个正在处于开发阶段的产品。当我发现一个问题,于是我新建了一个bug,并把它指派相应的开发人员,然后“安心地"等待程序员修复它。我将要写一个工作流来管理这个过程。到现在来说,一切听起来都很正常,步骤应该是:测试人员新建bug,开发人员修复bug,测试人员关闭bug。这就像我们刚才那个关于文稿的例子。

但这个方案是不牢靠的,究竟发生了什么呢?一个测试人员新建了一个bug,并且将它指派了比尔(Bill)。但是比尔说这不是我的问题,这应该是克莱夫(Clive)的问题,并且重新指派给了他;或者比尔说这根本就不是一个bug(或者类似说法),将其标识为不做处理;或者他要求测试人员给于更详细的信息;更甚者,他的心情很好,修复了bug并反馈给了测试人员;或者最初的测试人员有事不在,只有其他测试人员;或者测试人员撤销了这个错误的bug(不安全地)。每一个参与者都能做出一套不同选择在给定的位置上。

如果我以顺序工作流模式写这个例子会发生什么?可能会像这样(如果你能宽恕我这段伪代码):


         Tester T creates instance of bug workflow
         T adds bug details
         T assigns to developer D
LabelA: Switch
           D assigns to developer E
                 Goto LabelA
           D rejects bug to T:
                 Switch
                       T accepts rejection:
                       T updates bug and assigns to developer F:
                                Goto LabelA
                 End Switch
           D requests info from T:
           T submits info
                 Goto LabelA
           D submits solution to T:
           T withdraws bug:
         End Switch

你也学看出来了,出现在选择之内的循环和选择正在造成结构性的问题。(这里我捂着自己的鼻子使用了Goto,是为了让代码和设定的情形保持映射关系。)我们可以让这个例子更现实些,当有很多的bug接踵而来的时候,团队领导会把任务指派给团队中的人(或者是采取开发人员抢占的模式),并且增加了工程快照和设置bug优先级等方式等等,事情将会越来越糟糕。

这个例子引起的问题使用状态机模式是一个更好的方式。上面的伪代码变成了这样:

State: Initial
      Action: T adds bug details
      Action: T assigns to developer D; new state = Fixing

State: Fixing
      Action: D assigns to developer E
      Action: D rejects bug to T; new state = Rejected
      Action: D requests info;  new state = Pending Info
      Action: D submits solution; new state = Pending Approval
      Action: T withdraws bug; new state = Closed

State: Rejected
      Action: T accepts rejection; new state = Closed
      Action: T updates bug and assigns to developer F; new state = Fixing

State: Pending Info
      Action: T submits info; new state = Fixing

State:  Pending Approval
      Action: T rejects solution; new state = Fixing
      Action: T accepts solution; new state = Closed

State: Closed
这样是更清晰和更易于理解的,并且增加更多的特征也不会使整个结构变得复杂——仅仅是增加更多的状态机和动作。

实现这个状态机模式在Windows Workflow Foundation是非常简单的事情:只需要创建一个状态机工作流工程,然后定义状态机和所需要的动作即可。

评判使用状态机模式的标准是什么呢?简言之:是否重要的选择在工作流之外做出?(are the important choices being made outside the workflow?)是否用户在控制当中?(Is the user in control? )如果上面的答案是“是”,那么用顺序工作流模式想法去做会让任务一开始就陷入麻烦之中。从另一方面来说,那些选择在工作流之外的适合用状态机工作流模式。

如果工作流不要做任何选择,那那一种模式更适合呢?恩,一个状态机工作流控制了一整套选择,对于一个测试人员来说直到问题提交之后才接收到解决方案是没有意义的。这只有当bug工作流到达了适当的状态机活动——由大量的可能的分支,后才变得有效。

这是最后一点证明为何状态机工作流更适合这个bug程序。顺序工作流天生地,把所有可能的顺序行为编写到它的内部结构中去。但是在这,我们并不关心它。我们仅仅需要知道当前的状态,并且下一步可以做什么就够了,所以我们把时间花费到在进程中建模线路,尽管我们实际上也并不关心他们,并且那些线路是非常多的,就像在在这个bug程序一样多,所以通过顺序工作流模式能带来的好处不可避免地微乎其微了。


特别声明,文章翻译于http://blogs.msdn.com/davegreen/
版权由原作者所有。原文章关于Data-Driven类型的工作流的论述部分已被删节。