Статьи

Образцы: создание объектов в JavaScript

Этот пост исследует образцы , фабрики для объектов. Термин « пример » был предложен Алленом Вирфсом-Броком, чтобы избежать термина « класс» , который не очень подходит для JavaScript: примеры похожи на классы, но не являются классами.

экземпляров

Образец представляет собой конструкцию , которая создает объекты. Каждый из следующих видов примеров создает экземпляры одного фиксированного типа:

  • Литералы регулярных выражений: производят экземпляры RegExp:
        > /abc/ instanceof RegExp
        true
    
  • Инициализаторы массива: создание экземпляров массива
        > [1, 2, 3] instanceof Array
        true
    
  • Инициализаторы объектов: выглядят следующим образом.
        var jane = {
            name: "Jane",
            describe: function () {
                return "Person called "+this.name;
            }
        }
    

    Каждый объект, созданный инициализатором объекта (без нестандартных расширений), является прямым экземпляром Object:

        > jane instanceof Object
        true
    

Если вам нужна большая гибкость, вы можете обратиться к
функциональным образцам (раздел 2) и
объектным образцам (раздел 4), где вы можете указать тип экземпляров для каждого образца.

Образцы функций (конструкторы)

Функция может использоваться в качестве примера, если вызывается через новый оператор. Тогда это называется
конструктором . Ранее мы видели, как создать единственную «персону» Джейн через объектный инициализатор. Следующий конструктор является примером для этого типа объекта.

    function Person(name) {
        this.name = name;
    }
    Person.prototype.describe = function () {
        return "Person called "+this.name;
    };

Вот как вы используете оператор new для создания объекта:

    var jane = new Person("Jane");

Джейн считается в случае человека. Вы можете проверить эти отношения с помощью оператора instanceof:

    > jane instanceof Person
    true

Есть две части для настройки экземпляра Person: Свойства, специфичные для экземпляра, добавляются через конструктор:

    > jane.name
    'Jane'

Свойства, которые являются общими для всех экземпляров (в основном методов), наследуются от Person.prototype:

    > jane.describe()
    'Person called Jane'
    > jane.describe === Person.prototype.describe
    true

Вы можете проверить, что единственный объект Person.prototype действительно является прототипом всех экземпляров Person:

    > Person.prototype.isPrototypeOf(jane)
    true

Отношение prototype-of также используется для проверки того, является ли объект экземпляром конструктора. Выражение

    jane instanceof Person

на самом деле реализуется как

    Person.prototype.isPrototypeOf(jane)

Необязательные параметры

Часто хочется, чтобы один и тот же конструктор настраивал свои экземпляры разными способами. Следующий конструктор для двумерных точек может быть вызван либо с нулевым аргументом, либо с двумя аргументами. В первом случае создается точка (0,0), во втором — координата x и координата y.

    function Point(x, y) {
        if (arguments.length >= 2) {
            this.x = x;
            this.y = y;
        } else {
            this.x = 0;
            this.y = 0;
        }
    }

Эта идея может быть расширена до более чем двух возможных комбинаций аргументов. Например, следующий конструктор для точек с цветом может получить 3 аргумента, 2 аргумента или 0 аргументов.

    function ColorPoint(x, y, color) {
        if (arguments.length >= 3) {
            this.color = color;
        } else {
            this.color = black;
        }
        if (arguments.length >= 2) {
            this.x = x;
            this.y = y;
        } else {
            this.x = 0;
            this.y = 0;
        }
    }

Существует возможность написать этот код более компактно: отсутствующие параметры не определены в JavaScript, что означает, что мы хотим делать назначения, такие как

    this.x = (x !== undefined ? x : 0);
    // this.x = (... ? value : default)

Следующая функция реализует этот вид проверки:

    function valueOrDefault(value, theDefault) {
        return (value !== undefined && value !== null
            ? value : theDefault);
    }

Теперь ColorPoint стал более компактным:

    function ColorPoint(x, y, color) {
        this.x     = valueOrDefault(x, 0);
        this.y     = valueOrDefault(y, 0);
        this.color = valueOrDefault(color, "black");
    }

