Статьи

Подтип JavaScript встроен

Встроенные JavaScript-модули трудно подтипить. Этот пост объясняет почему и представляет решения.

терминология

Мы используем встроенную фразу
подтипа и избегаем термина «
расширение» , потому что он принят в JavaScript:

  • Подтип встроенного A: Создайте подконструктор B данного встроенного конструктора A. Экземпляры B также являются экземплярами A.
  • Расширение встроенного A: Добавление новых методов в A.prototype.

Есть два препятствия для подтипа встроенного: во-первых, экземпляры с внутренними свойствами. Во-вторых, конструктор, который нельзя вызвать как функцию.

Препятствие 1: экземпляры с внутренними свойствами

Большинство встроенных типов имеют экземпляры с так называемыми
внутренними свойствами (имена которых записаны в двойных квадратных скобках, например: [[PrimitiveValue]]). Внутренние свойства управляются механизмом JavaScript и обычно не доступны напрямую в JavaScript. Обычный метод подтипирования в JavaScript заключается в вызове супер-конструктора как функции с этим из суб-конструктора [2].

    function Super(x, y) {
        this.x = x;
        this.y = y;
    }
    function Sub(x, y, z) {
        // Add super-properties to sub-instance
        Super.call(this, x, y); // (*)
        // Add sub-property
        this.z = z;
    }

Большинство встроенных модулей игнорируют подэкземпляр, переданный как this (*), который описан ниже как «препятствие 2». Кроме того, добавление внутренних свойств к существующему экземпляру в общем случае невозможно, поскольку они имеют тенденцию фундаментально изменять природу экземпляра. Следовательно, вызов в (*) не может быть использован для добавления внутренних свойств. Следующие типы имеют экземпляры с внутренними свойствами:

  • Типы обёрток: экземпляры логических, числовых и строковых примитивов. Все они имеют внутреннее свойство [[PrimitiveValue]], значение которого возвращается valueOf (). Кроме того, экземпляры String поддерживают индексированный доступ к символам.

    • Boolean: внутреннее свойство [[PrimitiveValue]]
    • Номер: внутреннее свойство [[PrimitiveValue]]
    • Строка: внутреннее свойство [[PrimitiveValue]], пользовательский метод [[GetOwnProperty]], нормальная длина свойства. [[GetOwnProperty]] обращается к обернутой примитивной строке, когда используется индекс массива.
  • Массив: пользовательский внутренний метод [[DefineOwnProperty]] перехватывает установленные свойства. Это гарантирует, что свойство длины работает правильно, поддерживая длину в актуальном состоянии, когда элементы массива добавляются или удаляются, и удаляя лишние элементы, когда длина уменьшается.
  • Дата: внутреннее свойство [[PrimitiveValue]] хранит время, представленное экземпляром даты.
  • Функция: внутреннее свойство [[Call]] (код, который выполняется при вызове экземпляра) и, возможно, другие.
  • RegExp: внутреннее свойство [[Match]] в дополнение к не внутренним свойствам. Цитирование спецификации ECMAScript:


    Значение внутреннего свойства [[Match]] является зависимым от реализации представлением Pattern объекта RegExp.

Единственными встроенными конструкторами, которые не имеют внутренних свойств, являются Error и Object.

Работа вокруг. MyArray является подтипом Array. Он имеет размер получателя, который возвращает фактические элементы в массиве, игнорируя отверстия (где длина учитывает отверстия). Хитрость, используемая для реализации MyArray, заключается в том, что он создает экземпляр массива и копирует в него свои методы. Кредит: вдохновленный сообщением в блоге Бена Наделя [3].

    function MyArray(/*arguments*/) {
        var arr = [];
        // Don’t use the Array constructor which does not work for, e.g. [5]
        // (new Array(5) creates an array of length 5 with no elements in it)
        [].push.apply(arr, arguments);
        return copyOwnFrom(arr, MyArray.methods);
    }
    MyArray.methods = {
        get size() {
            var size = 0;
            for(var i=0; i < this.length; i++) {
                if (i in this) size++;
            }
            return size;
        }
    }

Приведенный выше код использует следующую вспомогательную функцию:

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

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

    > var a = new MyArray("a", "b")
    > a.length = 4;
    > a.length
    4
    > a.size
    2

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

    > a instanceof MyArray
    false
    > a instanceof Array
    true

Препятствие 2: конструктор, который нельзя вызвать как функцию

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

    function Super(x, y) {
        this.x = x;
        this.y = y;
    }
    function Sub(x, y, z) {
        // Add super-properties to sub-instance
        Super.call(this, x, y);  // (*)
        // Add sub-property
        this.z = z;
    }

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

    > var e = {}
    > Object.getOwnPropertyNames(Error.call(e))
    [ 'stack', 'arguments', 'type' ]
    > Object.getOwnPropertyNames(e)
    []

Ошибка возвращает экземпляр с собственными свойствами, но это новый экземпляр, а не e. Шаблон подтипа работает только в том случае, если Error добавляет к этому собственные свойства (например, в приведенном выше случае).

