Down integrate to GitHub
diff --git a/js/binary/decoder.js b/js/binary/decoder.js
index 45cf611..e0cd12e 100644
--- a/js/binary/decoder.js
+++ b/js/binary/decoder.js
@@ -44,7 +44,6 @@
  */
 
 goog.provide('jspb.BinaryDecoder');
-goog.provide('jspb.BinaryIterator');
 
 goog.require('goog.asserts');
 goog.require('goog.crypt');
@@ -53,164 +52,6 @@
 
 
 /**
- * Simple helper class for traversing the contents of repeated scalar fields.
- * that may or may not have been packed into a wire-format blob.
- * @param {?jspb.BinaryDecoder=} opt_decoder
- * @param {?function(this:jspb.BinaryDecoder):(number|boolean|string)=}
- *     opt_next The decoder method to use for next().
- * @param {?Array<number|boolean|string>=} opt_elements
- * @constructor
- * @struct
- */
-jspb.BinaryIterator = function(opt_decoder, opt_next, opt_elements) {
-  /** @private {?jspb.BinaryDecoder} */
-  this.decoder_ = null;
-
-  /**
-   * The BinaryDecoder member function used when iterating over packed data.
-   * @private {?function(this:jspb.BinaryDecoder):(number|boolean|string)}
-   */
-  this.nextMethod_ = null;
-
-  /** @private {?Array<number|boolean|string>} */
-  this.elements_ = null;
-
-  /** @private {number} */
-  this.cursor_ = 0;
-
-  /** @private {number|boolean|string|null} */
-  this.nextValue_ = null;
-
-  /** @private {boolean} */
-  this.atEnd_ = true;
-
-  this.init_(opt_decoder, opt_next, opt_elements);
-};
-
-
-/**
- * @param {?jspb.BinaryDecoder=} opt_decoder
- * @param {?function(this:jspb.BinaryDecoder):(number|boolean|string)=}
- *     opt_next The decoder method to use for next().
- * @param {?Array<number|boolean|string>=} opt_elements
- * @private
- */
-jspb.BinaryIterator.prototype.init_ =
-    function(opt_decoder, opt_next, opt_elements) {
-  if (opt_decoder && opt_next) {
-    this.decoder_ = opt_decoder;
-    this.nextMethod_ = opt_next;
-  }
-  this.elements_ = opt_elements || null;
-  this.cursor_ = 0;
-  this.nextValue_ = null;
-  this.atEnd_ = !this.decoder_ && !this.elements_;
-
-  this.next();
-};
-
-
-/**
- * Global pool of BinaryIterator instances.
- * @private {!Array<!jspb.BinaryIterator>}
- */
-jspb.BinaryIterator.instanceCache_ = [];
-
-
-/**
- * Allocates a BinaryIterator from the cache, creating a new one if the cache
- * is empty.
- * @param {?jspb.BinaryDecoder=} opt_decoder
- * @param {?function(this:jspb.BinaryDecoder):(number|boolean|string)=}
- *     opt_next The decoder method to use for next().
- * @param {?Array<number|boolean|string>=} opt_elements
- * @return {!jspb.BinaryIterator}
- */
-jspb.BinaryIterator.alloc = function(opt_decoder, opt_next, opt_elements) {
-  if (jspb.BinaryIterator.instanceCache_.length) {
-    var iterator = jspb.BinaryIterator.instanceCache_.pop();
-    iterator.init_(opt_decoder, opt_next, opt_elements);
-    return iterator;
-  } else {
-    return new jspb.BinaryIterator(opt_decoder, opt_next, opt_elements);
-  }
-};
-
-
-/**
- * Puts this instance back in the instance cache.
- */
-jspb.BinaryIterator.prototype.free = function() {
-  this.clear();
-  if (jspb.BinaryIterator.instanceCache_.length < 100) {
-    jspb.BinaryIterator.instanceCache_.push(this);
-  }
-};
-
-
-/**
- * Clears the iterator.
- */
-jspb.BinaryIterator.prototype.clear = function() {
-  if (this.decoder_) {
-    this.decoder_.free();
-  }
-  this.decoder_ = null;
-  this.nextMethod_ = null;
-  this.elements_ = null;
-  this.cursor_ = 0;
-  this.nextValue_ = null;
-  this.atEnd_ = true;
-};
-
-
-/**
- * Returns the element at the iterator, or null if the iterator is invalid or
- * past the end of the decoder/array.
- * @return {number|boolean|string|null}
- */
-jspb.BinaryIterator.prototype.get = function() {
-  return this.nextValue_;
-};
-
-
-/**
- * Returns true if the iterator is at the end of the decoder/array.
- * @return {boolean}
- */
-jspb.BinaryIterator.prototype.atEnd = function() {
-  return this.atEnd_;
-};
-
-
-/**
- * Returns the element at the iterator and steps to the next element,
- * equivalent to '*pointer++' in C.
- * @return {number|boolean|string|null}
- */
-jspb.BinaryIterator.prototype.next = function() {
-  var lastValue = this.nextValue_;
-  if (this.decoder_) {
-    if (this.decoder_.atEnd()) {
-      this.nextValue_ = null;
-      this.atEnd_ = true;
-    } else {
-      this.nextValue_ = this.nextMethod_.call(this.decoder_);
-    }
-  } else if (this.elements_) {
-    if (this.cursor_ == this.elements_.length) {
-      this.nextValue_ = null;
-      this.atEnd_ = true;
-    } else {
-      this.nextValue_ = this.elements_[this.cursor_++];
-    }
-  }
-  return lastValue;
-};
-
-
-
-/**
  * BinaryDecoder implements the decoders for all the wire types specified in
  * https://developers.google.com/protocol-buffers/docs/encoding.
  *
@@ -483,6 +324,32 @@
 
 
 /**
+ * Reads a signed zigzag encoded varint from the binary stream and invokes
+ * the conversion function with the value in two signed 32 bit integers to
+ * produce the result. Since this does not convert the value to a number, no
+ * precision is lost.
+ *
+ * It's possible for an unsigned varint to be incorrectly encoded - more than
+ * 64 bits' worth of data could be present. If this happens, this method will
+ * throw an error.
+ *
+ * Zigzag encoding is a modification of varint encoding that reduces the
+ * storage overhead for small negative integers - for more details on the
+ * format, see https://developers.google.com/protocol-buffers/docs/encoding
+ *
+ * @param {function(number, number): T} convert Conversion function to produce
+ *     the result value, takes parameters (lowBits, highBits).
+ * @return {T}
+ * @template T
+ */
+jspb.BinaryDecoder.prototype.readSplitZigzagVarint64 = function(convert) {
+  return this.readSplitVarint64(function(low, high) {
+    return jspb.utils.fromZigzag64(low, high, convert);
+  });
+};
+
+
+/**
  * Reads a 64-bit fixed-width value from the stream and invokes the conversion
  * function with the value in two signed 32 bit integers to produce the result.
  * Since this does not convert the value to a number, no precision is lost.
@@ -732,8 +599,24 @@
 
 
 /**
+ * Reads a signed, zigzag-encoded 64-bit varint from the binary stream
+ * losslessly and returns it as an 8-character Unicode string for use as a hash
+ * table key.
+ *
+ * Zigzag encoding is a modification of varint encoding that reduces the
+ * storage overhead for small negative integers - for more details on the
+ * format, see https://developers.google.com/protocol-buffers/docs/encoding
+ *
+ * @return {string} The decoded zigzag varint in hash64 format.
+ */
+jspb.BinaryDecoder.prototype.readZigzagVarintHash64 = function() {
+  return this.readSplitZigzagVarint64(jspb.utils.joinHash64);
+};
+
+
+/**
  * Reads a signed, zigzag-encoded 64-bit varint from the binary stream and
- * returns its valud as a string.
+ * returns its value as a string.
  *
  * Zigzag encoding is a modification of varint encoding that reduces the
  * storage overhead for small negative integers - for more details on the
@@ -743,9 +626,7 @@
  * string.
  */
 jspb.BinaryDecoder.prototype.readZigzagVarint64String = function() {
-  // TODO(haberman): write lossless 64-bit zig-zag math.
-  var value = this.readZigzagVarint64();
-  return value.toString();
+  return this.readSplitZigzagVarint64(jspb.utils.joinSignedDecimalString);
 };
 
 
