2010 May 13

From Java to JS, and Thinking in Modern Langs

有人看着标题可能就要发笑,两个完全不同的东西干嘛来放在一起比较呢?的确,本质上,Javascript(后文统称“JS”)是一种浏览器中的脚本语言,Java是更为强大的解释型语言。后者需要预先编译,并在自己的容器(JVM)中运行。但是仔细品味,其实两门语言的核心特性上,有太多的相似之处,不禁引人深思。借以本文,希望能给身处不同阵营的成序言一些关于语言融汇贯通的启发。

前世今天

曾几何时,JS还没有多大名气的时候,Java已经非常流行了。那个时候JS的名字乱七八糟,而且每个厂商也有自己的标准。Netscape的LiveScript,MS的JScript,总之这种目前最为流行的浏览器脚本语言,在当初连个确定的名字都没有。之后,随着标准的推出,大家习惯称为Javascript。Netscape也是为了借用Java的名气,推广自己的Javascript,才取名“Java”Script的。

其实在几年前,Java也被运行在客户端的,大家都知道的Applet技术不就是为了客户端编程的嵌入式小程序么?可是Applet并没有火,反而这种轻量、快速的JS流行起来,一发不可收拾。虽然,Java的性能和功能都比JS强大不止一点点,JS着实是把Java赶出了浏览器,而Java则是在服务器端站稳了脚跟。

尽管近年来,Java Web Start、Java FX的兴起意味着在RIA的风潮下,Java还是想重回浏览器市场,但此时的浏览器客户端内,JS已经不是那个之前连名字都不确定的脚本语言了,众多的框架和调试工具已经可以让人们意识到它就是浏览器端编程的成熟标准了。

这样,不管是Applet还是Web Start,都已经成为了类似Adobe Flex、Adobe Air的一种针对某些特定情景的选择了。当然,业界普遍认为这些技术的前景是无限光明的。另一方面,HTML5和ECMAScript v5的推出将继续延续这种传统HTML+CSS+JS的RIA解决方案。

数据类型

JS的基础数据:Number、Boolean、null、undefined
Java的基础数据:int、byte、short、long、char、float、double、boolean
JS中的Number实际是Java中的double,这意味着不管数字有多大,有多少精度,都被当作double。

JS中没有char,也没有String?JS中没有char(JS内部的每个字符是16位的),但是有String。但JS的String,即不算作基础类型,也不能算作引用类型。JS中的String是immutable的,即不可改变。虽然可以可以通过拼接和String的相关函数进行操作,但实际上是返回一个新的结果字符串。

讲到这里,java程序员就疯掉了。为什么JS会有这么“粗糙”的类型系统?
因为,JS是“若类型”(loose typing)。JS首先是有类型(typing)的,可是它不去检查,声明的时候也只是用var,不管你后面想申明一个Number,还是Boolean。
那不是很容易出错?JS的若类型允许你把这个变量存放任何类型的东西,也就不存在出错了。

Java的严格类型是为了编译前检查,可JS没有编译这个步骤,自然严格类型对JS来说也就失去了很大一部分作用。

换个角度来看,这种弱类型是不是很轻巧呢?是的!事实上,这种轻巧的类型系统再加上各种各样的字面量,就催生了伟大的JSON。

迷惑的对象系统

面向对象几乎被看作当代语言的一个特性。JS显然是基于对象的,但却不能说是面向对象的。因为它没有传统的基于继承树的继承系统,也就没有所谓的父类和子类。众多宣称是“面向对象”的编程语言,严格上来说,只能算作“基于对象”。但JS有自己的继承方式,就是原型继承。

使用到了对象的prototype属性。这个prototype属性是JS继承机制的基础。JS对象是“直接从另一个对象那里继承”的。默认情况下,每一种数据类型都又一个指向默认原型对象的引用,即prototype属性。当你更改这个属性值的时候,意味你想让当前对象“继承”于你新指定的那个对象。如果你修改prototype中的那个对象,你是想要修改所有当前类型的对象的方法、属性。示例如下:

var animal = {
    name: "general animal",
    eat: function(){
        alert(this.name + " is eating!");
    },
    sleep: function(){
        alert(this.name + " is sleeping!")
    }
}

function cat(){
    var o = function(){
    };
    o.prototype = animal;
    o.name = "cat";
    o.catchMice = function(){
        alert(this.name + " is catching mice!");
    }
    return o;
}

这个其实很像GoF设计模式中模板方法,只是这个东西是内置在JS之中的,无需你再去实现一个模板机制。Java种的模板方法很常用,例如Spring的JDBC Template。
尽管如此,一部分前端工程师选择了使用一种伪继承(Pseudo-Classical)的方式实现对象系统。采用这种思维,上面的那段示例应该编写如下:

function Animal(){
    this.name = "general animal";
}

Animal.prototype.eat = function(){
    alert(this.name + " is eating!");
};

Animal.prototype.sleep = function(){
    alert(this.name + " is sleeping!");
};

function Cat(){
    Animal.apply(this);
    this.name = "cat";
}

Cat.prototype = new Animal();

Cat.prototype.catchMice = function(){
    alert(this.name + " is catching mice!");
}

这样,程序员可以使用“new”关键字来实例化一个对象。这种方式,只是在表面上模拟传统的OOP风格,但是骨子里还是通过prototype实现了对象系统。
包括Google在内的诸多厂商大量采用了这种风格,分析其原因是前端工程师大量由传统程序员转型,或者说,至少受到了传统程序语言的较大影响,并希望获得这种OOP风格。尽管,在这种风格下,大部分JS的优雅特性都泯灭了。
这里,却体现出语言之间相互影响的典型案例。很多语言为了赶上OOP的风潮,都选择了OOP的风格,可是谁也说不清骨子里是否真是OOP。

动态特性

有趣的事情是,下面这段代码,可能让很多Java程序员迷惑:

var scope = "global";
function f(){
    alert(scope);
    var scope = "local";
    alert(scope);
}

Java程序员肯定会说,显示的是“global”;可是JS程序员会告诉你,显示的是“local”。这里尽管第二个scope的声明在第一个alert的后面,一个block内的var会被编译器提前到第一排,虽然此时scope还没有赋予新值。
这里我想说的是,JS没有严格的块级作用域(block scoping)。正是由于JS的动态特性,以往再OOP种所提倡的“越晚用到,就越晚声明”的原则是不适用的,JS里的变量应该在每个函数的开头就全部声明。
更疯狂的是,在众多的引用型数据类型里,竟然有Function?!function被看作了一种数据。

var sayHello = function(){
    alert("hello world");
};

这个被称为lambda函数。有人说JS是“Lisp in C’s clothing”,JS的很多特性都是有道理的。由于Java又沿用了很多C的语法特性,所以JS和Java的相似也是不奇怪的。
这种lambda函数,似乎已经不是函数了,它更像pointer,可是比pointer安全、简单。

当你发现function是数据类型的时候,你应该能想到一个function返回另外一个function的情景。

var producer = function(x){
    if (x == 1)
        return function(){
            alert("x == 1");
        };
    if (x == 2)
        return function(){
            alert("x == 2");
        };
}

既然JS里Function是一种对象,那么对象有自己的属性和方法就不奇怪了。
从这里,大多数Java程序员就已经疯掉了,JS的精髓所在就是这种方式所引发出的“闭包”。

闭包与prototype

所谓闭包,就是一个函数。但往往人们是指的一种特殊情况下的函数——即被另一个函数作为返回值所返回的函数。这个被返回的函数最大的特点就是依旧能访问原来外部函数的作用链(Scope Chain),好像原函数的那些变量都依旧存在,尽管你可能无法再显示地访问到原函数和它里面的那些变量了。
JS的闭包提供一些类C语言想都想不到的功能,例如像这样实现函数的柯里化:

var add = function(){
    var i, n = arguments.length, s = 0;
    for (i = 0; i < n; i++) {
        if (typeof arguments[i] == 'number' && isFinite(arguments[i])) {
            s += arguments[i];
        }
    }
    return s;
};

//From Javascript the Good Parts
Function.prototype.curry = function(){
    var slice = Array.prototype.slice, args = slice.apply(arguments), that = this;
    return function(){
        return that.apply(null, args.concat(slice.apply(arguments)));
    };
};

alert(add.curry(22).curry(11)()); //prompt 33

在这里,首先定义了一个add函数,它实际上接受无限个参数,并将其中的数字项目进行相加求和。

可最后一行没有直接调用add,并传入参数,而是执行了add的curry方法。
上例中,引用了《Javascript the Good Parts》中的一个实例代码,其扩展了Function这个构造函数的prototype对象,即为每个function添加了一个curry方法。该方法维护了一个运算数的数组args,以及总是指向原函数引用的that。通过闭包的作用,通过return返回的另一个function中,that依旧持有对原函数的引用,这里that没有被垃圾收集(Garbage Collection)!

这里,连续调用了两次curry方法,每次都返回一个新的function对象;其args和that都持有上一个函数对象的参数和函数本身的引用,这样迭代下来,最终执行效果就是把参数收集起来,传入了add函数。

可见,JS的闭包则是提高JS效率,完成复杂逻辑的利器!JS工程师对自己JS的这种强大的特性十分自豪,殊不知,其实Java也有闭包。

Java实际很早就提供了一种内部类(InnerClass)的东西。简单的来说,在一个类体申明另外一个类体,那么里面的那个类就叫做“内部类”。大部分学习过Java的同学都知道内部类,但大多数同学应该不会想到去使用它。

内部类在Java中的作用是提供一种使用接口之外的多继承机制。除此之外,内部类还提供了一种和JS中的闭包一样的效果,这里我们仅说明内部类的闭包特性:

public class TestClosure {

	private int number = 0;

	class Closure {
		public void increment() {
			System.out.println(number++); //More specifically, TestClosure.this.number++
		}
	}

	public Closure getCallback() {
		return new Closure();
	}

	public static void main(String[] args) {
		TestClosure tc = new TestClosure();
		Closure c = tc.getCallback();
		c.increment();
		c.increment();
		c.increment();
	}

}

这里有一个名为Closure的内部类,持有了对父类的所有成员访问权。这意味着Closure内的方法,可以访问父类TestClosure中的number。Java甚至提供了一种严格的访问规则,例如number也可以用TestClosure.this.number来进行引用。
上面那段代码,用JS来表现就是这样的:

function TestClosure(){
    var number = 0, Closure = {};
    Closure.increment = function(){
        return number++;
    }
    return Closure;
}

var tc = TestClosure(); // Important; there is no "new" keyword here!
console.log(tc.increment());
console.log(tc.increment());
console.log(tc.increment());

这里,TestClosure作为一个可以生产Closure对象的“构造函数”加工了一个Closure对象,并返回。这个Closure对象可以访问TestClosure下的任何变量或方法,自然也包括number。
实际上,大量的Java闭包被用于MVC界面的编程之中,例如Swing。在未来版本的Java种,会有更强大的闭包特性。笔者在这里猜测,应该这些特性是会像JS闭包学习的。

现代语言特性与人们的喜好

现在市面上的变成语言层出不穷,但权威机构的月度程序语言流行度统计分析,前10名之中,C语言是最稳定的,其次是C++、Java、JS。紧接着的,ActionScript、Ruby等语言浮动性非常大。

可见,年龄越长的主流语言其使用度就越高,也就是更加经得起“时间的考验”。
而这些“存活”下来的佼佼者,却越来越多的拥有一些共性。这就是文章讨论Java和JS两种使用环境完全不一样的语言的目的——即使环境完全不相关,这些语言的语法、风格特性都有惊人的共通之处,只是实现的方式、难以程度不大一样而已。

对于数据类型来说,一部分语言需要有强大的性能,那么对数据类型要尽可能的达到算法中较低的空间复杂度。这导致的是多种而全面的数据结构,伴随的可能是编译时的严格类型检查。而另一部分语言,它们的使用环境很少会要求有高强度的计算,甚至只是辅助性的,他们要求能够快速的完成开发流程、松散的维护一组数据,那么基本够用的数据类型、弱类型就可能是这个语言的基本特性了。

对于数据的访问,几乎大多数现代语言都使用了点访问符,区别在于访问的规则不一样。有的语言拥有块级的作用域,一组花括号(或其他界定符)之外是无法通过访问符得到花括号内部的引用的。但对于另一部分语言,你却可以设法让变量在整个程序的生命周期中被任意访问。这种灵活程度是一个语言设计中的博弈,较大的灵活程度可以得到一个轻盈、敏捷的编程风格,但是也意味着对程序员的要求也随之提高(这个在类C语言的程序员写JS的时候就很容易体现出来)。

由于上面所说的两个方面,数据结构的编写也会造成性能上巨大的差异。因为,同一种算法,其理论性能是确定的,但是实际性能由于实现载体不一样会造成很大的区别。人们更乐于谈论到,这种性能差异,而去忽视语言的其他差异。社区内部的一些诸如“Java还是C#”的问题就这样产生了。

当你仔细去观察这些已经有相应产品的编程语言,他们都是出色的,只是富有不同的特色罢了。现代语言经过用户的“自然选择”,已经进化到具有某一些通性。这种通性是我们掌握一种语言后,对其他语言的学习曲线变得更平缓。这就如同学过C语言的学生,去学习Java、C++会容易很多;学过Java、C++,去学习ActionScript也会觉得似曾相识。

但一种语言的存在必定拥有起特异性。这种特异性使得人们去在特定场合去使用这种语言,而放弃其他语言。例如,被大多C/C++程序员诟病的PowerScript等数据库开发的应用语言,在C/S架构下的数据库应用中仍有一席之地,特别是项目对于开发周期有较严格要求的情况下。同时,这种语言特性,也使得这种程序员很难完全掌握这门语言的全部特性。所以往往,对这些特性的掌握情况也就是衡量一名程序员对该门语言掌握程度的重要界定指标之一。

结束语

如今的大多数开发都逃脱不了少数几门语言。在网络应用的开发过程种,前端工程师和Java或C#程序员却俨然称为某种对立的存在。文章以JS和Java为例,从各自的语言特性出发,寻找其本质上的相同点和不同点。程序员在只有在充分了解语言与语言之间的这些特性之后,才能客观的去评价某一个语言以及更好地去驾驽自己所喜欢地语言。

参考文献

  • 《Thinking in Java》 4th Edition,P246,Using .this and .new
  • 《Thinking in Java》 4th Edition,P261, Closures & callbacks
  • 《Javascript the Good Parts》,P52,Functional
  • 《Javascript the Good Parts》,P37,Closure
  • 《Javascript the Good Parts》,P43,Curry
  • 《Javascript the Good Parts》,P47,Pseudoclassical
  • 《Javascript the Definitive Guide》5th Edition,Prototypes and Inheritance
  • 《Javascript 语言精髓》,为Javascript正名

5 Comments

  1. Posted May 14, 2010 at 2:49 pm | Permalink

    刚拜读完JavaScript:the Good Parts,看到这个很受用,尤其是Java的闭包,谢谢

  2. Posted May 15, 2010 at 12:33 pm | Permalink

    文章都很不错哦,周末愉快,以后会经常来逛的^-^

  3. Posted May 16, 2010 at 12:37 pm | Permalink

    我们坚信,没有什么不可以…

  4. Posted May 17, 2010 at 8:59 pm | Permalink

    每天顶你一次,生活就又丰富了一点..

  5. Posted May 17, 2010 at 9:04 pm | Permalink

    谢谢哈~这文章本来是准备投稿的~呵呵~结果嘛⋯⋯

Post a Comment

Your email is never shared. Required fields are marked *

*
*