Meningkatkan Warisan JavaScript Sederhana

John Resig (dari jQuery yang terkenal) menyediakan implementasi singkat dari Warisan JavaScript Sederhana. Pendekatannya mengilhami upaya saya untuk memperbaiki keadaan lebih jauh lagi. Saya telah menulis ulang fungsi Class.extend asli Resig untuk menyertakan keuntungan berikut:

  • Kinerja – lebih sedikit overhead selama definisi kelas, konstruksi objek, dan pemanggilan metode kelas dasar

  • Fleksibilitas – dioptimalkan untuk browser baru yang kompatibel dengan ECMAScript 5 (misalnya Chrome), namun memberikan "shim" yang setara untuk browser lama (misalnya IE6)

  • Kompatibilitas – memvalidasi dalam mode ketat dan memberikan kompatibilitas alat yang lebih baik (misalnya komentar VSDoc/JSDoc, Visual Studio IntelliSense, dll.)

  • Kesederhanaan – Anda tidak perlu menjadi "ninja" untuk memahami kode sumber (dan akan lebih mudah lagi jika Anda kehilangan fitur ECMAScript 5)

  • Kekokohan – lulus lebih banyak pengujian unit "kasus sudut" (misalnya mengganti toString di IE)

Karena sepertinya terlalu bagus untuk menjadi kenyataan, saya ingin memastikan logika saya tidak memiliki kelemahan atau bug mendasar, dan melihat apakah ada yang bisa menyarankan perbaikan atau menyangkal kode tersebut. Dengan itu, saya menyajikan fungsi classify:

function classify(base, properties)
{
    /// <summary>Creates a type (i.e. class) that supports prototype-chaining (i.e. inheritance).</summary>
    /// <param name="base" type="Function" optional="true">The base class to extend.</param>
    /// <param name="properties" type="Object" optional="true">The properties of the class, including its constructor and members.</param>
    /// <returns type="Function">The class.</returns>

    // quick-and-dirty method overloading
    properties = (typeof(base) === "object") ? base : properties || {};
    base = (typeof(base) === "function") ? base : Object;

    var basePrototype = base.prototype;
    var derivedPrototype;

    if (Object.create)
    {
        // allow newer browsers to leverage ECMAScript 5 features
        var propertyNames = Object.getOwnPropertyNames(properties);
        var propertyDescriptors = {};

        for (var i = 0, p; p = propertyNames[i]; i++)
            propertyDescriptors[p] = Object.getOwnPropertyDescriptor(properties, p);

        derivedPrototype = Object.create(basePrototype, propertyDescriptors);
    }
    else
    {
        // provide "shim" for older browsers
        var baseType = function() {};
        baseType.prototype = basePrototype;
        derivedPrototype = new baseType;

        // add enumerable properties
        for (var p in properties)
            if (properties.hasOwnProperty(p))
                derivedPrototype[p] = properties[p];

        // add non-enumerable properties (see https://developer.mozilla.org/en/ECMAScript_DontEnum_attribute)
        if (!{ constructor: true }.propertyIsEnumerable("constructor"))
            for (var i = 0, a = [ "constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "toString", "valueOf" ], p; p = a[i]; i++)
                if (properties.hasOwnProperty(p))
                    derivedPrototype[p] = properties[p];
    }

    // build the class
    var derived = properties.hasOwnProperty("constructor") ? properties.constructor : function() { base.apply(this, arguments); };
    derived.prototype = derivedPrototype;
    derived.prototype.constructor = derived;
    derived.prototype.base = derived.base = basePrototype;

    return derived;
}

Dan penggunaannya hampir identik dengan Resig kecuali untuk nama konstruktor (constructor vs. init) dan sintaksis untuk pemanggilan metode kelas dasar.

/* Example 1: Define a minimal class */
var Minimal = classify();

/* Example 2a: Define a "plain old" class (without using the classify function) */
var Class = function()
{
    this.name = "John";
};

Class.prototype.count = function()
{
    return this.name + ": One. Two. Three.";
};

/* Example 2b: Define a derived class that extends a "plain old" base class */
var SpanishClass = classify(Class,
{
    constructor: function()
    {
        this.name = "Juan";
    },
    count: function()
    {
        return this.name + ": Uno. Dos. Tres.";
    }
});

/* Example 3: Define a Person class that extends Object by default */
var Person = classify(
{
    constructor: function(name, isQuiet)
    {
        this.name = name;
        this.isQuiet = isQuiet;
    },
    canSing: function()
    {
        return !this.isQuiet;
    },
    sing: function()
    {
        return this.canSing() ? "Figaro!" : "Shh!";
    },
    toString: function()
    {
        return "Hello, " + this.name + "!";
    }
});

/* Example 4: Define a Ninja class that extends Person */
var Ninja = classify(Person,
{
    constructor: function(name, skillLevel)
    {
        Ninja.base.constructor.call(this, name, true);
        this.skillLevel = skillLevel;
    },
    canSing: function()
    {
        return Ninja.base.canSing.call(this) || this.skillLevel > 200;
    },
    attack: function()
    {
        return "Chop!";
    }
});

/* Example 4: Define an ExtremeNinja class that extends Ninja that extends Person */
var ExtremeNinja = classify(Ninja,
{
    attack: function()
    {
        return "Chop! Chop!";
    },
    backflip: function()
    {
        this.skillLevel++;
        return "Woosh!";
    }
});

var m = new Minimal();
var c = new Class();
var s = new SpanishClass();
var p = new Person("Mary", false);
var n = new Ninja("John", 100);
var e = new ExtremeNinja("World", 200);

Dan inilah tes QUnit saya yang semuanya lulus:

equals(m instanceof Object && m instanceof Minimal && m.constructor === Minimal, true);
equals(c instanceof Object && c instanceof Class && c.constructor === Class, true);
equals(s instanceof Object && s instanceof Class && s instanceof SpanishClass && s.constructor === SpanishClass, true);
equals(p instanceof Object && p instanceof Person && p.constructor === Person, true);
equals(n instanceof Object && n instanceof Person && n instanceof Ninja && n.constructor === Ninja, true);
equals(e instanceof Object && e instanceof Person && e instanceof Ninja && e instanceof ExtremeNinja && e.constructor === ExtremeNinja, true);

equals(c.count(), "John: One. Two. Three.");
equals(s.count(), "Juan: Uno. Dos. Tres.");

equals(p.isQuiet, false);
equals(p.canSing(), true);
equals(p.sing(), "Figaro!");

equals(n.isQuiet, true);
equals(n.skillLevel, 100);
equals(n.canSing(), false);
equals(n.sing(), "Shh!");
equals(n.attack(), "Chop!");

equals(e.isQuiet, true);
equals(e.skillLevel, 200);
equals(e.canSing(), false);
equals(e.sing(), "Shh!");
equals(e.attack(), "Chop! Chop!");
equals(e.backflip(), "Woosh!");
equals(e.skillLevel, 201);
equals(e.canSing(), true);
equals(e.sing(), "Figaro!");
equals(e.toString(), "Hello, World!");

Apakah ada yang melihat ada yang salah dengan pendekatan saya vs. pendekatan asli John Resig? Saran dan masukan dipersilakan!

CATATAN: Kode di atas telah dimodifikasi secara signifikan sejak saya pertama kali memposting pertanyaan ini. Di atas mewakili versi terbaru. Untuk melihat perkembangannya, silakan periksa riwayat revisi.


person Community    schedule 24.03.2010    source sumber
comment
Saya akan merekomendasikan Object.create dan traitsjs. Warisan tidak bagus dalam javascript, gunakan komposisi objek   -  person Raynos    schedule 28.07.2011
comment
Mungkin saya belum terbiasa, tetapi sintaksis ciri-cirinya membuat kepala saya pusing. Saya pikir saya akan lulus sampai mendapatkan pengikut...   -  person Will    schedule 28.07.2011


Jawaban (3)


Beberapa waktu lalu, saya melihat beberapa sistem objek untuk JS dan bahkan mengimplementasikan beberapa sistem objek saya sendiri, misalnya class.js (versi ES5) dan proto.js.

Alasan mengapa saya tidak pernah menggunakannya: Anda akan menulis jumlah kode yang sama. Contoh kasus: Contoh Ninja Resig (hanya menambahkan beberapa spasi):

var Person = Class.extend({
    init: function(isDancing) {
        this.dancing = isDancing;
    },

    dance: function() {
        return this.dancing;
    }
});

var Ninja = Person.extend({
    init: function() {
        this._super(false);
    },

    swingSword: function() {
        return true;
    }
});

19 baris, 264 byte.

JS standar dengan Object.create() (yang merupakan fungsi ECMAScript 5, tetapi untuk tujuan kita dapat diganti dengan ES3 khusus clone() implementasi):

function Person(isDancing) {
    this.dancing = isDancing;
}

Person.prototype.dance = function() {
    return this.dancing;
};

function Ninja() {
    Person.call(this, false);
}

Ninja.prototype = Object.create(Person.prototype);

Ninja.prototype.swingSword = function() {
    return true;
};

17 baris, 282 byte. Imo, byte tambahan sebenarnya tidak sebanding dengan kompleksitas tambahan dari sistem objek terpisah. Cukup mudah untuk mempersingkat contoh standar dengan menambahkan beberapa fungsi khusus, tetapi sekali lagi: itu tidak sepadan.

person Christoph    schedule 24.03.2010
comment
Dulu saya berpikiran sama seperti Anda, tetapi sekarang saya harus tidak setuju. Pada dasarnya, semua yang saya dan John Resig lakukan adalah membuat metode tunggal (memperluas) yang menghubungkan perilaku objek/prototipe persis seperti yang Anda miliki dalam contoh di atas (yaitu ini bukan sistem objek baru). Satu-satunya perbedaan adalah sintaksnya lebih pendek, lebih ketat, dan lebih sedikit rawan kesalahan saat menggunakan metode perluasan. - person Will; 25.03.2010
comment
Setelah merenungkannya, saya setuju bahwa menggunakan sintaks wire-up standar sama singkatnya dengan menggunakan sintaks extend. Saya masih berpikir yang terakhir ini jauh lebih bersih, lebih sedikit rawan kesalahan, dan lebih merupakan pendekatan konvensi dibandingkan konfigurasi, jika itu masuk akal. - person Will; 25.03.2010
comment
Saya memeras otak selama berjam-jam untuk mencoba mencari tahu pendekatan Resig dan tidak bisa mendapatkannya. Ini sangat mudah. Terima kasih!! - person Big McLargeHuge; 26.07.2012
comment
Tunggu sebentar... bagaimana Anda mendefinisikan konstruktor khusus? misalnya setel Ninja.strength = function () { Math.random(); } saat mendefinisikan Ninja baru? - person Big McLargeHuge; 26.07.2012

Tidak secepat itu. Itu tidak berhasil.

Mempertimbangkan:

var p = new Person(true);
alert("p.dance()? " + p.dance()); => true

var n = new Ninja();
alert("n.dance()? " + n.dance()); => false
n.dancing = true;
alert("n.dance()? " + n.dance()); => false

base hanyalah objek lain yang diinisialisasi dengan anggota default yang membuat Anda berpikir itu berfungsi.

EDIT: sebagai catatan, ini adalah implementasi Java saya sendiri (walaupun lebih bertele-tele) seperti warisan dalam Javascript, dibuat pada tahun 2006 saat saya terinspirasi oleh Dean Edward's Base.js (dan saya setuju dengannya ketika katanya versi John hanyalah penulisan ulang Base.js-nya). Anda dapat melihatnya beraksi (dan langkah debug di Firebug) di sini.

/**
 * A function that does nothing: to be used when resetting callback handlers.
 * @final
 */
EMPTY_FUNCTION = function()
{
  // does nothing.
}

var Class =
{
  /**
   * Defines a new class from the specified instance prototype and class
   * prototype.
   *
   * @param {Object} instancePrototype the object literal used to define the
   * member variables and member functions of the instances of the class
   * being defined.
   * @param {Object} classPrototype the object literal used to define the
   * static member variables and member functions of the class being
   * defined.
   *
   * @return {Function} the newly defined class.
   */
  define: function(instancePrototype, classPrototype)
  {
    /* This is the constructor function for the class being defined */
    var base = function()
    {
      if (!this.__prototype_chaining 
          && base.prototype.initialize instanceof Function)
        base.prototype.initialize.apply(this, arguments);
    }

    base.prototype = instancePrototype || {};

    if (!base.prototype.initialize)
      base.prototype.initialize = EMPTY_FUNCTION;

    for (var property in classPrototype)
    {
      if (property == 'initialize')
        continue;

      base[property] = classPrototype[property];
    }

    if (classPrototype && (classPrototype.initialize instanceof Function))
      classPrototype.initialize.apply(base);

    function augment(method, derivedPrototype, basePrototype)
    {
      if (  (method == 'initialize')
          &&(basePrototype[method].length == 0))
      {
        return function()
        {
          basePrototype[method].apply(this);
          derivedPrototype[method].apply(this, arguments);
        }
      }

      return function()
      {
        this.base = function()
                    {
                      return basePrototype[method].apply(this, arguments);
                    };

        return derivedPrototype[method].apply(this, arguments);
        delete this.base;
      }
    }

    /**
     * Provides the definition of a new class that extends the specified
     * <code>parent</code> class.
     *
     * @param {Function} parent the class to be extended.
     * @param {Object} instancePrototype the object literal used to define
     * the member variables and member functions of the instances of the
     * class being defined.
     * @param {Object} classPrototype the object literal used to define the
     * static member variables and member functions of the class being
     * defined.
     *
     * @return {Function} the newly defined class.
     */
    function extend(parent, instancePrototype, classPrototype)
    {
      var derived = function()
      {
        if (!this.__prototype_chaining
            && derived.prototype.initialize instanceof Function)
          derived.prototype.initialize.apply(this, arguments);
      }

      parent.prototype.__prototype_chaining = true;

      derived.prototype = new parent();

      delete parent.prototype.__prototype_chaining;

      for (var property in instancePrototype)
      {
        if (  (instancePrototype[property] instanceof Function)
            &&(parent.prototype[property] instanceof Function))
        {
            derived.prototype[property] = augment(property, instancePrototype, parent.prototype);
        }
        else
          derived.prototype[property] = instancePrototype[property];
      }

      derived.extend =  function(instancePrototype, classPrototype)
                        {
                          return extend(derived, instancePrototype, classPrototype);
                        }

      for (var property in classPrototype)
      {
        if (property == 'initialize')
          continue;

        derived[property] = classPrototype[property];
      }

      if (classPrototype && (classPrototype.initialize instanceof Function))
        classPrototype.initialize.apply(derived);

      return derived;
    }

    base.extend = function(instancePrototype, classPrototype)
                  {
                    return extend(base, instancePrototype, classPrototype);
                  }
    return base;
  }
}

Dan inilah cara Anda menggunakannya:

var Base = Class.define(
{
  initialize: function(value) // Java constructor equivalent
  {
    this.property = value;
  }, 

  property: undefined, // member variable

  getProperty: function() // member variable accessor
  {
    return this.property;
  }, 

  foo: function()
  {
    alert('inside Base.foo');
    // do something
  }, 

  bar: function()
  {
    alert('inside Base.bar');
    // do something else
  }
}, 
{
  initialize: function() // Java static initializer equivalent
  {
    this.property = 'Base';
  },

  property: undefined, // static member variables can have the same
                                 // name as non static member variables

  getProperty: function() // static member functions can have the same
  {                                 // name as non static member functions
    return this.property;
  }
});

var Derived = Base.extend(
{
  initialize: function()
  {
    this.base('derived'); // chain with parent class's constructor
  }, 

  property: undefined, 

  getProperty: function()
  {
    return this.property;
  }, 

  foo: function() // override foo
  {
    alert('inside Derived.foo');
    this.base(); // call parent class implementation of foo
    // do some more treatments
  }
}, 
{
  initialize: function()
  {
    this.property = 'Derived';
  }, 

  property: undefined, 

  getProperty: function()
  {
    return this.property;
  }
});

var b = new Base('base');
alert('b instanceof Base returned: ' + (b instanceof Base));
alert('b.getProperty() returned: ' + b.getProperty());
alert('Base.getProperty() returned: ' + Base.getProperty());

b.foo();
b.bar();

var d = new Derived('derived');
alert('d instanceof Base returned: ' + (d instanceof Base));
alert('d instanceof Derived returned: ' + (d instanceof Derived));
alert('d.getProperty() returned: ' + d.getProperty());  
alert('Derived.getProperty() returned: ' + Derived.getProperty());

d.foo();
d.bar();
person Gregory Pakosz    schedule 24.03.2010
comment
Sial, terima kasih telah menunjukkan kelemahan besar itu. Saya akan mencoba lagi, tapi mungkin saya akan langsung membahas fungsi asli John Resig. - person Will; 25.03.2010
comment
Tentu saja fungsi John baik-baik saja (sekali lagi dia seharusnya memuji Dean Edwards). Bagaimanapun, silakan, coba lagi seperti yang saya lakukan saat itu: ini adalah bagian yang menyenangkan dan memahami cara kerja bahasa ini akan membuat Anda (merasa) menjadi programmer yang lebih baik. Menariknya, saya tidak pernah benar-benar menggunakan implementasi saya, itu hanya untuk kepentingan itu :) Saya juga tidak mengerti gunanya mencoba mengecilkan jumlah logika maksimum menjadi jumlah kode minimal: tentu versi saya bertele-tele, tetapi setiap kali saya kembali membacanya, saya mengerti apa yang terjadi. - person Gregory Pakosz; 25.03.2010
comment
Saya yakin semuanya berhasil sekarang. Saya telah membuat sedikit perubahan pada panggilan metode dasar untuk menggunakan sintaks base.method.call(this) yang memperbaiki masalah yang Anda laporkan. Apakah Anda melihat ada masalah lain dalam penerapannya? Saya tidak yakin ini adalah latihan yang sia-sia. Saya yakin salah satu alasan sebagian besar pengembang menghindari pewarisan JavaScript adalah karena ilmu hitam yang terkait dengan pemahaman implementasi, atau sintaksis pewarisan buruk yang dipaksakan. Saya yakin ini membantu mengatasi kedua masalah tersebut (tentu saja asalkan benar). - person Will; 25.03.2010
comment
Memahami bagian dalam tidak ada gunanya. Namun saya selalu percaya tidak ada gunanya persaingan ukuran antara jQuery, Mootools, dll; tapi sekali lagi saya tidak pernah benar-benar menghadapi penurunan pemuatan halaman di proyek kesayangan saya yang disebabkan oleh skrip yang membengkak. Maka saya tidak cukup ahli (walaupun saya yakin saya telah melakukan pekerjaan rumah dengan baik dalam mengimplementasikan Java seperti warisan) dalam Javascript untuk memutuskan ini adalah cara yang harus dilakukan: para ahli seperti Douglas Crockford menyatakan bahwa seseorang harus berusaha untuk sepenuhnya menerima prototipe, dan membebaskan diri mereka sendiri dari batasan model klasik - person Gregory Pakosz; 25.03.2010

Ini sesederhana yang bisa Anda dapatkan. Itu diambil dari http://www.sitepoint.com/javascript-inheritance/.

// copyPrototype is used to do a form of inheritance.  See http://www.sitepoint.com/blogs/2006/01/17/javascript-inheritance/#
// Example:
//    function Bug() { this.legs = 6; }
//    Insect.prototype.getInfo = function() { return "a general insect"; }
//    Insect.prototype.report = function() { return "I have " + this.legs + " legs"; }
//    function Millipede() { this.legs = "a lot of"; }
//    copyPrototype(Millipede, Bug);  /* Copy the prototype functions from Bug into Millipede */
//    Millipede.prototype.getInfo = function() { return "please don't confuse me with a centipede"; } /* ''Override" getInfo() */
function copyPrototype(descendant, parent) {
  var sConstructor = parent.toString();
  var aMatch = sConstructor.match(/\s*function (.*)\(/);
  if (aMatch != null) { descendant.prototype[aMatch[1]] = parent; }
  for (var m in parent.prototype) {

    descendant.prototype[m] = parent.prototype[m];
  }
};
person John Fisher    schedule 24.03.2010
comment
Sederhana saja, tetapi tidak berguna (tidak ada akses dasar/super), atau IMO yang cantik. - person Will; 25.03.2010
comment
@Will: Anda dapat mengakses metode induk. Periksa tautan untuk penjelasan lebih lanjut. - person John Fisher; 25.03.2010
comment
@JohnFisher Saya rasa yang Anda maksud adalah Bug.prototype daripada Insert.prototype di komentar kode. - person Larry Battle; 06.08.2012
comment
@LarryBattle: Saya tidak bermaksud apa-apa, karena saya tidak membuat kodenya. Itu dari situs web. - person John Fisher; 06.08.2012