node.js require() 奇怪的行为

node.js require() weird behavior

更新 - 我添加了更多信息。该问题似乎取决于 require 语句的顺序。不幸的是,修复一个会破坏另一个,请帮忙!!!

我不知道这里发生了什么。我有 5 个文件如下所示,我使用 index.js 只是为了演示这个问题。如果首先需要 HDate,则 HDate.make 在 HDateTime class 内失败。如果首先需要 HDateTime,则 HDateTime.make 在 HDate class 中失败。看来这可能是一个循环引用问题,但我一直无法弄清楚如何解决这个问题。

index.js - 在 DATETIME

失败
var HDate = require('./HDate');
var HDateTime = require('./HDateTime');
var HTimeZone = require('./HTimeZone');

var utc = HTimeZone.UTC;
console.log('DATE: ' + HDate.make(2011, 1, 2));
console.log('MIDNIGHT: ' + HDate.make(2011, 1, 2).midnight(utc));
console.log('DATETIME: ' + HDateTime.make(2011, 1, 2, 3, 4, 5, utc, 0));

ERROR OUTPUT:
C:\Users\Shawn\nodehaystack>node index.js
DATE: 2011-01-02
MIDNIGHT: 2011-01-02T00:00:00Z UTC
C:\Users\Shawn\nodehaystack\HDateTime.js:95
    return HDateTime.make(HDate.make(arg1, arg2, arg3), HTime.make(arg4, arg5,
                                ^
TypeError: undefined is not a function
    at Function.HDateTime.make (C:\Users\Shawn\nodehaystack\HDateTime.js:95:33)
    at Object.<anonymous> (C:\Users\Shawn\nodehaystack\index.js:8:38)
    at Module._compile (module.js:460:26)
    at Object.Module._extensions..js (module.js:478:10)
    at Module.load (module.js:355:32)
    at Function.Module._load (module.js:310:12)
    at Function.Module.runMain (module.js:501:10)
    at startup (node.js:129:16)
    at node.js:814:3

index.js - 在午夜失败

var HDateTime = require('./HDateTime');
var HDate = require('./HDate');
var HTimeZone = require('./HTimeZone');

var utc = HTimeZone.UTC;
console.log('DATE: ' + HDate.make(2011, 1, 2));
console.log('DATETIME: ' + HDateTime.make(2011, 1, 2, 3, 4, 5, utc, 0));
console.log('MIDNIGHT: ' + HDate.make(2011, 1, 2).midnight(utc));

ERROR OUTPUT:
C:\Users\Shawn\nodehaystack>node index.js
DATE: 2011-01-02
DATETIME: 2011-01-02T03:04:05Z UTC
C:\Users\Shawn\nodehaystack\HDate.js:46
HDate.prototype.midnight = function(tz) { return HDateTime.make(this, HTime.MI
                                                           ^
TypeError: undefined is not a function
    at HVal.HDate.midnight (C:\Users\Shawn\nodehaystack\HDate.js:46:60)
    at Object.<anonymous> (C:\Users\Shawn\nodehaystack\index.js:8:51)
    at Module._compile (module.js:460:26)
    at Object.Module._extensions..js (module.js:478:10)
    at Module.load (module.js:355:32)
    at Function.Module._load (module.js:310:12)
    at Function.Module.runMain (module.js:501:10)
    at startup (node.js:129:16)
    at node.js:814:3

HDate.js

var HVal = require('./HVal');
var HDateTime = require('./HDateTime');
var HTime = require('./HTime');

/** Private constructor */
function HDate(year, month, day) {
  /** Four digit year such as 2011 */
  this.year  = year;
  /** Month as 1-12 (Jan is 1, Dec is 12) */
  this.month = month;
  /** Day of month as 1-31 */
  this.day   = day;
};
HDate.prototype = Object.create(HVal.prototype);

/** int - Hash is based on year, month, day */
HDate.prototype.hashCode = function() { return (this.year << 16) ^ (this.month << 8) ^ this.day; };

/** String - Encode as "YYYY-MM-DD" */
HDate.prototype.toZinc = function() {
  var s = this.year + "-";
  if (this.month < 10) s += "0"; s += this.month + "-";
  if (this.day < 10)   s += "0"; s += this.day;
  return s;
};

/** boolean - Equals is based on year, month, day */
HDate.prototype.equals = function(that) { return that instanceof HDate && this.year === that.year && this.month === that.month && this.day === that.day; };

/** int - Return sort order as negative, 0, or positive */
HDate.prototype.compareTo = function(that) {
  if (this.year < that.year)   return -1; else if (this.year > that.year) return 1;
  if (this.month < that.month) return -1; else if (this.month > that.month) return 1;
  if (this.day < that.day)     return -1; else if (this.day > that.day)     return 1;
  return 0;
};

/** Convert this date into HDateTime for midnight in given timezone. */
HDate.prototype.midnight = function(tz) { return HDateTime.make(this, HTime.MIDNIGHT, tz); };

/** Return date in future given number of days */
HDate.prototype.plusDays = function(numDays) {
  if (numDays == 0) return this;
  if (numDays < 0) return this.minusDays(-numDays);
  var year  = this.year;
  var month = this.month;
  var day   = this.day;
  for (; numDays > 0; --numDays) {
    day++;
    if (day > HDate.daysInMonth(year, month)) {
      day = 1;
      month++;
      if (month > 12) { month = 1; year++; }
    }
  }
  return HDate.make(year, month, day);
};

/** Return date in past given number of days */
HDate.prototype.minusDays = function(numDays) {
  if (numDays == 0) return this;
  if (numDays < 0) return this.plusDays(-numDays);
  var year  = this.year;
  var month = this.month;
  var day   = this.day;
  for (; numDays > 0; --numDays) {
    day--;
    if (day <= 0) {
      month--;
      if (month < 1) { month = 12; year--; }
      day = HDate.daysInMonth(year, month);
    }
  }
  return HDate.make(year, month, day);
};

/** Return day of week: Sunday is 1, Saturday is 7 */
HDate.prototype.weekday = function() { return new Date(this.year, this.month-1, this.day).getDay() + 1; };

/** Export for use in testing */
module.exports = HDate;

/** Construct from basic fields, javascript Date instance, or String in format "YYYY-MM-DD" */
HDate.make = function(arg, month, day) {
  if (arg instanceof Date) {
    return new HDate(arg.getFullYear(), arg.getMonth() + 1, arg.getDate());
  } else if (HVal.typeis(arg, 'string', String)) {
    try {
      var s = arg.split('-');
      return new HDate(parseInt(s[0]), parseInt(s[1]), parseInt(s[2]));
    } catch(err) {
      throw err;
    }
  } else {
    if (arg < 1900) throw new Error("Invalid year");
    if (month < 1 || month > 12) throw new Error("Invalid month");
    if (day < 1 || day > 31) throw new Error("Invalid day");
    return new HDate(arg, month, day);
  }
};

/** Get HDate for current time in default timezone */
HDate.today = function() { return HDateTime.now().date; };

/** Return if given year a leap year */
HDate.isLeapYear = function(year) {
  if ((year & 3) != 0) return false;
  return (year % 100 != 0) || (year % 400 == 0);
};

/** Return number of days in given year (xxxx) and month (1-12) */
var daysInMon     = [ -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
var daysInMonLeap = [ -1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
HDate.daysInMonth = function(year, mon) { return HDate.isLeapYear(year) ? daysInMonLeap[mon] : daysInMon[mon]; };

HDateTime.js

var moment = require('moment-timezone');

var HVal = require('./HVal');
var HDate = require('./HDate');
var HTime = require('./HTime');
var HTimeZone = require('./HTimeZone');
// require('./io/HZincReader');

/** Private constructor */
function HDateTime(date, time, tz, tzOffset) {
  /** HDate - Date component of the timestamp */
  this.date     = date;
  /** HTime - Time component of the timestamp */
  this.time     = time;
  /** int - Offset in seconds from UTC including DST offset */
  this.tz       = tz;
  /** HTimeZone - Timezone as Olson database city name */
  this.tzOffset = (typeof(tzOffset) == 'undefined' ? 0 : tzOffset);
  /** long - millis since Epoch */
  this.millis;
}
HDateTime.prototype = Object.create(HVal.prototype);

/** long - Get this date time as Java milliseconds since epoch */
HDateTime.prototype.millis = function() {
  if (this.millis <= 0) {
    var d = new Date(this.date.year, this.date.month-1, this.date.day, this.time.hour, this.time.min, this.time.sec, this.time.ms);
//TODO: implement UTC timezone
    this.millis = d.getTime();
  }
  return this.millis;
};

/** int - Hash code is based on date, time, tzOffset, and tz */
HDateTime.prototype.hashCode = function() { return this.date.hashCode() ^ this.time.hashCode() ^ tzOffset ^ this.tz.hashCode(); };

/** String - Encode as "YYYY-MM-DD'T'hh:mm:ss.FFFz zzzz" */
HDateTime.prototype.toZinc = function() {
  var s = this.date.toZinc() + "T" + this.time.toZinc();

  if (this.tzOffset == 0) s += "Z";
  else {
    var offset = this.tzOffset;
    if (offset < 0) { s += "-"; offset = -offset; }
    else { s += "+"; }
    var zh = offset / 3600;
    var zm = (offset % 3600) / 60;
    if (zh < 10) s += "0"; s += zh + ":";
    if (zm < 10) s += "0"; s += zm;
  }
  s += " " + this.tz;
  return s;
};

/** boolean - Equals is based on date, time, tzOffset, and tz */
HDateTime.prototype.equals = function(that) {
  return that instanceof HDateTime && this.date === that.date 
  && this.time === that.time && this.tzOffset === that.tzOffset && this.tz === that.tz;
};

/** int - Comparison based on millis. */
HDateTime.prototype.compareTo = function(that) {
  var thisMillis = this.millis();
  var thatMillis = that.millis();
  if (thisMillis < thatMillis) return -1;
  else if (thisMillis > thatMillis) return 1;
  return 0;
};

/** Export for use in testing */
module.exports = HDateTime;

function utcDate(year, month, day, hour, min, sec, ms) {
  var d = new Date();
  d.setUTCFullYear(year);
  d.setUTCMonth(month-1);
  d.setUTCDate(day);
  d.setUTCHours(hour);
  d.setUTCMinutes(min);
  d.setUTCSeconds(sec);
  d.setUTCMilliseconds(ms);

  return d;
}

/** Construct from various values */
HDateTime.make = function(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) {
  if (arg7 instanceof HTimeZone) {
    return HDateTime.make(HDate.make(arg1, arg2, arg3), HTime.make(arg4, arg5, arg6), arg7, arg8);
  } else if (arg6 instanceof HTimeZone) {
    return HDateTime.make(HDate.make(arg1, arg2, arg3), HTime.make(arg4, arg5, 0), arg6, arg7);
  } else if (arg3 instanceof HTimeZone) {
    // use Date to decode millis to fields
    var d = utcDate(arg1.year, arg1.month, arg1.day, arg2.hour, arg2.min, arg2.sec, arg2.ms);
    var tzOffset = arg4;
    if (typeof(tzOffset) == 'undefined') {
      // convert to designated timezone
      d = moment(d).tz(arg3.js.name);
      tzOffset = d.utcOffset() * 60;
    }

    var ts = new HDateTime(arg1, arg2, arg3, tzOffset);
    ts.millis = d.valueOf() + (tzOffset * -1000);

    return ts;
  } else if (this.typeis(arg1, 'string', String)) {
// TODO: Implemet parsing
//    var val = new HZincReader(arg1).readScalar();
//    if (typeof(val) == HDateTime) return val;
//    throw new Error("Parse Error: " + arg1);
  } else {
    var tz = arg2;
    if (typeof(tz) == 'undefined') tz = HTimeZone.DEFAULT;

    var d = new Date(arg1);
    // convert to designated timezone
    d = moment(d).tz(tz.js.name);
    tzOffset = d.utcOffset() * 60;

    var ts = new HDateTime(HDate.make(d), HTime.make(d), tz, tz.getOffset());
    ts.millis = arg1;
  }
};

/** HDateTime - Get HDateTime for given timezone */
HDateTime.now = function(tz) { return make(new Date().getTime(), tz); };

HTime.js

var HVal = require('./HVal');
// require('./io/HZincReader');

/** Private constructor */
function HTime(hour, min, sec, ms) { 
  /** int - Hour of day as 0-23 */
  this.hour = hour;
  /** int - Minute of hour as 0-59 */
  this.min  = min;
  /** int - Second of minute as 0-59 */
  this.sec  = (typeof(sec) == 'undefined' ? 0 : sec);
  /** int - Fractional seconds in milliseconds 0-999 */
  this.ms  = (typeof(ms) == 'undefined' ? 0 : ms);
};
HTime.prototype = Object.create(HVal.prototype);

/** int - Hash code is based on hour, min, sec, ms */
HTime.prototype.hashCode = function() { return (this.hour << 24) ^ (this.min << 20) ^ (this.sec << 16) ^ this.ms; };

/** boolean - Equals is based on hour, min, sec, ms */
HTime.prototype.equals = function(that) { 
  return that instanceof HTime && this.hour === that.hour && 
      this.min === that.min && this.sec === that.sec && this.ms === that.ms;
};

/** int - Return sort order as negative, 0, or positive */
HTime.prototype.compareTo = function(that) {
  if (this.hour < that.hour) return -1; else if (this.hour > that.hour) return 1;
  if (this.min < that.min)   return -1; else if (this.min > that.min)   return 1;
  if (this.sec < that.sec)   return -1; else if (this.sec > that.sec)   return 1;
  if (this.ms < that.ms)     return -1; else if (this.ms > that.ms)     return 1;

  return 0;
};

/** String - Encode as "hh:mm:ss.FFF" */
HTime.prototype.toZinc = function() {
  var s = "";
  if (this.hour < 10) s += "0"; s += this.hour + ":";
  if (this.min  < 10) s += "0"; s += this.min + ":";
  if (this.sec  < 10) s += "0"; s += this.sec;
  if (this.ms != 0) {
    s += ".";
    if (this.ms < 10) s += "0";
    if (this.ms < 100) s += "0";
    s += this.ms;
  }

  return s;
};

/** Export for use in testing */
module.exports = HTime;

/** Singleton for midnight 00:00 */
HTime.MIDNIGHT = new HTime(0, 0, 0, 0);

/** Construct with all fields, with Javascript Date object, or Parse from string fomat "hh:mm:ss.FF" */
HTime.make = function(arg1, min, sec, ms) {
  if (HVal.typeis(arg1, 'string', String)) {
// TODO: Implemet parsing
//    var val = new HZincReader(arg1).readScalar();
//    if (val instanceof HTime) return val;
//    throw new Error("Parse Error: " + arg1);
  } else if (arg1 instanceof Date) {
    return new HTime(arg1.getHours(), arg1.getMinutes(), arg1.getSeconds(), arg1.getMilliseconds());
  } else {
    return new HTime(arg1, min, sec, ms);
  }
};

HTimeZone.js

var moment = require('moment-timezone');
var HVal = require('./HVal');

/** Package private constructor */
function HTimeZone(name, js) {
  /** Haystack timezone name */
  this.name = name;
  /** Javascript (moment) representation of this timezone. */
  this.js = js;
};

/** String - Return Haystack timezone name */
HTimeZone.prototype.toString = function() { return this.name; };

module.exports = HTimeZone;

HTimeZone.make = function(arg1, checked) {
  if (typeof(checked) == 'undefined') return HTimeZone.make(arg1, true);

  if (HVal.typeis(arg1, 'string', String)) {
    /**
     * Construct with Haystack timezone name, raise exception or
     * return null on error based on check flag.
     */
    // lookup in cache
    var tz = cache[arg1];
    if (typeof(tz) != 'undefined') return tz;

    // map haystack id to Javascript full id
    var jsId = toJS[arg1];
    if (typeof(jsId) == 'undefined') {
      if (checked) throw new Error("Unknown tz: " + arg1);
      return undefined;
    }

    // resolve full id to HTimeZone and cache
    var js = moment.tz.zone(jsId)
    tz = new HTimeZone(arg1, js);
    cache[arg1] = tz;
    return tz;
  } else {
    /**
     * Construct from Javascript timezone.  Throw exception or return
     * null based on checked flag.
     */

    var jsId = arg1.name;
    if (jsId.startsWith("GMT")) fixGMT(jsId);

    var name = fromJS[jsId];
    if (typeof(name) != 'undefined') return HTimeZone.make(name);
    if (checked) throw new Error("Invalid Java timezone: " + arg1.name);
    return;
  }
};

function fixGMT(jsId) {
  // Javscript (moment) IDs can be in the form "GMT[+,-]h" as well as
  // "GMT", and "GMT0".  In that case, convert the ID to "Etc/GMT[+,-]h".
  // V8 uses the format "GMT[+,-]hh00 (used for default timezone), this also
  // needs converted.

  if (jsId.indexOf("+")<0 && jsId.indexOf("-")<0) return "Etc/" + jsId; // must be "GMT" or "GMT0" which are fine

  // get the prefix
  var pre = jsId.substring(0, 4);
  var num = parseInt(jsId.substring(4, 6));

  // ensure we have a valid value
  if ((pre.substring(3)=="+" && num<13) || (pre.substring(3)=="-" & num<15)) return "Etc/" + pre + num;

  // nothing we could do, return what was passed
  return jsId;
}

var cache = {};
var toJS = {};
var fromJS = {};
{
  try {
    // only time zones which start with these
    // regions are considered valid timezones
    var regions = {};
    regions["Africa"]     = "ok";
    regions["America"]    = "ok";
    regions["Antarctica"] = "ok";
    regions["Asia"]       = "ok";
    regions["Atlantic"]   = "ok";
    regions["Australia"]  = "ok";
    regions["Etc"]        = "ok";
    regions["Europe"]     = "ok";
    regions["Indian"]     = "ok";
    regions["Pacific"]    = "ok";

    // iterate Javascript timezone IDs available
    var ids = moment.tz.names();

    for (var i=0; i<ids.length; ++i) {
      var js = ids[i];

      // skip ids not formatted as Region/City
      var slash = js.indexOf('/');
      if (slash < 0) continue;
      var region = js.substring(0, slash);
      if (typeof(regions[region]) == 'undefined') continue;

      // get city name as haystack id
      slash = js.lastIndexOf('/');
      var haystack = js.substring(slash+1);

      // store mapping b/w Javascript <-> Haystack

      toJS[haystack] = js;
      fromJS[js] = haystack;
    }
  } catch (err) {
    console.log(err.stack);
  }

  var utc;
  try {
    utc = HTimeZone.make(moment.tz.zone("Etc/UTC"));
  } catch (err) {
    console.log(err.stack);
  }

  var def;
  try {
    // check if configured with system property
    var defName = process.env["haystack.tz"];
    if (defName != null) {
      def = make(defName, false);
      if (typeof(def) == 'undefined') console.log("WARN: invalid haystack.tz system property: " + defName);
    }

    // if we still don't have a default, try to use Javascript's
    if (typeof(def) == 'undefined') {
      var date = new Date().toString();
      var gmtStart = date.indexOf("GMT");
      var gmtEnd = date.indexOf(" ", gmtStart);
      def = fromJS[fixGMT(date.substring(gmtStart, gmtEnd))];
    }
  } catch (err) {
    console.log(err.stack);
    def = utc;
  }

  /** UTC timezone */
  HTimeZone.UTC = utc;

  /** Default timezone for VM */
  HTimeZone.DEFAULT = def;
}

问题在于如何导出函数。

你说 module.exports = HDate 这很好 但后来你做了 module.exports.make = function(){} 这也很好。 但是两者一起工作不会像您刚刚体验的那样可靠。

要修复该问题,可以将 class 导出为 module.exports.hdate 或将 make 函数添加到 HDate:

Hdate.make = myFunction() {}

这显然适用于您像这样导出的所有方法。

如果这能解决您的问题,请告诉我:)

我发现这个问题的最佳解决方案是移动所有可能导致循环依赖的 require 语句,以确保它们在我的 class 的构造函数之后和 module.exports 之后陈述。例如...

var HVal = require('./HVal');

/**
 * HDate models a date (day in year) tag value.
 * @see {@link http://project-haystack.org/doc/TagModel#tagKinds|Project     Haystack}
 *
 * @constructor
 * @private
 * @extends {HVal}
 * @param {int} year - Four digit year such as 2011
 * @param {int} month - Month as 1-12 (Jan is 1, Dec is 12)
 * @param {int} day - Day of month as 1-31
 */
function HDate(year, month, day) {
  this.year = year;
  this.month = month;
  this.day = day;
}
HDate.prototype = Object.create(HVal.prototype);
module.exports = HDate;

var HDateTime = require('./HDateTime'),
    HTime = require('./HTime');

在我的所有 classes 中始终如一地执行此方法可以防止任何周期性问题。