富有表现力的 javascript

Author Avatar
tanglijun 8月 19, 2015

Javascript 是现在最流行、应用最广泛的语言之一。由于所有现代浏览器都嵌入了 Javascript 解释器,所以在大多数地方都能见到其身影。作为一种语言,它在我们的日常生活中起着非常重要的作用,支持着我们访问的网站,帮助Web呈现出多姿多彩的界面。

那为什么有些人还把它看作一种玩具式的语言,认为它不值得职业程序员关注呢?我们认为其原因在于,人们没有认清这种语言的全部能力及其在当今的编程世界中的独特性。Javascript 是一种极富表现力的语言,它具有一些C家族语言所罕见的特性。

Javascript 允许你用各种方式完成同样的任务,还允许你在面向对象编程的过程中借用函数式编程中的概念来丰富其实现方式。

Javascript 的灵活性

Javascript 最强大的特性是其灵活性。作为 Javascript 程序员,只要你愿意,可以把程序写得很简单,也可以写得很复杂。这种语言也支持不同的编程风格。你既可以才用函数式编程风格,也可以采用更复杂一点的面向对象编程风格。即使你根本不懂函数式编程或面向对象编程,也能写出较为复杂的程序。使用这种语言,哪怕只采用编写一个个简单的函数的方式,你也能高效的完成任务。这可能是某些人把 Javascript 视同玩具的原因之一,但我们却认为这是一个优点。程序员只要使用这种语言的一个很小的、易于学习的子集就能完成一些有用的任务。这也意味着当你成长为一个更高级的程序员时,Javascript 在你手中的威力也在增长。

Javascript 允许你模仿其他语言的编程模式和惯用法。它也形成了自己的一些编程模式和惯用法。那些较为传统的服务器端编程语言具有的面向对象特性,Javascript 都有。

我们来看一个用不同方法完成同样任务的例子:启动和停止一个动画。

如果你习惯于过程式的程序设计,那么可以这样做

function startAnimation() {}

function stopAnimation() {}

这种做法很简单,但你无法创建可以保存状态并且具有一些仅对其内部状态进行操作的方法的动画对象。下面的代码定义了一个类,你可以用它创建这种对象

var Anim = function () {};

Anim.prototype.start = function () {};
Anim.prototype.stop = function () {};

var myAnim = new Anim();

myAnim.start();
myAnim.stop();

上述代码的定义了一个名为 Anim 的类,并把两个方法赋给该类的 prototype 属性。如果你更喜欢把类的定义封装在一条声明中,则可以改用下面的代码

var Anim = function () {};

Anim.prototype = {
  start: function () {},
  stop: function () {}
}

这在传统的面向对象程序员看来可能更眼熟一点,它们习惯于看到类的方法声明内嵌在类的声明之中。要是你以前用过这样的编程风格,可能想尝试一下下面的例子

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

var Anim = function () {};

Anim.method('start', function () {});
Anim.method('stop', function () {});

Function.prototype.method 用于为类添加新方法。它有两个参数。第一个是字符串,表示新方法的名称;第二个是用作新方法的函数。

你可以进一步修改 Function.prototype.method,使其可被链式调用。这只需要让它返回 this 值即可

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

var Anim = function () {};

Anim.
  method('start', function () {}).
  method('stop', function () {});

你已经见识了完成同一项任务的5种不同方法,它们的风格略有差异。基于自己的编程背景,你可能觉得其中的某种方法比别的方法更为合意。这是件好事:Javascript 允许你用最合适于手头项目的编程风格进行工作。不同的风格在代码篇幅、编码效率和执行性能方面各有特点。

弱类型语言