Вы также можете использовать || оператор:

    function ColorPoint(x, y, color) {
        this.x     = x || 0;
        this.y     = y || 0;
        this.color = color || "black";
    }

Объяснение:

    left || right

вычисляется влево, если логическое значение (слева) равно true, и вправо, в противном случае. Предостережение: с || все следующие значения будут интерпретироваться как отсутствующий аргумент:

  • неопределенный, ноль
  • ложный
  • +0, -0, NaN
  • «»

Поэтому вы не можете назначить пустую строку цвету, потому что

    > "" || "black"
    'black'

Опционные объекты

Позиционные необязательные параметры ограничивают то, какие параметры могут быть пропущены: если параметр, который вы хотите пропустить, не находится в конце, вы должны вставить значение заполнителя, например, undefined или null. Кроме того, если есть много параметров, вы быстро теряете значение каждого из них. Идея
объекта option заключается в предоставлении необязательных параметров в качестве объекта, который создается через литерал объекта. Для ColorPoint это будет выглядеть следующим образом.

    > new ColorPoint({ color: "black" })
    { color: 'black', x: 0, y: 0 }

    > new ColorPoint({ x: 33 })
    { x: 33, y: 0, color: 'black' }

    > new ColorPoint()
    { x: 0, y: 0, color: 'black' }

С помощью вспомогательной функции очень просто перенести необязательные значения в строящийся экземпляр и заполнить значения по умолчанию для отсутствующих свойств:

    function ColorPoint(options) {
        _.defaults(this, options, {
            x: 0,
            y: 0,
            color: "black"
        });
    }

Выше мы использовали функции по
умолчанию из библиотеки Underscore
[1] . Он копирует все свойства опций, которые там еще не существуют. И аналогично заполняет значения по умолчанию через третий аргумент.
[2] содержит больше информации об объектах опций.

Цепные сеттеры

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

    > new ColorPoint().setX(12).setY(7)
    { x: 12, y: 7, color: 'black' }

    > new ColorPoint().setColor("red")
    { x: 0, y: 0, color: 'red' }

Естественно, каждый из сеттеров должен возвращать это, чтобы сеттеры могли быть соединены в цепочку и чтобы конечный результат такой цепочки всегда был экземпляром ColorPoint.

    function ColorPoint() {
        // Set default values
        this.x     = 0;
        this.y     = 0;
        this.color = "black";
    }
    ColorPoint.prototype.setX = function (x) {
        this.x = x;
        return this;
    }
    ColorPoint.prototype.setY = function (y) {
        this.y = y;
        return this;
    }
    ColorPoint.prototype.setColor = function (color) {
        this.color = color;
        return this;
    }

Учитывая, как механически пишутся такие сеттеры, мы можем написать метод с помощью Setters, который сделает это за нас:

    var ColorPoint = function (x, y, color) {
        this.x     = 0;
        this.y     = 0;
        this.color = "black";
    }.withSetters(
        "x", "y", "color"
    );

withSetters — это метод, который при применении к функции добавляет сеттеры в прототип этой функции:

    Function.prototype.withSetters = function (/*setter names*/) {
        var Constr = this;
        Array.prototype.forEach.call(arguments, function (propName) {
            var capitalized = propName[0].toUpperCase() + propName.slice(1);
            var setterName = "set" + capitalized;
            Constr.prototype[setterName] = function (value) {
                this[propName] = value;
                return this;
            };
        });
        return this;
    };

Уточняя этот подход, мы можем использовать литерал объекта для одновременного указания как имен сеттера, так и значений по умолчанию:

    var ColorPoint = function (x, y, color) {
        this.applyDefaults();
    }.withSetters({
        x: 0,
        y: 0,
        color: "black"
    });

Значения по умолчанию добавляются в свежий экземпляр ColorPoint с помощью метода applyDefaults. Помимо этого метода, не так много изменений — теперь имена сеттеров извлекаются из объекта.

    Function.prototype.withSetters = function (props) {
        var Constr = this;
        Constr.prototype.applyDefaults = function () {
            _.defaults(this, props);
        }
        Object.keys(props).forEach(function (propName) {
            var capitalized = propName[0].toUpperCase() + propName.slice(1);
            var setterName = "set" + capitalized;
            Constr.prototype[setterName] = function (value) {
                this[propName] = value;
                return this;
            };
        });
        return this;
    };

