Статьи

Свойства в JavaScript: определение и назначение


Знаете ли вы, что определение свойства — это не то же самое, что присвоение ему?
Этот пост в блоге объясняет разницу и ее последствия. Это было вызвано
электронным письмом Аллена Вирфса-Брока в списке рассылки es-обсудить.

Определение против присвоения

Определение. Чтобы определить свойство, используется такая функция, как

    Object.defineProperty(obj, propName, propDesc)

Основное назначение этой функции — добавить
собственное (прямое) свойство в
obj , атрибуты которого
(доступный для записи и т. Д., См. Ниже) определены в
propDesc . Вторичная цель состоит в том, чтобы изменить атрибуты свойства, включая его значение.

Назначение. Чтобы присвоить свойству, используется выражение, такое как

    obj.prop = value

Основная цель такого выражения — изменить значение. Перед выполнением этого изменения JavaScript проверяет цепочку прототипов
[1] объекта
obj : если где-то в объекте obj или в одном из его прототипов имеется установщик, то
присвоение является вызовом этого установщика. Назначение имеет побочный эффект создания свойства, если оно еще не существует — как собственное свойство
obj с атрибутами по умолчанию.

В следующих двух разделах более подробно рассматривается, как работают определения и назначения. Не стесняйтесь пропустить их. Вы все еще должны понимать секту. 4 «Последствия» и позже.

Резюме: атрибуты свойств и внутренние свойства

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

Виды недвижимости

JavaScript различает три вида свойств:

  • Свойства именованного средства доступа: свойство, которое существует благодаря получателю или установщику.
  • Свойства именованных данных: свойство, которое h является значением. Это самые распространенные свойства. Они включают методы.
  • Внутренние свойства: используются внутри JavaScript и не доступны напрямую через язык. Однако могут быть косвенные способы доступа к ним. Пример: каждый объект имеет внутреннее свойство с именем [[Prototype]]. Вы не можете непосредственно прочитать его, но все равно получите его значение через Object.getPrototypeOf () . В то время как внутренние свойства именуются именем в квадратных скобках, они являются безымянными в том смысле, что они невидимы и не имеют обычного строкового имени свойства.

Атрибуты недвижимости

Атрибуты свойств — это поля, которые есть у каждого свойства и влияющие на его работу. Существуют следующие атрибуты:

  • Все свойства:

    • [[Enumerable]]: если свойство не перечисляемо, его нельзя увидеть некоторыми операциями, такими как for ... in и Object.keys () [2] .
    • [[Настраиваемый]]: если свойство не настраивается, ни один из атрибутов (кроме [[Значение]]) не может быть изменен с помощью определения.
  • Свойства именованных данных:

    • [[Value]]: это значение свойства.
    • [[Writable]]: определяет, можно ли изменить значение.
  • Свойства именованного средства доступа:

    • [[Get]]: содержит метод получения.
    • [[Set]]: содержит метод установки.

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

Дескриптор свойства — это набор атрибутов свойства, закодированных как объект. Например:

    {
        value: 123,
        writable: false
    }

Вы можете видеть, что имена свойств соответствуют именам атрибутов [[Value]] и [[Writable]]. Дескрипторы свойств используются такими функциями, как
Object.defineProperty ,
Object.getOwnPropertyDescriptor и
Object.create, которые либо изменяют, либо возвращают атрибуты свойства. Если в дескрипторе отсутствуют свойства, применяются следующие значения по умолчанию:

Имущество Значение по умолчанию
значение не определено
получить не определено
поставил не определено
записываемый ложный
перечислимый ложный
конфигурируемый ложный

Внутренние свойства

Есть
несколько внутренних свойств, которыми обладают все объекты. Среди прочего, следующие четыре:

  • [[Prototype]]: прототип объекта.
  • [[Extensible]]: является ли этот объект расширяемым , можно ли добавить к нему новые свойства?
  • [[DefineOwnProperty]]: определить свойство. Смотрите объяснение ниже.
  • [[Put]]: назначить свойство. Смотрите объяснение ниже.

Детали определения и назначения

Определение свойства

Определение свойства обрабатывается внутренним методом


[[
DefineOwnProperty ]] (P, Desc, Throw)

