การปรับปรุงการสืบทอด JavaScript อย่างง่าย

John Resig (จากชื่อเสียงของ jQuery) นำเสนอ การสืบทอด JavaScript อย่างง่าย อย่างกระชับโดยย่อ แนวทางของเขาเป็นแรงบันดาลใจให้ฉันพยายามปรับปรุงสิ่งต่างๆ ให้ดียิ่งขึ้นไปอีก ฉันได้เขียนฟังก์ชัน Class.extend ดั้งเดิมของ Resig ใหม่เพื่อให้มีข้อดีดังต่อไปนี้:

  • ประสิทธิภาพ – โอเวอร์เฮดน้อยลงระหว่างการกำหนดคลาส การสร้างออบเจ็กต์ และการเรียกเมธอดคลาสพื้นฐาน

  • ความยืดหยุ่น – ปรับให้เหมาะสมสำหรับเบราว์เซอร์รุ่นใหม่ที่เข้ากันได้กับ ECMAScript 5 (เช่น Chrome) แต่มี "shim" ที่เทียบเท่าสำหรับเบราว์เซอร์รุ่นเก่า (เช่น IE6)

  • ความเข้ากันได้ – ตรวจสอบในโหมดเข้มงวดและให้ความเข้ากันได้ของเครื่องมือที่ดีขึ้น (เช่น ความคิดเห็น VSDoc/JSDoc, Visual Studio IntelliSense ฯลฯ)

  • ความเรียบง่าย – คุณไม่จำเป็นต้องเป็น "นินจา" เพื่อทำความเข้าใจซอร์สโค้ด (และจะง่ายกว่านั้นอีกหากคุณสูญเสียฟีเจอร์ ECMAScript 5)

  • ความทนทาน – ผ่านการทดสอบหน่วย "ตัวพิมพ์เล็ก" มากขึ้น (เช่น การแทนที่ toString ใน IE)

เพราะมันเกือบจะดูดีเกินกว่าที่จะเป็นจริงได้ ฉันต้องการให้แน่ใจว่าตรรกะของฉันไม่มีข้อบกพร่องหรือข้อบกพร่องพื้นฐานใดๆ และดูว่ามีใครสามารถแนะนำการปรับปรุงหรือหักล้างโค้ดได้หรือไม่ ด้วยเหตุนี้ ฉันจึงนำเสนอฟังก์ชัน 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;
}

และการใช้งานเกือบจะเหมือนกับ Resig ยกเว้นชื่อ Constructor (constructor vs. init) และไวยากรณ์สำหรับการเรียกเมธอดคลาสพื้นฐาน