diff --git a/js/binary/decoder_test.js b/js/binary/decoder_test.js
index c5be805..393f2f7 100644
--- a/js/binary/decoder_test.js
+++ b/js/binary/decoder_test.js
@@ -235,6 +235,95 @@
     });
   });
 
+  describe('sint64', function() {
+    var /** !jspb.BinaryDecoder */ decoder;
+
+    var hashA =
+        String.fromCharCode(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
+    var hashB =
+        String.fromCharCode(0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
+    var hashC =
+        String.fromCharCode(0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21);
+    var hashD =
+        String.fromCharCode(0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF);
+    beforeEach(function() {
+      var encoder = new jspb.BinaryEncoder();
+
+      encoder.writeZigzagVarintHash64(hashA);
+      encoder.writeZigzagVarintHash64(hashB);
+      encoder.writeZigzagVarintHash64(hashC);
+      encoder.writeZigzagVarintHash64(hashD);
+
+      decoder = jspb.BinaryDecoder.alloc(encoder.end());
+    });
+
+    it('reads 64-bit integers as decimal strings', function() {
+      const signed = true;
+      expect(decoder.readZigzagVarint64String())
+          .toEqual(jspb.utils.hash64ToDecimalString(hashA, signed));
+      expect(decoder.readZigzagVarint64String())
+          .toEqual(jspb.utils.hash64ToDecimalString(hashB, signed));
+      expect(decoder.readZigzagVarint64String())
+          .toEqual(jspb.utils.hash64ToDecimalString(hashC, signed));
+      expect(decoder.readZigzagVarint64String())
+          .toEqual(jspb.utils.hash64ToDecimalString(hashD, signed));
+    });
+
+    it('reads 64-bit integers as hash strings', function() {
+      expect(decoder.readZigzagVarintHash64()).toEqual(hashA);
+      expect(decoder.readZigzagVarintHash64()).toEqual(hashB);
+      expect(decoder.readZigzagVarintHash64()).toEqual(hashC);
+      expect(decoder.readZigzagVarintHash64()).toEqual(hashD);
+    });
+
+    it('reads split 64 bit zigzag integers', function() {
+      function hexJoin(bitsLow, bitsHigh) {
+        return `0x${(bitsHigh >>> 0).toString(16)}:0x${
+            (bitsLow >>> 0).toString(16)}`;
+      }
+      function hexJoinHash(hash64) {
+        jspb.utils.splitHash64(hash64);
+        return hexJoin(jspb.utils.split64Low, jspb.utils.split64High);
+      }
+
+      expect(decoder.readSplitZigzagVarint64(hexJoin))
+          .toEqual(hexJoinHash(hashA));
+      expect(decoder.readSplitZigzagVarint64(hexJoin))
+          .toEqual(hexJoinHash(hashB));
+      expect(decoder.readSplitZigzagVarint64(hexJoin))
+          .toEqual(hexJoinHash(hashC));
+      expect(decoder.readSplitZigzagVarint64(hexJoin))
+          .toEqual(hexJoinHash(hashD));
+    });
+
+    it('does zigzag encoding properly', function() {
+      // Test cases direcly from the protobuf dev guide.
+      // https://engdoc.corp.google.com/eng/howto/protocolbuffers/developerguide/encoding.shtml?cl=head#types
+      var testCases = [
+        {original: '0', zigzag: '0'},
+        {original: '-1', zigzag: '1'},
+        {original: '1', zigzag: '2'},
+        {original: '-2', zigzag: '3'},
+        {original: '2147483647', zigzag: '4294967294'},
+        {original: '-2147483648', zigzag: '4294967295'},
+        // 64-bit extremes, not in dev guide.
+        {original: '9223372036854775807', zigzag: '18446744073709551614'},
+        {original: '-9223372036854775808', zigzag: '18446744073709551615'},
+      ];
+      var encoder = new jspb.BinaryEncoder();
+      testCases.forEach(function(c) {
+        encoder.writeZigzagVarint64String(c.original);
+      });
+      var buffer = encoder.end();
+      var zigzagDecoder = jspb.BinaryDecoder.alloc(buffer);
+      var varintDecoder = jspb.BinaryDecoder.alloc(buffer);
+      testCases.forEach(function(c) {
+        expect(zigzagDecoder.readZigzagVarint64String()).toEqual(c.original);
+        expect(varintDecoder.readUnsignedVarint64String()).toEqual(c.zigzag);
+      });
+    });
+  });
+
   /**
    * Tests reading and writing large strings
    */
diff --git a/js/binary/encoder.js b/js/binary/encoder.js
index b2013f6..0b48e05 100644
--- a/js/binary/encoder.js
+++ b/js/binary/encoder.js
@@ -232,13 +232,27 @@
  * @param {string} value The integer to convert.
  */
 jspb.BinaryEncoder.prototype.writeZigzagVarint64String = function(value) {
-  // TODO(haberman): write lossless 64-bit zig-zag math.
-  this.writeZigzagVarint64(parseInt(value, 10));
+  this.writeZigzagVarintHash64(jspb.utils.decimalStringToHash64(value));
 };
 
 
 /**
- * Writes a 8-bit unsigned integer to the buffer. Numbers outside the range
+ * Writes a 64-bit hash string (8 characters @ 8 bits of data each) to the
+ * buffer as a zigzag varint.
+ * @param {string} hash The hash to write.
+ */
+jspb.BinaryEncoder.prototype.writeZigzagVarintHash64 = function(hash) {
+  var self = this;
+  jspb.utils.splitHash64(hash);
+  jspb.utils.toZigzag64(
+      jspb.utils.split64Low, jspb.utils.split64High, function(lo, hi) {
+        self.writeSplitVarint64(lo >>> 0, hi >>> 0);
+      });
+};
+
+
+/**
+ * Writes an 8-bit unsigned integer to the buffer. Numbers outside the range
  * [0,2^8) will be truncated.
  * @param {number} value The value to write.
  */
@@ -294,7 +308,7 @@
 
 
 /**
- * Writes a 8-bit integer to the buffer. Numbers outside the range
+ * Writes an 8-bit integer to the buffer. Numbers outside the range
  * [-2^7,2^7) will be truncated.
  * @param {number} value The value to write.
  */
diff --git a/js/binary/reader.js b/js/binary/reader.js
index d1ab407..9e193ac 100644
--- a/js/binary/reader.js
+++ b/js/binary/reader.js
@@ -49,6 +49,7 @@
 goog.require('goog.asserts');
 goog.require('jspb.BinaryConstants');
 goog.require('jspb.BinaryDecoder');
+goog.require('jspb.utils');
 
 
 
@@ -941,7 +942,7 @@
 
 
 /**
- * Reads a 64-bit varint or fixed64 field from the stream and returns it as a
+ * Reads a 64-bit varint or fixed64 field from the stream and returns it as an
  * 8-character Unicode string for use as a hash table key, or throws an error
  * if the next field in the stream is not of the correct wire type.
  *
@@ -955,6 +956,20 @@
 
 
 /**
+ * Reads an sint64 field from the stream and returns it as an 8-character
+ * Unicode string for use as a hash table key, or throws an error if the next
+ * field in the stream is not of the correct wire type.
+ *
+ * @return {string} The hash value.
+ */
+jspb.BinaryReader.prototype.readSintHash64 = function() {
+  goog.asserts.assert(
+      this.nextWireType_ == jspb.BinaryConstants.WireType.VARINT);
+  return this.decoder_.readZigzagVarintHash64();
+};
+
+
+/**
  * Reads a 64-bit varint field from the stream and invokes `convert` to produce
  * the return value, or throws an error if the next field in the stream is not
  * of the correct wire type.
@@ -972,6 +987,25 @@
 
 
 /**
+ * Reads a 64-bit zig-zag varint field from the stream and invokes `convert` to
+ * produce the return value, or throws an error if the next field in the stream
+ * is not of the correct wire type.
+ *
+ * @param {function(number, number): T} convert Conversion function to produce
+ *     the result value, takes parameters (lowBits, highBits).
+ * @return {T}
+ * @template T
+ */
+jspb.BinaryReader.prototype.readSplitZigzagVarint64 = function(convert) {
+  goog.asserts.assert(
+      this.nextWireType_ == jspb.BinaryConstants.WireType.VARINT);
+  return this.decoder_.readSplitVarint64(function(lowBits, highBits) {
+    return jspb.utils.fromZigzag64(lowBits, highBits, convert);
+  });
+};
+
+
+/**
  * Reads a 64-bit varint or fixed64 field from the stream and returns it as a
  * 8-character Unicode string for use as a hash table key, or throws an error
  * if the next field in the stream is not of the correct wire type.
diff --git a/js/binary/reader_test.js b/js/binary/reader_test.js
index 618e9ad..daa0ab6 100644
--- a/js/binary/reader_test.js
+++ b/js/binary/reader_test.js
@@ -414,6 +414,7 @@
     var writer = new jspb.BinaryWriter();
     writer.writeInt64String(1, '4294967296');
     writer.writeSfixed64String(2, '4294967298');
+    writer.writeInt64String(3, '3');  // 3 is the zig-zag encoding of -2.
     var reader = jspb.BinaryReader.alloc(writer.getResultBuffer());
 
     function rejoin(lowBits, highBits) {
@@ -426,6 +427,10 @@
     reader.nextField();
     expect(reader.getFieldNumber()).toEqual(2);
     expect(reader.readSplitFixed64(rejoin)).toEqual(0x100000002);
+
+    reader.nextField();
+    expect(reader.getFieldNumber()).toEqual(3);
+    expect(reader.readSplitZigzagVarint64(rejoin)).toEqual(-2);
   });
 
   /**
@@ -490,6 +495,11 @@
         jspb.BinaryReader.prototype.readSint64,
         jspb.BinaryWriter.prototype.writeSint64,
         1, -Math.pow(2, 63), Math.pow(2, 63) - 513, Math.round);
+
+    doTestSignedField_(
+        jspb.BinaryReader.prototype.readSintHash64,
+        jspb.BinaryWriter.prototype.writeSintHash64, 1, -Math.pow(2, 63),
+        Math.pow(2, 63) - 513, jspb.utils.numberToHash64);
   });
 
 
diff --git a/js/binary/utils.js b/js/binary/utils.js
index 0cf0ef0..f0425e3 100644
--- a/js/binary/utils.js
+++ b/js/binary/utils.js
@@ -296,7 +296,7 @@
  * @return {number}
  */
 jspb.utils.joinUint64 = function(bitsLow, bitsHigh) {
-  return bitsHigh * jspb.BinaryConstants.TWO_TO_32 + bitsLow;
+  return bitsHigh * jspb.BinaryConstants.TWO_TO_32 + (bitsLow >>> 0);
 };
 
 
@@ -322,6 +322,33 @@
   return sign ? -result : result;
 };
 
+/**
+ * Converts split 64-bit values from standard two's complement encoding to
+ * zig-zag encoding. Invokes the provided function to produce final result.
+ *
+ * @param {number} bitsLow
+ * @param {number} bitsHigh
+ * @param {function(number, number): T} convert Conversion function to produce
+ *     the result value, takes parameters (lowBits, highBits).
+ * @return {T}
+ * @template T
+ */
+jspb.utils.toZigzag64 = function(bitsLow, bitsHigh, convert) {
+  // See
+  // https://engdoc.corp.google.com/eng/howto/protocolbuffers/developerguide/encoding.shtml?cl=head#types
+  // 64-bit math is: (n << 1) ^ (n >> 63)
+  //
+  // To do this in 32 bits, we can get a 32-bit sign-flipping mask from the
+  // high word.
+  // Then we can operate on each word individually, with the addition of the
+  // "carry" to get the most significant bit from the low word into the high
+  // word.
+  var signFlipMask = bitsHigh >> 31;
+  bitsHigh = (bitsHigh << 1 | bitsLow >>> 31) ^ signFlipMask;
+  bitsLow = (bitsLow << 1) ^ signFlipMask;
+  return convert(bitsLow, bitsHigh);
+};
+
 
 /**
  * Joins two 32-bit values into a 64-bit unsigned integer and applies zigzag
@@ -331,21 +358,33 @@
  * @return {number}
  */
 jspb.utils.joinZigzag64 = function(bitsLow, bitsHigh) {
-  // Extract the sign bit and shift right by one.
-  var sign = bitsLow & 1;
-  bitsLow = ((bitsLow >>> 1) | (bitsHigh << 31)) >>> 0;
-  bitsHigh = bitsHigh >>> 1;
+  return jspb.utils.fromZigzag64(bitsLow, bitsHigh, jspb.utils.joinInt64);
+};
 
-  // Increment the split value if the sign bit was set.
-  if (sign) {
-    bitsLow = (bitsLow + 1) >>> 0;
-    if (bitsLow == 0) {
-      bitsHigh = (bitsHigh + 1) >>> 0;
-    }
-  }
 
-  var result = jspb.utils.joinUint64(bitsLow, bitsHigh);
-  return sign ? -result : result;
+/**
+ * Converts split 64-bit values from zigzag encoding to standard two's
+ * complement encoding. Invokes the provided function to produce final result.
+ *
+ * @param {number} bitsLow
+ * @param {number} bitsHigh
+ * @param {function(number, number): T} convert Conversion function to produce
+ *     the result value, takes parameters (lowBits, highBits).
+ * @return {T}
+ * @template T
+ */
+jspb.utils.fromZigzag64 = function(bitsLow, bitsHigh, convert) {
+  // 64 bit math is:
+  //   signmask = (zigzag & 1) ? -1 : 0;
+  //   twosComplement = (zigzag >> 1) ^ signmask;
+  //
+  // To work with 32 bit, we can operate on both but "carry" the lowest bit
+  // from the high word by shifting it up 31 bits to be the most significant bit
+  // of the low word.
+  var signFlipMask = -(bitsLow & 1);
+  bitsLow = ((bitsLow >>> 1) | (bitsHigh << 31)) ^ signFlipMask;
+  bitsHigh = (bitsHigh >>> 1) ^ signFlipMask;
+  return convert(bitsLow, bitsHigh);
 };
 
 
diff --git a/js/binary/utils_test.js b/js/binary/utils_test.js
index d4c1ef7..c1b0789 100644
--- a/js/binary/utils_test.js
+++ b/js/binary/utils_test.js
@@ -465,6 +465,53 @@
     }
   });
 
+  /**
+   * Tests zigzag conversions.
+   */
+  it('can encode and decode zigzag 64', function() {
+    function stringToHiLoPair(str) {
+      jspb.utils.splitDecimalString(str);
+      return {
+        lo: jspb.utils.split64Low >>> 0,
+        hi: jspb.utils.split64High >>> 0
+      };
+    }
+    function makeHiLoPair(lo, hi) {
+      return {lo: lo >>> 0, hi: hi >>> 0};
+    }
+    // Test cases direcly from the protobuf dev guide.
+    // https://engdoc.corp.google.com/eng/howto/protocolbuffers/developerguide/encoding.shtml?cl=head#types
+    var testCases = [
+      {original: stringToHiLoPair('0'), zigzag: stringToHiLoPair('0')},
+      {original: stringToHiLoPair('-1'), zigzag: stringToHiLoPair('1')},
+      {original: stringToHiLoPair('1'), zigzag: stringToHiLoPair('2')},
+      {original: stringToHiLoPair('-2'), zigzag: stringToHiLoPair('3')},
+      {
+        original: stringToHiLoPair('2147483647'),
+        zigzag: stringToHiLoPair('4294967294')
+      },
+      {
+        original: stringToHiLoPair('-2147483648'),
+        zigzag: stringToHiLoPair('4294967295')
+      },
+      // 64-bit extremes
+      {
+        original: stringToHiLoPair('9223372036854775807'),
+        zigzag: stringToHiLoPair('18446744073709551614')
+      },
+      {
+        original: stringToHiLoPair('-9223372036854775808'),
+        zigzag: stringToHiLoPair('18446744073709551615')
+      },
+    ];
+    for (const c of testCases) {
+      expect(jspb.utils.toZigzag64(c.original.lo, c.original.hi, makeHiLoPair))
+          .toEqual(c.zigzag);
+      expect(jspb.utils.fromZigzag64(c.zigzag.lo, c.zigzag.hi, makeHiLoPair))
+          .toEqual(c.original);
+    }
+  });
+
 
   /**
    * Tests counting packed varints.
diff --git a/js/binary/writer.js b/js/binary/writer.js
index 017d481..8c5bff8 100644
--- a/js/binary/writer.js
+++ b/js/binary/writer.js
@@ -236,12 +236,11 @@
 
 /**
  * Converts the encoded data into a base64-encoded string.
- * @param {boolean=} opt_webSafe True indicates we should use a websafe
- *     alphabet, which does not require escaping for use in URLs.
+ * @param {!goog.crypt.base64.Alphabet=} alphabet Which flavor of base64 to use.
  * @return {string}
  */