Работа вокруг. Внутри вспомогательного конструктора создайте новый суперэкземпляр и скопируйте его свойства во вложенный экземпляр.

    function MyError() {
        // Use Error as a function
        var superInstance = Error.apply(null, arguments);
        copyOwnFrom(this, superInstance);
    }
    MyError.prototype = Object.create(Error.prototype);
    MyError.prototype.constructor = MyError;

Опробовать новый тип ошибки:

    try {
        throw new MyError("Something happened");
    } catch (e) {
        console.log("Properties: "+Object.getOwnPropertyNames(e));
    }

Вывод на Node.js:

    Properties: stack,arguments,message,type

Соотношение instanceof должно быть таким:

    > new MyError() instanceof Error
    true
    > new MyError() instanceof MyError
    true

Предостережение. Основная причина для подтипа Ошибка состоит в том, чтобы иметь свойство стека во вложенных экземплярах. Увы, Firefox, похоже, хранит это значение во внутреннем свойстве, поэтому вышеупомянутый подход там не работает (Firefox 8).

Другое решение: делегирование

Делегирование — очень чистая альтернатива подтипам. Например, чтобы создать свой собственный тип массива, вы сохраняете массив в свойстве:

    function MyArray(/*arguments*/) {
        this.array = [];
        // Don’t use the Array constructor which does not work for, e.g. [5]
        // (new Array(5) creates an array of length 5 with no elements in it)
        [].push.apply(this.array, arguments);
    }
    Object.defineProperties(MyArray.prototype, {
        size: {
            get: function () {
                var size = 0;
                for(var i=0; i < this.array.length; i++) {
                    if (i in this.array) size++;
                }
                return size;
            }
        },
        length: {
            get: function () {
                return this.array.length;
            },
            set: function (value) {
                return this.array.length = value;
            }
        }
    });

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

    MyArray.prototype.get = function (index) {
        return this.array[index];
    }
    MyArray.prototype.set = function (index, value) {
        return this.array[index] = value;
    }

Нормальные методы Array.prototype могут быть переданы через следующий бит метапрограммирования.

    [ "toString", "push", "pop" ].forEach(function (name) {
        MyArray.prototype[name] = function () {
            return Array.prototype[name].apply(this.array, arguments);
        }
    });

Используя MyArray:

    > var a = new MyArray("a", "b");
    > a.length = 4;
    > a.push("c")
    5
    > a.length
    5
    > a.size
    3
    > a.set(0, "x");
    > a.toString()
    'x,b,,,c'

Будущее: ECMAScript.next

ECMAScript.next поможет двумя способами. Во-первых, это позволит вам назначать произвольные прототипы экземплярам с внутренними свойствами. Во-вторых, это, вероятно, позволит вам переопределить оператор []. Затем вы можете моделировать массивы без необходимости подтипа массива.

1. Создание специальных экземпляров с произвольным прототипом. Будет оператор, который позволит вам присвоить произвольный прототип функции или экземпляру массива. Например, экземпляр массива arr со всеми необходимыми внутренними свойствами, который имеет прототип MyArrayProto:

    var arr = MyArrayProto <| [ "a", "b", "c" ];

Экземпляр функции с прототипом MyFunctionProto создается следующим образом:

    let func = MyFunctionProto <| function () {}

Можно было бы ожидать, что будет проще автоматически сделать экземпляр особенным, если конструктор подтипирует встроенный тип, такой как Array или Function. Но, учитывая природу внутренних свойств, приведенное выше решение, вероятно, является наиболее простым.

<| Оператор может использоваться для создания подтипа SubArray of Array, который работает с new и instanceof [source: Allen Wirfs-Brock ]:

    // Connect constructors and prototypes
    var SubArray = Array <| function(...values) {
        // Connect instance and prototype
        return SubArray.prototype <| [...values];
    }
   
    // Subtype methods
    SubArray.prototype.method1 = function() {...};
    SubArray.prototype.method2 = function() {...};

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

    var s = new SubArray(1,2,3);
    console.log(s.length);  // 3
    s[3] = 4;
    console.log(s.length);  // 4
    console.log(Object.isArray(s));  // true
    console.log(s instanceof SubArray);  // true
    console.log(s instanceof Array);  // true

2. Пользовательские квадратные скобки геттеров и сеттеров. Аллен Уирфс-Брок (Allen Wirfs-Brock) предлагает «
Реформирование объектной модели: разделение [] и доступ к свойству», что делает оператор [] перезаписываемым.

  • Лучше всего теперь использовать новые методы Object.getProperty () и Object.setProperty () всякий раз, когда вы хотите использовать произвольную строку в качестве имени свойства.
  • Object.prototype использует эти методы для реализации operator [] (чтобы унаследованный код работал как положено).
  • Новые классы коллекций могут предоставлять свои собственные реализации для []. Одна возможность — использовать ключи, которые не являются строками.

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

 

С http://www.2ality.com/2011/12/subtyping-builtins.html