11.4 方法 2:把函数也放入散列中

    first class

    Perl 语言中使用包把多个函数归集在一起。接下来,我们要介绍的是 JavaScript 语言中使用的另一种归集方法。这种方法把函数也放入散列中。

    大家使用的编程语言大多应该可以把字符串赋值给变量。也可以把它作为函数的参数传递或作为函数的返回值返回。或许你会觉得这是理所当然的事情。事实上,并非所有的语言都如此。比如,FORTRAN 66 中就不能把字符串赋值给变量。另外,C 语言中就不可以把数组作为参数来调用函数 10。

    10看起来像是把数组或者字符串传递给了函数,事实上只是指向数组开头位置的指针。

    像这种不受限制,可以赋值给变量,也可以作为函数的参数传递,又可以作为函数的返回值返回的值被称为 first class 的值。这好比不受任何歧视的一等公民(first-class citizen)。最新出现的一些编程语言中,如 Java 语言、Perl 语言和 Python 语言,字符串就是 first class 的值。

    在 JavaScript 语言中,函数也是 first class 的值。函数可以被赋值给变量,可以作为函数的返回值返回。接下来,我们来考察利用这一特征可以实现哪些功能。

    把函数放入散列中

    JavaScript 中的散列是用如下语句定义的。

    JavaScript
    {count: 0, name: "麻雀"}

    这里像 0 和 " 麻雀 " 这样表达值的部分,可以放入函数 function( ){ … }(图 11.5)。

    空标题文档 - 图1

    图 11.5 counter 是散列

    接下来,我们来看这是如何实现的。下面的代码和 11.3.2 节中介绍的内容大体相同,实现了野鸟计数器的功能。

    JavaScript
    var counter = {
    count: 0,
    name: "麻雀",

    push: function(){
    this.count++;
    console.log(this.name + ": " +
    this.count + "只");
    },
    reset: function(){
    this.count = 0;
    console.log(this.name + ": " + "重置");
    }
    }

    counter.push(); //-> 麻雀: 1只
    counter.push(); //-> 麻雀: 2只
    counter.push(); //-> 麻雀: 3只
    counter.reset();//-> 麻雀: 重置
    counter.push(); //-> 麻雀: 1只

    在 Perl 语言中是通过包来实现的,而在 JavaScript 语言中是通过散列实现的。此外有没有别的不同之处呢?乍一看,还有关键字 this 的使用这一区别。this 是一个限定词,在函数 my_method( )与对象 obj 绑定之后通过 obj.my_method( ) 的形式调用此函数时,this 用于 在 my_method( ) 函数中引用 obj 对象本身。前面的 Perl 语言的例子中,我们用 my $value = shift 语句来显式地获取参数,而 JavaScript 语言中却是隐式地借助了 this 这一变量来表示。上述的例子中通过 counter.push( ) 语句来调用函数,因此 push 函数中使用的 this 变量指的就是 counter。所以这个例子中把 this 替换成 counter 程序也是可以正常执行的。

    这个例子和使用包的例子都有一个共通的问题,就是只能创建一个计数器。但是这里要创建多个计数器也不是困难的事情。下面我们就来展示这是如何做到的。

    创建多个计数器

    为了创建多个计数器,我们需要定义一个散列初始化的函数。编写这样一个函数是出乎意料的简单,只需要把创建散列的代码挪到 makeCounter 函数中就可以。这样就达到了和 Perl 语言中使用包一样的效果 11,即:

    11因篇幅所限,这里省略了 reset 方法。
    • 可以创建多个对象

    • 外观上是一个完整不可分的整体

    • 无需人为记住初始化方法

    JavaScript
    function makeCounter(){
    return {
    count: 0,
    push: function(){
    this.count++;
    console.log(this.count + "只");
    }
    }
    }

    var c1 = makeCounter();
    var c2 = makeCounter();
    c1.push(); //-> 1只
    c2.push(); //-> 1只
    c1.push(); //-> 2只

    把共享的属性放入原型中

    刚才的代码中,每次创建计数器时都会重新定义一个 push 函数。用以下语句确认 c1.push 和 c2.push 是否相同时,返回值是 false。这意味着两者是不同的。

    JavaScript
    console.log(c1.push === c2.push); //-> false

    这是什么原因呢?在每次调用 makeCounter 时 push: funtion( ){…} 会被执行,定义一个新的函数。也就是说,创建 100 个计数器,就会有 100 个内容相同的 push 函数被定义。如果内存和 CPU 可以无限供应时,这不是什么问题。但现实没有这么美好。如果能把 push 函数这样所有计数器都共享的属性归集起来,个别计数器只是对其做引用,这样应该能节省内存和时间(图 11.6)。

    空标题文档 - 图2

    图 11.6 把共享的属性归集起来

    然而,归集起来后就要记得放入了哪里,使用的时候还要显式地指示出来,这样很麻烦。把共享的内容放在别的对象中,那就必须记住这一内容放在哪个对象中,人工来记忆这些是令人不快的。如果语言处理器能代劳并做出正确的判断就好了。

    原型的操作

    为解决这一问题,JavaScript 语言引入了原型的概念 12。当向一个对象查询 x 的值时,如果这个对象自己知道就自己给出答案。如果不知道,它会去查询它的原型再给出答案。下面的代码中,obj 对象就不知道 x 的值是多少。

    12这里介绍的是用委托的方式实现原型这一概念的情景,不同语言中也可以在实例化时通过负责实现。至于这种方法中当实例化后原型发生了变更会怎样,在不同语言中是存在差异的。
    JavaScript
    obj = {}
    obj.proto = {x: 1}

    console.log(obj); // -> {}
    console.log(obj.proto); // -> { x: 1 }
    console.log(obj.x); // -> 1

    在查询 x 的值时 (obj.x),obj 转向它的原型 (obj.proto) 才给出答案(图 11.7)13。

    13通过 proto 这个名字访问原型并不是标准功能,根据处理器的不同会有不同的可能性。JavaScript 1.8.1 开始引入了 Object.getPrototypeOf (object) 的表达方式。

    空标题文档 - 图3

    图 11.7 查询 obj.x 时发生的事情

    用 new 运算符实现高效表达

    除此之外,JavaScript 语言还引入了一种使得原型处理的表述更加方便的运算符。在函数 f 前使用 new 运算符后会执行以下四个操作:

    • 创建新的对象 x

    • 新创建的对象 x 的原型变为函数 f 的原型

    • 把新创建的对象 x 传给 this,执行函数 f 的内容

    • 返回对象 x

    JavaScript
    var Counter = function() {
    this.count = 0; // ❶
    }

    Counter.prototype.push = function(){ // ❷
    this.count++;
    console.log(this.count + "只");
    }
    var c1 = new Counter(); // ❸
    c1.push(); //-> 1只
    c1.push(); //-> 2只
    var c2 = new Counter();
    console.log(c1.push === c2.push) //-> true // ❹ 相同

    这段代码中,首先通过❷句在 Counter 的原型中追加了一个名为 push 的新的函数。在随后的❸句执行了上述四个操作。首先创建了一个空的对象,然后将这个对象的原型设为 Counter 的原型。这个原型中定义了 push 函数。接下来,把这个对象传给 this,再调用 Counter。Counter 中包含❶句的内容。执行这些内容会往这个对象中追加一个名字为 count 值为 0 的属性。最后,返回一个原型中包含 push 函数的、带有 count 属性的对象。不经意间我们就实现了图 11.6 右边部分的结构。❹句返回值是 true,由此可知 push 函数已经是共享的了。

    这就是面向对象吗

    在 Java 语言中学过面向对象的读者可能会心里犯嘀咕,觉得到现在为止讲的内容远远不够。有些人甚至可能要发怒,质问为什么关于类的话题还没有任何涉及。

    笔者认为,不知道面向对象的读者当中很多读者也不知道何为类。只是大家看的此类书籍都是从类开始讲面向对象的。

    的确,Java 语言是一门编程必从类开始的语言。笔者也深知,在教 授 Java 语言程序设计时,不先详细讲解并使之学会使用类是不行的。然而,类并不是从程序设计语言诞生之日起就存在的。它充其量只不过是几十年前某个人出于提高便利性的需要,试着创作出来的东西而已。尽管如此,我们总是被提醒类的存在并被鼓励去使用它,但又不太明白类为什么是必要的。

    类的存在只不过是因为人们觉得有了它编写程序会更方便些,而约定的一种事项。它并不是什么物理法则或宇宙真理,仅仅是人们的一种约定而已。所以,为了理解为什么会有这样一种约定,我们需要考虑语言设计者的意图。