Статьи

Пересмотр объектов JavaScript

Во время каникул я потратил некоторое время на изучение событий в ES6 (следующая версия JavaScript). При рассмотрении некоторых предложений, таких как определения минимального класса , Proxy API и Weak Maps ; Я заметил, что большинство этих улучшений широко используют функции управления объектами, представленные в ES5 (т. Е. ECMAScript5 — текущий стандарт JavaScript).

Одним из основных направлений ES5 было улучшение структуры объектов JavaScript и манипулирования ими. Представленные в нем функции имеют большой смысл, особенно если вы работаете с большими и сложными приложениями.

Мы очень неохотно принимаем функции ES5, особенно из-за проблем совместимости браузера. Мы редко видим производственный код, который использует эти функции. Тем не менее, все современные браузеры (т.е. IE9, FF4, Opera 12 и Chrome) имеют механизмы JavaScript, которые реализуют стандарт ES5. Кроме того, функции ES5 можно использовать в проектах на базе Node.js. Поэтому я думаю, что было бы целесообразно вернуться к объектным функциям ES5 и посмотреть, как они могут быть полезны в реальных сценариях.

Данные и свойства доступа

ES5 представляет два вида свойств объекта — данные и средства доступа. Свойство данных непосредственно отображает имя на значение (например. Целое число, строка, логический, массив, объект или функция). Свойство аксессора отображает имя к определенному геттеру и / или функции сеттера.

var square = {
    length: 10,
    get area() { return this.length * this.length },
    set area(val) { this.length = Math.sqrt(val) }
}

Здесь мы определили squareобъект lengthкак свойство данных и свойство areaдоступа.

> square.length
  10
> square.area
  100
> square.area = 400
  400 
> square.length
  20

Когда мы получаем доступ к areaсвойству, его получатель вычисляет и возвращает значение в терминах lengthсвойства. Кроме того, когда мы присваиваем значение area, его функция установки меняет lengthсвойство.

Дескриптор свойства

ES5 позволяет вам более детально контролировать свойства, определенные в объекте. С каждым свойством связана специальная коллекция атрибутов, известная как дескриптор свойства .

Вы можете проверить атрибуты, связанные со свойством, вызвав Object.getOwnPropertyDescriptorметод.

> Object.getOwnPropertyDescriptor(square, "length")
{
    configurable: true
    enumerable: true
    value: 20
    writable: true
}

> Object.getOwnPropertyDescriptor(square, "area")
{
    configurable: true
    enumerable: true
    get: function area() { return this.length * this.length }
    set: function area(val) { this.length = Math.sqrt(val) }
}

Как видно из приведенных выше двух примеров — valueи writeableатрибуты определены только для дескрипторов свойств данных , а getи / или setопределены для дескрипторов свойств аксессоров . И атрибуты, configurableи enumerableатрибуты применяются к любому виду дескриптора свойства.

writableАтрибут указать , является ли значение может быть присвоено свойству. Если writableесть false, свойство становится доступным только для чтения. Как следует из названия, configurableуказывает, можно ли настраивать атрибуты свойства, а также можно ли удалить свойство из объекта (используя deleteоперацию). enumerableАтрибут определяет , должно ли свойство быть видимым в for..inпетле или Object.keysметодах.

Мы можем изменить эти атрибуты в дескрипторе свойства , используя Object.definePropertyметод.

Object.defineProperty(square, "length", {
    value: 10,
    writable: false
});

Это сделает lengthсвойство squareдоступным только для чтения и постоянно установленным в 10.

> square.length
  10
> square.area = 400
  400
> square.length
  10
> square.area
  100

Взломостойкие объекты

В некоторых случаях вам нужно сохранять объекты в их текущем состоянии во время выполнения без каких-либо дополнительных расширений или модификаций свойств. ES5 предоставляет три уровня управления, которые вы можете применить к объектам.

Вызов preventExtensionsметода сделает объект нерасширяемым. Это означает, что никакие дополнительные свойства не могут быть определены для объекта.

> Object.preventExtensions(square);

> Object.defineProperty(square, "text", { value: "hello" });
  TypeError: Cannot define property:text, object is not extensible.

Запечатывание объекта предотвратит как определение новых свойств, так и удаление существующих свойств в объекте.

> Object.seal(square);

> delete square.length
  false

