JavaScript: переопределить Date.prototype.constructor

Я хотел бы изменить поведение стандартного объекта Date. Годы между 0..99, переданные конструктору, должны интерпретироваться как fullYear (без добавления 1900). Но моя следующая функция не работает

var oDateConst = Date.prototype.constructor; // save old contructor

Date.prototype.constructor = function () {
    var d = oDateConst.apply(oDateConst, arguments); // create object with it
    if ( ((arguments.length == 3) || (arguments.length == 6))
        && ((arguments[0] < 100) && (arguments[0] >= 0))) {
        d.setFullYear(arguments[0]);
    }
    return d;
}

Почему он никогда не вызван? Как бы вы решили эту проблему?

Ответ 1

Причина, по которой она никогда не вызвана, заключается в том, что вы изменяете свойство constructor на Date.prototype. Однако вы, вероятно, все еще создаете дату, используя код new Date(). Поэтому он никогда не использует ваш конструктор. Вы действительно хотите создать свой собственный конструктор Date:

function MyDate() {
    var d = Date.apply(Date, arguments);
    if ((arguments.length == 3 || arguments.length == 6)
        && (arguments[0] < 100 && arguments[0] >= 0)) {
        d.setFullYear(arguments[0]);
    return d;
}

Затем вы можете создать свою новую дату следующим образом:

var d = MyDate();

Изменить: Вместо использования Date.apply я бы предпочел использовать следующую функцию instantiate, которая позволяет применять аргументы к функции конструктора:

var bind = Function.bind;
var unbind = bind.bind(bind);

function instantiate(constructor, args) {
    return new (unbind(constructor, null).apply(null, args));
}

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

function myDate() {
    var date = instantiate(Date, arguments);
    var args = arguments.length;
    var arg = arguments[0];

    if ((args === 3 || args == 6) && arg < 100 && arg >= 0)
        date.setFullYear(arg);
    return date;
}

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

Date = function (Date) {
    MyDate.prototype = Date.prototype;

    return MyDate;

    function MyDate() {
        var date = instantiate(Date, arguments);
        var args = arguments.length;
        var arg = arguments[0];

        if ((args === 3 || args == 6) && arg < 100 && arg >= 0)
            date.setFullYear(arg);
        return date;
    }
}(Date);

Ответ 2

Piggybacking на переопределении конструктора даты Aadit M Shah - это должен быть ответ, но у меня недостаточно SO rep для этого - как упоминалось в @sakthi, вы потеряете свои собственные методы Date, сделав это именно так. Это засасывает бит, потому что методы Date неперечислимы, поэтому вам нужно реализовать немного взлома, чтобы клонировать их.

Во-первых, я бы порекомендовал не делать этого так, как вам нужно. В моем случае я работал в приложении с кучей устаревшего кода, который строит даты, используя формат "m-d-yyyy", который работает в хроме, но не сафари. Я не мог просто найти/заменить приложение, потому что было много случаев, когда строки даты вытаскивались из уровня сервиса или базы данных. Поэтому я решил переопределить конструктор Date в случае, когда в формате "m-d-yyyy" присутствует аргумент datestring. Я хотел, чтобы это было как можно минимально-инвазивным, так что оно функционирует как обычная дата в противном случае.

Вот мои изменения - это должно позволить вам переопределить дату с некоторыми изменениями в конструкторе, но все остальное одинаково. Вы захотите изменить конструктор MyDate до того, как экземпляр будет вызван, чтобы делать то, что вы хотите обработать конструктор. Это произойдет до того, как будет применен конструктор даты Date.

var bind = Function.bind;
var unbind = bind.bind(bind);

function instantiate(constructor, args) {
    return new (unbind(constructor, null).apply(null, args));
}

Date = function (Date) {

    // copy date methods - this is a pain in the butt because they're mostly nonenumerable
    // Get all own props, even nonenumerable ones
    var names = Object.getOwnPropertyNames(Date);
    // Loop through them
    for (var i = 0; i < names.length; i++) {
        // Skip props already in the MyDate object
        if (names[i] in MyDate) continue;
        // Get property description from o
        var desc = Object.getOwnPropertyDescriptor(Date, names[i]);
        // Use it to create property on MyDate
        Object.defineProperty(MyDate, names[i], desc);
    }

    return MyDate;

    function MyDate() {
        // we only care about modifying the constructor if a datestring is passed in
        if (arguments.length === 1 && typeof (arguments[0]) === 'string') {
            // if you're adding other date transformations, add them here

            // match dates of format m-d-yyyy and convert them to cross-browser-friendly m/d/yyyy
            var mdyyyyDashRegex = /(\d{1,2})-(\d{1,2})-(\d{4})/g;
            arguments[0] = arguments[0].replace(mdyyyyDashRegex, function (match, p1, p2, p3) {
                return p1 + "/" + p2 + "/" + p3;
            });
        }

        // call the original Date constructor with whatever arguments are passed in here
        var date = instantiate(Date, arguments);

        return date;
    }
}(Date);

ссылки:

Ответ 3

В отношении техники, упомянутой в статье Мэтью Альберта, помимо точки, которую опубликовал Дэн Хлавенка, есть еще один сценарий тестирования, который терпит неудачу. См. Следующий код:

typeof Date() == typeof new Date()     //Should be false, but it returns true

В устаревшем проекте есть вероятность того, что описанный выше сценарий может сломать несколько сценариев. Помимо вышеизложенного и того, что указал Дэн Хлавенка, я согласен, что это наиболее полное решение до сих пор.

Ответ 4

Вот решение, которое очень гибкое. Он обрабатывает (я считаю) все разные случаи.

DateStub - позволяет заглушить функцию даты.
Если у вас есть новая дата (...).... ", и вы хотите проверить его, это для вас. Это также работает с "моментами".

/**
 * DateStub - Allows for stubbing out the Date function.  If you have
 *      'new Date(...)....' sprinkled throughout your code,
 *      and you want to test it, this is for you.
 *
 * @param {string} arguments Provide 0 to any number of dates in string format.
 *
 * @return a date object corresponding to the arguments passed in.
 *      If you pass only one date in, this will be used by all 'new Date()' calls.
 *      This also provides support for 'Date.UTC()', 'Date.now()', 'Date.parse()'.
 *      Also, this works with the moments library.
 *
 * Examples:
    {   // Test with 1 argument
        Date = DateStub('1/2/2033');        // Override 'Date'

        expect(new Date().toString())
            .toEqual('Sun Jan 02 2033 00:00:00 GMT-0500 (EST)');
        expect(new Date().toString())
            .toEqual('Sun Jan 02 2033 00:00:00 GMT-0500 (EST)');

        Date = DateStub.JavaScriptDate;     // Reset 'Date'
    }
    {   // Call subsequent arguments, each time 'new Date()' is called
        Date = DateStub('1/2/1111', '1/2/1222'
                        , '1/2/3333', '1/2/4444');  // Override 'Date'

        expect(new Date().toString())
            .toEqual('Mon Jan 02 1111 00:00:00 GMT-0500 (EST)');
        expect(new Date().toString())
            .toEqual('Sun Jan 02 1222 00:00:00 GMT-0500 (EST)');
        expect(new Date().toString())
            .toEqual('Fri Jan 02 3333 00:00:00 GMT-0500 (EST)');
        expect(new Date().toString())
            .toEqual('Sat Jan 02 4444 00:00:00 GMT-0500 (EST)');

        Date = DateStub.JavaScriptDate;     // Reset 'Date'
    }
    {   // Test 'Date.now()'.  You can also use: 'Date.UTC()', 'Date.parse()'
        Date = DateStub('1/2/2033');

        expect(new Date(Date.now()).toString())
                .toEqual('Sun Jan 02 2033 00:00:00 GMT-0500 (EST)');

        Date = DateStub.JavaScriptDate;     // Reset 'Date'
    }
 *
 * For more info:  [email protected]
 */

const DateStub =
    function () {
        function CustomDate(date) {
            if (!!date) { return new DateStub.JavaScriptDate(date); }
            return getNextDate();
        };
        function getNextDate() {
            return dates[index >= length ? index - 1 : index++];
        };

        if (Date.name === 'Date') {
            DateStub.prototype = Date.prototype;
            DateStub.JavaScriptDate = Date;

            // Add Date.* methods.
            CustomDate.UTC = Date.UTC;
            CustomDate.parse = Date.parse;
            CustomDate.now = getNextDate;
        }

        var dateArguments = (arguments.length === 0)
            ? [(new DateStub.JavaScriptDate()).toString()] : arguments
            , length = dateArguments.length
            , index = 0
            , dates = [];
        for (var i = 0; i < length; i++) {
            dates.push(new DateStub.JavaScriptDate(dateArguments[i]));
        }

        return CustomDate;
    };

module.exports = DateStub;

// If you have a test file, and are using node:
// Add this to the top:  const DateStub = require('./DateStub');