在 Javascript 中,定义变量时不必声明其类型。但这并不意味变量没有类型。一个变量可以属于几种类型之一,这取决于其包含的数据。Javascript 中有3种原始类型:布尔型、数值型和字符串类型(不区分整数和浮点数是 Javascript 与大多数其他主流语言的一个不同之处)。此外,还有对象类型和包含可执行代码的函数类型,前者是一种复合数据类型(数组是一种特殊的对象,它包含着一批值的有序集合)。最后,还有空类型 (null) 和未定义类型 (undefined) 这两种数据类型。原始数据类型按值传递,而其他数据类型则按引用传递。如果不了解这一点的话,你可能会碰到一些意想不到的问题。

与其他弱类型语言一样,Javascript 中的变量可以根据所赋的值改变类型。原始类型之间也可以进行类型转换。toString 方法可以把数值或布尔值转变为字符串。parseFloat 和 parseInt 函数可以把字符串转变为数值。双重“非”操作可以把字符串或数值转变为布尔值

var bool = !!0;

弱类型的变量带来了极大的灵活性。因为 Javascript 会根据需要进行类型转换,所以一般来说,你不用为类型错误操心。

函数是一等对象

在 Javascript 中,函数是一等对象。它们可以存储在变量中,可以作为参数传给其他函数,可以作为返回值从其他函数传出,还可以在运行时进行构造。在与函数打交道时,这些特性带来了极大的灵活性和极强的表达能力。这正是用以构建传统的面向对象框架的基础。

可以用 function() {} 这样的语法创建匿名函数。它们没有函数名,但可以被赋给变量。下面是一个匿名函数的示例

(function () {
  var foo = 10;
  var bar = 2;

  alert(foo * bar);
}());

这个函数在定义之后便立即执行,甚至不用赋给一个变量。出现在函数声明之后的一对括号立即对函数进行了调用。括号中空无一物,但也并不是非得如此;

(function (foo, bar) {
  alert(foo * bar);
}(10, 2));

这个匿名函数与前一个等价,只不过变量没有在函数内部var声明,而是作为参数从外部传入而已。这个函数也可以返回一个值。这个返回值可以被赋给一个变量

var baz = function (foo, bar) {
  return foo * bar;
}(10, 2);

匿名函数最有趣的用途是用来创建闭包。闭包是一个受到保护的变量空间,有内嵌函数生成。Javascript 具有函数级的作用域。这意味着定义在函数内部的变量在函数外部不能被访问。Javascript 的作用域又是词法性质的 (lexically scoped) 。这意味着函数运行在定义它的作用域中,而不是在调用它的作用域中。把这两个因素结合起来,就能通过把变量包裹在匿名函数中而对其加以保护。你可以这样创建类的私用 (private) 变量

var baz;

(function () {
  var foo = 10;
  var bar = 2;

  baz = function () {
    return foo * bar;
  };
}());

baz();

变量 foo 和 bar 定义在匿名函数中。因为函数 baz 定义在这个闭包中,所以它能访问这两个变量,即使是在该闭包执行结束后。

对象的易变性

在 Javascript 中,一切都是对象(除了那三种原始数据类型。即便是这些类型,在必要的时候也会被自动包装为对象),而且所有对象都是易变的 (mutable) 。这意味着你能使用一些在大多数别的语言中不允许的技术,例如为函数添加属性

function displayError(message) {
  displayError.numTimesExecuted++;
  alert(message);
}

displayError.numTimesExecuted = 0;

这也意味着你可以对先前定义的类和实例化的对象进行修改

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype = {
  getName: function () {
    return this.name;
  },
  getAge: function () {
    return this.age;
  }
}

var alice = new Person('Alice', 93);
var bill = new Person('Bill', 30);

Person.prototype.getGreeting = function () {
  return 'Hi' + this.getName() + '!';
};

alice.displayGreeting = function () {
  alert(this.getGreeting());
};

这个例子中,类的 getGreeting 方法是在已经创建了类的两个实例之后才添加的,但这两个实例仍然能获得这个方法,其原因在于 prototype 对象的工作机制。对象 alice 还得到了displayGreeting 方法,而别的实例却没有。