Если мы сделаем еще один шаг и остановим объект, это также не позволит изменить существующие значения свойств в объекте. В этот момент весь объект эффективно становится постоянным.

> Object.freeze(square);

> square.length = 20
  20 
> square.length
  10

Вы можете использовать методы Object.isSealed, Object.isFrozenи Object.isExtensibleпрограммно проверить состояние объекта.

Даже если объект защищен, его прототип все равно можно расширить. Проверьте следующий пример:

var obj = Object.create({}, { onlyProp: { value: true } });
Object.preventExtensions(obj);

var proto = Object.getPrototypeOf(obj);
proto.anotherProp = true;

> obj.anotherProp
  true

Перечисления

Часто мы используем объекты JavaScript в качестве ассоциативных массивов или коллекций. В таких случаях мы склонны использовать for...inциклы для перечисления свойств. Однако цикл будет проходить по всем перечислимым свойствам, доступным в цепочке прототипов объекта, что приведет к нежелательным результатам.

Чтобы избежать таких побочных эффектов, JSLint предлагает вручную проверить, определено ли данное свойство в объекте.

for (name in object) { if (object.hasOwnProperty(name)) { .... } }

ES5 предоставляет Object.keysметод, который бы возвращал массив собственных перечисляемых свойств объекта.

Мы можем использовать этот метод для безопасной итерации по списку свойств:

Object.keys(obj).forEach( function(key) {
    console.log(key);
});

Примечание. Array.forEach также является новой функцией, представленной в ES5.

наследование

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

Стандартный способ создания нового объекта — использование функции конструктора. Таким образом, вновь созданный объект унаследует прототип функции конструктора.

var Person = function(first_name, last_name) {
    this.first_name = first_name;
    this.last_name = last_name;
}

Person.prototype = {
    say: function(msg) {
        return this.first_name + " says " + msg;
    }
}

var ron = new Person("Ron", "Swanson");

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

Для тех, кто предпочитает использовать альтернативный синтаксис, ES5 предоставляет Object.createметод. Он принимает объект-прототип и дескриптор свойства в качестве аргументов.

Вот альтернативная реализация, которая может быть использована для создания Personобъектов с использованием шаблонаObject.create и модуля .

var Person = (function() {

    var proto = {
        say: function(msg) {
            return this.first_name + " says " + msg;
        }
    }

    return {
        init: function(first_name, last_name) {
            return Object.create(proto, {
                first_name: { value: first_name, enumerable: true },
                last_name: { value: last_name, enumerable: true }
            });
        }
    }

})();

var ron = Person.init("Ron", "Swanson");

Однако по сравнению с функциями конструктора использование Object.createможет быть значительно медленным . Поэтому выберите, какую реализацию вы хотите использовать, в зависимости от контекста и требований.

Даже если вы предпочитаете использовать функции конструктора, Object.createони пригодятся, если вы хотите иметь несколько уровней наследования.

var Person = function(first_name, last_name) {
    this.first_name = first_name;
    this.last_name = last_name;
};

Person.prototype = {
    say: function(msg) {
        return this.first_name + " says " + msg;
    }
};

var Employee = function(first_name, last_name) {
    Person.call(this, first_name, last_name);
}

Employee.prototype = Object.create(Person.prototype, {
    department: { value: "", enumerable: true },
    designation:{ value: "", enumerable: true }
});

var ron = new Employee("Ron", "Swanson");

Мы расширили Personпрототип, чтобы создать прототип Employee.

Клонирование объектов

Наконец, давайте посмотрим, как создать мелкий клон объекта, используя методы объекта ES5.

var clone = function(obj) {
    // create clone using given object's prototype
    var cloned_obj = Object.create(Object.getPrototypeOf(obj)); 

    // copy all properties
    var props = Object.getOwnPropertyNames(obj);
    props.forEach(function(prop) {
        var propDescriptor = Object.getOwnPropertyDescriptor(obj, prop);            
        Object.defineProperty(cloned_obj, prop, propDescriptor);
    });

    return cloned_obj;
}

Здесь мы извлекаем прототип данного объекта и используем его для создания клона. Затем мы просматриваем все свойства, определенные в объекте (включая не перечисляемые свойства), и копируем дескрипторы их свойств в клон.

Дальнейшее чтение

Если вы хотите узнать больше об объектах JavaScript и о том, как ими манипулировать, я бы порекомендовал вам ознакомиться со следующими ресурсами: