精华内容
下载资源
问答
  • Tampio是一种面向对象的编程语言,看起来像一种自然语言-芬兰语。 它以著名的芬兰程序员的名字命名。 要查看实际使用的语言,请参见。 (由于技术原因,它被命名为“ ristiruutu”而不是“ ristinolla”,这是芬兰...
  •  关于JavaScript面向对象的编程以下是我的理解,有不对的地方请指出;   JavaScript是一种基于对象的语言,遇到的东西几乎都是对象,但又不是完整的面向对象的语言,因为他的语法中没有类(class) ,使用中我们...

     关于JavaScript面向对象的编程以下是我的理解,有不对的地方请指出;

     

    JavaScript是一种基于对象的语言,遇到的东西几乎都是对象,但又不是完整的面向对象的语言,因为他的语法中没有类(class) ,使用中我们可以吧属性和方法封装成一个对象,有时候也会从原型对象中生成一个实例对象。我们可以通过下面代码实现:

     

    我们先把猫看作一个对象,它有两个属性 : 颜色和名字 
    
    var  cat ={
    
        name :"",
        color: ""
    }
    
    
    我们可以根据这个原型对象生成两个实例对象 
    
         var cat1 = {}; // 创建一个空对象
         cat1.name = "大毛";       // 按照原型对象的属性赋值
         cat1.color = "黄色";
      var cat2 = {};
           cat2.name = "二毛";
         cat2.color = "黑色";
    
    
    
    这就是简单的封装了,把两个属性封装在一个对象里面。但是,这样的写法有两个缺点,一是如果多生成几个实例,写起来就非常麻烦;二是实例与原型之间,没有任何办法,可以看出有什么联系。

     

     我们还可以用构造函数的模式,构造函数其实就是一个普通函数,但是在它内部使用了this变量,我们再用new对函数进行实例化,this变量也会绑定在实例对象上。

     

     

     

    function Cat(name,color){
        this.name=name;
        this.color=color;
      }
    
    
    
    我们可以用new运算符生成实例对象
    
    var cat1 = new Cat("大毛","黄色");
      var cat2 = new Cat("二毛","黑色");
      alert(cat1.name); // 大毛
      alert(cat1.color); // 黄色
     

     

    cat1和cat2 会自动含有constructor指向他们的构造函数

     

     

          
          alert(cat1.constructor == Cat); //true
      alert(cat2.constructor == Cat); //true
       

     

     

    我们还可以为cat添加不变的属性,比如type或者添加一个eat()方法,在这我们用一种节省内存的方法 来做,将不变的属性和方法直接定义到prototype对象上。

     

     

    function Cat(name,color){
        this.name = name;
        this.color = color;
      }
      Cat.prototype.type = "猫科动物";
      Cat.prototype.eat = function(){alert("吃老鼠")};
    
    然后我们可以生成实例
    
    var cat1 = new Cat("大毛","黄色");
      var cat2 = new Cat("二毛","黑色");
      alert(cat1.type); // 猫科动物
      cat1.eat(); // 吃老鼠
     

     

     

     

    对于构造函数的继承我们说两种方法,一个是 prototype模式,一个是直接继承prototype对第一种的改进

     

    有一个动物的构造函数
    function Animal(){
        this.species = "动物";
      }
    还有一个"猫"对象的构造函数。
    
      function Cat(name,color){
        this.name = name;
        this.color = color;
      }

     

     现在用第一种方法继承 

     

        Cat.prototype = new Animal();
      Cat.prototype.constructor = Cat;
      var cat1 = new Cat("大毛","黄色");
      alert(cat1.species); // 动物

     

     

    Cat.prototype = new Animal();

     

    代码的第一行,我们将Cat的prototype对象指向一个Animal的实例。

    相当于完全删除了prototype 对象原先的值,然后赋予一个新值。

     

    Cat.prototype.constructor = Cat;
    

      

    第二行的意思是任何一个prototype对象都有一个constructor属性,指向它的构造函数。如果没有"Cat.prototype = new Animal();"这一行,Cat.prototype.constructor是指向Cat的;加了这一行以后,Cat.prototype.constructor指向Animal。

     

     验证一下
    
    alert(Cat.prototype.constructor == Animal); //true

     

    每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性

     

    alert(cat1.constructor == Cat.prototype.constructor); // true

     

    因此,在运行"Cat.prototype = new Animal();"这一行之后,cat1.constructor也指向Animal!所以我们要注意使用第二行,改constructor的值 

     

    还有一种就是 直接继承prototype,这是对上一种方法的改进,由于Animal对象中,不变的属性都可以直接写入Animal.prototype。所以,我们也可以让Cat()跳过 Animal(),直接继承Animal.prototype。

     

    我们先将Animal对象改写

     

     function Animal(){ }
      Animal.prototype.species = "动物";

     

    将Cat的prototype对象,然后指向Animal的prototype对象,这样就完成了继承。

     

     

        Cat.prototype = Animal.prototype;
      Cat.prototype.constructor = Cat;
      var cat1 = new Cat("大毛","黄色");
      alert(cat1.species); // 动物

     

    但是我们要注意的是第二行实际上把Animal.prototype对象的constructor属性也改掉了

     

      alert(Animal.prototype.constructor); // Cat

     

     

     

    展开全文
  • ECMAScript是一种面向对象的语言,参见如下引文(参考资源</a>): <p>ECMAScript is an object-oriented programming language for performing computations and manipulating ...
  • javascript面向对象编程

    2011-03-20 10:58:00
    JavaScript使用面向对象的技术创建高级 Web 应用程序Ray Djajadinata  本文讨论: JavaScript 是基于原型的语言用 JavaScript 进行面向对象的编程JavaScript 编码技巧JavaScript 的未来本文使用了以下技术: ...
    JavaScript
    使用面向对象的技术创建高级 Web 应用程序
    Ray Djajadinata

     

    本文讨论:
    • JavaScript 是基于原型的语言
    • 用 JavaScript 进行面向对象的编程
    • JavaScript 编码技巧
    • JavaScript 的未来
    本文使用了以下技术:
    JavaScript

    最近,我面试了一个有五年 Web 应用程序开发经验的软件开发人员。四年半来她一直在从事 JavaScript 相关的工作,她自认为 JavaScript 技能非常好,但在不久之后我就发现实际上她对 JavaScript 知之甚少。话虽这样说,但我确实没有责备她的意思。JavaScript 真的是很有趣。很多人(包括我自己,直到最近!)都认为自己很擅长 JavaScript 语言,因为他们都知道 C/C++/C#,或者有一些以前的编程经验。
    在某种程度上,这种假设并不是完全没有根据的。用 JavaScript 很容易做些简单的事情。入门的门槛很低,该语言很宽松,它不需要您知道很多细节就可以开始用它进行编码。甚至非编程人员也可能用它在几个小时内为主页编写一些有用的脚本。
    的确,直到最近,仅仅凭借 MSDN® DHTML 参考资料和我的 C++/C# 经验,我也总能勉强利用这点 JavaScript 知识完成一些任务。只是当我开始编写真实的 AJAX 应用程序时,我才意识到实际上我的 JavaScript 知识还非常不够。这个新一代的 Web 应用程序的复杂性和交互性需要程序员以完全不同的方法来编写 JavaScript 代码。它们是真正的 JavaScript 应用程序!我们在编写一次性脚本时一直采用的方法已完全不再有效。
    面向对象编程 (OOP) 是一种流行的编程方法,很多 JavaScript 库中都使用这种方法,以便更好地管理和维护基本代码。JavaScript 支持 OOP,但与诸如 C++、C# 或 Visual Basic® 等流行的 Microsoft® .NET Framework 兼容语言相比,它支持 OOP 的方式非常不同,因此主要使用这些语言的开发人员开始可能会觉得在 JavaScript 中使用 OOP 很奇怪而且不直观。我写本文就是为了深入讨论 JavaScript 语言实际上如何支持面向对象编程,以及您如何使用这一支持在 JavaScript 中高效地进行面向对象开发。下面首先讨论对象(还能先讨论其他别的什么呢?)。

     

    JavaScript 对象是词典
    在 C++ 或 C# 中,在谈论对象时,是指类或结构的实例。对象有不同的属性和方法,具体取决于将它们实例化的模板(即类)。而 JavaScript 对象却不是这样。在 JavaScript 中,对象只是一组名称/值对,就是说,将 JavaScript 对象视为包含字符串关键字的词典。我们可以使用熟悉的“.”(点)运算符或“[]”运算符,来获得和设置对象的属性,这是在处理词典时通常采用的方法。以下代码段
    var userObject = new Object();
    userObject.lastLoginTime = new Date();
    alert(userObject.lastLoginTime);    
    
    的功能与下面的代码段完全相同:
    var userObject = {}; // equivalent to new Object()
    userObject[“lastLoginTime”] = new Date();
    alert(userObject[“lastLoginTime”]);
    
    我们还可以直接在 userObject 的定义中定义 lastLoginTime 属性,如下所示:
    var userObject = { “lastLoginTime”: new Date() };
    alert(userObject.lastLoginTime);
    
    注意,它与 C# 3.0 对象初始值非常相似。而且,熟悉 Python 的人会发现在第二和第三个代码段中实例化 userObject 的方法与在 Python 中指定词典的方法完全相同。唯一的差异是 JavaScript 对象/词典只接受字符串关键字,而不是像 Python 词典那样接受可哈希化的对象。
    这些示例还显示 JavaScript 对象比 C++ 或 C# 对象具有更大的可延展性。您不必预先声明属性 lastLoginTime — 如果 userObject 没有该名称的属性,该属性将被直接添加到 userObject。如果记住 JavaScript 对象是词典,您就不会对此感到吃惊了,毕竟,我们一直在向词典添加新关键字(和其各自的值)。
    这样,我们就有了对象属性。对象方法呢?同样,JavaScript 与 C++/C# 不同。若要理解对象方法,首先需要仔细了解一下 JavaScript 函数。

     

    JavaScript 函数是最棒的
    在很多编程语言中,函数和对象通常被视为两样不同的东西。在 JavaScript 中,其差别很模糊 — JavaScript 函数实际上是具有与它关联的可执行代码的对象。请如此看待普通函数:
    function func(x) {
        alert(x);
    }
    func(“blah”);
    
    这就是通常在 JavaScript 中定义函数的方法。但是,还可以按以下方法定义该函数,您在此创建匿名函数对象,并将它赋给变量 func
    var func = function(x) {
        alert(x);
    };
    func(“blah2”);
    
    甚至也可以像下面这样,使用 Function 构造函数:
    var func = new Function(“x”, “alert(x);”);
    func(“blah3”);
    
    此示例表明函数实际上只是支持函数调用操作的对象。最后一个使用 Function 构造函数来定义函数的方法并不常用,但它展示的可能性非常有趣,因为您可能注意到,该函数的主体正是 Function 构造函数的 String 参数。这意味着,您可以在运行时构造任意函数。
    为了进一步演示函数是对象,您可以像对其他任何 JavaScript 对象一样,在函数中设置或添加属性:
    function sayHi(x) {
        alert(“Hi, “ + x + “!”);
    }
    sayHi.text = “Hello World!”;
    sayHi[“text2”] = “Hello World... again.”;
    
    alert(sayHi[“text”]); // displays “Hello World!”
    alert(sayHi.text2); // displays “Hello World... again.”
    
    作为对象,函数还可以赋给变量、作为参数传递给其他函数、作为其他函数的值返回,并可以作为对象的属性或数组的元素进行存储等等。图 1 提供了这样一个示例。

    // assign an anonymous function to a variable
    var greet = function(x) {
        alert(“Hello, “ + x);
    };
    greet(“MSDN readers”);
    
    // passing a function as an argument to another
    function square(x) {
        return x * x;
    }
    function operateOn(num, func) {
        return func(num);
    }
    // displays 256
    alert(operateOn(16, square));
    
    // functions as return values
    function makeIncrementer() {
        return function(x) { return x + 1; };
    }
    var inc = makeIncrementer();
    // displays 8
    alert(inc(7));
    
    // functions stored as array elements
    var arr = [];
    arr[0] = function(x) { return x * x; };
    arr[1] = arr[0](2);
    arr[2] = arr[0](arr[1]);
    arr[3] = arr[0](arr[2]);
    // displays 256
    alert(arr[3]);
    
    // functions as object properties
    var obj = { “toString” : function() { return “This is an object.”; } };
    // calls obj.toString()
    alert(obj);
    
    记住这一点后,向对象添加方法将是很容易的事情:只需选择名称,然后将函数赋给该名称。因此,我通过将匿名函数分别赋给相应的方法名称,在对象中定义了三个方法:
    var myDog = {
        “name” : “Spot”,
        “bark” : function() { alert(“Woof!”); },
        “displayFullName” : function() {
            alert(this.name + “ The Alpha Dog”);
        },
        “chaseMrPostman” : function() { 
            // implementation beyond the scope of this article 
        }    
    };
    myDog.displayFullName(); 
    myDog.bark(); // Woof!
    
    C++/C# 开发人员应当很熟悉 displayFullName 函数中使用的“this”关键字 — 它引用一个对象,通过对象调用方法(使用 Visual Basic 的开发人员也应当很熟悉它,它在 Visual Basic 中叫做“Me”)。因此在上面的示例中,displayFullName 中的“this”的值是 myDog 对象。但是,“this”的值不是静态的。通过不同对象调用“this”时,它的值也会更改以便指向相应的对象,如图 2 所示。

    function displayQuote() {
        // the value of “this” will change; depends on 
        // which object it is called through
        alert(this.memorableQuote);    
    }
    
    var williamShakespeare = {
        “memorableQuote”: “It is a wise father that knows his own child.”, 
        “sayIt” : displayQuote
    };
    
    var markTwain = {
        “memorableQuote”: “Golf is a good walk spoiled.”, 
        “sayIt” : displayQuote
    };
    
    var oscarWilde = {
        “memorableQuote”: “True friends stab you in the front.” 
        // we can call the function displayQuote
        // as a method of oscarWilde without assigning it 
        // as oscarWilde’s method. 
        //”sayIt” : displayQuote
    };
    
    williamShakespeare.sayIt(); // true, true
    markTwain.sayIt(); // he didn’t know where to play golf
    
    // watch this, each function has a method call()
    // that allows the function to be called as a 
    // method of the object passed to call() as an
    // argument. 
    // this line below is equivalent to assigning
    // displayQuote to sayIt, and calling oscarWilde.sayIt().
    displayQuote.call(oscarWilde); // ouch!
    
    图 2 中的最后一行表示的是将函数作为对象的方法进行调用的另一种方式。请记住,JavaScript 中的函数是对象。每个函数对象都有一个名为 call 的方法,它将函数作为第一个参数的方法进行调用。就是说,作为函数第一个参数传递给 call 的任何对象都将在函数调用中成为“this”的值。这一技术对于调用基类构造函数来说非常有用,稍后将对此进行介绍。
    有一点需要记住,绝不要调用包含“this”(却没有所属对象)的函数。否则,将违反全局命名空间,因为在该调用中,“this”将引用全局对象,而这必然会给您的应用程序带来灾难。例如,下面的脚本将更改 JavaScript 的全局函数 isNaN 的行为。一定不要这样做!
    alert(“NaN is NaN: “ + isNaN(NaN));
    
    function x() {
        this.isNaN = function() { 
            return “not anymore!”;
        };
    }
    // alert!!! trampling the Global object!!!
    x();
    
    alert(“NaN is NaN: “ + isNaN(NaN));
    
    到这里,我们已经介绍了如何创建对象,包括它的属性和方法。但如果注意上面的所有代码段,您会发现属性和方法是在对象定义本身中进行硬编码的。但如果需要更好地控制对象的创建,该怎么做呢?例如,您可能需要根据某些参数来计算对象的属性值。或者,可能需要将对象的属性初始化为仅在运行时才能获得的值。也可能需要创建对象的多个实例(此要求非常常见)。
    在 C# 中,我们使用类来实例化对象实例。但 JavaScript 与此不同,因为它没有类。您将在下一节中看到,您可以充分利用这一情况:函数在与“new”运算符一起使用时,函数将充当构造函数。

     

    构造函数而不是类
    前面提到过,有关 JavaScript OOP 的最奇怪的事情是,JavaScript 不像 C# 或 C++ 那样,它没有类。在 C# 中,在执行类似下面的操作时:
    Dog spot = new Dog();
    
    将返回一个对象,该对象是 Dog 类的实例。但在 JavaScript 中,本来就没有类。与访问类最近似的方法是定义构造函数,如下所示:
    function DogConstructor(name) {
        this.name = name;
        this.respondTo = function(name) {
            if(this.name == name) {
                alert(“Woof”);        
            }
        };
    }
    
    var spot = new DogConstructor(“Spot”);
    spot.respondTo(“Rover”); // nope
    spot.respondTo(“Spot”); // yeah!
    
    那么,结果会怎样呢?暂时忽略 DogConstructor 函数定义,看一看这一行:
    var spot = new DogConstructor(“Spot”);
    
    “new”运算符执行的操作很简单。首先,它创建一个新的空对象。然后执行紧随其后的函数调用,将新的空对象设置为该函数中“this”的值。换句话说,可以认为上面这行包含“new”运算符的代码与下面两行代码的功能相当:
    // create an empty object
    var spot = {}; 
    // call the function as a method of the empty object
    DogConstructor.call(spot, “Spot”);
    
    正如在 DogConstructor 主体中看到的那样,调用此函数将初始化对象,在调用期间关键字“this”将引用此对象。这样,就可以为对象创建模板!只要需要创建类似的对象,就可以与构造函数一起调用“new”,返回的结果将是一个完全初始化的对象。这与类非常相似,不是吗?实际上,在 JavaScript 中构造函数的名称通常就是所模拟的类的名称,因此在上面的示例中,可以直接命名构造函数 Dog:
    // Think of this as class Dog
    function Dog(name) {
        // instance variable 
        this.name = name;
        // instance method? Hmmm...
        this.respondTo = function(name) {
            if(this.name == name) {
                alert(“Woof”);        
            }
        };
    }
    
    var spot = new Dog(“Spot”);
    
    在上面的 Dog 定义中,我定义了名为 name 的实例变量。使用 Dog 作为其构造函数所创建的每个对象都有它自己的实例变量名称副本(前面提到过,它就是对象词典的条目)。这就是希望的结果。毕竟,每个对象都需要它自己的实例变量副本来表示其状态。但如果看看下一行,就会发现每个 Dog 实例也都有它自己的 respondTo 方法副本,这是个浪费;您只需要一个可供各个 Dog 实例共享的 respondTo 实例!通过在 Dog 以外定义 respondTo,可以避免此问题,如下所示:
    function respondTo() {
        // respondTo definition
    }
    
    function Dog(name) {
        this.name = name;
        // attached this function as a method of the object
        this.respondTo = respondTo;
    }
    
    这样,所有 Dog 实例(即用构造函数 Dog 创建的所有实例)都可以共享 respondTo 方法的一个实例。但随着方法数的增加,维护工作将越来越难。最后,基本代码中将有很多全局函数,而且随着“类”的增加,事情只会变得更加糟糕(如果它们的方法具有相似的名称,则尤甚)。但使用原型对象可以更好地解决这个问题,这是下一节的主题。

     

    原型
    在使用 JavaScript 的面向对象编程中,原型对象是个核心概念。在 JavaScript 中对象是作为现有示例(即原型)对象的副本而创建的,该名称就来自于这一概念。此原型对象的任何属性和方法都将显示为从原型的构造函数创建的对象的属性和方法。可以说,这些对象从其原型继承了属性和方法。当您创建如下所示的新 Dog 对象时:
    var buddy = new Dog(“Buddy“);
    
    buddy 所引用的对象将从它的原型继承属性和方法,尽管仅从这一行可能无法明确判断原型来自哪里。对象 buddy 的原型来自构造函数(在这里是函数 Dog)的属性。
    在 JavaScript 中,每个函数都有名为“prototype”的属性,用于引用原型对象。此原型对象又有名为“constructor”的属性,它反过来引用函数本身。这是一种循环引用,图 3 更好地说明了这种循环关系。
    图 3 每个函数的原型都有一个 Constructor 属性 
    现在,通过“new”运算符用函数(上面示例中为 Dog)创建对象时,所获得的对象将继承 Dog.prototype 的属性。在图 3 中,可以看到 Dog.prototype 对象有一个回指 Dog 函数的构造函数属性。这样,每个 Dog 对象(从 Dog.prototype 继承而来)都有一个回指 Dog 函数的构造函数属性。图 4 中的代码证实了这一点。图 5 显示了构造函数、原型对象以及用它们创建的对象之间的这一关系。

    图 5 实例继承其原型 
    某些读者可能已经注意到图 4 中对 hasOwnProperty 和 isPrototypeOf 方法的调用。这些方法是从哪里来的呢?它们不是来自 Dog.prototype。实际上,在 Dog.prototype 和 Dog 实例中还可以调用其他方法,比如 toString、toLocaleString 和 valueOf,但它们都不来自 Dog.prototype。您会发现,就像 .NET Framework 中的 System.Object 充当所有类的最终基类一样,JavaScript 中的 Object.prototype 是所有原型的最终基础原型。(Object.prototype 的原型是 null。)
    在此示例中,请记住 Dog.prototype 是对象。它是通过调用 Object 构造函数创建的(尽管它不可见):
    Dog.prototype = new Object();
    
    因此,正如 Dog 实例继承 Dog.prototype 一样,Dog.prototype 继承 Object.prototype。这使得所有 Dog 实例也继承了 Object.prototype 的方法和属性。
    每个 JavaScript 对象都继承一个原型链,而所有原型都终止于 Object.prototype。注意,迄今为止您看到的这种继承是活动对象之间的继承。它不同于继承的常见概念,后者是指在声明类时类之间的发生的继承。因此,JavaScript 继承动态性更强。它使用简单算法实现这一点,如下所示:当您尝试访问对象的属性/方法时,JavaScript 将检查该属性/方法是否是在该对象中定义的。如果不是,则检查对象的原型。如果还不是,则检查该对象的原型的原型,如此继续,一直检查到 Object.prototype。图 6 说明了此解析过程。
    图 6 在原型链中解析 toString() 方法 (单击该图像获得较大视图)
    JavaScript 动态地解析属性访问和方法调用的方式产生了一些特殊效果:
    • 继承原型对象的对象上可以立即呈现对原型所做的更改,即使是在创建这些对象之后。
    • 如果在对象中定义了属性/方法 X,则该对象的原型中将隐藏同名的属性/方法。例如,通过在 Dog.prototype 中定义 toString 方法,可以改写 Object.prototype 的 toString 方法。
    • 更改只沿一个方向传递,即从原型到它的派生对象,但不能沿相反方向传递。
    图 7 说明了这些效果。图 7 还显示了如何解决前面遇到的不需要的方法实例的问题。通过将方法放在原型内部,可以使对象共享方法,而不必使每个对象都有单独的函数对象实例。在此示例中,rover 和 spot 共享 getBreed 方法,直至在 spot 中以任何方式改写 toString 方法。此后,spot 有了它自己版本的 getBreed 方法,但 rover 对象和用新 GreatDane 创建的后续对象仍将共享在 GreatDane.prototype 对象中定义的那个 getBreed 方法实例。

    function GreatDane() { }
    
    var rover = new GreatDane();
    var spot = new GreatDane();
    
    GreatDane.prototype.getBreed = function() {
        return “Great Dane”;
    };
    
    // Works, even though at this point
    // rover and spot are already created.
    alert(rover.getBreed());
    
    // this hides getBreed() in GreatDane.prototype
    spot.getBreed = function() {
        return “Little Great Dane”;
    };
    alert(spot.getBreed()); 
    
    // but of course, the change to getBreed 
    // doesn’t propagate back to GreatDane.prototype
    // and other objects inheriting from it,
    // it only happens in the spot object
    alert(rover.getBreed());
    
    静态属性和方法
    有时,您需要绑定到类而不是实例的属性或方法,也就是,静态属性和方法。在 JavaScript 中很容易做到这一点,因为函数是可以按需要设置其属性和方法的对象。由于在 JavaScript 中构造函数表示类,因此可以通过在构造函数中设置静态方法和属性,直接将它们添加到类中,如下所示:
        function DateTime() { }
    
        // set static method now()
        DateTime.now = function() {
            return new Date();
        };
    
        alert(DateTime.now());
    
    在 JavaScript 中调用静态方法的语法与在 C# 中几乎完全相同。这不应当让人感到吃惊,因为构造函数的名称实际上是类的名称。这样,就有了类、公用属性/方法,以及静态属性/方法。还需要其他什么吗?当然,私有成员。但 JavaScript 本身并不支持私有成员(同样,也不支持受保护成员)。任何人都可以访问对象的所有属性和方法。但我们有办法让类中包含私有成员,但在此之前,您首先需要理解闭包。

     

    闭包
    我没有自觉地学习过 JavaScript。我必须快点了解它,因为我发现如果没有它,在实际工作中编写 AJAX 应用程序的准备就会不充分。开始,我感到我的编程水平好像降了几个级别。(JavaScript!我的 C++ 朋友会怎么说?)但一旦我克服最初的障碍,我就发现 JavaScript 实际上是功能强大、表现力强而且非常简练的语言。它甚至具有其他更流行的语言才刚刚开始支持的功能。
    JavaScript 的更高级功能之一是它支持闭包,这是 C# 2.0 通过它的匿名方法支持的功能。闭包是当内部函数(或 C# 中的内部匿名方法)绑定到它的外部函数的本地变量时所发生的运行时现象。很明显,除非此内部函数以某种方式可被外部函数访问,否则它没有多少意义。示例可以更好说明这一点。
    假设需要根据一个简单条件筛选一个数字序列,这个条件是:只有大于 100 的数字才能通过筛选,并忽略其余数字。为此,可以编写类似图 8 中的函数。

    function filter(pred, arr) {
        var len = arr.length;
        var filtered = []; // shorter version of new Array();
        // iterate through every element in the array...
        for(var i = 0; i < len; i++) {
            var val = arr[i];
            // if the element satisfies the predicate let it through
            if(pred(val)) {
                filtered.push(val);
            }
        }
        return filtered;
    }
    
    var someRandomNumbers = [12, 32, 1, 3, 2, 2, 234, 236, 632,7, 8];
    var numbersGreaterThan100 = filter(
        function(x) { return (x > 100) ? true : false; }, 
        someRandomNumbers);
    
    // displays 234, 236, 632
    alert(numbersGreaterThan100);
    
    但是,现在要创建不同的筛选条件,假设这次只有大于 300 的数字才能通过筛选,则可以编写下面这样的函数:
    var greaterThan300 = filter(
        function(x) { return (x > 300) ? true : false; }, 
        someRandomNumbers);
    
    然后,也许需要筛选大于 50、25、10、600 如此等等的数字,但作为一个聪明人,您会发现它们全部都有相同的谓词“greater than”,只有数字不同。因此,可以用类似下面的函数分开各个数字:
    function makeGreaterThanPredicate(lowerBound) {
        return function(numberToCheck) {
            return (numberToCheck > lowerBound) ? true : false;
        };
    }
    
    这样,您就可以编写以下代码:
    var greaterThan10 = makeGreaterThanPredicate(10);
    var greaterThan100 = makeGreaterThanPredicate(100);
    alert(filter(greaterThan10, someRandomNumbers));
    alert(filter(greaterThan100, someRandomNumbers));
    
    通过观察函数 makeGreaterThanPredicate 返回的内部匿名函数,可以发现,该匿名内部函数使用 lowerBound,后者是传递给 makeGreaterThanPredicate 的参数。按照作用域的一般规则,当 makeGreaterThanPredicate 退出时,lowerBound 超出了作用域!但在这里,内部匿名函数仍然携带 lowerBound,甚至在 makeGreaterThanPredicate 退出之后的很长时间内仍然如此。这就是我们所说的闭包:因为内部函数关闭了定义它的环境(即外部函数的参数和本地变量)。
    开始可能感觉不到闭包的功能很强大。但如果应用恰当,它们就可以非常有创造性地帮您将想法转换成代码,这个过程非常有趣。在 JavaScript 中,闭包最有趣的用途之一是模拟类的私有变量。

     

    模拟私有属性
    现在介绍闭包如何帮助模拟私有成员。正常情况下,无法从函数以外访问函数内的本地变量。函数退出之后,由于各种实际原因,该本地变量将永远消失。但是,如果该本地变量被内部函数的闭包捕获,它就会生存下来。这一事实是模拟 JavaScript 私有属性的关键。假设有一个 Person 类:
    function Person(name, age) {
        this.getName = function() { return name; };
        this.setName = function(newName) { name = newName; };
        this.getAge = function() { return age; };
        this.setAge = function(newAge) { age = newAge; };
    }
    
    参数 name 和 age 是构造函数 Person 的本地变量。Person 返回时,name 和 age 应当永远消失。但是,它们被作为 Person 实例的方法而分配的四个内部函数捕获,实际上这会使 name 和 age 继续存在,但只能严格地通过这四个方法访问它们。因此,您可以:
    var ray = new Person(“Ray”, 31);
    alert(ray.getName());
    alert(ray.getAge());
    ray.setName(“Younger Ray”);
    // Instant rejuvenation!
    ray.setAge(22);
    alert(ray.getName() + “ is now “ + ray.getAge() + 
          “ years old.”);
    
    未在构造函数中初始化的私有成员可以成为构造函数的本地变量,如下所示:
    function Person(name, age) {
        var occupation;
        this.getOccupation = function() { return occupation; };
        this.setOccupation = function(newOcc) { occupation = 
                             newOcc; };
      
        // accessors for name and age    
    }
    
    注意,这些私有成员与我们期望从 C# 中产生的私有成员略有不同。在 C# 中,类的公用方法可以访问它的私有成员。但在 JavaScript 中,只能通过在其闭包内拥有这些私有成员的方法来访问私有成员(由于这些方法不同于普通的公用方法,它们通常被称为特权方法)。因此,在 Person 的公用方法中,仍然必须通过私有成员的特权访问器方法才能访问私有成员:
    Person.prototype.somePublicMethod = function() {
        // doesn’t work!
        // alert(this.name);
        // this one below works
        alert(this.getName());
    };
    
    Douglas Crockford 是著名的发现(或者也许是发布)使用闭包来模拟私有成员这一技术的第一人。他的网站 javascript.crockford.com 包含有关 JavaScript 的丰富信息,任何对 JavaScript 感兴趣的开发人员都应当仔细研读。

     

    从类继承
    到这里,我们已经了解了构造函数和原型对象如何使您在 JavaScript 中模拟类。您已经看到,原型链可以确保所有对象都有 Object.prototype 的公用方法,以及如何使用闭包来模拟类的私有成员。但这里还缺少点什么。您尚未看到如何从类派生,这在 C# 中是每天必做的工作。遗憾的是,在 JavaScript 中从类继承并非像在 C# 中键入冒号即可继承那样简单,它需要进行更多操作。另一方面,JavaScript 非常灵活,可以有很多从类继承的方式。
    例如,有一个基类 Pet,它有一个派生类 Dog,如图 9 所示。这个在 JavaScript 中如何实现呢?Pet 类很容易。您已经看见如何实现它了:
    图 9 类 
    // class Pet
    function Pet(name) {
        this.getName = function() { return name; };
        this.setName = function(newName) { name = newName; };
    }
    
    Pet.prototype.toString = function() {
        return “This pet’s name is: “ + this.getName();
    };
    // end of class Pet
    
    var parrotty = new Pet(“Parrotty the Parrot”);
    alert(parrotty);
    
    现在,如何创建从 Pet 派生的类 Dog 呢?在图 9 中可以看到,Dog 有另一个属性 breed,它改写了 Pet 的 toString 方法(注意,JavaScript 的约定是方法和属性名称使用 camel 大小写,而不是在 C# 中建议的 Pascal 大小写)。图 10 显示如何这样做。

    // class Dog : Pet 
    // public Dog(string name, string breed)
    function Dog(name, breed) {
        // think Dog : base(name) 
        Pet.call(this, name);
        this.getBreed = function() { return breed; };
        // Breed doesn’t change, obviously! It’s read only.
        // this.setBreed = function(newBreed) { name = newName; };
    }
    
    // this makes Dog.prototype inherits
    // from Pet.prototype
    Dog.prototype = new Pet();
    
    // remember that Pet.prototype.constructor
    // points to Pet. We want our Dog instances’
    // constructor to point to Dog.
    Dog.prototype.constructor = Dog;
    
    // Now we override Pet.prototype.toString
    Dog.prototype.toString = function() {
        return “This dog’s name is: “ + this.getName() + 
            “, and its breed is: “ + this.getBreed();
    };
    // end of class Dog
    
    var dog = new Dog(“Buddy”, “Great Dane”);
    // test the new toString()
    alert(dog);
    
    // Testing instanceof (similar to the is operator)
    // (dog is Dog)? yes
    alert(dog instanceof Dog);
    // (dog is Pet)? yes
    alert(dog instanceof Pet);
    // (dog is Object)? yes
    alert(dog instanceof Object);
    
    所使用的原型 — 替换技巧正确设置了原型链,因此假如使用 C#,测试的实例将按预期运行。而且,特权方法仍然会按预期运行。

     

    模拟命名空间
    在 C++ 和 C# 中,命名空间用于尽可能地减少名称冲突。例如,在 .NET Framework 中,命名空间有助于将 Microsoft.Build.Task.Message 类与 System.Messaging.Message 区分开来。JavaScript 没有任何特定语言功能来支持命名空间,但很容易使用对象来模拟命名空间。如果要创建一个 JavaScript 库,则可以将它们包装在命名空间内,而不需要定义全局函数和类,如下所示:
    var MSDNMagNS = {};
    
    MSDNMagNS.Pet = function(name) { // code here };
    MSDNMagNS.Pet.prototype.toString = function() { // code };
    
    var pet = new MSDNMagNS.Pet(“Yammer”);
    
    命名空间的一个级别可能不是唯一的,因此可以创建嵌套的命名空间:
    var MSDNMagNS = {};
    // nested namespace “Examples”
    MSDNMagNS.Examples = {}; 
    
    MSDNMagNS.Examples.Pet = function(name) { // code };
    MSDNMagNS.Examples.Pet.prototype.toString = function() { // code };
    
    var pet = new MSDNMagNS.Examples.Pet(“Yammer”);
    
    可以想象,键入这些冗长的嵌套命名空间会让人很累。 幸运的是,库用户可以很容易地为命名空间指定更短的别名:
    // MSDNMagNS.Examples and Pet definition...
    
    // think “using Eg = MSDNMagNS.Examples;” 
    var Eg = MSDNMagNS.Examples;
    var pet = new Eg.Pet(“Yammer”);
    alert(pet);
    
    如果看一下 Microsoft AJAX 库的源代码,就会发现库的作者使用了类似的技术来实现命名空间(请参阅静态方法 Type.registerNamespace 的实现)。有关详细信息,请参与侧栏“OOP 和 ASP.NET AJAX”。

     

    应当这样编写 JavaScript 代码吗?
    您已经看见 JavaScript 可以很好地支持面向对象的编程。尽管它是一种基于原型的语言,但它的灵活性和强大功能可以满足在其他流行语言中常见的基于类的编程风格。但问题是:是否应当这样编写 JavaScript 代码?在 JavaScript 中的编程方式是否应与 C# 或 C++ 中的编码方式相同?是否有更聪明的方式来模拟 JavaScript 中没有的功能?每种编程语言都各不相同,一种语言的最佳做法,对另一种语言而言则可能并非最佳。
    在 JavaScript 中,您已看到对象继承对象(与类继承类不同)。因此,使用静态继承层次结构建立很多类的方式可能并不适合 JavaScript。也许,就像 Douglas Crockford 在他的文章 Prototypal Inheritance in JavaScript 中说的那样,JavaScript 编程方式是建立原型对象,并使用下面的简单对象函数建立新的对象,而后者则继承原始对象:
        function object(o) {
            function F() {}
            F.prototype = o;
            return new F();
        }
    
    然后,由于 JavaScript 中的对象是可延展的,因此可以方便地在创建对象之后,根据需要用新字段和新方法增大对象。
    这的确很好,但它不可否认的是,全世界大多数开发人员更熟悉基于类的编程。实际上,基于类的编程也会在这里出现。按照即将颁发的 ECMA-262 规范第 4 版(ECMA-262 是 JavaScript 的官方规范),JavaScript 2.0 将拥有真正的类。因此,JavaScript 正在发展成为基于类的语言。但是,数年之后 JavaScript 2.0 才可能会被广泛使用。同时,必须清楚当前的 JavaScript 完全可以用基于原型的风格和基于类的风格读取和写入 JavaScript 代码。

     

    展望
    随着交互式胖客户端 AJAX 应用程序的广泛使用,JavaScript 迅速成为 .NET 开发人员最重要的工具之一。但是,它的原型性质可能一开始会让更习惯诸如 C++、C# 或 Visual Basic 等语言的开发人员感到吃惊。我已发现我的 JavaScript 学习经历给予了我丰富的体验,虽然其中也有一些挫折。如果本文能使您的体验更加顺利,我会非常高兴,因为这正是我的目标。
    OOP 和 ASP.NET AJAX
    在 ASP.NET AJAX 中实现的 OOP 与在本文中讨论的规范实现稍有不同。这主要有两个原因:ASP.NET AJAX 版本提供了更多反射可能性(它是诸如 xml 脚本等的声明性语法和参数验证所必需的),而且 ASP.NET AJAX 的目标是将使用 .NET 的开发人员所熟悉的某些其他构造(例如属性、事件、枚举和接口)转换成 JavaScript。
    在 JavaScript 当前广泛使用的版本中,它缺少 .NET 开发人员所熟悉的几个 OOP 的关键概念,而 ASP.NET AJAX 可以模拟其中的大多数。
    根据命名约定(要遵守的示例),类可以有属性访问器,以及多播事件(符合紧密反映由 .NET 提供的约定的模式)。私有变量遵守成员以下划线开头则为私有的约定。很少有机会用到真正的私有变量,此策略是为了使调试程序能够检测到这些变量。引入接口也是为了使类型检查能够避免常见的鸭子定型法(一种类型方案,它基于的概念是:如果有什么物体走路和叫声像鸭子,那么它就是鸭子,或至少可以将它视为鸭子)。

    类和反射
    在 JavaScript 中,没有办法知道函数的名称。即使这是可能的,但在大多数情况下也没有什么用,因为类构造函数通常是通过向命名空间变量分配匿名函数来构造的。实际构成类型名称的是此变量的完全限定名称,它同样不可访问,并且构造函数本身对它一无所知。为了规避此限制,并使 JavaScript 类有丰富的反射,ASP.NET AJAX 需要将类型名称进行注册。
    ASP.NET AJAX 中的反射 API 将检查所有类型(无论是内置类型、类、接口、命名空间、或者甚至是枚举),而它们包括的类似 .NET Framework 的函数(例如 isInstanceOfType 和 inheritsFrom)可以在运行时检查类的层次结构。ASP.NET AJAX 还会在调试模式下执行某些类型检查,这对开发人员更早捕获 Bug 很有帮助。

    注册类层次结构和调用基础函数
    若要在 ASP.NET AJAX 中定义类,您需要将其构造函数赋给变量(注意,构造函数如何调用基础函数):
    MyNamespace.MyClass = function() {
        MyNamespace.MyClass.initializeBase(this);
        this._myProperty = null;
    }
    Then, you need to define the class members itself in its prototype:
    
    MyNamespace.MyClass.prototype = {
        get_myProperty: function() { return this._myProperty;},
        set_myProperty: function(value) { this._myProperty = value; },
        doSomething: function() {
            MyNamespace.MyClass.callBaseMethod(this, “doSomething”);
            /* do something more */
        }
    }
    
    最终注册类:
    MyNamespace.MyClass.registerClass(
        “MyNamespace.MyClass “, MyNamespace.BaseClass);
    
    此处不需要管理构造函数和原型层次结构,因为这由 registerClass 函数自动完成的。

    展开全文
  • lua模拟面向对象编程

    2018-05-11 19:42:22
    所以类和对象都只能用表格来模拟声明一下,其实表格功能已经能应付大部分场景了,而且lua脚本语言本就不是面向对象语言(它优势是轻量级简单快速),硬要模拟面向对象有时候会搞得不伦不类,所以以下只为学习,不...

    原文地址http://www.daileinote.com/computer/lua/14

    lua中跟对象和类最像的就是表格有方法和属性,所以类和对象都只能用表格来模拟
    声明一下,其实表格的功能已经能应付大部分场景了,而且lua脚本语言本就不是面向对象语言(它的优势是轻量级简单快速),硬要模拟面向对象有时候会搞得不伦不类,所以以下只为学习,不建议应用到正式环境。

    例子

    --声明一个基类Web
    local Web = {name='freecls', url='http://www.freecls.com', dt='2018', server='aliyun'}
    function Web:say_name()
        print(self.name)
    end
    
    function Web:say_url()
        print(self.url)
    end
    
    function Web:new(name,url)
        local o = {}
        setmetatable(o,self)
        
        --将o的元表的__index元方法设置成Web
        --那么o中没有的属性就会去Web表格中寻找
        self.__index = self
        
        --如果传递了name,url就设置到表格o里
        --如果没有就沿用Web里面的内容
        if name ~= nil then o.name = name end
        if url ~= nil then o.url = url end
        
        return o
    end
    
    
    local w1 = Web:new('沧浪水')
    
    --沧浪水	http://www.freecls.com	2018	aliyun
    print(w1.name, w1.url, w1.dt, w1.server)
    
    w1:say_name()       --沧浪水
    
    
    
    local w2 = Web:new('戴磊', 'http://www.freecls.com/u/info/2712')
    
    --戴磊	http://www.freecls.com/u/info/2712	2018	aliyun
    print(w2.name, w2.url, w2.dt, w2.server)
    
    w2:say_name()       --戴磊

     

    模拟继承

    --声明一个基类Web
    --注意这个Web表格为了能被继承改动了很多
    local Web = {name='freecls', url='http://www.freecls.com', dt='2018', server='aliyun'}
    function Web:say_name()
        print(self.name)
    end
    
    function Web:say_url()
        print(self.url)
    end
    
    function Web:new(name,url)
        local o = {}
        setmetatable(o,self)
        
        --将o的元表的__index元方法设置成Web
        --那么o中没有的属性就会去Web表格中寻找
        self.__index = self
        
        if name ~= nil then o.name = name else o.name = self.name end
        if url ~= nil then o.url = url else o.url = self.url end
        o.dt = self.dt
        o.server = self.server
        
        o.say_name = self.say_name
        o.say_url = self.say_url
        
        return o
    end
    
    
    local Web1 = {age=22,sex='男'}
    
    function Web1:how_old()
        print(self.age)
    end
    
    --模拟继承
    function Web1:new(age,sex)
        local o = Web:new()
        setmetatable(o, self)
        self.__index = self
        
        return o
    end
    
    local w1 = Web1:new()
    w1.name = '沧浪水'
    
    --22	男	沧浪水	http://www.freecls.com	2018	aliyun
    print(w1.age, w1.sex, w1.name, w1.url, w1.dt, w1.server)
    
    w1:say_url()        --http://www.freecls.com
    w1:say_name()       --沧浪水
    w1:how_old()        --22

     

    模拟重写

    local Web = {name='freecls', url='http://www.freecls.com', dt='2018', server='aliyun'}
    function Web:say_name()
        print(self.name)
    end
    
    function Web:say_url()
        print(self.url)
    end
    
    function Web:new(name,url)
        local o = {}
        setmetatable(o,self)
        
        --将o的元表的__index元方法设置成Web
        --那么o中没有的属性就会去Web表格中寻找
        self.__index = self
        
        if name ~= nil then o.name = name else o.name = self.name end
        if url ~= nil then o.url = url else o.url = self.url end
        o.dt = self.dt
        o.server = self.server
        
        o.say_name = self.say_name
        o.say_url = self.say_url
        
        return o
    end
    
    
    local Web1 = {age=22,sex='男'}
    
    function Web1:how_old()
        print(self.age)
    end
    
    --这个方法会失效,重写失败
    function Web1:say_name()
        print(self.name..' 重写')
    end
    
    --模拟继承
    function Web1:new(age,sex)
        local o = Web:new()
        setmetatable(o, self)
        self.__index = self
        
        --重写成功
        function o:say_name()
            print(self.name..' 重写')
        end
        
        return o
    end
    
    local w1 = Web1:new()
    w1.name = '沧浪水'
    
    --22	男	沧浪水	http://www.freecls.com	2018	aliyun
    print(w1.age, w1.sex, w1.name, w1.url, w1.dt, w1.server)
    
    w1:say_url()        --http://www.freecls.com
    w1:say_name()       --沧浪水 重写
    w1:how_old()        --22

     

    总结

    1.本文只是对lua模拟面向对象做简单的介绍,如果有疑问可以给我留言
    2.lua的版本为5.1,运行环境centos7 64位
    3.原文地址http://www.daileinote.com/computer/lua/14

    展开全文
  • 大家都知道,Go不是面向对象(Object Oriented,后面简称为OO)语言。本文以Java语言为例,介绍传统OO编程拥有特性,以及在Go语言中如何模拟这些特性。文中出现示例代码都取自Cosmos-SDK或Tendermint源代码。...

            大家都知道,Go不是面向对象(Object Oriented,后面简称为OO)语言。本文以Java语言为例,介绍传统OO编程拥有的特性,以及在Go语言中如何模拟这些特性。文中出现的示例代码都取自Cosmos-SDK或Tendermint源代码。以下是本文将要介绍的OO编程的主要概念:

    • 类(Class)

      • 字段(Field)

        • 实例字段
        • 类字段
      • 方法(Method)

        • 实例方法
        • 类方法
        • 构造函数(Constructor)
      • 信息隐藏
      • 继承

        • 利斯科夫替换原则(Liskov Substitution Principle,LSP)
        • 方法重写(Overriding)
        • 方法重载(Overloading)
        • 多态
    • 接口(Interface)

      • 扩展
      • 实现

    传统OO语言很重要的一个概念就是,类相当于一个模版,可以用来创建实例(或者对象)。在Java里,使用class关键子来自定义一个类:

    class StdTx {
      // 字段省略
    }

    Go并不是传统意义上的OO语言,甚至根本没有"类"的概念,所以也没有class关键字,直接用struct定义结构体即可:

    type StdTx struct {
      // 字段省略
    }

    字段

    类的状态可以分为两种:每个实例各自的状态(简称实例状态),以及类本身的状态(简称类状态)。类或实例的状态由字段构成,实例状态由实例字段构成,类状态则由类字段构成。

    实例字段

    在Java的类里定义实例字段,或者在Go的结构体里定义字段,写法差不多,当然语法略有不同。仍以Cosmos-SDK提供的标准交易为例,先给出Java的写法:

    class StdTx {
      Msg[]          msgs;
      StdFee         fee;
      StdSignature[] StdSignatures
      String         memo;
    }

    再给出Go的写法:

    type StdTx struct {
        Msgs       []sdk.Msg      `json:"msg"`
        Fee        StdFee         `json:"fee"`
        Signatures []StdSignature `json:"signatures"`
        Memo       string         `json:"memo"`
    }

    类字段

    在Java里,可以用static关键字定义类字段(因此也叫做静态字段):

    class StdTx {
      static long maxGasWanted = (1 << 63) - 1;
      
      Msg[]          msgs;
      StdFee         fee;
      StdSignature[] StdSignatures
      String         memo;
    }

    Go语言没有对应的概念,只能用全局变量来模拟:

    var maxGasWanted = uint64((1 << 63) - 1)

    方法

    为了写出更容易维护的代码,外界通常需要通过方法来读写实例或类状态,读写实例状态的方法叫做实例方法,读写类状态的方法则叫做类方法。大部分OO语言还有一种特殊的方法,叫做构造函数,专门用于创建类的实例。

    实例方法

    在Java中,有明确的返回值,且没有用static关键字修饰的方法即是实例方法。在实例方法中,可以隐式或显式(通过this关键字)访问当前实例。下面以Java中最简单的Getter/Setter方法为例演示实例方法的定义:

    class StdTx {
      
      private String memo;
      // 其他字段省略
      
      public voie setMemo(String memo) {this.memo = memo; } // 使用this关键字
      public String getMemo() { return memo; }              // 不用this关键字
      
    }

    实例方法当然只能在类的实例(也即对象)上调用:

    StdTx stdTx = new StdTx();     // 创建类实例
    stdTx.setMemo("hello");        // 调用实例方法
    String memo = stdTx.getMemo(); // 调用实例方法

    Go语言则通过显式指定receiver来给结构体定义方法(Go只有这么一种方法,所以也就不用区分是什么方法了):

    // 在func关键字后面的圆括号里指定receiver
    func (tx StdTx) GetMemo() string { return tx.Memo }

    方法调用看起来则和Java一样:

    stdTx := StdTx{ ... }   // 创建结构体实例
    memo := stdTx.GetMemo() // 调用方法

    类方法

    在Java里,可以用static关键字定义类方法(因此也叫做静态方法):

    class StdTx {
      private static long maxGasWanted = (1 << 63) - 1;
      
      public static long getMaxGasWanted() {
        return maxGasWanted;
      }
    }

    类方法直接在类上调用:StdTx.getMaxGasWanted()。Go语言没有对应的概念,只能用普通函数(不指定receiver)来模拟(下面这个函数在Cosmos-SDK中并不存在,仅仅是为了演示而已):

    func MaxGasWanted() long {
      return maxGasWanted
    }

    构造函数

    在Java里,和类同名且不指定返回值的实例方法即是构造函数

    class StdTx {
      StdTx(String memo) {
        this.memo = memo;
      }
    }

    使用关键字new调用构造函数就可以创建类实例(参加前面出现的例子)。Go语言没有提供专门的构造函数概念,但是很容易使用普通的函数来模拟:

    func NewStdTx(msgs []sdk.Msg, fee StdFee, sigs []StdSignature, memo string) StdTx {
        return StdTx{
            Msgs:       msgs,
            Fee:        fee,
            Signatures: sigs,
            Memo:       memo,
        }
    }

    信息隐藏

    如果不想让代码变得不可维护,那么一定要把类或者实例状态隐藏起来,不必要对外暴露的方法也要隐藏起来。Java语言提供了4种可见性:

    Java类/字段/方法可见性 类内可见 包内可见 子类可见 完全公开
    用public关键字修饰
    用protected关键字修饰
    不用任何可见性修饰符修饰
    用private关键字修饰

    相比之下,Go语言只有两种可见性:完全公开,或者包内可见。如果全局变量、函数、方法、结构体、结构体字段等等以大写字母开头,则完全公开,否则仅在同一个包内可见。

    继承

    在Java里,类通过extends关键字继承其他类。继承其他类的类叫做子类(Subclass),被继承的类叫做超类(Superclass),子类会继承超类的所有非私有字段和方法。以Cosmos-SDK提供的账户体系为例:

    class BaseAccount { /* 字段和方法省略 */ }
    class BaseVestingAccount extends BaseAccount { /* 字段和方法省略 */ }
    class ContinuousVestingAccount extends BaseVestingAccount { /* 字段和方法省略 */ }
    class DelayedVestingAccount extends BaseVestingAccount { /* 字段和方法省略 */ }

    Go没有"继承"这个概念,只能通过"组合"来模拟。在Go里,如果结构体的某个字段(暂时假设这个字段也是结构体类型,并且可以是指针类型)没有名字,那么外围结构体就可以从内嵌结构体那里"继承"方法。下面是Account类继承体系在Go里面的表现:

    type BaseAccount struct { /* 字段省略 */ }
    
    type BaseVestingAccount struct {
        *BaseAccount
        // 其他字段省略
    }
    
    type ContinuousVestingAccount struct {
        *BaseVestingAccount
        // 其他字段省略
    }
    
    type DelayedVestingAccount struct {
        *BaseVestingAccount
    }

    比如BaseAccount结构体定义了GetCoins()方法:

    func (acc *BaseAccount) GetCoins() sdk.Coins {
        return acc.Coins
    }

    那么BaseVestingAccountDelayedVestingAccount等结构体都"继承"了这个方法:

    dvacc := auth.DelayedVestingAccount{ ... }
    coins := dvacc.GetCoins() // 调用BaseAccount#GetCoins()

    利斯科夫替换原则

    OO编程的一个重要原则是利斯科夫替换原则(Liskov Substitution Principle,后面简称LSP)。简单来说,任何超类能够出现的地方(例如局部变量、方法参数等),都应该可以替换成子类。以Java为例:

    BaseAccount bacc = new BaseAccount();
    bacc = new DelayedVestingAccount(); // LSP

    很遗憾,Go的结构体嵌套不满足LSP:

    bacc := auth.BaseAccount{}
    bacc = auth.DelayedVestingAccount{} // compile error: cannot use auth.DelayedVestingAccount literal (type auth.DelayedVestingAccount) as type auth.BaseAccount in assignment

    在Go里,只有使用接口时才满足SLP。接口在后面会介绍。

    方法重写

    在Java里,子类可以重写(Override)超类的方法。这个特性非常重要,因为这样就可以把很多一般的方法放到超类里,子类按需重写少量方法即可,尽可能避免重复代码。仍以账户体系为例,账户的SpendableCoins()方法计算某一时间点账户的所有可花费余额。那么BaseAccount提供默认实现,子类重写即可:

    class BaseAccount {
      // 其他字段和方法省略
      Coins SpendableCoins(Time time) {
        return GetCoins(); // 默认实现
      }
    }
    
    class ContinuousVestingAccount {
      // 其他字段和方法省略
      Coins SpendableCoins(Time time) {
        // 提供自己的实现
      }
    }
    
    class DelayedVestingAccount {
      // 其他字段和方法省略
      Coins SpendableCoins(Time time) {
        // 提供自己的实现
      }
    }

    在Go语言里可以通过在结构体上重新定义方法达到类似的效果:

    func (acc *BaseAccount) SpendableCoins(_ time.Time) sdk.Coins {
        return acc.GetCoins()
    }
    
    func (cva ContinuousVestingAccount) SpendableCoins(blockTime time.Time) sdk.Coins {
        return cva.spendableCoins(cva.GetVestingCoins(blockTime))
    }
    
    func (dva DelayedVestingAccount) SpendableCoins(blockTime time.Time) sdk.Coins {
        return dva.spendableCoins(dva.GetVestingCoins(blockTime))
    }

    在结构体实例上直接调用重写的方法即可:

    dvacc := auth.DelayedVestingAccount{ ... }
    coins := dvacc.SpendableCoins(someTime) // DelayedVestingAccount#SpendableCoins()

    方法重载

    为了讨论的完整性,这里简单介绍一下方法重载。在Java里,同一个类(或者超类和子类)可以允许有同名方法,只要这些方法的签名(由参数个数、顺序、类型共同确定)各不相同即可。以Cosmos-SDK提供的Dec类型为例:

    public class Dec {
      // 字段省略
      public Dec mul(int i) { /* 代码省略 */ }
      public Dec mul(long i) { /* 代码省略 */ }
      // 其他方法省略
    }

    无论是方法还是普通函数,在Go语言里都无法进行重载(不支持),因此只能起不同的名字:

    type Dec struct { /* 字段省略 */ }
    func (d Dec) MulInt(i Int) Dec { /* 代码省略 */ }
    func (d Dec) MulInt64(i int64) Dec { /* 代码省略 */ }
    // 其他方法省略

    多态

    方法的重写要配合多态)(具体来说,这里只关心动态分派)才能发挥全部威力。以Tendermint提供的Service为例,Service可以启动、停止、重启等等。下面是Service接口的定义(Go语言):

    type Service interface {
        Start()   error
        OnStart() error
        Stop()    error
        OnStop()  error
        Reset()   error
        OnReset() error
        // 其他方法省略
    }

    翻译成Java代码是下面这样:

    interface Servive {
      void start()   throws Exception;
      void onStart() throws Exception;
      void stop()    throws Exception;
      void onStop()  throws Exception;
      void reset()   throws Exception;
      void onRest()  throws Exception;
      // 其他方法省略
    }

    不管是何种服务,启动、停止、重启都涉及到判断状态,因此Start()Stop()Reset()方法非常适合在超类里实现。具体的启动、停止、重启逻辑则因服务而异,因此可以由子类在OnStart()OnStop()OnReset()方法中提供。以Start()OnStart()方法为例,下面先给出用Java实现的BaseService基类(只是为了说明多态,因此忽略了线程安全、异常处理等细节):

    public class BaseService implements Service {
      private boolean started;
      private boolean stopped;
      
      public void onStart() throws Exception {
        // 默认实现;如果不想提供默认实现,这个方法可以是abstract
      }
      
      public void start() throws Exception {
        if (started) { throw new AlreadyStartedException(); }
        if (stopped) { throw new AlreadyStoppedException(); }
        onStart(); // 这里会进行dynamic dispatch
        started = true;
      }
      
      // 其他字段和方法省略
    }

    很遗憾,在Go语言里,结构体嵌套+方法重写并不支持多态。因此在Go语言里,不得不把代码写的更tricky一些。下面是Tendermint里BaseService结构体的定义:

    type BaseService struct {
        Logger  log.Logger
        name    string
        started uint32 // atomic
        stopped uint32 // atomic
        quit    chan struct{}
    
        // The "subclass" of BaseService
        impl Service
    }

    再来看OnStart()Start()方法:

    func (bs *BaseService) OnStart() error { return nil }
    
    func (bs *BaseService) Start() error {
        if atomic.CompareAndSwapUint32(&bs.started, 0, 1) {
            if atomic.LoadUint32(&bs.stopped) == 1 {
                bs.Logger.Error(fmt.Sprintf("Not starting %v -- already stopped", bs.name), "impl", bs.impl)
                // revert flag
                atomic.StoreUint32(&bs.started, 0)
                return ErrAlreadyStopped
            }
            bs.Logger.Info(fmt.Sprintf("Starting %v", bs.name), "impl", bs.impl)
            err := bs.impl.OnStart() // 重点看这里
            if err != nil {
                // revert flag
                atomic.StoreUint32(&bs.started, 0)
                return err
            }
            return nil
        }
        bs.Logger.Debug(fmt.Sprintf("Not starting %v -- already started", bs.name), "impl", bs.impl)
        return ErrAlreadyStarted
    }

    可以看出,为了模拟多态效果,BaseService结构体里多出一个难看的impl字段,并且在Start()方法里要通过这个字段去调用OnStart()方法。毕竟Go不是真正意义上的OO语言,这也是不得已而为之。

    例子:Node

    为了进一步加深理解,我们来看一下Tendermint提供的Node结构体是如何继承BaseService的。Node结构体表示Tendermint全节点,下面是它的定义:

    type Node struct {
        cmn.BaseService
        // 其他字段省略
    }

    可以看到,Node嵌入("继承")了BaseServiceNewNode()函数创建Node实例,函数中会初始化BaseService

    func NewNode(/* 参数省略 */) (*Node, error) {
        // 省略无关代码
        node := &Node{ ... }
        node.BaseService = *cmn.NewBaseService(logger, "Node", node)
        return node, nil
    }

    可以看到,在调用NewBaseService()函数创建BaseService实例时,传入了node指针,这个指针会被赋值给BaseServiceimpl字段:

    func NewBaseService(logger log.Logger, name string, impl Service) *BaseService {
        return &BaseService{
            Logger: logger,
            name:   name,
            quit:   make(chan struct{}),
            impl:   impl,
        }
    }

    经过这么一番折腾之后,Node只需重写OnStart()方法即可,这个方法会在"继承"下来的Start()方法中被正确调用。下面的UML"类图"展示了BaseServiceNode之间的关系:

    +-------------+
    | BaseService |<>---+
    +-------------+     |
           △            |
           |            |
    +-------------+     |
    |    Node     |<----+
    +-------------+

    接口

    Java和Go都支持接口,并且用起来也非常类似。前面介绍过的Cosmos-SDK里的Account以及Temdermint里的Service,其实都有相应的接口。Service接口的代码前面已经给出过,下面给出Account接口的完整代码以供参考:

    type Account interface {
        GetAddress() sdk.AccAddress
        SetAddress(sdk.AccAddress) error // errors if already set.
    
        GetPubKey() crypto.PubKey // can return nil.
        SetPubKey(crypto.PubKey) error
    
        GetAccountNumber() uint64
        SetAccountNumber(uint64) error
    
        GetSequence() uint64
        SetSequence(uint64) error
    
        GetCoins() sdk.Coins
        SetCoins(sdk.Coins) error
    
        // Calculates the amount of coins that can be sent to other accounts given
        // the current time.
        SpendableCoins(blockTime time.Time) sdk.Coins
    
        // Ensure that account implements stringer
        String() string
    }

    在Go语言里,使用接口+各种不同实现可以达到LSP的效果,具体用法也比较简单,这里略去代码演示。

    扩展

    在Java里,接口可以使用extends关键字扩展其他接口,仍以Account系统为例:

    interface VestingAccount extends Account {
        Coins getVestedCoins(Time blockTime);
        Coint getVestingCoins(Time blockTime);
        // 其他方法省略
    }

    在Go里,在接口里直接嵌入其他接口即可:

    type VestingAccount interface {
        Account
    
        // Delegation and undelegation accounting that returns the resulting base
        // coins amount.
        TrackDelegation(blockTime time.Time, amount sdk.Coins)
        TrackUndelegation(amount sdk.Coins)
    
        GetVestedCoins(blockTime time.Time) sdk.Coins
        GetVestingCoins(blockTime time.Time) sdk.Coins
    
        GetStartTime() int64
        GetEndTime() int64
    
        GetOriginalVesting() sdk.Coins
        GetDelegatedFree() sdk.Coins
        GetDelegatedVesting() sdk.Coins
    }

    实现

    对于接口的实现,Java和Go表现出了不同的态度。在Java中,如果一个类想实现某接口,那么必须用implements关键字显式声明,并且必须一个不落的实现接口里的所有方法(除非这个类被声明为抽象类,那么检查推迟进行),否则编译器就会报错:

    class BaseAccount implements Account {
      // 必须实现所有方法
    }

    Go语言则不然,只要一个结构体定义了某个接口的全部方法,那么这个结构体就隐式实现了这个接口:

    type BaseAccount struct { /* 字段省略 */ } // 不需要,也没办法声明要实现那个接口
    func (acc BaseAccount) GetAddress() sdk.AccAddress { /* 代码省略 */ }
    // 其他方法省略

    Go的这种做法很像某些动态语言里的鸭子类型。可是有时候想像Java那样,让编译器来保证某个结构体实现了特定的接口,及早发现问题,这种情况怎么办?其实做法也很简单,Cosmos-SDK/Tendermint里也不乏这样的例子,大家一看便知:

    var _ Account = (*BaseAccount)(nil)
    var _ VestingAccount = (*ContinuousVestingAccount)(nil)
    var _ VestingAccount = (*DelayedVestingAccount)(nil)

    通过定义一个不使用的、具有某种接口类型的全局变量,然后把nil强制转换为结构体(指针)并赋值给这个变量,这样就可以触发编译器类型检查,起到及早发现问题的效果。

    总结

    本文以Java为例,讨论了OO编程中最主要的一些概念,并结合Tendermint/Comsos-SDK源代码介绍了如何在Golang中模拟这些概念。下表对本文中讨论的OO概念进行了总结:

    OO概念 Java 在Golang中对应/模拟
    class struct
    实例字段 instance field filed
    类字段 static field global var
    实例方法 instance method method
    类方法 static method func
    构造函数 constructor func
    信息隐藏 modifier 由名字首字母大小写决定
    子类继承 extends embedding
    LSP 完全满足 只对接口有效
    方法重写 overriding 可以重写method,但不支持多态
    方法重载 overloading 不支持
    多态(方法动态分派) 完全支持 不支持,但可以通过一些tricky方式来模拟
    接口 interface interface
    接口扩展 extends embedding
    接口实现 显式实现(编译器检查) 隐式实现(鸭子类型)

    本文由CoinEx Chain团队Chase写作,转载无需授权。

    展开全文
  • 对于编程语言的初学者来讲,OOP不是一个很容易理解的编程方式,大家虽然都按老师讲的都知道OOP的三大特性是继承、封装、多态,并且大家也都知道了如何定义类、方法等面向对象的常用语法,但是一到真正写程序的时候,...
  • 如何理解Java面向对象Java是一种面向对象的程序开发语言,准确的理解java的面向对象才能更好更快地掌握JAVA的编程思想和方法,以下仅供参考!1、什么是对象?首先要说明一点,对象不是java的,事实上他只是一种概念...
  • 面向对象

    2021-04-01 15:25:13
    对象的概述:Java 是面向对象的编程语言,对象就是面向对象程序设计的核心,对象是抽象的。 ​ 对象是类的一个实例(对象不是找个女朋友),有状态和行为。 创建对象: 对象是根据类创建的。在Java中,使用关键字 new ...
  • Java面向对象的理解

    2017-06-12 22:01:40
    Java是一门面向对象的语言面向对象的特征有四个:1-抽象;2-继承;3-封装;4-多态 这些后面会作详解,下面来了解一下面向对象和面向过程的区别。 面向对象和面向过程有很大的区别,面向对象是把构成问题...
  • 用JavaScript进行面向对象的编程 JavaScript编码技巧 JavaScript的未来 本文使用了以下技术: JavaScript 目录 JavaScript对象是词典 JavaScript函数是最棒的 构造函数而不是类 原型 静态属性和方法 ...
  • JavaScript 是基于原型的语言用 JavaScript 进行面向对象的编程JavaScript 编码技巧JavaScript 的未来本文使用了以下技术: JavaScript 目录JavaScript 对象是词典JavaScript 函数是最棒的构造函数而不是类原型静态...
  • 面向接口编程

    2019-06-25 17:42:27
    不是说好的面向对象编程嘛。。哈哈哈。 原谅我是一个菜渣。这篇文章简单描述了什么是面向接口编程。 谈到接口时候,通常会涉及以下几种含义。经常说一个库或者模块对外提供了某某API接口。通过主动暴露接口...
  • 众所周知,python是门面向对象编程语言面向对象是一个程序设计和编程思想,即一切皆为对象。 面向对象 面向对象三要素:封装,继承, 多态 封装? 把一致行为或者公共部分封装成一个类或者方法。 比如把一定...
  • 用 JavaScript 进行面向对象的编程 JavaScript 编码技巧 JavaScript 的未来 本文使用了以下技术:JavaScript 目录 JavaScript 对象是词典JavaScript 函数是最棒的构造函数而不是类原型静态...
  • 用 JavaScript 进行面向对象的编程 JavaScript 编码技巧 JavaScript 的未来 本文使用了以下技术: JavaScript  目录 JavaScript 对象是词典 JavaScript 函数是最棒的 构造函数而不是类 原型 静态属性和...
  • JavaScript 是基于原型的语言用 JavaScript 进行面向对象的编程JavaScript 编码技巧JavaScript 的未来 本文使用了以下技术: JavaScript 目录 JavaScript 对象是词典 JavaScript 函数是最棒的 构造函数而...
  • 先简单说一下我自己,距离初学编程这么久了,一直很好奇何为面向对象,虽然知道目前主流的Java,C++是面向对象的语言,但是编程过程中自我感觉一直是 C++和 Java的代码与C语言相比,一样的功能代码反而更复杂,和有...
  • 如果您只是想尝试学习一种新的编程语言的乐趣,请继续阅读! 我对Leola的主要目标是努力学习更多有关编译器和语言理论的知识。 此外,我还喜欢将其用于个人用途-因此,我对使它具有高性能,稳定和功能丰富有浓厚的...
  • 面向对象三大特征

    2016-01-26 18:22:00
    之前接触过C语言的基础,后来又学了一点VB,只是这都不是面向对象的语言,当然在编程中也就无法体会到面向对象的思想。 近期接触了软件project、UML之后才渐渐了解了面向对象的概念,继而又简单学习了一下C++、C#...
  • 纯粹是功能性的,而不是命令性的或面向对象的。 渴望评估,而不是懒惰的评估。 动态键入和解释。最终可能会探索静态类型和编译。 以数据编码和以数据编码。内置的数据结构(列表和字典)还用于将代码本身构造为多个...

空空如也

空空如也

1 2 3 4 5 ... 15
收藏数 291
精华内容 116
关键字:

以下不是面向对象的编程语言的是