Методы инициализации

Иногда существует несколько взаимоисключающих «режимов» или способов инициализации объекта. Использовать один и тот же конструктор для всех режимов довольно сложно. Было бы лучше иметь четкое представление о том, какой режим активен. Проблема использования одного и того же имени для различных операций хорошо иллюстрируется хорошо знакомым конструктором JavaScript-массива. Первая операция — создать пустой массив заданной длины:

    > new Array(3)
    [ , ,  ]

Вторая операция — создать массив с заданными элементами:

    > new Array("a", "b", "c")
    [ 'a', 'b', 'c' ]

Однако вторая операция проблематична, потому что, если вы хотите создать массив, в котором есть одно натуральное число, операция 1 вступает во владение и не позволяет вам это сделать. Лучшее решение — снова использовать цепные сеттеры и дать каждой операции другое имя. В качестве примера давайте рассмотрим конструктор Angle, экземпляры которого можно инициализировать в градусах или радианах.

    > new Angle().initDegrees(180).toString()
    '3.141592653589793rad'

Обратите внимание, как мы отделили создание экземпляров (создание экземпляров) с помощью нового Angle () от инициализации с помощью initDegrees (). Конструкторы обычно выполняют обе задачи; здесь мы делегировали последнее задание методу. Угол может быть реализован следующим образом.

    function Angle() {
    }
    Angle.prototype.initRadians = function (rad) {
        this.rad = rad;
        return this;
    };
    Angle.prototype.initDegrees = function (deg) {
        this.rad = deg * Math.PI / 180;
        return this;
    };
    Angle.prototype.toString = function () {
        return this.rad+"rad";
    };

initRadians и initDegrees на самом деле не являются установщиками, они являются
методами инициализации . Методы инициализации отличаются от методов установки двумя способами: во-первых, они могут иметь более одного параметра или не иметь параметров. Напротив, сеттеры обычно имеют ровно один параметр. Во-вторых, вы часто хотите убедиться, что если метод вызывается для экземпляра, этот экземпляр был предварительно инициализирован. И, возможно, не следует инициализировать более одного раза. Следующий код использует логический флаг _initialized для проверки этих двух проверок.

    function Angle() {
        this._initialized = false;
    }

    Angle.prototype._forbidInitialized = function () {
        if (this._initialized) {
            throw new Error("Already initialized");
        }
        this._initialized = true;
    };
    Angle.prototype.initRadians = function (rad) {
        this._forbidInitialized();
        this.rad = rad;
        return this;
    };
    Angle.prototype.initDegrees = function (deg) {
        this._forbidInitialized();
        this.rad = deg * Math.PI / 180;
        return this;
    };

    Angle.prototype._forceInitialized = function () {
        if (!this._initialized) {
            throw new Error("Not initialized");
        }
    };
    Angle.prototype.toString = function () {
        this._forceInitialized();
        return this.rad+"rad";
    };

Статические фабричные методы

Статические фабричные методы являются альтернативой методам инициализации. Это методы конструктора, которые создают экземпляры. Учитывая, что конструктор похож на класс в языках на основе классов, такие методы часто называют
статическими . Они называются
фабричными методами , потому что они производят экземпляры. Создание угла с помощью статического метода фабрики вместо конструктора и метода инициализации выглядит следующим образом.

    > var a = Angle.withDegrees(180);
    > a.toString()
    '3.141592653589793rad'
    > a instanceof Angle
    true

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

    function Angle(rad) {
        this.rad = rad;
    }
    Angle.withRadians = function (rad) {
        return new Angle(rad);
    };
    Angle.withDegrees = function (deg) {
        return new Angle(deg * Math.PI / 180);
    };
    Angle.prototype.toString = function () {
        return this.rad+"rad";
    };

Смысл фабричных методов в том, чтобы заменить конструктор. Поэтому мы хотим исключить возможность случайного вызова нового Angle () или даже просто Angle как функции. Просто скрывать Angle не получится, потому что нам это нужно, например.