P это имя свойства.
Throw указывает, как операция должна
отклонить изменение: если
Throw имеет значение
true, генерируется исключение. В противном случае операция молча прерывается. Когда вызывается [[DefineOwnProperty]], выполняются следующие шаги.

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

    • Преобразование свойства данных в свойство средства доступа или наоборот
    • Изменение [[Настраиваемый]] или [[Перечислимый]]
    • Изменение [[Доступно для записи]]
    • Изменение [[Value]], если [[Writable]] равно false
    • Изменение [[Получить]] или [[Установить]]
  • В противном случае существующее собственное свойство настраивается и может быть изменено, как указано.

Если
Desc точно отражает текущие атрибуты
этого [P], то определение никогда не отклоняется.

Двумя функциями для определения свойства являются Object.defineProperty и Object.defineProperties . Например:

    Object.defineProperty(obj, propName, desc)

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

    obj.[[DefineOwnProperty]](propName, desc, true)

Присвоение недвижимости

Присвоение свойству обрабатывается внутренним методом


[[
Put ]] (P, Value, Throw)

P и
Throw работают так же, как с [[DefineOwnProperty]]. Когда вызывается [[Put]], выполняются следующие шаги.

  • Если в цепочке прототипов есть свойство только для чтения, имя которого равно P : reject.
  • Если где-то в цепочке прототипов есть установщик с именем P : вызовите установщик.
  • Если нет собственного свойства с именем P : если объект является расширяемым, то создайте новое свойство.
        this.[[DefineOwnProperty]](
            P,
            {
                value: Value,
                writable: true,
                enumerable: true,
                configurable: true
            },
            Throw
        )
    

    Если объект не является расширяемым, то отклонить.

  • Иначе, есть собственное свойство с именем P , которое доступно для записи. Invoke
        this.[[DefineOwnProperty]](P, { value: Value }, Throw)
    

    Это обновляет значение P , но сохраняет его атрибуты (такие как перечисляемость) неизменными

Оператор присваивания (
= ) вызывает [[Put]]. Например:

    obj.prop = v;

Внутренне это вызывает вызов

    obj.[[Put]]("prop", v, isStrictModeOn)

То есть оператор присваивания генерирует только если выполняется в строгом режиме. [[Put]] не возвращает Value , но оператор присваивания возвращает
.

Последствия

В этом разделе описываются некоторые последствия того, как работают определение и назначение свойств.

Назначение вызывает сеттер в прототипе, определение не

Учитывая следующий пустой объект
obj , у прототипа
proto которого есть пара getter / setter, называемая
foo .

    var proto = {
        get foo() {
            console.log("Getter");
            return "a";
        },
        set foo(x) {
            console.log("Setter: "+x);
        },
    };
    var obj = Object.create(proto);

В чем разница между определением свойства
foo объекта
obj и назначением ему? Если вы определите, то вы намерены создать новую собственность. Они всегда создаются в первом объекте цепочки прототипов, что в данном случае означает в
obj :

    > Object.defineProperty(obj, "foo", { value: "b" });
    > obj.foo
    'b'
    > proto.foo
    Getter
    'a'

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

    > obj.foo = "b";
    Setter: b
    'b'

Вы можете сделать свойство доступным только для чтения, только определив получатель. Ниже панель свойств
объекта
proto2 является таким свойством и наследуется
obj2 .

    "use strict";
    var proto2 = {
        get bar() {
            console.log("Getter");
            return "a";
        },
    };
    var obj2 = Object.create(proto2);

Мы используем строгий режим, чтобы при выполнении назначения было выдано исключение. В противном случае, присваивание будет просто проигнорировано (но также не изменит
obj ). При назначении мы хотим изменить
панель, которая запрещена из-за того, что
панель доступна только для чтения.

    > obj2.bar = "b";
    TypeError: obj.bar is read-only

Однако мы можем определить что-то новое и, таким образом, переопределить
панель свойств
proto :

    > Object.defineProperty(obj2, "bar", { value: "b" });
    > obj2.bar
    'b'
    > proto2.bar
    Getter
    'a'

Свойства только для чтения в прототипах предотвращают назначение, но не обязательно определение

