Есть ли способ для Object.freeze() даты JavaScript?

Согласно MDN Object.freeze() документация:

Метод Object.freeze() замеряет объект: это значит, что новые свойства не добавляются к нему; предотвращает удаление существующих свойств; и предотвращает изменение существующих свойств или их перечислимость, настраиваемость или возможность записи. По сути, объект делается эффективно неизменным. Метод возвращает замороженный объект.

Я ожидал, что вызов замораживания даты позволит предотвратить изменения этой даты, но он, похоже, не работает. Вот что я делаю (работает Node.js v5.3.0):

let d = new Date()
Object.freeze(d)
d.setTime(0)
console.log(d) // Wed Dec 31 1969 16:00:00 GMT-0800 (PST)

Я бы ожидал, что вызов setTime приведет к ошибке или не сделает ничего. Любые идеи о том, как заморозить дату?

Ответ 1

Есть ли способ для Object.freeze() даты JavaScript?

Я так не думаю. Вы можете приблизиться, хотя смотрите ниже. Но сначала посмотрим, почему просто Object.freeze не работает.

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

Было бы , если Date использовало свойство объекта для хранения своего внутреннего значения времени, но это не так. Вместо этого он использует [[DateValue]] внутренний слот. Внутренние слоты не являются свойствами:

Внутренние слоты соответствуют внутреннему состоянию, связанному с объектами и используемому различными алгоритмами спецификации ECMAScript. Внутренние слоты не являются объектными свойствами...

Так что замораживание объекта не влияет на его способность мутировать его внутренний слот [[DateValue]].


Вы можете заморозить Date или эффективно так или иначе: Замените все его методы мутатора на функции no-op (или функции, которые вызывают ошибку), а затем freeze it. Но как наблюдаемый zzzzBov (хороший!), Это не мешает кому-либо выполнять Date.prototype.setTime.call(d, 0) (в преднамеренной попытке обойти замороженный объект или как побочный продукт какого-то сложного кода, который они используют). Так что это близко, но нет сигары.

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

"use strict";

let d = new Date();
freezeDate(d);
d.setTime(0);
snippet.log(d);

function nop() {
}

function freezeDate(d) {
  allNames(d).forEach(name => {
    if (name.startsWith("set") && typeof d[name] === "function") {
      d[name] = nop;
    }
  });
  Object.freeze(d);
  return d;
}
function allNames(obj) {
  var names = Object.create(null); // Or use Map here
  var thisObj;
  
  for (thisObj = obj; thisObj; thisObj = Object.getPrototypeOf(thisObj)) {
    Object.getOwnPropertyNames(thisObj).forEach(name => {
      names[name] = 1;
    });
  }
  return Object.keys(names);
}
<!-- Script provides the `snippet` object, see http://meta.stackexchange.com/a/242144/134069 -->
<script src="//tjcrowder.github.io/simple-snippets-console/snippet.js"></script>

Ответ 2

Из MDN docs на Object.freeze (выделено мной):

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

Метод объекта Date setTime не меняет свойство объекта Date, поэтому он продолжает работать, несмотря на то, что он заморозил экземпляр.

Ответ 3

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

Ответ 4

Это действительно хороший вопрос!

T.J. Ответ Crowder имеет отличное решение, но он заставил меня подумать: что еще мы можем сделать? Как мы можем обойти Date.prototype.setTime.call(yourFrozenDate)?

Первая попытка: "Обертка"

Один прямой способ - предоставить функцию AndrewDate, которая завершает дату. У него есть все, что у даты есть минус сеттеры:

function AndrewDate(realDate) {
    var proto = Date.prototype;
    var propNames = Object.getOwnPropertyNames(proto)
        .filter(propName => !propName.startsWith('set'));

    return propNames.reduce((ret, propName) => {
        ret[propName] = proto[propName].bind(realDate);
        return ret;
    }, {});
}

var date = AndrewDate(new Date());
date.setMonth(2); // TypeError: d.setMonth is not a function

Что это значит, это создать объект, который имеет все свойства, которые имеет объект фактической даты, и использует Function.prototype.bind, чтобы установить их this.

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

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

Вторая попытка: Прокси

function SuperAndrewDate(realDate) {
    return new Proxy(realDate, {
        get(target, prop) {
            if (!prop.startsWith('set')) {
                return Reflect.get(target, prop);
            }
        }
    });
}

var proxyDate = SuperAndrewDate(new Date());

И мы решили это!

... вид. Смотрите, Firefox теперь единственный, который реализует прокси-серверы, и для некоторых причудливых причин объекты даты не могут быть проксимированы. Кроме того, вы заметите, что все еще можете делать такие вещи, как 'setDate' in proxyDate, и вы увидите доработки на консоли. Чтобы преодолеть это, необходимо предоставить больше ловушек; в частности, has, enumerate, ownKeys, getOwnPropertyDescriptor и кто знает какие странные краевые случаи есть!

... Так что, подумав, этот ответ почти бессмыслен. Но, по крайней мере, нам было весело, правда?

Ответ 5

Принятый ответ на самом деле ошибочен, я боюсь. Фактически вы можете заморозить экземпляр любого объекта, включая экземпляр Date.. В ответ @zzzzBov ответ, замораживающий экземпляр объекта, не означает, что состояние объекта становится постоянным.

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

var date = new Date();
date.x = 4;
console.log(date.x); // 4
Object.freeze(date);
date.x = 20; // this assignment fails silently, freezing has made property x to be non-writable
date.y = 5; // this also fails silently, freezing ensures you can't add new properties to an object
console.log(date.x); // 4, unchanged
console.log(date.y); // undefined

Но вы можете добиться такого поведения, которое, как я полагаю, вы желаете:

var date = (function() {
    var actualDate = new Date();

    return Object.defineProperty({}, "value", {
        get: function() {
            return new Date(actualDate.getTime())
        },
        enumerable: true
    });
})();

console.log(date.value); // Fri Jan 29 2016 00:01:20 GMT+0100 (CET)
date.value.setTime(0);
console.log(date.value); // Fri Jan 29 2016 00:01:20 GMT+0100 (CET)
date.value = null;       // fails silently
console.log(date.value); // Fri Jan 29 2016 00:01:20 GMT+0100 (CET)