-jspb.BinaryWriter.prototype.getResultBase64String = function(opt_webSafe) {
-  return goog.crypt.base64.encodeByteArray(this.getResultBuffer(), opt_webSafe);
+jspb.BinaryWriter.prototype.getResultBase64String = function(alphabet) {
+  return goog.crypt.base64.encodeByteArray(this.getResultBuffer(), alphabet);
 };
 
 
@@ -451,6 +450,19 @@
 
 
 /**
+ * Writes a zigzag varint field to the buffer without range checking.
+ * @param {number} field The field number.
+ * @param {string?} value The value to write.
+ * @private
+ */
+jspb.BinaryWriter.prototype.writeZigzagVarintHash64_ = function(field, value) {
+  if (value == null) return;
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
+  this.encoder_.writeZigzagVarintHash64(value);
+};
+
+
+/**
  * Writes an int32 field to the buffer. Numbers outside the range [-2^31,2^31)
  * will be truncated.
  * @param {number} field The field number.
@@ -563,7 +575,7 @@
 
 
 /**
- * Writes a sint32 field to the buffer. Numbers outside the range [-2^31,2^31)
+ * Writes an sint32 field to the buffer. Numbers outside the range [-2^31,2^31)
  * will be truncated.
  * @param {number} field The field number.
  * @param {number?} value The value to write.
@@ -577,7 +589,7 @@
 
 
 /**
- * Writes a sint64 field to the buffer. Numbers outside the range [-2^63,2^63)
+ * Writes an sint64 field to the buffer. Numbers outside the range [-2^63,2^63)
  * will be truncated.
  * @param {number} field The field number.
  * @param {number?} value The value to write.
@@ -591,15 +603,25 @@
 
 
 /**
- * Writes a sint64 field to the buffer. Numbers outside the range [-2^63,2^63)
+ * Writes an sint64 field to the buffer from a hash64 encoded value. Numbers
+ * outside the range [-2^63,2^63) will be truncated.
+ * @param {number} field The field number.
+ * @param {string?} value The hash64 string to write.
+ */
+jspb.BinaryWriter.prototype.writeSintHash64 = function(field, value) {
+  if (value == null) return;
+  this.writeZigzagVarintHash64_(field, value);
+};
+
+
+/**
+ * Writes an sint64 field to the buffer. Numbers outside the range [-2^63,2^63)
  * will be truncated.
  * @param {number} field The field number.
  * @param {string?} value The decimal string to write.
  */
 jspb.BinaryWriter.prototype.writeSint64String = function(field, value) {
   if (value == null) return;
-  goog.asserts.assert((+value >= -jspb.BinaryConstants.TWO_TO_63) &&
-                      (+value < jspb.BinaryConstants.TWO_TO_63));
   this.writeZigzagVarint64String_(field, value);
 };
 
@@ -913,6 +935,22 @@
 
 
 /**
+ * Writes a 64-bit field to the buffer as a zigzag encoded varint.
+ * @param {number} field The field number.
+ * @param {number} lowBits The low 32 bits.
+ * @param {number} highBits The high 32 bits.
+ */
+jspb.BinaryWriter.prototype.writeSplitZigzagVarint64 = function(
+    field, lowBits, highBits) {
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
+  var encoder = this.encoder_;
+  jspb.utils.toZigzag64(lowBits, highBits, function(lowBits, highBits) {
+    encoder.writeSplitVarint64(lowBits >>> 0, highBits >>> 0);
+  });
+};
+
+
+/**
  * Writes an array of numbers to the buffer as a repeated 32-bit int field.
  * @param {number} field The field number.
  * @param {?Array<number>} value The array of ints to write.
@@ -987,6 +1025,23 @@
 
 
 /**
+ * Writes an array of 64-bit values to the buffer as a zigzag varint.
+ * @param {number} field The field number.
+ * @param {?Array<T>} value The value.
+ * @param {function(T): number} lo Function to get low bits.
+ * @param {function(T): number} hi Function to get high bits.
+ * @template T
+ */
+jspb.BinaryWriter.prototype.writeRepeatedSplitZigzagVarint64 = function(
+    field, value, lo, hi) {
+  if (value == null) return;
+  for (var i = 0; i < value.length; i++) {
+    this.writeSplitZigzagVarint64(field, lo(value[i]), hi(value[i]));
+  }
+};
+
+
+/**
  * Writes an array of numbers formatted as strings to the buffer as a repeated
  * 64-bit int field.
  * @param {number} field The field number.
@@ -1096,6 +1151,20 @@
 
 
 /**
+ * Writes an array of hash64 strings to the buffer as a repeated signed 64-bit
+ * int field.
+ * @param {number} field The field number.
+ * @param {?Array<string>} value The array of ints to write.
+ */
+jspb.BinaryWriter.prototype.writeRepeatedSintHash64 = function(field, value) {
+  if (value == null) return;
+  for (var i = 0; i < value.length; i++) {
+    this.writeZigzagVarintHash64_(field, value[i]);
+  }
+};
+
+
+/**
  * Writes an array of numbers to the buffer as a repeated fixed32 field. This
  * works for both signed and unsigned fixed32s.
  * @param {number} field The field number.
@@ -1412,6 +1481,29 @@
 
 
 /**
+ * Writes an array of 64-bit values to the buffer as a zigzag varint.
+ * @param {number} field The field number.
+ * @param {?Array<T>} value The value.
+ * @param {function(T): number} lo Function to get low bits.
+ * @param {function(T): number} hi Function to get high bits.
+ * @template T
+ */
+jspb.BinaryWriter.prototype.writePackedSplitZigzagVarint64 = function(
+    field, value, lo, hi) {
+  if (value == null) return;
+  var bookmark = this.beginDelimited_(field);
+  var encoder = this.encoder_;
+  for (var i = 0; i < value.length; i++) {
+    jspb.utils.toZigzag64(
+        lo(value[i]), hi(value[i]), function(bitsLow, bitsHigh) {
+          encoder.writeSplitVarint64(bitsLow >>> 0, bitsHigh >>> 0);
+        });
+  }
+  this.endDelimited_(bookmark);
+};
+
+
+/**
  * Writes an array of numbers represented as strings to the buffer as a packed
  * 64-bit int field.
  * @param {number} field
@@ -1533,8 +1625,24 @@
   if (value == null || !value.length) return;
   var bookmark = this.beginDelimited_(field);
   for (var i = 0; i < value.length; i++) {
-    // TODO(haberman): make lossless
-    this.encoder_.writeZigzagVarint64(parseInt(value[i], 10));
+    this.encoder_.writeZigzagVarintHash64(
+        jspb.utils.decimalStringToHash64(value[i]));
+  }
+  this.endDelimited_(bookmark);
+};
+
+
+/**
+ * Writes an array of hash 64 strings to the buffer as a packed signed 64-bit
+ * int field.
+ * @param {number} field The field number.
+ * @param {?Array<string>} value The array of decimal strings to write.
+ */
+jspb.BinaryWriter.prototype.writePackedSintHash64 = function(field, value) {
+  if (value == null || !value.length) return;
+  var bookmark = this.beginDelimited_(field);
+  for (var i = 0; i < value.length; i++) {
+    this.encoder_.writeZigzagVarintHash64(value[i]);
   }
   this.endDelimited_(bookmark);
 };
diff --git a/js/binary/writer_test.js b/js/binary/writer_test.js
index b4860a1..0590464 100644
--- a/js/binary/writer_test.js
+++ b/js/binary/writer_test.js
@@ -42,6 +42,7 @@
 goog.require('goog.testing.asserts');
 goog.require('jspb.BinaryReader');
 goog.require('jspb.BinaryWriter');
+goog.require('jspb.utils');
 
 
 /**
@@ -128,8 +129,13 @@
     var writer = new jspb.BinaryWriter();
     writer.writeBytes(1, new Uint8Array([127]));
     assertEquals('CgF/', writer.getResultBase64String());
-    assertEquals('CgF/', writer.getResultBase64String(false));
-    assertEquals('CgF_', writer.getResultBase64String(true));
+    assertEquals(
+        'CgF/',
+        writer.getResultBase64String(goog.crypt.base64.Alphabet.DEFAULT));
+    assertEquals(
+        'CgF_',
+        writer.getResultBase64String(
+            goog.crypt.base64.Alphabet.WEBSAFE_NO_PADDING));
   });
 
   it('writes split 64 fields', function() {
@@ -201,4 +207,116 @@
       String(4 * 2 ** 32 + 3),
     ]);
   });
+
+  it('writes zigzag 64 fields', function() {
+    // Test cases direcly from the protobuf dev guide.
+    // https://engdoc.corp.google.com/eng/howto/protocolbuffers/developerguide/encoding.shtml?cl=head#types
+    var testCases = [
+      {original: '0', zigzag: '0'},
+      {original: '-1', zigzag: '1'},
+      {original: '1', zigzag: '2'},
+      {original: '-2', zigzag: '3'},
+      {original: '2147483647', zigzag: '4294967294'},
+      {original: '-2147483648', zigzag: '4294967295'},
+      // 64-bit extremes, not in dev guide.
+      {original: '9223372036854775807', zigzag: '18446744073709551614'},
+      {original: '-9223372036854775808', zigzag: '18446744073709551615'},
+    ];
+    function decimalToLowBits(v) {
+      jspb.utils.splitDecimalString(v);
+      return jspb.utils.split64Low >>> 0;
+    }
+    function decimalToHighBits(v) {
+      jspb.utils.splitDecimalString(v);
+      return jspb.utils.split64High >>> 0;
+    }
+
+    var writer = new jspb.BinaryWriter();
+    testCases.forEach(function(c) {
+      writer.writeSint64String(1, c.original);
+      writer.writeSintHash64(1, jspb.utils.decimalStringToHash64(c.original));
+      jspb.utils.splitDecimalString(c.original);
+      writer.writeSplitZigzagVarint64(
+          1, jspb.utils.split64Low, jspb.utils.split64High);
+    });
+
+    writer.writeRepeatedSint64String(2, testCases.map(function(c) {
+      return c.original;
+    }));
+
+    writer.writeRepeatedSintHash64(3, testCases.map(function(c) {
+      return jspb.utils.decimalStringToHash64(c.original);
+    }));
+
+    writer.writeRepeatedSplitZigzagVarint64(
+        4, testCases.map(function(c) {
+          return c.original;
+        }),
+        decimalToLowBits, decimalToHighBits);
+
+    writer.writePackedSint64String(5, testCases.map(function(c) {
+      return c.original;
+    }));
+
+    writer.writePackedSintHash64(6, testCases.map(function(c) {
+      return jspb.utils.decimalStringToHash64(c.original);
+    }));
+
+    writer.writePackedSplitZigzagVarint64(
+        7, testCases.map(function(c) {
+          return c.original;
+        }),
+        decimalToLowBits, decimalToHighBits);
+
+    // Verify by reading the stream as normal int64 fields and checking with
+    // the canonical zigzag encoding of each value.
+    var reader = jspb.BinaryReader.alloc(writer.getResultBuffer());
+    testCases.forEach(function(c) {
+      reader.nextField();
+      expect(reader.getFieldNumber()).toEqual(1);
+      expect(reader.readUint64String()).toEqual(c.zigzag);
+      reader.nextField();
+      expect(reader.getFieldNumber()).toEqual(1);
+      expect(reader.readUint64String()).toEqual(c.zigzag);
+      reader.nextField();
+      expect(reader.getFieldNumber()).toEqual(1);
+      expect(reader.readUint64String()).toEqual(c.zigzag);
+    });
+
+    testCases.forEach(function(c) {
+      reader.nextField();
+      expect(reader.getFieldNumber()).toEqual(2);
+      expect(reader.readUint64String()).toEqual(c.zigzag);
+    });
+
+    testCases.forEach(function(c) {
+      reader.nextField();
+      expect(reader.getFieldNumber()).toEqual(3);
+      expect(reader.readUint64String()).toEqual(c.zigzag);
+    });
+
+    testCases.forEach(function(c) {
+      reader.nextField();
+      expect(reader.getFieldNumber()).toEqual(4);
+      expect(reader.readUint64String()).toEqual(c.zigzag);
+    });
+
+    reader.nextField();
+    expect(reader.getFieldNumber()).toEqual(5);
+    expect(reader.readPackedUint64String()).toEqual(testCases.map(function(c) {
+      return c.zigzag;
+    }));
+
+    reader.nextField();
+    expect(reader.getFieldNumber()).toEqual(6);
+    expect(reader.readPackedUint64String()).toEqual(testCases.map(function(c) {
+      return c.zigzag;
+    }));
+
+    reader.nextField();
+    expect(reader.getFieldNumber()).toEqual(7);
+    expect(reader.readPackedUint64String()).toEqual(testCases.map(function(c) {
+      return c.zigzag;
+    }));
+  });
 });
diff --git a/js/compatibility_tests/v3.0.0/binary/proto_test.js b/js/compatibility_tests/v3.0.0/binary/proto_test.js
index 14d0f42..1364834 100644
--- a/js/compatibility_tests/v3.0.0/binary/proto_test.js
+++ b/js/compatibility_tests/v3.0.0/binary/proto_test.js
@@ -172,7 +172,7 @@
  * @return {boolean}
  */
 function bytesCompare(arr, expected) {
-  if (goog.isString(arr)) {
+  if (typeof arr === 'string') {
     arr = goog.crypt.base64.decodeStringToUint8Array(arr);
   }
   if (arr.length != expected.length) {
@@ -477,8 +477,8 @@
     var msg = new proto.jspb.test.TestAllTypes();
 
     function assertGetters() {
-      assertTrue(goog.isString(msg.getRepeatedBytesList_asB64()[0]));
-      assertTrue(goog.isString(msg.getRepeatedBytesList_asB64()[1]));
+      assertTrue(typeof msg.getRepeatedBytesList_asB64()[0] === 'string');
+      assertTrue(typeof msg.getRepeatedBytesList_asB64()[1] === 'string');
       assertTrue(msg.getRepeatedBytesList_asU8()[0] instanceof Uint8Array);
       assertTrue(msg.getRepeatedBytesList_asU8()[1] instanceof Uint8Array);
 
diff --git a/js/compatibility_tests/v3.0.0/binary/utils_test.js b/js/compatibility_tests/v3.0.0/binary/utils_test.js
index d27e5ea..abc36aa 100644
--- a/js/compatibility_tests/v3.0.0/binary/utils_test.js
+++ b/js/compatibility_tests/v3.0.0/binary/utils_test.js
@@ -355,7 +355,7 @@
      */
     function test(x, opt_bits) {
       jspb.utils.splitFloat32(x);
-      if (goog.isDef(opt_bits)) {
+      if (opt_bits !== undefined) {
         if (opt_bits != jspb.utils.split64Low) throw 'fail!';
       }
       if (truncate(x) != jspb.utils.joinFloat32(jspb.utils.split64Low,
@@ -422,10 +422,10 @@
      */
     function test(x, opt_highBits, opt_lowBits) {
       jspb.utils.splitFloat64(x);
-      if (goog.isDef(opt_highBits)) {
+      if (opt_highBits !== undefined) {
         if (opt_highBits != jspb.utils.split64High) throw 'fail!';
       }
-      if (goog.isDef(opt_lowBits)) {
+      if (opt_lowBits !== undefined) {
         if (opt_lowBits != jspb.utils.split64Low) throw 'fail!';
       }
       if (x != jspb.utils.joinFloat64(jspb.utils.split64Low,
diff --git a/js/compatibility_tests/v3.0.0/message_test.js b/js/compatibility_tests/v3.0.0/message_test.js
index b779143..79a12e0 100644
--- a/js/compatibility_tests/v3.0.0/message_test.js
+++ b/js/compatibility_tests/v3.0.0/message_test.js
@@ -1057,8 +1057,9 @@
 
   it('testFloatingPointFieldsSupportNan', function() {
     var assertNan = function(x) {
-      assertTrue('Expected ' + x + ' (' + goog.typeOf(x) + ') to be NaN.',
-          goog.isNumber(x) && isNaN(x));
+      assertTrue(
+          'Expected ' + x + ' (' + goog.typeOf(x) + ') to be NaN.',
+          typeof x === 'number' && isNaN(x));
     };
 
     var message = new proto.jspb.test.FloatingPointFields([
diff --git a/js/compatibility_tests/v3.0.0/proto3_test.js b/js/compatibility_tests/v3.0.0/proto3_test.js
index fab0fd4..d020a11 100644
--- a/js/compatibility_tests/v3.0.0/proto3_test.js
+++ b/js/compatibility_tests/v3.0.0/proto3_test.js
@@ -50,7 +50,7 @@
  * @return {boolean}
  */
 function bytesCompare(arr, expected) {
-  if (goog.isString(arr)) {
+  if (typeof arr === 'string') {
     arr = goog.crypt.base64.decodeStringToUint8Array(arr);
   }
   if (arr.length != expected.length) {
diff --git a/js/compatibility_tests/v3.1.0/binary/proto_test.js b/js/compatibility_tests/v3.1.0/binary/proto_test.js
index 26e1d30..ff9d972 100644
--- a/js/compatibility_tests/v3.1.0/binary/proto_test.js
+++ b/js/compatibility_tests/v3.1.0/binary/proto_test.js
@@ -172,7 +172,7 @@
  * @return {boolean}
  */
 function bytesCompare(arr, expected) {
-  if (goog.isString(arr)) {
+  if (typeof arr === 'string') {
     arr = goog.crypt.base64.decodeStringToUint8Array(arr);
   }
   if (arr.length != expected.length) {
@@ -477,8 +477,8 @@
     var msg = new proto.jspb.test.TestAllTypes();
 
     function assertGetters() {
-      assertTrue(goog.isString(msg.getRepeatedBytesList_asB64()[0]));
-      assertTrue(goog.isString(msg.getRepeatedBytesList_asB64()[1]));
+      assertTrue(typeof msg.getRepeatedBytesList_asB64()[0] === 'string');
+      assertTrue(typeof msg.getRepeatedBytesList_asB64()[1] === 'string');
       assertTrue(msg.getRepeatedBytesList_asU8()[0] instanceof Uint8Array);
       assertTrue(msg.getRepeatedBytesList_asU8()[1] instanceof Uint8Array);
 
diff --git a/js/compatibility_tests/v3.1.0/binary/utils_test.js b/js/compatibility_tests/v3.1.0/binary/utils_test.js
index d27e5ea..abc36aa 100644
--- a/js/compatibility_tests/v3.1.0/binary/utils_test.js
+++ b/js/compatibility_tests/v3.1.0/binary/utils_test.js
@@ -355,7 +355,7 @@
      */
     function test(x, opt_bits) {
       jspb.utils.splitFloat32(x);
-      if (goog.isDef(opt_bits)) {
+      if (opt_bits !== undefined) {
         if (opt_bits != jspb.utils.split64Low) throw 'fail!';
       }
       if (truncate(x) != jspb.utils.joinFloat32(jspb.utils.split64Low,
@@ -422,10 +422,10 @@
      */
     function test(x, opt_highBits, opt_lowBits) {
       jspb.utils.splitFloat64(x);
-      if (goog.isDef(opt_highBits)) {
+      if (opt_highBits !== undefined) {
         if (opt_highBits != jspb.utils.split64High) throw 'fail!';
       }
-      if (goog.isDef(opt_lowBits)) {
+      if (opt_lowBits !== undefined) {
         if (opt_lowBits != jspb.utils.split64Low) throw 'fail!';
       }
       if (x != jspb.utils.joinFloat64(jspb.utils.split64Low,
diff --git a/js/compatibility_tests/v3.1.0/message_test.js b/js/compatibility_tests/v3.1.0/message_test.js
index d5c7374..80a1c52 100644
--- a/js/compatibility_tests/v3.1.0/message_test.js
+++ b/js/compatibility_tests/v3.1.0/message_test.js
@@ -1009,8 +1009,9 @@
 
   it('testFloatingPointFieldsSupportNan', function() {
     var assertNan = function(x) {
-      assertTrue('Expected ' + x + ' (' + goog.typeOf(x) + ') to be NaN.',
-          goog.isNumber(x) && isNaN(x));
+      assertTrue(
+          'Expected ' + x + ' (' + goog.typeOf(x) + ') to be NaN.',
+          typeof x === 'number' && isNaN(x));
     };
 
     var message = new proto.jspb.test.FloatingPointFields([
diff --git a/js/compatibility_tests/v3.1.0/proto3_test.js b/js/compatibility_tests/v3.1.0/proto3_test.js
index 3c929ef..696af33 100644
--- a/js/compatibility_tests/v3.1.0/proto3_test.js
+++ b/js/compatibility_tests/v3.1.0/proto3_test.js
@@ -50,7 +50,7 @@
  * @return {boolean}
  */
 function bytesCompare(arr, expected) {
-  if (goog.isString(arr)) {
+  if (typeof arr === 'string') {
     arr = goog.crypt.base64.decodeStringToUint8Array(arr);
   }
   if (arr.length != expected.length) {
diff --git a/js/map.js b/js/map.js
index b9a48af..589a293 100644
--- a/js/map.js
+++ b/js/map.js
@@ -461,15 +461,21 @@
  *    The BinaryReader parsing callback for type V, if V is a message type
  *
  * @param {K=} opt_defaultKey
- *    The default value for the type of map keys. Accepting map
- *    entries with unset keys is required for maps to be backwards compatible
- *    with the repeated message representation described here: goo.gl/zuoLAC
+ *    The default value for the type of map keys. Accepting map entries with
+ *    unset keys is required for maps to be backwards compatible with the
+ *    repeated message representation described here: goo.gl/zuoLAC
+ *
+ * @param {V=} opt_defaultValue
+ *    The default value for the type of map values. Accepting map entries with
+ *    unset values is required for maps to be backwards compatible with the
+ *    repeated message representation described here: goo.gl/zuoLAC
  *
  */
 jspb.Map.deserializeBinary = function(map, reader, keyReaderFn, valueReaderFn,
-                                      opt_valueReaderCallback, opt_defaultKey) {
+                                      opt_valueReaderCallback, opt_defaultKey,
+                                      opt_defaultValue) {
   var key = opt_defaultKey;
-  var value = undefined;
+  var value = opt_defaultValue;
 
   while (reader.nextField()) {
     if (reader.isEndGroup()) {
@@ -484,7 +490,11 @@
       // Value.
       if (map.valueCtor_) {
         goog.asserts.assert(opt_valueReaderCallback);
-        value = new map.valueCtor_();
+        if (!value) {
+          // Old generator still doesn't provide default value message.
+          // Need this for backward compatibility.
+          value = new map.valueCtor_();
+        }
         valueReaderFn.call(reader, value, opt_valueReaderCallback);
       } else {
         value =
diff --git a/js/maps_test.js b/js/maps_test.js
index 4640c98..1cbff7b 100755
--- a/js/maps_test.js
+++ b/js/maps_test.js
@@ -36,10 +36,18 @@
 goog.require('proto.jspb.test.MapValueMessage');
 goog.require('proto.jspb.test.TestMapFields');
 goog.require('proto.jspb.test.TestMapFieldsOptionalKeys');
+goog.require('proto.jspb.test.TestMapFieldsOptionalValues');
 goog.require('proto.jspb.test.MapEntryOptionalKeysStringKey');
 goog.require('proto.jspb.test.MapEntryOptionalKeysInt32Key');
 goog.require('proto.jspb.test.MapEntryOptionalKeysInt64Key');
 goog.require('proto.jspb.test.MapEntryOptionalKeysBoolKey');
+goog.require('proto.jspb.test.MapEntryOptionalValuesStringValue');
+goog.require('proto.jspb.test.MapEntryOptionalValuesInt32Value');
+goog.require('proto.jspb.test.MapEntryOptionalValuesInt64Value');
+goog.require('proto.jspb.test.MapEntryOptionalValuesBoolValue');
+goog.require('proto.jspb.test.MapEntryOptionalValuesDoubleValue');
+goog.require('proto.jspb.test.MapEntryOptionalValuesEnumValue');
+goog.require('proto.jspb.test.MapEntryOptionalValuesMessageValue');
 
 // CommonJS-LoadFromFile: test_pb proto.jspb.test
 goog.require('proto.jspb.test.MapValueMessageNoBinary');
@@ -54,7 +62,12 @@
   var arr = map.toArray();
   assertEquals(arr.length, entries.length);
   for (var i = 0; i < arr.length; i++) {
-    assertElementsEquals(arr[i], entries[i]);
+    if (Array.isArray(arr[i])) {
+      assertTrue(Array.isArray(entries[i]));
+      assertArrayEquals(arr[i], entries[i]);
+    } else {
+      assertElementsEquals(arr[i], entries[i]);
+    }
   }
 }
 
@@ -265,8 +278,10 @@
       var decoded = msgInfo.deserializeBinary(serialized);
       checkMapFields(decoded);
     });
+
     /**
-     * Tests deserialization of undefined map keys go to default values in binary format.
+     * Tests deserialization of undefined map keys go to default values in
+     * binary format.
      */
     it('testMapDeserializationForUndefinedKeys', function() {
       var testMessageOptionalKeys = new proto.jspb.test.TestMapFieldsOptionalKeys();
@@ -298,6 +313,67 @@
         [false, 'd']
       ]);
     });
+
+    /**
+     * Tests deserialization of undefined map values go to default values in
+     * binary format.
+     */
+    it('testMapDeserializationForUndefinedValues', function() {
+      var testMessageOptionalValues =
+          new proto.jspb.test.TestMapFieldsOptionalValues();
+      var mapEntryStringValue =
+          new proto.jspb.test.MapEntryOptionalValuesStringValue();
+      mapEntryStringValue.setKey("a");
+      testMessageOptionalValues.setMapStringString(mapEntryStringValue);
+      var mapEntryInt32Value =
+          new proto.jspb.test.MapEntryOptionalValuesInt32Value();
+      mapEntryInt32Value.setKey("b");
+      testMessageOptionalValues.setMapStringInt32(mapEntryInt32Value);
+      var mapEntryInt64Value =
+          new proto.jspb.test.MapEntryOptionalValuesInt64Value();
+      mapEntryInt64Value.setKey("c");
+      testMessageOptionalValues.setMapStringInt64(mapEntryInt64Value);
+      var mapEntryBoolValue =
+          new proto.jspb.test.MapEntryOptionalValuesBoolValue();
+      mapEntryBoolValue.setKey("d");
+      testMessageOptionalValues.setMapStringBool(mapEntryBoolValue);
+      var mapEntryDoubleValue =
+          new proto.jspb.test.MapEntryOptionalValuesDoubleValue();
+      mapEntryDoubleValue.setKey("e");
+      testMessageOptionalValues.setMapStringDouble(mapEntryDoubleValue);
+      var mapEntryEnumValue =
+          new proto.jspb.test.MapEntryOptionalValuesEnumValue();
+      mapEntryEnumValue.setKey("f");
+      testMessageOptionalValues.setMapStringEnum(mapEntryEnumValue);
+      var mapEntryMessageValue =
+          new proto.jspb.test.MapEntryOptionalValuesMessageValue();
+      mapEntryMessageValue.setKey("g");
+      testMessageOptionalValues.setMapStringMsg(mapEntryMessageValue);
+      var deserializedMessage = msgInfo.deserializeBinary(
+        testMessageOptionalValues.serializeBinary()
+       );
+      checkMapEquals(deserializedMessage.getMapStringStringMap(), [
+        ['a', '']
+      ]);
+      checkMapEquals(deserializedMessage.getMapStringInt32Map(), [
+        ['b', 0]
+      ]);
+      checkMapEquals(deserializedMessage.getMapStringInt64Map(), [
+        ['c', 0]
+      ]);
+      checkMapEquals(deserializedMessage.getMapStringBoolMap(), [
+        ['d', false]
+      ]);
+      checkMapEquals(deserializedMessage.getMapStringDoubleMap(), [
+        ['e', 0.0]
+      ]);
+      checkMapEquals(deserializedMessage.getMapStringEnumMap(), [
+        ['f', 0]
+      ]);
+      checkMapEquals(deserializedMessage.getMapStringMsgMap(), [
+        ['g', []]
+      ]);
+    });
   }
 
 
diff --git a/js/message.js b/js/message.js
index 0957c6d..52c541e 100644
--- a/js/message.js
+++ b/js/message.js
@@ -1860,7 +1860,6 @@
  * @param {Function} constructor The message constructor.
  */
 jspb.Message.registerMessageType = function(id, constructor) {
-  jspb.Message.registry_[id] = constructor;
   // This is needed so we can later access messageId directly on the contructor,
   // otherwise it is not available due to 'property collapsing' by the compiler.
   /**
@@ -1868,15 +1867,6 @@
    */
   constructor.messageId = id;
 };
-
-
-/**
- * The registry of message ids to message constructors.
- * @private
- */
-jspb.Message.registry_ = {};
-
-
 /**
  * The extensions registered on MessageSet. This is a map of extension
  * field number to field info object. This should be considered as a
diff --git a/js/package.json b/js/package.json
index 202e8e4..37dfe67 100644
--- a/js/package.json
+++ b/js/package.json
@@ -8,11 +8,11 @@
   ],
   "dependencies": {},
   "devDependencies": {
-    "glob": "~6.0.4",
-    "google-closure-compiler": "~20190301.0.0",
-    "google-closure-library": "~20190301.0.0",
-    "gulp": "~4.0.1",
-    "jasmine": "~2.4.1"
+    "glob": "~7.1.4",
+    "google-closure-compiler": "~20190819.0.0",
+    "google-closure-library": "~20190819.0.0",
+    "gulp": "~4.0.2",
+    "jasmine": "~3.4.0"
   },
   "scripts": {
     "test": "node ./node_modules/gulp/bin/gulp.js test"
diff --git a/js/testbinary.proto b/js/testbinary.proto
index 2e54845..a141285 100644
--- a/js/testbinary.proto
+++ b/js/testbinary.proto
@@ -232,6 +232,56 @@
 
 // End mock-map entries
 
+// These proto are 'mock map' entries to test the above map deserializing with
+// undefined values. Make sure TestMapFieldsOptionalValues is written to be
+// deserialized by TestMapFields
+message MapEntryOptionalValuesStringValue {
+  optional string key = 1;
+  optional string value = 2;
+}
+
+message MapEntryOptionalValuesInt32Value {
+  optional string key = 1;
+  optional int32 value = 2;
+}
+
+message MapEntryOptionalValuesInt64Value {
+  optional string key = 1;
+  optional int64 value = 2;
+}
+
+message MapEntryOptionalValuesBoolValue {
+  optional string key = 1;
+  optional bool value = 2;
+}
+
+message MapEntryOptionalValuesDoubleValue {
+  optional string key = 1;
+  optional double value = 2;
+}
+
+message MapEntryOptionalValuesEnumValue {
+  optional string key = 1;
+  optional MapValueEnum value = 2;
+}
+
+message MapEntryOptionalValuesMessageValue {
+  optional string key = 1;
+  optional MapValueMessage value = 2;
+}
+
+message TestMapFieldsOptionalValues {
+  optional MapEntryOptionalValuesStringValue map_string_string = 1;
+  optional MapEntryOptionalValuesInt32Value map_string_int32 = 2;
+  optional MapEntryOptionalValuesInt64Value map_string_int64 = 3;
+  optional MapEntryOptionalValuesBoolValue map_string_bool = 4;
+  optional MapEntryOptionalValuesDoubleValue map_string_double = 5;
+  optional MapEntryOptionalValuesEnumValue map_string_enum = 6;
+  optional MapEntryOptionalValuesMessageValue map_string_msg = 7;
+}
+
+// End mock-map entries
+
 enum MapValueEnum {
   MAP_VALUE_FOO = 0;
   MAP_VALUE_BAR = 1;