Свойство только для чтения в прототипе не позволяет присваиванию добавлять собственное свойство с тем же именем, вам нужно использовать определение, если вы хотите это сделать. Это ограничение было недавно введено в ECMAScript 5.1. То есть присваивание
obj.foo = "b" не создает автоматически свойство
foo, если в одном из прототипов obj есть свойство только для чтения с таким именем
. Это продемонстрировано с помощью следующего примера, где
OBJ «s прототип
прото имеет свойство только для чтения
Foo , значение которого
„а“ .

    "use strict";
    var proto = Object.defineProperties(
        {},
        {
            foo: {  // attributes of property foo:
                value: "a",
                writable: false,  // read-only
                configurable: true  // explained later
            }
        });
    var obj = Object.create(proto);

Назначение. Результатом присвоения является исключение:

    > obj.foo = "b";
    TypeError: obj.foo is read-only

Удивительно, что унаследованное свойство может влиять на возможность создания собственного свойства
[3] . Но это имеет смысл, потому что так же работает свойство только для получения.

Определение. С помощью определения мы хотим создать новое собственное свойство:

    > Object.defineProperty(obj, "foo", { value: "b" });
    > obj.foo
    'b'
    > proto.foo
    'a'

Создание свойства таким способом можно предотвратить, сделав
proto.foo дополнительно неконфигурируемым.

Оператор присваивания не меняет свойства в прототипах

Учитывая следующую настройку, где
obj наследует свойство
foo от
proto .

    var proto = { foo: "a" };
    var obj = Object.create(proto);

Вы не можете изменить
proto.foo , назначив
obj.foo . Это создает новое собственное свойство:

    > obj.foo = "b";
    'b'
    > obj.foo
    'b'
    > proto.foo
    'a'

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

  1. Методы. Позволяют вносить исправления в методы непосредственно в прототипе, но предотвращают случайные изменения через потомков прототипа.
  2. Свойства, не относящиеся к методам: прототип предоставляет общие значения по умолчанию для потомков. Можно переопределить эти значения через потомка, но не изменять их. Это считается анти-паттерном и не рекомендуется. Чище назначать значения по умолчанию в конструкторах.

Только определение позволяет создать свойство с произвольными атрибутами

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

Свойства литерала объекта добавляются через определение

Дан следующий объектный литерал.

    var obj = {
        foo: 123
    };

Это внутренне переведено в серию утверждений. У вас есть два варианта. Во-первых, через назначение:

    var obj = new Object();
    obj.foo = 123;

Во-вторых, через определение:

    var obj = new Object();
    Object.defineProperties(obj, {
        foo: {
            value: 123,
            enumerable: true,
            configurable: true,
            writable: true
        }
    });

Второй вариант лучше выражает семантику литерала объекта: для создания свежих свойств. По той же причине
Object.create получает дескрипторы свойств через свой второй аргумент.

Атрибуты методов

Одна из возможностей (предложенная Wirfs-Brock) — дать методам следующие атрибуты:

    "use strict";
    function Stack() {
    }
    Object.defineProperties(Stack.prototype, {
        push: {
            writable: false,
            configurable: true,
            value: function (x) { /* ... */ }
        }
    });

Идея состоит в том, чтобы предотвратить случайное назначение:

    > var s = new Stack();

    > s.push = 5;
    TypeError: s.push is read-only

Однако, поскольку
push настраивается, мы можем переопределить его в экземпляре через определение свойства.

    > var s = new Stack();
    > Object.defineProperty(s, "push",
          { value: function () { return "yes" }})

    > s.push()
    'yes'

Определение также позволит нам заменить
Stack.prototype.push .

Вывод

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

  1. Если вы хотите создать новое свойство, используйте определение.
  2. Если вы хотите изменить значение свойства, используйте присваивание.

В комментариях medikoo напоминает нам, что использование дескрипторов свойств для достижения # 1 является медленным. И я действительно часто использую назначение для создания свойств, потому что это также более удобно. К счастью, ECMAScript.next может сделать определение более быстрым и удобным: существует предложение для оператора «
определения свойств », альтернативы
Object.defineProperties . Учитывая, что разница между определением и назначением является тонкой, но важной, такая помощь в определении будет приветствоваться.

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

  1. Прототипы как классы — введение в наследование JavaScript
  2. Свойства JavaScript: наследование и перечислимость
  3. Исправление ошибки запрета на переопределение только для чтения [страница в вики ECMAScript со справочной информацией по этому вопросу]