Статьи

JavaScript наследование на примере

Этот пост в блоге иллюстрирует несколько тем наследования JavaScript на примере: мы начнем с наивных реализаций конструктора Point и его подконструктора ColorPoint, а затем улучшим их, шаг за шагом. Этот пост должен быть понятен, но там, где это не так, вы можете ознакомиться с более подробным введением в наследование JavaScript в [1] .

Конструкторы

Конструкторы — это фабрики для объектов (аналогично классам в языках классов). Каждая функция foo может быть вызвана двумя способами:

  • как функция: foo (arg1, arg2)
  • как конструктор: new foo (arg, arg2)

Следующая функция является конструктором для точек:

    function Point(x, y) {
        this.x = x;
        this.y = y;
        this.dist = function () {
            return Math.sqrt((this.x*this.x)+(this.y*this.y));
        };
        this.toString = function () {
            return "("+this.x+", "+this.y+")";
        };
    }

Когда мы выполняем new Point (), задача конструктора состоит в том, чтобы установить свежий объект, переданный ему через неявный параметр this. Point () добавляет четыре свойства к этому объекту. Свойства dist и toString называются
методами , потому что их значения являются функциями. Объект возвращается конструктором и считается его экземпляром:

   > var p = new Point(3, 1);
    > p instanceof Point
    true

Вы можете вызвать методы и получить доступ к свойствам не-метода:

  > p.dist()
    3.1622776601683795
    > p.toString()
    '(3, 1)'
    > p.x
    3
    > p.y
    1

Методы не должны быть в каждом экземпляре, они должны быть разделены между экземплярами, чтобы сохранить память. Для этой цели вы можете использовать
прототип : объект, сохраненный в Point.prototype, становится прототипом экземпляров Point. Свойства объекта являются общими для всех объектов, прототип которых он составляет, поэтому Point.prototype — это место, где вы размещаете методы:

   function Point(x, y) {
        this.x = x;
        this.y = y;
    }
    Point.prototype = {
        dist: function () {
            return Math.sqrt((this.x*this.x)+(this.y*this.y));
        },
        toString: function () {
            return "("+this.x+", "+this.y+")";
        }
    }

Мы присваиваем объект Point.prototype через
литерал объекта, который имеет два свойства dist и toString. Теперь существует четкое разделение ответственности: конструктор отвечает за настройку данных конкретного экземпляра, прототип содержит общие данные (т. Е. Методы). Обратите внимание, что прототипы высоко оптимизированы в механизмах JavaScript, поэтому обычно нет никакого снижения производительности для размещения методов там. Методы вызываются так же, как и раньше, вы не замечаете, хранятся ли они в экземпляре или в прототипе. Остается одна проблема: для каждой функции f должно выполняться следующее уравнение
[2] :

    f.prototype.constructor === f

Каждая функция настроена так по умолчанию. Но мы заменили значение по умолчанию Point.prototype. Чтобы удовлетворить уравнению, мы можем либо добавить конструктор свойства к литералу объекта выше, либо сохранить значение по умолчанию, не заменяя его, добавив в него методы:

   function Point(x, y) {
        this.x = x;
        this.y = y;
    }
    Point.prototype.dist = function () {
        return Math.sqrt((this.x*this.x)+(this.y*this.y));
    };
    Point.prototype.toString = function () {
        return "("+this.x+", "+this.y+")";
    };

Свойство конструктора не так важно; в основном это позволяет вам определить, какой конструктор создал данный экземпляр:

 > var p = new Point(2, 2)
    > p.constructor
    [Function: Point]
    > p.constructor.name
    'Point'

простирающийся

В JavaScript термин
расширение объекта означает деструктивное добавление к нему новых свойств: чтобы расширить объект A с помощью объекта B, мы (поверхностно) копируем свойства B в A. Несколько необычное определение этого термина в JavaScript происходит из-за инфраструктуры Prototype, который имеет метод
Object.extend () . Следующее является наивной реализацией:

  function extend(target, source) {
        for (var propName in source) {
            target[propName] = source[propName];
        }
        return target;
    }

Проблема с этим кодом заключается в том, что for-in выполняет итерацию по всем свойствам объекта, в том числе наследуемым от прототипа. Это можно увидеть здесь:

  > extend({}, new Point())
    { x: undefined,
      y: undefined,
      dist: [Function],
      toString: [Function] }

Нам нужны «собственные» (прямые) свойства x и y экземпляра Point. Но нам не нужны его унаследованные свойства dist и toString. Почему унаследованные свойства копируются в первый аргумент? Потому что for-in видит все свойства объекта, в том числе наследуемые. Point наследует несколько свойств от Object, например valueOf:

    > var p = new Point(7, 1);
    > p.valueOf
    [Function: valueOf]

