IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    (一)我的Javascript系列:Javascript的面向对象旅程(上) - starok

    starok发表于 2015-09-05 10:41:00
    love 0

    今宵酒醒何处,杨柳岸,晓风残月

    导引

    Javascript拥有它独特的面向对象机制,这种机制与传统的面向对象语言(如Java)有着很大的不同。像Java一样,它有对象的概念,可是描述对象的类去哪了?它的对象是通过构造函数生成的吗?它能继承吗,从而有子类的概念吗?原型是什么?原型链又是什么?种种这些,就像谜一样。在这篇文章里,我并不打算一一去解答这些谜,而是通过一种别样的方式去探索Javascript语言关于面向对象设计的脉络。我想从零开始,遵循几个预设的原则,尝试着去重现这门语言的设计过程。这不是在阐述历史,也不是要证明什么;这只是一个探索的过程。

    准备:新语言设计的几个原则

    首先,我不加证明地给出新语言设计要遵循的几个原则:

    1. 新语言要简洁小巧,不能附加太多的概念

    2. 新语言要足够强大,足够灵活,而不仅仅只是一门领域驱动语言或玩具语言

    3. 新语言的设计应该分步骤进行,越来越强大的特性是一点一点添加进去的

    4. 新特性的加入要基于当前的设计,而不能颠覆它

    5. 新语言应该尽可能地像传统的流行的语言,从而降低学习者的难度

    以上,原则1决定了新语言不能使用像Java那样的基于类的面向对象方式,因为这会引入类模板的概念,从而附带着引入类型体系、继承甚至是接口等很多复杂的概念。原则2又决定了新语言必须具有面向对象的能力,否则就称不上所谓的强大。原则3为两者的矛盾提供了解决思路,那就是一开始不要考虑这些,只提供最基本的语言要素,然后再逐步添加面向对象的特性。原则4是对原则3的补充,新特性的加入是基于当前设计的一种改良,新特性的引入是个简单自然的过程,可以通过某种变通的方式达到。原则5指出我们应该引入哪些特性,即那些被广大开发者所熟知的特性,例如面向对象以及面向对象的种种。另外,原则5也解释了为什么新设计的Javascript语言使用类C的语法。

    第一步:结构化编程语言的完备

    首先,不去考虑面向对象的特性,而只含有限几个基本数据类型。我不再赘述诸如变量名、变量、操作符、控制结构(if/else,while这些)等基本概念了,这些是设计语言必须要考量的因素,但与我讨论Javascript面向对象的模式无关。

    新语言最先给出几个最基本的数据类型:

    1. number
    2. string
    3. boolean

    以上只给出这几个最基本的数据类型,而隐藏掉任何的面向对象概念。不仅如此,连函数都没有提供。不过怎么能够没有函数呢?考虑添加进去:

    4. function

    这里,function的加入不是作为语法的一部分,而是作为一种数据类型。这是一个很明智的考量。这样函数可以当成一个回调作为另一个函数的参数了,从而较容易地实现基于事件驱动的编程模型。而浏览器就是一个基于事件驱动的例子。另外,函数作为一种数据,可以在代码中很容易地生成新函数,从而较容易地实现元编程。

    加入函数之后,就朝着结构化编程迈向一大步了。如果语言设计到这里就结束,也未尝不可。接着为语言提供一些库函数,并加入一些浏览器相关的库函数就成为一个功能完备的语言了。当然这里还缺少一个数组类型。没事,添加一个就足够了。

    不过,由于没有提供一个类似于C语言的结构体,使得同一类的数据不能集中处理,多多少少有些不便。例如,用C我们可以定义如下的结构体:

    struct Person {
    char *name;
    int age;
    }

    struct Person sam;
    sam.name = "Sam";
    sam.age = 18;

    这样可以把sam的name属性和age属性集中在同一个结构下。但是新语言目前没有给出这样的数据类型,亟待引入。这时,想到的是一种名值对集合的数据结构,这个结构称之为Javascript对象。

    Javascript对象不同于其他语言的对象,它更多地像是一种数据集合,类似于哈希表。在这里,键值限定为string类型,每个键就是一个名字。它能做的操作如下:

    1. 可以添加一对新的名值对
    2. 可以为已经存在的名字设定新值
    3. 可以删除已经存在的名值对

    这恐怕就是Javascript对象能够做的所有操作了。这样引入的数据类型,既限制了大量复杂概念的引入,又带来了使用上的便捷,是个划算的方案。

    下面是一个使用Javascript对象的示例代码:

    var sam = {}; //新建一个空对象

    sam['name'] = 'sam'; //新增一对名值对

    sam['name'] = 'Sam'; //对已存在的名值对进行更改

    sam.age = 18; //等价于sam['age'] = 18

    sam.friend = {name: 'Jim'}; //值可以是对象,从而可以实现任意深度的嵌套

    function fall_in_love(man, woman) {
    man.girlfriend = woman;
    }
    sam.fall_in_love = fall_in_love; //值可以是函数

    sam.fall_in_love(sam, 'Alma'); //存储的函数可以调用的,这为sam交了个女朋友

    delete sam.girlfriend; //分手了,可以删除掉存储的女朋友

    新增加的对象类型,可以进一步地利用,例如相应的库函数可以集中起来,例如与数学相关的库函数,可以集中定义在同一个对象里,然后为这个对象取个名字Math就好了。另外,数组类型也被考虑取消了。因为数组类型本质上也是一种名值对的集合,只不过它的名字都是数字类型。对于数组来说,这样的书写形式a[0]等价于对象形式的书写形式a['0']。这是一种用哈希表来模拟数组的方式,它的缺点是带来了性能上的损失,不能一概地说这种方式绝对的好。但终究还是做出了这样的决定了,同时又取消了array这个类型,语言上更简洁了。

    总结:最终来说,新语言一共给出了以下七种基本数据类型:

    1. number
    2. string
    3. boolean
    4. function
    5. object
    6. null
    7. undefined

    最终演变成这七种数据类型是在现在的Javascript语言中可以看到的。其中新引入的null类型,它的值只有null一个,用来表示引用的对象不存在这个概念。而undefined类型,它的作用与null类似,从字面上理解就是未定义。所以null和undefined都可以表示不存在这个意思,同样的意思有两种表示方式,只会带来困惑。

    第二步:增加this

    目前的新语言只是一种完备的结构化编程语言。不过,实际上只要做一点改变,它就可以成为一种完备的面向对象的编程语言了。只不过,这种面向对象的方式不是基于类模板的;实际上它有专门的术语,称为基于原型的。感兴趣的朋友可以自己查看参考资料。

    对象其实是属性和方法的封装形式。C语言的结构体,只是属性的封装;由于在新语言中,函数也是一种单纯的数据类型,所以我们之前定义的对象,可以同时包含属性和函数。不过,这并不能说是完整意义上的封装。这里的函数差一般对象的方法的地方,在于函数不具有识别本身对象的能力。例如之前定义过的fall_in_love函数:

    function fall_in_love(man, woman) {
    man.girlfriend = woman;
    }
    sam.fall_in_love = fall_in_love;
    sam.fall_in_love(sam, 'Alma');

    这里的fall_in_love函数,在sam对其调用时,还需要将sam作为参数传递给它,就好像它根本不认识sam一样。然而,作为sam的一个函数,或者说是sam自己要去调用这个函数,那么这个函数理应知道sam的存在才合理。换句话说,我们希望像下面这样定义和调用fall_in_love函数:

    function fall_in_love(woman) {
    this.girlfriend = woman;
    }
    sam.fall_in_love = fall_in_love;
    sam.fall_in_love('Alma');

    这里的this,在真正调用时,就应该指的是sam这个对象才对。我们还可以像下面这样复用这个函数:

    var jim = {};
    jim.fall_in_love = fall_in_love;
    jim.fall_in_love('Amy');

    函数fall_in_love可以指派给对象jim,而这个指派只是一个简单的对象属性赋值。当jim真正调用fall_in_love这个函数时,语句中的this就应该是jim了。也就是说,fall_in_love函数中的this,应该可以动态地指代;并且,永远地应该指代调用者。

    为了实现这样的效果,新语言的函数被赋予了一种称为call的能力。它就像是一个方法调用(实际上它就是),它的第一个参数会被绑定到this,接下来的参数就对应函数声明的参数了。例如我们可以定义下面这个一般的函数:

    function fall_in_love(woman) {
    this.girlfriend = woman;
    }

    var sam = {};
    fall_in_love.call(sam, 'Alma'); //this是sam,woman是'Alma'

    var jim = {};
    fall_in_love.call(jim, 'Amy'); //this是jim,woman是'Amy'

    sam.fall_in_love = fall_in_love;
    sam.fall_in_love('Alma'); //等价于fall_in_love(sam, 'Alma');

    通过call,就有了动态控制this的能力。而一般的写法如sam.fall_in_love('Alma')就是fall_in_love(sam, 'Alma')的等价形式。前一种写法就很像Java那样的对象对方法的调用了。这样,一般的函数就具有访问其调用者的能力,看上去更像是属于对象的一个方法了。从此,Javascript对象就不只包含属性,也可以包含一系列的方法了,从而成为了一个真正的对象。

    接下来是我的个人拙见。如果新语言只设计到这一步就停止的话,我认为是未尝不可。语言设计到这一阶段,基本就已经达到了能力上的自给自足了。与一般面向对象语言相比,除了类模板之外,该有的也都有了。剩下的就是基于现有的设计,去完善相应的机制。例如,一个对象是不是可以加入copy的机制,使得可以基于现有的对象生成一个具有相同结构的对象;再例如,一个对象又是不是可以添加extend的机制,使得一个对象可以将另一个对象的内容包含到自身;等等。这一切,都是去围绕这种名值对的结构去花心思、做文章。在这里,关键是不被世俗的狭见所束缚,完全去忘记类型的概念,而只关注对象的行为。这种想法,如果去论述它的妙处,足可以又写一片文章了。具体的可参考“鸭子类型”。

    然而,新语言的设计没有沿着这种思路走下去。因为原则5的存在,它不得不去考虑增加那些我们所熟知的基于类的面向对象的一些特性,包括类、构造函数、继承、类型体系。很难说引入这些,就不是在引入大量复杂的概念。不禁让人觉得这违反到原则1。这当中的窍门在于,当拨开这些复杂概念笼罩的那团迷雾之后,会发现这仅仅是基本的名值对对象形式的一种变通;甚至连变通都不是,只是名值对对象的一种形式。为了不违反原则1,我们只能去模仿它,使得它尽可能看上去像那回事。

    未完待续。。。


    本文链接:(一)我的Javascript系列:Javascript的面向对象旅程(上),转载请注明。



沪ICP备19023445号-2号
友情链接