/* 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);

และนี่คือการทดสอบ QUnit ของฉันที่ผ่านทั้งหมด:

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!");

มีใครเห็นสิ่งผิดปกติในแนวทางของฉันเทียบกับแนวทางดั้งเดิมของ John Resig หรือไม่ ยินดีรับข้อเสนอแนะและข้อเสนอแนะ!

หมายเหตุ: โค้ดด้านบนได้รับการแก้ไขอย่างมากตั้งแต่ฉันโพสต์คำถามนี้ครั้งแรก ด้านบนแสดงถึงเวอร์ชันล่าสุด หากต้องการดูว่ามีการพัฒนาอย่างไร โปรดตรวจสอบประวัติการแก้ไข


person Community    schedule 24.03.2010    source แหล่งที่มา
comment
ฉันอยากจะแนะนำ Object.create และ traitsjs การสืบทอดไม่ดีในจาวาสคริปต์ ใช้องค์ประกอบของวัตถุ   -  person Raynos    schedule 28.07.2011
comment
บางทีฉันอาจจะยังไม่ชินกับมัน แต่ไวยากรณ์ของลักษณะทำให้ฉันหัวหมุน ฉันคิดว่าฉันจะผ่านไปจนกว่าจะได้รับดังต่อไปนี้ ...   -  person Will    schedule 28.07.2011


คำตอบ (3)


ก่อนหน้านี้ ฉันดูระบบอ็อบเจ็กต์ต่างๆ สำหรับ JS และแม้แต่ใช้งานระบบของฉันเองบางส่วน เช่น class.js (เวอร์ชัน ES5) และ proto.js

เหตุผลที่ฉันไม่เคยใช้มันเลย: คุณจะต้องเขียนโค้ดเท่าเดิม กรณีตัวอย่าง: ตัวอย่าง Ninja ของ Resig (เพิ่มช่องว่างบางส่วนเท่านั้น):

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 บรรทัด 264 ไบต์

JS มาตรฐานที่มี Object.create() (ซึ่งเป็นฟังก์ชัน ECMAScript 5 แต่สำหรับวัตถุประสงค์ของเรา สามารถแทนที่ได้ด้วย ES3 แบบกำหนดเอง clone() การใช้งาน):

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 บรรทัด 282 ไบต์ เหมือนกัน, ไบต์พิเศษนั้นไม่ได้เพิ่มความซับซ้อนของระบบอ็อบเจ็กต์ที่แยกออกมาจริงๆ เป็นเรื่องง่ายพอที่จะทำให้ตัวอย่างมาตรฐานสั้นลงโดยการเพิ่มฟังก์ชันที่กำหนดเองบางอย่าง แต่ขอย้ำอีกครั้งว่า มันไม่คุ้มค่าเลย

person Christoph    schedule 24.03.2010
comment
ฉันเคยคิดเหมือนคุณ แต่ตอนนี้ฉันต้องไม่เห็นด้วย โดยพื้นฐานแล้ว John Resig และฉันได้ทำทั้งหมดคือการสร้างวิธีการเดียว (extend) ซึ่งเชื่อมโยงพฤติกรรมของวัตถุ/ต้นแบบที่แน่นอนที่คุณมีในตัวอย่างข้างต้น (นั่นคือ ไม่ใช่ระบบวัตถุใหม่) ข้อแตกต่างเพียงอย่างเดียวคือไวยากรณ์จะสั้นกว่า เข้มงวดกว่า และมีแนวโน้มที่จะเกิดข้อผิดพลาดน้อยลงเมื่อใช้วิธีขยาย - person Will; 25.03.2010
comment
หลังจากไตร่ตรองดูแล้ว ฉันยอมรับว่าการใช้ไวยากรณ์แบบ wire-up มาตรฐานนั้นสั้นพอๆ กับการใช้ไวยากรณ์แบบขยาย ฉันยังคงคิดว่าอย่างหลังนั้นสะอาดกว่ามาก มีแนวโน้มที่จะเกิดข้อผิดพลาดน้อยกว่า และเป็นแบบแผนมากกว่าวิธีการกำหนดค่ามากกว่า หากนั่นสมเหตุสมผล - person Will; 25.03.2010
comment
ฉันกำลังทำลายสมองเป็นเวลาหลายชั่วโมงในการพยายามคิดหาแนวทางของ Resig แต่ก็ไม่เข้าใจ นี่มันตรงไปตรงมามาก ขอบคุณ!! - person Big McLargeHuge; 26.07.2012
comment
เดี๋ยวก่อน... คุณจะกำหนด Constructor แบบกำหนดเองได้อย่างไร? เช่น. ตั้ง Ninja.strength = function () { Math.random(); } เมื่อกำหนดนินจาใหม่? - person Big McLargeHuge; 26.07.2012

ไม่เร็วนัก มันก็ไม่ได้ผล

พิจารณา:

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 เป็นเพียงวัตถุอื่นที่เริ่มต้นด้วยสมาชิกเริ่มต้นที่ทำให้คุณคิดว่ามันใช้งานได้

แก้ไข: สำหรับบันทึก นี่คือการใช้งาน Java ของฉันเอง (แม้ว่าจะมีรายละเอียดมากกว่า) เช่นเดียวกับการสืบทอดใน Javascript ซึ่งสร้างขึ้นในปี 2549 ในขณะที่ฉันได้รับแรงบันดาลใจจาก Base.js ของ Dean Edward (และฉันเห็นด้วยกับเขาเมื่อ เขาบอกว่าเวอร์ชันของ John เป็นเพียงการเขียน Base.js ของเขาใหม่) คุณสามารถดูการทำงานจริง (และแก้ไขข้อบกพร่องใน Firebug) ที่นี่

/**
 * 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;
  }
}

และนี่คือวิธีที่คุณใช้:

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
สาปขอบคุณสำหรับการชี้ให้เห็นข้อบกพร่องที่สำคัญนั้น ฉันจะลองอีกครั้ง แต่ฉันอาจจะจบลงที่ฟังก์ชันดั้งเดิมของ John Resig - person Will; 25.03.2010
comment
แน่นอนว่าการทำงานของจอห์นนั้นสมบูรณ์แบบ (แต่เขาควรให้เครดิต Dean Edwards อีกครั้ง) อย่างไรก็ตาม ลองเข้าไปลองอีกครั้งเหมือนที่ฉันเคยทำในตอนนั้น มันเป็นส่วนหนึ่งของความสนุกและการทำความเข้าใจการทำงานภายในของภาษาเหล่านี้จะทำให้คุณ (รู้สึก) เป็นโปรแกรมเมอร์ที่ดีขึ้น ที่น่าสนใจคือฉันไม่เคยใช้การใช้งานจริง ๆ เลย มันเป็นเพียงเพื่อประโยชน์ของมัน :) นอกจากนี้ ฉันไม่เห็นประเด็นของการพยายามลดขนาดตรรกะสูงสุดให้เหลือโค้ดเพียงเล็กน้อย: แน่นอนว่าเวอร์ชันของฉันมีรายละเอียดมาก แต่ทุกครั้งที่ฉันกลับมาอ่านอีกครั้งฉันก็เข้าใจสิ่งที่เกิดขึ้น - person Gregory Pakosz; 25.03.2010
comment
ฉันเชื่อว่าทุกอย่างใช้งานได้แล้ว ฉันได้ทำการเปลี่ยนแปลงเล็กน้อยกับการเรียกเมธอดพื้นฐานเพื่อใช้ไวยากรณ์ base.method.call(this) ซึ่งแก้ไขปัญหาที่คุณรายงาน คุณเห็นปัญหาอื่นๆ ในการใช้งานหรือไม่ ฉันไม่แน่ใจว่านี่เป็นการออกกำลังกายที่ไร้จุดหมาย ฉันเชื่อว่าหนึ่งในเหตุผลที่นักพัฒนาส่วนใหญ่อายที่จะรับมรดก JavaScript ก็เนื่องมาจากมนต์ดำที่เกี่ยวข้องกับการทำความเข้าใจการใช้งาน หรือไวยากรณ์การสืบทอดที่น่าเกลียดที่พวกเขาถูกบังคับให้ทำ ฉันเชื่อว่าสิ่งนี้จะช่วยแก้ไขข้อกังวลทั้งสองข้อได้ (หากถูกต้องแน่นอน) - person Will; 25.03.2010
comment
การเข้าใจเรื่องภายในไม่ไร้จุดหมาย อย่างไรก็ตาม ฉันเชื่อมาโดยตลอดว่าการแข่งขันด้านขนาดระหว่าง jQuery, Mootools ฯลฯ นั้นแทบจะไม่มีประเด็นเลย แต่อีกครั้งที่ฉันไม่เคยเผชิญกับการชะลอตัวของการโหลดหน้าเว็บในโปรเจ็กต์สัตว์เลี้ยงของฉันซึ่งมีสาเหตุมาจากสคริปต์ที่ป่อง จากนั้นฉันก็ไม่เชี่ยวชาญเพียงพอ (แม้ว่าฉันเชื่อว่าฉันได้ทำการบ้านที่ดีในการนำ Java ไปใช้เหมือนกับการสืบทอด) ใน Javascript เพื่อตัดสินใจว่านี่คือหนทางที่จะไป: ผู้เชี่ยวชาญอย่าง Douglas Crockford ระบุว่าเราควรมุ่งมั่นที่จะยอมรับต้นแบบอย่างเต็มที่ และปลดปล่อยตัวเอง จากขอบเขตของโมเดลคลาสสิก - person Gregory Pakosz; 25.03.2010

นี่เป็นเรื่องง่าย ๆ เท่าที่คุณจะได้รับ ข้อความนี้นำมาจาก 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
มันเรียบง่าย แต่ก็ไม่มีประโยชน์ (ไม่มีการเข้าถึงฐาน/ขั้นสูง) หรือ IMO ที่สวยงาม - person Will; 25.03.2010
comment
@Will: คุณสามารถเข้าถึงวิธีการหลักได้ ตรวจสอบลิงค์เพื่อดูคำอธิบายเพิ่มเติม - person John Fisher; 25.03.2010
comment
@JohnFisher ฉันคิดว่าคุณหมายถึง Bug.prototype มากกว่า Insert.prototype ในความคิดเห็นของโค้ด - person Larry Battle; 06.08.2012
comment
@LarryBattle: ฉันไม่ได้หมายถึงอะไรเลย เนื่องจากฉันไม่ได้สร้างโค้ด มันมาจากเว็บไซต์ - person John Fisher; 06.08.2012