Эти свойства не копируются, потому что for-in может видеть только
перечислимые свойства
[3] и они не перечислимы:

 > p.propertyIsEnumerable("valueOf")
    false
    > p.propertyIsEnumerable("dist")
    true

Чтобы исправить extension (), мы должны убедиться, что учитываются только собственные свойства источника.

    function extend(target, source) {
    for (var propName in source) {
    // Is propName an own property of source?
    if (source.hasOwnProperty(propName)) {
    target[propName] = source[propName];
    }
    }
    return target;
    }

Есть еще одна проблема: приведенный выше код завершается ошибкой, если у источника есть собственное свойство с именем hasOwnProperty
[4] :

    > extend({}, { hasOwnProperty: 123 })
    TypeError: Property 'hasOwnProperty' is not a function

Ошибка вызвана тем, что source.hasOwnProperty (строка 4) получает доступ к собственному свойству (число) вместо унаследованного метода. Мы можем решить эту проблему, ссылаясь на этот метод напрямую, а не через источник:

    function extend(target, source) {
        var hasOwnProperty = Object.prototype.hasOwnProperty;
        for (var propName in source) {
            // Invoke hasOwnProperty() with this = source
            if (hasOwnProperty.call(source, propName)) {
                target[propName] = source[propName];
            }
        }
        return target;
    }

На движках ECMAScript 5 (или на более старых движках, в которых загружен shim
[5] ) лучше использовать следующую версию extend (), поскольку она сохраняет атрибуты свойств,
такие как перечисляемость:

  function extend(target, source) {
        Object.getOwnPropertyNames(source)
        .forEach(function(propName) {
            Object.defineProperty(target, propName,
                Object.getOwnPropertyDescriptor(source, propName));
        });
        return target;
    }

Установка прототипа объекта

До сих пор мы видели, как добавить свойства к объекту разрушительным образом. Мы также видели, что прототипы делают то же самое, но не разрушительно: его свойства «проявляются» в экземпляре, но не относятся к его собственным свойствам. Было бы неплохо, если бы мы могли более непосредственно влиять на этот тип наследования и устанавливать прототип объекта без конструктора. Учитывая, что прототип объекта является такой фундаментальной, сильно оптимизированной функцией, единственный стандартный способ сделать это — создать новый объект. То есть вы можете установить прототип объекта только один раз при его создании. Следующий код использует ECMAScript 5 Object.create () для создания нового объекта, прототипом которого является объект proto.

    var proto = { bla: true };
    
    var obj = Object.create(proto);
    obj.foo = 123;
    obj.bar = "abc";

У obj есть как унаследованные, так и собственные свойства:

  > obj.bla
    true
    > obj.foo
    123

Шим ECMAScript 5 использует код, подобный следующему, чтобы сделать Object.create доступным в старых браузерах.

 if (Object.create === undefined) {
        Object.create = function (proto) {
            function Tmp() {}
            Tmp.prototype = proto;
            // New empty object whose prototype is proto
            return new Tmp();
        };
    }

Приведенный выше код использует временный конструктор для создания одного экземпляра с заданным прототипом. До сих пор мы игнорировали необязательный второй параметр Object.create (), который позволяет вам определять свойства вновь создаваемого объекта:

   var obj = Object.create(proto, {
        foo: { value: 123 },
        bar: { value: "abc" }
    });

Свойства определяются через
дескрипторы свойств . С помощью дескриптора вы можете указать атрибуты свойств, такие как перечисляемость, а не только значения. В качестве упражнения давайте реализуем protoChain (), упрощенную версию Object.create (). Это позволяет избежать сложностей дескрипторов свойств и просто расширяет новый объект вторым параметром. Например:

    var obj = protoChain(proto, {
        foo: 123,
        bar: "abc"
    });

Мы можем обобщить приведенную выше идею на произвольное количество параметров:

    protoChain(obj_0, obj_1, ..., obj_n-1, obj_n)

Помните, что мы должны создавать свежие объекты, чтобы назначать прототипы. Следовательно, protoChain () возвращает поверхностную копию obj_n, прототип которой представляет собой поверхностную копию obj_n-1 и т. Д. Obj_0 — единственный объект в возвращаемой цепочке, который не был продублирован. protoChain () может быть реализован так:

    function protoChain() {
        if (arguments.length === 0) return null;
        var prev = arguments[0];
        for(var i=1; i < arguments.length; i++) {
            // Create duplicate of arguments[i] with prototype prev
            prev = Object.create(prev);
            extend(prev, arguments[i]);
        }
        return prev;
    }

подтипировании

Идея создания подтипов заключается в создании нового конструктора, основанного на существующем. Новый конструктор называется суб-конструктором, а существующий — супер-конструктором. Ниже приведен суб-конструктор Point:

   function ColorPoint(x, y, color) {
        Point.call(this, x, y);
        this.color = color;
    }

