Is there a largest array length for which this technique is guaranteed
  to work? Might the answer be different in a WebWorker, since background threads
  often have smaller stack sizes?
To help answer your second (2) questions in a particular environment, here's a script you can run that uses a binary search to figure out the answer:
(function () {
  // Returns whether or not the action threw an exception.
  function throwsException(action) {
    try {
      action();
    } catch (e) {
      return true;
    }
    return false;
  }
  // Performs the action for various values between lower and upper and returns
  // the maximum value for the action to return true.
  function findMaxValueForAction(action, lower, upper) {
    var best;
    while (upper - lower > 1) {
      var previousUpper = upper;
      var guess = Math.floor((lower + upper) / 2);
      if (action(guess)) {
        // If the action is successful then the lower needs to be updated to what just succeeded. 
        lower = guess;
        // Is the (successful) lower better than the current best?
        if (best === undefined || lower > best) {
          // If so update the current best.
          best = lower;
        }
      } else {
        // If the action was unsuccessful the new upper bound is 1 less than what we just tried.
        upper = guess - 1;
      }
    }
    return best;
  }
  var maxArraySize = findMaxValueForAction(function (value) {    
    return !throwsException(function () {
      var array = new Array(value);
    });
  }, 0, Number.MAX_SAFE_INTEGER);
  var maxArrayApplySize = findMaxValueForAction(function (value) {
    return !throwsException(function () {
      var array = new Array(value);
      Math.max.apply(null, array);
    });
  }, 0, maxArraySize);
  return [
    'Max "value" for "new Array(value)": ' + maxArraySize, 
    'Max "value" for "Math.max.apply(null, new Array(value))": ' + maxArrayApplySize
  ];
})().forEach(function(message) { console.log(message) });