与对象的易变性相关的还有内省 (introspection) 的概念。你可以在运行时检查对象所具有的属性和方法,还可以使用这种信息动态实例化类和执行其方法(这种技术成为反射 (reflection) ),甚至不需要在开发时知道它们的名称。这些技术在动态脚本编程中发挥着重要作用,而静态语言(例如 C++ )则缺乏这样的特性。

如果你习惯使用 C++ 或 Java 这类语言,可能会觉得这很奇怪,因为在那些语言中,不能对已经实例化的对象进行扩展,也不能对已经定义好的类进行修改。而在 Javascript 中,任何东西都可以在运行时修改。这是一个强有力的工具,许多别的语言中无法办到的事都能借助于它而办到。当然,这也有其不利之处。你可以定义一个具有一套方法的类,却不能肯定这些方法在以后总是完好如初。这是 Javascript 中很少进行类型检查的原因之一。

继承

继承在 Javascript 中不像在别的面向对象语言中那样简单。Javascript 使用的是基于对象的(原型式 (prototypal) )继承,它可以用来模仿基于类的(类式 (classical) )继承。两种方式都可以,根据手头任务的实际情况,有时其中的某种会更合适一些。它们在性能上也有不同的表现,这也是在进行选择时需要考虑的重要因素。

Javascript 中的设计模式

1995 年,GoF 合作出版了一本名为《设计模式》的书。这本书整理记录了对象间相互作用的各种方式,并针对不同类型的对象创造了一套通用术语。用以创建这些不同类型的对象的套路被称为设计模式 (design pattern) 。

Javascript 的强大的表现力赋予了程序员在运用设计模式编写代码时极大的创造性。在 Javascript 中使用设计模式主要有如下3原因

  • 可维护性。设计模式有助于降低模块间的耦合程度。这使对代码进行重构和换用不同模块变得更容易,也使程序员在大型团队中的工作以及与其他程序员的合作变得更容易。
  • 沟通。设计模式为处理不同类型的对象提供了一套通用的术语。程序员因此可以更简明地描述自己的系统的工作方法。你不用进行冗长的说明,往往这样一句话就足够了:“它使用了工厂模式”。每个模式都有自己的名称,这意味着你可以在较高层面上对其进行讨论,而不必涉足过多的细节。
  • 性能。某些模式是起优化作用的模式。它们可以大幅提高程序的运行速度,并减少需要传送到客户端的代码量。这方面最重要的例子是享元模式和代理模式。

你也可能出于如下两个理由而不使用设计模式。

  • 复杂性。获得可维护性往往要付出代价,那就是代码可能会变得更加复杂、更难被程序设计新手理解。
  • 性能。尽管某些模式能提升性能,但多数模式对代码的性能都有所拖累。这种拖累可能微不足道,也可能完全不能接受,这取决于项目的具体需求。

实现设计模式比较容易,而懂得应该在什么时候使用什么模式则较为困难。未搞懂设计模式的用途就盲目套用,是一种不安全的做法。你应该尽量保证所选用的模式就是最恰当的那种,并且不要过度牺牲性能。

小结

Javascript 的丰富表现力是其力量之源。即使这种语言缺少一些有用的内置特性,拜其灵活性所赐,你也能自己加入这些特性。完成一项任务可以有多种方式,你可以根据自己的技术背景和喜好选择编写代码的方式。

Javascript 是弱类型语言。程序员在定义变量时并不指定其类型。函数是一等对象,并且可以动态创建,因此你可以创建闭包。所有对象和类都是易变的,可以在运行时修改。可供使用的继承范型有两种,即原型式继承和类式继承,它们各有其优缺点。

Javascript 中的设计模式颇有助益,但其不当应用也会产生负面效果。在 Javascript 这类轻灵的语言中,过度复杂的架构会很快把应用程序拖入泥沼。你使用的编程风格和选择的设计模式应该与所要完成的具体工作相称。

许可协议:署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
本文链接:https://tanglj.cn/2015/08/19/attractive-javascript/