Охрана конструктора, подход 1. Конструктор выдает ошибку, если она не вызывается со значением, известным только фабричным методам. Мы сохраняем значение в секрете, помещая его в выражение для немедленного вызова функции (IIFE, [3] ).

    var Angle = function () {
        var constrGuard = {};
        function Angle(guard, rad) {
            if (guard !== constrGuard) {
                throw new Error("Must use a factory method");
            }
            this.rad = rad;
        }
        Angle.withRadians = function (rad) {
            return new Angle(constrGuard, rad);
        };
        Angle.withDegrees = function (deg) {
            return new Angle(constrGuard, deg * Math.PI / 180);
        };
        Angle.prototype.toString = function () {
            return this.rad+"rad";
        };
        return Angle;
    }();

Охрана конструктора, подход 2. Альтернативой является не использование конструктора для создания экземпляра Angle, а

    Object.create(Angle.prototype)

Методы фабрики теперь используют функцию createInstance, которая реализует этот подход.

    var Angle = function () {
        function Angle() {
            throw new Error("Must use a factory method");
        }
        function createInstance(rad) {
            var inst = Object.create(Angle.prototype);
            inst.rad = rad;
            return inst;
        }
        Angle.withRadians = function (rad) {
            return createInstance(rad);
        };
        Angle.withDegrees = function (deg) {
            return createInstance(deg * Math.PI / 180);
        };
        Angle.prototype.toString = function () {
            return this.rad+"rad";
        };
        return Angle;
    }();

Взаимодействие:

    > var a = Angle.withDegrees(180);
    > a.toString()
    '3.141592653589793rad'
    > a instanceof Angle
    true

Создание экземпляров нескольких типов в одном месте

Иногда существует иерархия типов, и вы хотите создать экземпляры этих типов в одном месте. Например, выражения:

    Expression
    +-- Addition
    +-- Integer

Тип Expression имеет два подтипа: Integer и Addition (чьи операнды могут быть выражениями). Предполагая, что выражение является абстрактным и никогда не будет создаваться, синтаксический анализ текстовой строки создаст экземпляр либо сложения, либо целочисленного значения. Обычный способ реализации такой операции — через фабричный метод:

    Expression.parse = function (str) {
        if (/^[-+]?[0-9]+$/.test(str)) {
            return new Integer(str);
        }
        ...
    }

Однако JavaScript также позволяет вам возвращать произвольный объект из конструктора. Следовательно, пока обычно выполняется следующее утверждение:

    new C() instanceof C

это не должно:

    > function C() { return {} }
    > new C() instanceof C
    false

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

    function Expression(str) {
        if (/^[-+]?[0-9]+$/.test(str)) {
            return new Integer(str);
        }
        ...
    }

Тогда справедливо следующее утверждение:

    new Expression("-125") instanceof Integer

Большинство механизмов JavaScript выполняют автоматическую оптимизацию и не создают экземпляр конструктора, если к нему нет доступа и другой (явно) возвращен.

Новый оператор

мольба

    new <function-valued expression>(arg1, arg2, ...)
    var inst = new Constr();
    var inst = new Constr;

Функция выражение со значением выше, как правило , либо

  1. любое выражение в скобках, которое оценивает функцию
  2. идентификатор, за которым может следовать один или несколько обращений к свойству

# 1 имеет значение, когда вы хотите применить новое к результату функции, например, следующую функцию foo ().

    function foo() {
        function bar(arg) {
            this.arg = arg;
        }
        return bar;
    }

Вызвать результат функции foo () просто, вы просто добавляете скобки с аргументами:

    foo()("hello")

Чтобы использовать результат как операнд нового, вы должны добавить дополнительные скобки:

    > new (foo())("hello")
    { arg: 'hello' }

Без скобок вокруг foo () вы вызываете foo как конструктор, а затем вызываете результат как функцию с аргументом «hello». То, что операнд new заканчивается первыми скобками, является преимуществом, когда речь идет о вызове метода для вновь созданного объекта. В качестве примера рассмотрим конструктор для цветов.

    function Color(name) {
        this.name = name;
    }
    Color.prototype.toString = function () {
        return "Color("+this.name+")";
    }

