ECMAScript ed 5.1 (aka ES5) required that ISO 8601 date strings be parsed as UTC, however ISO required them to be parsed as local. Now the current ECMAScript standard (version 6) is consistent with ISO and requires ISO compliant strings with no timezone to be parsed as local.
<update> ECMAScript 2016 (ed 7) changed again so that YYYY-MM-DD format date strings are parsed as UTC, so no longer consistent with ISO 8601.</update>.
So whether you get one behaviour or the other depends on the browser (and if you try it with IE 8 you'll get NaN). Prior to ES5, parsing of all strings was implementation dependent.
"2015-12-25" is a valid ISO 8601 string with no timezone, so should be treated as UTC under ES5 rules and local by ed 6 rules.
"2015-12-25 00:00:00" is not a valid ISO string (missing the "T" between the date and time) so browsers can parse it however they like (parsing of non–ISO strings is implementation dependent in all versions of ECMAScript).
The bottom line is do not use the Date constructor to parse strings (or Date.parse). Parse them yourself.
To parse ISO-like strings, use something like the following. It treats strings with not offset as UTC (per ES5), however perhaps that should now be changed to be local:
/**
* Parse an ISO string with or without an offset
* e.g. '2014-04-02T20:00:00-0600'
* '2014-04-02T20:00:00Z'
* '2014-02'
*
* Allows decimal seconds if supplied
* e.g. '2014-04-02T20:00:00.123-0600'
*
* If no offset is supplied (or it's Z), treat as UTC (per ECMA-262)
*
* If date only, e.g. '2014-04-02' or '2014-02', treat as UTC date (per ECMA-262)
* All parts after year are optional
* Don't allow two digit years to be converted to 20th century years
* @param {string} s - ISO 860 date string
*/
function parseISOString(s) {
var invalidDate = new Date(NaN);
var t = s.split(/\D+/g);
var hasOffset = /[-+]\d{4}$/.test(s);
// Whether decimal seconds are present changes the offset field and ms value
var hasDecimalSeconds = /[T ]\d{2}:\d{2}:\d{2}\.\d+/i.test(s);
var offset = hasDecimalSeconds? t[7] : t[6];
var offSign;
var yr = +t[0],
mo = t[1]? --t[1] : 0,
da = +t[2] || 1,
hr = +t[3] || 0,
min = +t[4] || 0,
sec = +t[5] || 0,
ms = hasDecimalSeconds? +t[6] : 0,
offSign = hasOffset? /-\d{4}$/.test(s)? 1 : -1 : 0,
offHr = hasOffset? offset/100 | 0 : 0,
offMin = hasOffset? offset%100 : 0;
// Ensure time values are in range, otherwise invalid date.
// Values can't be -ve as splitting on non-digit character
if (hr > 24 || min > 59 || sec > 59 || ms > 1000 || offHr > 24 || offMin > 59){
return invalidDate;
}
// Create a date object from date parts, check for validity
// Avoid two digit years being converted to 20th century
var d = new Date();
d.setUTCFullYear(yr, mo, da);
// Check that date values are valid
if (d.getUTCFullYear() != yr || d.getUTCDate() != da) {
return invalidDate;
}
// If there's an offset, apply it to minutes to get a UTC time value
min = hasOffset? +min + offSign * (offHr * 60 + offMin) : min;
// Set UTC time values of d
d.setUTCHours(hr, min, sec, ms);
return d;
}
It can be reduced to less than half that, but it's written to be easy to read and maintain.