Приведенный выше код устанавливает свойства экземпляра x, y и цвет. Это делается путем передачи this (экземпляра ColorPoint) в Point: Point вызывается как функция, но метод call () позволяет нам сохранить this для ColorPoint. Поэтому Point () добавляет x и y для нас, а мы сами добавляем цвет. Нам все еще нужно позаботиться о методах: с одной стороны, мы хотим наследовать методы Point, с другой стороны, мы хотим определить наши собственные методы. Это простой способ сделать это с помощью extend ():

  extend(ColorPoint.prototype, Point.prototype);
    ColorPoint.prototype.toString = function () {
        return this.color+" "+Point.prototype.toString.call(this);
    };

Сначала мы копируем методы из Point.prototype в ColorPoint.prototype, а затем добавляем наш собственный метод: мы заменяем toString () в Point версией, чей результат объединяет цвет с выходными данными Point.prototype.toString (). Мы напрямую ссылаемся на последний метод и вызываем его с помощью ColorPoint. Для получения дополнительной информации о вызовах методов супер-прототипа обратитесь к
[6] . ColorPoint работает как положено:

    > var cp = new ColorPoint(5, 3, "red");
    > cp.toString()
    'red (5, 3)'

В качестве улучшения мы можем избежать добавления избыточных свойств в ColorPoint.prototype, сделав Point.prototype его прототипом.

   ColorPoint.prototype = Object.create(Point.prototype);
    ColorPoint.prototype.constructor = ColorPoint;
    ColorPoint.prototype.toString = function () {
        return this.color+" "+Point.prototype.toString.call(this);
    };

В строке 1 мы заменили значение по умолчанию ColorPoint.prototype и, таким образом, должны установить свойство конструктора в строке 2. Хотя написание одного конструктора довольно просто, приведенный выше код слишком сложен для выполнения вручную. Вспомогательная функция наследует () сделает это проще:

    ColorPoint.prototype.toString = function () {
        return this.color+" "+Point.prototype.toString.call(this);
    };
    inherits(ColorPoint, Point);

Функция наследует () смоделирован Node.js в
util.inherits () . Это дает вам подтип при сохранении простоты обычных конструкторов. Требования:

  • Не должно иметь значения, вызываем ли мы методИС () до или после добавления методов в прототип.
  • Методоризмы () должен гарантировать, что свойство конструктора установлено правильно.

Это реализация:

 function inherits(SubC, SuperC) {
        var subProto = Object.create(SuperC.prototype);
        // At the very least, we keep the "constructor" property
        // At most, we keep additions that have already been made
        extend(subProto, SubC.prototype);
        SubC.prototype = subProto;
    };

Есть еще одна вещь, которую мы можем улучшить: ColorPoint.prototype.toString () делает следующий вызов.

    Point.prototype.toString.call(this)

Это не идеально, потому что мы жестко закодировали супер-конструктор. Вместо этого мы бы предпочли использовать:

    ColorPoint._super.toString.call(this)

Для того, чтобы сделать приведенный выше код возможным, у функцииоля (только) нужно сделать следующее присваивание:

    SubC._super = SuperC.prototype;

Здесь мы расходимся с Node.js, где SubC.super_ относится к SuperC. Конструктор ColorPoint по-прежнему содержит жестко заданную ссылку на Point. Это можно устранить, заменив Point.call (…) на

    ColorPoint._super.constructor.call(this, x, y);

Не совсем красиво, но это делает работу.
Наша окончательная версия ColorPoint выглядит следующим образом:

    function ColorPoint(x, y, color) {
        ColorPoint._super.constructor.call(this, x, y);
        this.color = color;
    }
    ColorPoint.prototype.toString = function () {
        return this.color+" "+ColorPoint._super.toString.call(this);
    };
    inherits(ColorPoint, Point);

Вы можете скачать исходный код как
пример наследования проекта
на GitHub.

Вывод

На нашем работающем примере мы увидели, как реализовать конструкторы, как расширить объекты, как установить прототип объекта и как наследовать конструктор. Мы не рассматривали, как сохранить конфиденциальность данных — это тема для другого поста в блоге.

Рекомендации

  1. Прототипы как классы — введение в наследование JavaScript
  2. Что случилось со свойством «конструктор» в JavaScript?
  3. Свойства JavaScript: наследование и перечислимость
  4. Подводные камни использования объектов в качестве карт в JavaScript
  5. es5-shim: используйте ECMAScript 5 в старых браузерах
  6. Пристальный взгляд на супер-ссылки в JavaScript и ECMAScript.next

 

С http://www.2ality.com/2012/01/js-inheritance-by-example.html