diff --git a/benchmark/es/map-bench.js b/benchmark/es/map-bench.js index 047fc05abdc92f..4663d71bdd3186 100644 --- a/benchmark/es/map-bench.js +++ b/benchmark/es/map-bench.js @@ -4,7 +4,10 @@ const common = require('../common.js'); const assert = require('assert'); const bench = common.createBenchmark(main, { - method: ['object', 'nullProtoObject', 'fakeMap', 'map'], + method: [ + 'object', 'nullProtoObject', 'nullProtoLiteralObject', 'storageObject', + 'fakeMap', 'map' + ], millions: [1] }); @@ -36,6 +39,37 @@ function runNullProtoObject(n) { bench.end(n / 1e6); } +function runNullProtoLiteralObject(n) { + const m = { __proto__: null }; + var i = 0; + bench.start(); + for (; i < n; i++) { + m['i' + i] = i; + m['s' + i] = String(i); + assert.strictEqual(String(m['i' + i]), m['s' + i]); + m['i' + i] = undefined; + m['s' + i] = undefined; + } + bench.end(n / 1e6); +} + +function StorageObject() {} +StorageObject.prototype = Object.create(null); + +function runStorageObject(n) { + const m = new StorageObject(); + var i = 0; + bench.start(); + for (; i < n; i++) { + m['i' + i] = i; + m['s' + i] = String(i); + assert.strictEqual(String(m['i' + i]), m['s' + i]); + m['i' + i] = undefined; + m['s' + i] = undefined; + } + bench.end(n / 1e6); +} + function fakeMap() { const m = {}; return { @@ -84,6 +118,12 @@ function main(conf) { case 'nullProtoObject': runNullProtoObject(n); break; + case 'nullProtoLiteralObject': + runNullProtoLiteralObject(n); + break; + case 'storageObject': + runStorageObject(n); + break; case 'fakeMap': runFakeMap(n); break; diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index cdca2d4e89d1ba..a8e04d543a9f56 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -31,7 +31,6 @@ const common = require('_http_common'); const checkIsHttpToken = common._checkIsHttpToken; const checkInvalidHeaderChar = common._checkInvalidHeaderChar; const outHeadersKey = require('internal/http').outHeadersKey; -const StorageObject = require('internal/querystring').StorageObject; const CRLF = common.CRLF; const debug = common.debug; @@ -143,7 +142,7 @@ Object.defineProperty(OutgoingMessage.prototype, '_headerNames', { get: function() { const headers = this[outHeadersKey]; if (headers) { - const out = new StorageObject(); + const out = Object.create(null); const keys = Object.keys(headers); for (var i = 0; i < keys.length; ++i) { const key = keys[i]; @@ -552,7 +551,7 @@ OutgoingMessage.prototype.getHeaderNames = function getHeaderNames() { // Returns a shallow copy of the current outgoing headers. OutgoingMessage.prototype.getHeaders = function getHeaders() { const headers = this[outHeadersKey]; - const ret = new StorageObject(); + const ret = Object.create(null); if (headers) { const keys = Object.keys(headers); for (var i = 0; i < keys.length; ++i) { diff --git a/lib/events.js b/lib/events.js index ac080117665296..c958b02f413bb3 100644 --- a/lib/events.js +++ b/lib/events.js @@ -23,12 +23,6 @@ var domain; -// This constructor is used to store event handlers. Instantiating this is -// faster than explicitly calling `Object.create(null)` to get a "clean" empty -// object (tested with v8 v4.9). -function EventHandlers() {} -EventHandlers.prototype = Object.create(null); - function EventEmitter() { EventEmitter.init.call(this); } @@ -71,7 +65,7 @@ EventEmitter.init = function() { } if (!this._events || this._events === Object.getPrototypeOf(this)._events) { - this._events = new EventHandlers(); + this._events = Object.create(null); this._eventsCount = 0; } @@ -241,7 +235,7 @@ function _addListener(target, type, listener, prepend) { events = target._events; if (!events) { - events = target._events = new EventHandlers(); + events = target._events = Object.create(null); target._eventsCount = 0; } else { // To avoid recursion in the case that type === "newListener"! Before @@ -356,7 +350,7 @@ EventEmitter.prototype.removeListener = if (list === listener || list.listener === listener) { if (--this._eventsCount === 0) - this._events = new EventHandlers(); + this._events = Object.create(null); else { delete events[type]; if (events.removeListener) @@ -379,7 +373,7 @@ EventEmitter.prototype.removeListener = if (list.length === 1) { list[0] = undefined; if (--this._eventsCount === 0) { - this._events = new EventHandlers(); + this._events = Object.create(null); return this; } else { delete events[type]; @@ -408,11 +402,11 @@ EventEmitter.prototype.removeAllListeners = // not listening for removeListener, no need to emit if (!events.removeListener) { if (arguments.length === 0) { - this._events = new EventHandlers(); + this._events = Object.create(null); this._eventsCount = 0; } else if (events[type]) { if (--this._eventsCount === 0) - this._events = new EventHandlers(); + this._events = Object.create(null); else delete events[type]; } @@ -428,7 +422,7 @@ EventEmitter.prototype.removeAllListeners = this.removeAllListeners(key); } this.removeAllListeners('removeListener'); - this._events = new EventHandlers(); + this._events = Object.create(null); this._eventsCount = 0; return this; } diff --git a/lib/fs.js b/lib/fs.js index 8c92db957b4663..9b5d99eb8076d5 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -43,7 +43,6 @@ const internalUtil = require('internal/util'); const assertEncoding = internalFS.assertEncoding; const stringToFlags = internalFS.stringToFlags; const getPathFromURL = internalURL.getPathFromURL; -const { StorageObject } = require('internal/querystring'); Object.defineProperty(exports, 'constants', { configurable: false, @@ -1560,7 +1559,7 @@ if (isWindows) { nextPart = function nextPart(p, i) { return p.indexOf('/', i); }; } -const emptyObj = new StorageObject(); +const emptyObj = Object.create(null); fs.realpathSync = function realpathSync(p, options) { if (!options) options = emptyObj; @@ -1580,8 +1579,8 @@ fs.realpathSync = function realpathSync(p, options) { return maybeCachedResult; } - const seenLinks = new StorageObject(); - const knownHard = new StorageObject(); + const seenLinks = Object.create(null); + const knownHard = Object.create(null); const original = p; // current character position in p @@ -1700,8 +1699,8 @@ fs.realpath = function realpath(p, options, callback) { return; p = pathModule.resolve(p); - const seenLinks = new StorageObject(); - const knownHard = new StorageObject(); + const seenLinks = Object.create(null); + const knownHard = Object.create(null); // current character position in p var pos; diff --git a/lib/internal/querystring.js b/lib/internal/querystring.js index c5dc0f63c7b30b..d1684418097100 100644 --- a/lib/internal/querystring.js +++ b/lib/internal/querystring.js @@ -23,13 +23,7 @@ const isHexTable = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // ... 256 ]; -// Instantiating this is faster than explicitly calling `Object.create(null)` -// to get a "clean" empty object (tested with v8 v4.9). -function StorageObject() {} -StorageObject.prototype = Object.create(null); - module.exports = { hexTable, - isHexTable, - StorageObject + isHexTable }; diff --git a/lib/internal/url.js b/lib/internal/url.js index 040771c228067e..bc95e52656d1a5 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -3,8 +3,7 @@ const util = require('util'); const { hexTable, - isHexTable, - StorageObject + isHexTable } = require('internal/querystring'); const binding = process.binding('url'); const context = Symbol('context'); @@ -97,6 +96,26 @@ class TupleOrigin { } } +// This class provides the internal state of a URL object. An instance of this +// class is stored in every URL object and is accessed internally by setters +// and getters. It roughly corresponds to the concept of a URL record in the +// URL Standard, with a few differences. It is also the object transported to +// the C++ binding. +// Refs: https://url.spec.whatwg.org/#concept-url +class URLContext { + constructor() { + this.flags = 0; + this.scheme = undefined; + this.username = undefined; + this.password = undefined; + this.host = undefined; + this.port = undefined; + this.path = []; + this.query = undefined; + this.fragment = undefined; + } +} + function onParseComplete(flags, protocol, username, password, host, port, path, query, fragment) { if (flags & binding.URL_FLAGS_FAILED) @@ -121,7 +140,7 @@ function onParseComplete(flags, protocol, username, password, // Reused by URL constructor and URL#href setter. function parse(url, input, base) { const base_context = base ? base[context] : undefined; - url[context] = new StorageObject(); + url[context] = new URLContext(); binding.parse(input.trim(), -1, base_context, undefined, onParseComplete.bind(url)); diff --git a/lib/querystring.js b/lib/querystring.js index 1533c8d87b7577..1976c8e125ad46 100644 --- a/lib/querystring.js +++ b/lib/querystring.js @@ -25,7 +25,6 @@ const { Buffer } = require('buffer'); const { - StorageObject, hexTable, isHexTable } = require('internal/querystring'); @@ -281,7 +280,7 @@ const defEqCodes = [61]; // = // Parse a key/val string. function parse(qs, sep, eq, options) { - const obj = new StorageObject(); + const obj = Object.create(null); if (typeof qs !== 'string' || qs.length === 0) { return obj; diff --git a/lib/url.js b/lib/url.js index 395a583cb784bc..4b2ef9b68ea756 100644 --- a/lib/url.js +++ b/lib/url.js @@ -23,7 +23,7 @@ const { toASCII } = process.binding('config').hasIntl ? process.binding('icu') : require('punycode'); -const { StorageObject, hexTable } = require('internal/querystring'); +const { hexTable } = require('internal/querystring'); const internalUrl = require('internal/url'); exports.parse = urlParse; exports.resolve = urlResolve; @@ -197,7 +197,7 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { } } else if (parseQueryString) { this.search = ''; - this.query = new StorageObject(); + this.query = Object.create(null); } return this; } @@ -390,7 +390,7 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { } else if (parseQueryString) { // no query string, but parseQueryString still requested this.search = ''; - this.query = new StorageObject(); + this.query = Object.create(null); } var firstIdx = (questionIdx !== -1 &&