Вы можете создать цвет и сразу же вызвать метод для него:

    > new Color("green").toString()
    'Color(green)'

Вышеупомянутое выражение эквивалентно

    (new Color("green")).toString()

Однако доступ к свойству перед круглыми скобками считается частью операнда new. Это удобно, когда вы хотите поместить конструктор внутри объекта:

    var namespace = {};
    namespace.Color = function (name) {
        this.name = name;
    };

Взаимодействие:

    > new namespace.Color("red")
    { name: 'red' }

Оператор new игнорирует связанное значение для `this`

Функции имеют метод bind (), который позволяет вам создать новую функцию с фиксированным значением для этого, чьи первые от 0 до n параметров уже заполнены. С учетом определений.

    var obj = {};
    function F() {
        console.log("this === obj? " + (this === obj));
    }

Затем вы можете создать новую функцию через bind:

    var boundF = F.bind(obj);

boundF работает как положено, когда вызывается как функция:

    > boundF()
    this === obj? true
    undefined

Результат boundF () не определен. Тем не менее, новый переопределяет связанный это с новым экземпляром:

    > new boundF() instanceof F
    this === obj? false
    true
    > function Constr() { console.log(this === obj) }
    undefined
    > (Constr.bind(obj))()
    true
    undefined
    > new (Constr.bind(obj))()
    false
    {}

Новый оператор не работает с apply ()

Вы можете вызвать конструктор Date следующим образом.

    new Date(2011, 11, 24)

Если вы хотите предоставить аргументы через массив, вы не можете использовать apply:

    > new Date.apply(null, [2011, 11, 24])
    TypeError: function apply() { [native code] } is not a constructor

Причина очевидна: новый ожидает, что Date.apply будет конструктором. Требуется больше работы для конструкторов, которые применяют достижения для функций
[4] .

Образцы объектов

Обычно для создания объектов используются
примеры функций :

    function Person(name) {
        this.name = name;
    }
    Person.prototype.describe = function () {
        return "Person called "+this.name;
    };

Экземпляр создается с помощью нового оператора:

    new Person("Jane") instanceof Person

ECMAScript 5 представил новый способ создания экземпляров: Object.create создает объект, прототип которого является заданным аргументом:

    Object.create(Person.prototype) instanceof Person

Есть еще две проблемы, которые мы хотели бы решить.

Работа напрямую с прототипом объекта. Человек был именем функции образца. Мы хотим сделать объект-прототип экземпляром объекта и дать ему это имя. Это выглядит следующим образом.

    var Person = {
        describe: function () {
            return "Person called "+this.name;
        }
    };

Мы больше не можем работать с instanceof, потому что это оператор примера функции. Однако isPrototypeOf () работает хорошо. Теперь верно следующее утверждение.

    Person.isPrototypeOf(Object.create(Person))

Инициализация. Мы до сих пор не инициализировали наши экземпляры Person. Для этого мы вводим метод init ().

    var Person = {
        init: function (name) {
            this.name = name;
            return this;
        },
        describe: function () {
            return "Person called "+this.name;
        }
    };

Мы используем этот пример объекта следующим образом:

    > var jane = Object.create(Person).init("Jane");
    > Person.isPrototypeOf(jane)
    true
    > jane.describe()
    'Person called Jane'

Вы можете обратиться к
[5] для получения дополнительной информации об объектных образцах, включая библиотеку для работы с ними.

Темы, не затронутые этим постом

В этом сообщении не были затронуты две темы, относящиеся к созданию объектов:

  • Подтип [6] и как подтипить встроенные модули JavaScript [7] .
  • Хранение данных экземпляра в секрете. Вы можете прочитать « Частные пользователи в JavaScript » Крокфорда для получения дополнительной информации.

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

  1. Попробовать Underscore на Node.js
  2. Параметры ключевых слов в JavaScript и ECMAScript.next
  3. Область видимости переменной JavaScript и ее подводные камни
  4. Spreading arrays into arguments in JavaScript
  5. Prototypes as classes – an introduction to JavaScript inheritance
  6. JavaScript inheritance by example
  7. Subtyping JavaScript built-ins

 

From http://www.2ality.com/2012/02/exemplars.html