• OOP 诡异教程 转


     

    • 本文分上、下两篇,站在一个难以名状的角度上研究了 JavaScript 语言中面向对象机制的起源、内涵和发展,带领读者从原始森林走向高楼大厦。文章作者 lichray 是个 ECMAScript 的狂热追随者,mozilla.org 邮件列表里的无名潜水员。
    • 文章中使用了 Rhino 解释器,行开头有 "js>" 表示那是输入,输入下一行没有这个标记的表示解释器回馈消息。
    • PS: 读懂本文需要对 JavaScript 闭包和逃逸变量有较深入的了解。

    一. 对象和消息
    考虑一下我们平常怎么说话的。我们叫某某人做某事,用下面的句式:
    forest run!
    其中"!"是语气的标志,对于编程语言来说是没有意义的,全部换成".":
    forrest run.
    不知道如果我告诉大家上面这句话就是 Smalltalk 语言中一个合法语句大家会怎么想。好了,不谈这个。这样我们就得到了一种语法,"宾"谓结构:
    ObjectVerb ::
      Object Verb.
    如果让它支持多个 Verb,比如
    forrest run, jump, stop.
    可以扩展成这样:
    ObjectVerb ::
      Object VerbList.
    VerbList ::
      Verb
      Verb , VerbList
    很明显,对于 JavaScript 来说,上面的 BNF 不可能和任何一个产生式匹配。问题出在哪儿?我们要帮 JavaScript 指定,谁是 Object,谁是 Verb。鉴于 Object 只有一个,Verb 有多个,我们可以用括号来区分它们,然后把最后那个句号去掉:
    ObjectVerb ::
      Object ( VerbList )
    这样上面的那句话就变成了下面的形式:
    forrest (run, jump, stop)
    很像函数调用,是吧?不过还有一个问题,现在这些 Verb(s) 对于 JavaScript 来说是“裸词”(Perl 语),我们可以避开再去定义这些标识符,用字符串代替;最后再说明一下 Object 是什么:
    forrest ('run', 'jump', 'stop')
    那么现在我们第一个“模仿”自然语言的程序版本出现了,加上下面针对 JavaScript 的文法:
    Object ::
      Identifier
    Verb ::
      StringLiteral

    二. 实现消息传递
    有了文法,一切都好办。看得出来,我们下面的工作是定义能创建一个新 Object 的函数,函数中有一些动作,产生的新 Object 是一个能处理这些消息的函数。创建 Forrest Gump 的函数还可以创建 Tom,Mike 等等;他们都是 People:
    function People () {
      function run () {
        print("I'm running!")
      }
      function jump () {
        print("I'm jumping!")
      }
      function stop () {
        print("I can't stop!")
      }  
      return (function (verb) {
        switch (verb) {
          case 'run': run(); break
          case 'jump': jump() ;break
          case 'stop': stop() ;break
        }
      })
    }
    为了简单起见还可以把返回的那个函数写成这样:
        (function (verb) {
          eval(verb)();
        }
      })
    Ok。现在我们来试一试这个智商低于 85 的 Forrest Gump 怎么样:
    js> forrest = People()
    js> forrest('run')
    I'm running!
    js> forrest('jump')
    I'm jumping!
    js> forrest('stop')
    I can't stop!
    事情就是这样。我们成功地创造了对象,还让他做动作、说话。
    不过,这个实现并不是我们上文中最后一个文法所指出的。它不支持连续发送指令。改一改。要加入顺序执行指令的办法:
    function People () {
      function run () {
        print("I'm running!")
      }
      function jump () {
        print("I'm jumping!")
      }
      function stop () {
        print("I can't stop!")
      }
      function _do_verbs_ (verblist) {
        for (var i=0; i <> forrest = People()
    js> forrest('jump','run','jump','stop')
    I'm jumping!
    I'm running!
    I'm jumping!
    I can't stop!

    三. 利用消息传递处理状态
    什么是状态?我们在进行面向对象编程时,把状态表示为对象的一组数据,我们称之为“属性(property)”。在我们的消息传递编程风格中,可以直接把这些数据堆到产生对象的那个函数中去。下面给 Forrest 加入一个状态,Forrest 口袋里的钱。先得声明原先有多少钱:
    forrest = People(1000)
    然后,我们希望可以执行这样的代码,让 forrest 支出 200 美元:
    forrest('pay', 200)
    但很明显,我们无法分清 200 是 Verb 还是 'pay' 所要求的数据。我们只得简化文法,只允许一次发送一个消息,以保全我们的脑细胞:
    forrest('pay')(200)
    也就是说,我们需要让 forrest('pay') 这一表达式返回一个能改变状态的函数,而不仅仅是调用函数来显示一句话。也就是说,如果我们想让 Forrest 急得跳起来,我们先得跳起来:
    forrest('jump')()
    新时代的 Forrest 实现如下(省略了一点多余的代码):
    function People (money) {
      //var money = money
      function pay (dollars) {
        money -= dollars
      }
      function restMoney () {
        return money
      }
      function run () {
        print("I'm running!")
      }
      return (function (verb) {
        return eval(verb)
      })
    }
    试一下。先支出 200 美元,然后看看他还剩多少钱:
    js> forrest=People(1000)
    js> forrest('restMoney')()
    1000
    js> forrest('pay')(200)
    js> forrest('restMoney')()
    800
    当然,我们的 Forrest 还可以赚钱。下面这个版本比较彻底地说明了消息传递编程风格的一切。可以直接修改钱之后,我们可以不需要在创建 Object 的时候就说明原有多少钱;当然,使用注释中的版本更自然:
    function People (/* money */) {
      var money = 0; // var money = money ? money : 0;
      function setMoney (dollars) {
        money = dollars
      }
      function addMoney (dollars) {
        money += dollars
      }
      function pay (dollars) {
        money -= dollars
      }
      function restMoney () {
        return money
      }
      return (function (verb) {
        return eval(verb)
      })
    }
    试一下吧:
    js> forrest = People()
    js> forrest('addMoney')(1000)
    js> forrest('restMoney')()
    1000
    js> forrest('pay')(200)
    js> forrest('restMoney')()
    800
    上篇完。小结一下:消息传递的编程风格指的是,把函数 A 的执行上下文当作对象的数据环境,在此定义对象的动词(函数),然后从此上下文中返回一个可以接受、处理消息的函数(常为匿名)。用函数 A 产生消息处理器作为对象,向此对象传递参数作为消息,以此执行函数 A 环境中定义的动作,这些动作还可能改变所在上下文中用一组数据定义的对象状态。
    这是最终确定的 JavaScript 基于消息传递编程风格的文章“OOP 诡异教程(上)”的下篇。原文地址:http://let-in.blogspot.com/2007/06/oop.html。原来的想法是以风格开头,谈到 JavaScript 的内部机制,但作者 lichray 迟迟没有动键盘,认为不如利用已有的风格做一套机制出来,这样可能更有意义。于是,就有了这个更加“诡异”的下篇。

    四. 扩展的实现
    上文最后给出了一个“看上去很美”的基于消息传递的编程风格,比如构造一个 People 类的代码类似:

    function People () {
      var money = 0
      function setMoney (dollars) {
        money = dollars
      }
      function pay (dollars) {
        money -= dollars
      }
      return (function (verb) {
        return eval(verb)
      })
    }

    有了这样的语法我们就可以描述不少句子了。但是存在一个问题:现实中的 Objects 之间是存在关系的——比如,forrest 是个 IQ 为 75 的傻子,傻子是 People 的一种。而我们仅仅是生搬硬套了一种语法而割裂了这种 "is-a" 关系。现在我们的工作,目的之一就是让这样一个“真切”的世界从我们已有的编程风格的地基上拔地而起。
    到底应该怎样做才能使 Fool 产生的对象都能响应 People 的消息呢?我们要给 Fool 产生的对象(也就是返回的那个匿名函数啦)都添加这样一种能力:如果在 Fool 中响应不了消息,那就反馈给 People 响应。

    function Fool (iq) {
      var IQ = iq || 0
      function init (iq) {
        IQ = iq
      }
      return (function (verb) {
        try {
          return eval(verb)
        } catch (e) {
          return People()(verb)
        }
      })
    }

    js> forrest = Fool()
    js> forrest('init')(75)
    js> forrest('IQ')
    75
    js> forrest('money')
    0

    五. 语法扩展和代码生成
    这下代码量增加了很多,强迫潜在的使用者们在创建每个类时都这样写那实在是令人抓狂。本来这篇文章应该不提此类问题的解决,但考虑到有益于读者理解“机制”这个抽象概念,这里给出一个可行的方案——把普通的类代码用 Function() 函数重编译为可用的 JavaScript 函数。也就是说,我们能给出类扩展的代码并指定被扩展的类来获取类似上文的代码:

    Fool = extend('People()', function (iq){
      var IQ = iq || 0
      function init (iq) {
        IQ = iq
      }
    })

    为了方便字符串操作,我们希望编译后的代码的参数部分(如 People())都集中出现在一个位置且尽可能便于定位。在函数头添加一句

    var origin = People()

    当然是可行的,这样还能使 Fool 内部显式引用到其超类。但这样还不够漂亮。我们修改编译后的样例代码为:

    function () {
      return (function (origin) {
        var IQ = 0
        function init (iq) {
          IQ = iq
        }
        return (function (verb) {
          try {
            return eval(verb)
          } catch (e) {
            return origin(verb)
          }
        })
      })(People())
    }

    这个利用参数传递变量的小技巧不值得学习,实际效率不高。但在这篇文章中,这样绑定特殊变量的技术是标准方案。
    那么,extend() 函数的实现为:

    function extend (originc, code) {
      function argsArea (code) {
        // 题外话,正则表达式也有不值得使用的时候
        return code.slice(code.indexOf('(')+1, code.indexOf(')'))
      }
      function bodyCode (code) {
        // 不用 trim() 了,没事儿找事儿
        return code.slice(code.indexOf('{')+1, code.lastIndexOf('}'))
      }
      function format (body) {
        var objc = bodyCode(function () {
          return (function (verb) {
            try {
              return eval(verb)
            } catch (e) {
            return origin(verb)
            }
          })
        }.toString())
        return 'return (function (origin) {'+body+objc+'})('+originc+')'
      }
      var $ = code.toString()
      return Function(argsArea($), format(bodyCode($)))
    }

    这样前文提到过的 extend 的实例代码就可以正常运行了,测试代码不再重复。

    六. 机制完备化
    这样,我们的基于消息传递编程风格的一套面向对象机制就确定下来了。机制是宪法,是语言的根本大法,有了它,我们就可以通过修改代码生成器,很快地给这套机制进行完备化。
    想法有很多,例子只举两个。
    第一个例子:类的定义中应该能直接引用到将产生的对象 self。答案只有一句话:把返回的那个作为对象的匿名函数命名为 self。
    第二个例子:既然是单继承模式,应当存在一个顶层类 AbsObj,使没有指定继承的类自动继承它。答案也只有一句话:在 extend 函数体第一行添加代码:

    if (arguments.length == 1) {
      code = originc
      originc = 'AbsObj()'
    }

    然后手工构造设计 AbsObj 类,为空也无所谓。不过当然了,一般都会给顶层类添加一些全局性质的消息绑定。由于是“底层操作”,基本上都需要修改 extend 函数。做了一个简单的:

    function AbsObj () {
      //检测是否能响应此 verb,要再用一次异常处理
      function canHandle(verb){
        try {
          // 别担心这里的 self 会传递不过去
          self(verb)
        } catch (e) {
          return false
        }
        return true
      }
      function toString() {} // 这个搞起来其实很麻烦~`
      var self = function (verb) {
        return eval(verb)
      }
      return self
    }

    js> Obj=extend(function(){x=5})
    js> o=Obj()
    js> o('canHandle')('x')
    true
    js> o('canHandle')('y')
    false

    文章写完了,小结一下。消息传递的编程不仅仅是一种代码风格,还可以成长为一种完备的机制。这种完备性远不只是这两篇加起来不到300行的文章所能覆盖的(例如非常彻底的“万物皆对象”,因为只要是能响应消息的函数,连接一下 AbsObj 就是合法对象了;类,函数都可以),大家可以试着玩一玩,顺便体会一下这个计算模型的透明和强大。
    另外,熟悉函数式编程的朋友可以帮忙思考一下:这样一个基于闭包变换的计算模型实质上是函数式的,再配合动态的函数式的对象级继承(用一个匿名类代换一下)就能在纯 FP 真正下实现 OOP 了。可惜的是每一次更新操作都要重新生成对象,性能代价大了点,不知道大家有什么好想法。
  • 相关阅读:
    高校宿舍管理系统详细需求分析说明书
    手把手教你实现"短信轰炸"
    爬取小说网站章节和小说语音播放
    报刊订阅管理系统的设计与实现
    宾馆客房管理系统的设计与实现
    Analysis of requirement specification of parking management system
    手把手教你微信好友头像形成指定的文字
    html、css/bootStrap、js/Jquery、ajax与json数据交互总结
    详细介绍idea实现javaweb项目登入注册(华东交通大学教务处信息管理系统)、模糊查询
    2020研究生考试各专业各学校的研究生录取分数和复试录取人数比率(各大学校研究生录取信息的官网直通车)
  • 原文地址:https://www.cnblogs.com/winner/p/1348530.html
Copyright © 2020-2023  润新知