Skip to content

Commit 9a3de7d

Browse files
committed
Optimize d3.{map,set}.
Rather than always escaping keys with a null-prefix, only escape keys that can conflict with built-in properties (either on Object, d3_Map or d3_Set). By only prefixing these special keys, we can avoid the cost of constructing new strings in the common case. To check whether a key is special, we check whether it is in an empty map instance. In addition, keys that are already null-prefixed must be prefixed a second time to correctly unescape.
1 parent 1fc660a commit 9a3de7d

File tree

6 files changed

+135
-54
lines changed

6 files changed

+135
-54
lines changed

d3.js

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -245,58 +245,81 @@
245245
d3_class(d3_Map, {
246246
has: d3_map_has,
247247
get: function(key) {
248-
return this[d3_map_prefix + key];
248+
return this[d3_map_escape(key)];
249249
},
250250
set: function(key, value) {
251-
return this[d3_map_prefix + key] = value;
251+
return this[d3_map_escape(key)] = value;
252252
},
253253
remove: d3_map_remove,
254254
keys: d3_map_keys,
255255
values: function() {
256256
var values = [];
257-
this.forEach(function(key, value) {
258-
values.push(value);
259-
});
257+
for (var key in this) {
258+
if (d3_map_hasOwnProperty.call(this, key)) {
259+
values.push(this[key]);
260+
}
261+
}
260262
return values;
261263
},
262264
entries: function() {
263265
var entries = [];
264-
this.forEach(function(key, value) {
265-
entries.push({
266-
key: key,
267-
value: value
268-
});
269-
});
266+
for (var key in this) {
267+
if (d3_map_hasOwnProperty.call(this, key)) {
268+
entries.push({
269+
key: d3_map_unescape(key),
270+
value: this[key]
271+
});
272+
}
273+
}
270274
return entries;
271275
},
272276
size: d3_map_size,
273277
empty: d3_map_empty,
274278
forEach: function(f) {
275-
for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) f.call(this, key.slice(1), this[key]);
279+
for (var key in this) {
280+
if (d3_map_hasOwnProperty.call(this, key)) {
281+
f.call(this, d3_map_unescape(key), this[key]);
282+
}
283+
}
276284
}
277285
});
278-
var d3_map_prefix = "\x00", d3_map_prefixCode = d3_map_prefix.charCodeAt(0);
286+
var d3_map_prefix = "\x00", d3_map_builtin = new d3_Map(), d3_map_hasOwnProperty = Object.prototype.hasOwnProperty;
287+
function d3_map_escape(key) {
288+
return (key += "") in d3_map_builtin || key[0] === d3_map_prefix ? d3_map_prefix + key : key;
289+
}
290+
function d3_map_unescape(key) {
291+
return (key += "")[0] === d3_map_prefix ? key.slice(1) : key;
292+
}
279293
function d3_map_has(key) {
280-
return d3_map_prefix + key in this;
294+
return d3_map_escape(key) in this;
281295
}
282296
function d3_map_remove(key) {
283-
key = d3_map_prefix + key;
284-
return key in this && delete this[key];
297+
return (key = d3_map_escape(key)) in this && delete this[key];
285298
}
286299
function d3_map_keys() {
287300
var keys = [];
288-
this.forEach(function(key) {
289-
keys.push(key);
290-
});
301+
for (var key in this) {
302+
if (d3_map_hasOwnProperty.call(this, key)) {
303+
keys.push(d3_map_unescape(key));
304+
}
305+
}
291306
return keys;
292307
}
293308
function d3_map_size() {
294309
var size = 0;
295-
for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) ++size;
310+
for (var key in this) {
311+
if (d3_map_hasOwnProperty.call(this, key)) {
312+
++size;
313+
}
314+
}
296315
return size;
297316
}
298317
function d3_map_empty() {
299-
for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) return false;
318+
for (var key in this) {
319+
if (d3_map_hasOwnProperty.call(this, key)) {
320+
return false;
321+
}
322+
}
300323
return true;
301324
}
302325
d3.nest = function() {
@@ -370,21 +393,23 @@
370393
function d3_Set() {}
371394
d3_class(d3_Set, {
372395
has: d3_map_has,
373-
add: function(value) {
374-
this[d3_map_prefix + value] = true;
375-
return value;
376-
},
377-
remove: function(value) {
378-
value = d3_map_prefix + value;
379-
return value in this && delete this[value];
396+
add: function(key) {
397+
this[d3_map_escape(key)] = true;
398+
return key;
380399
},
400+
remove: d3_map_remove,
381401
values: d3_map_keys,
382402
size: d3_map_size,
383403
empty: d3_map_empty,
384404
forEach: function(f) {
385-
for (var value in this) if (value.charCodeAt(0) === d3_map_prefixCode) f.call(this, value.slice(1));
405+
for (var key in this) {
406+
if (d3_map_hasOwnProperty.call(this, key)) {
407+
f.call(this, d3_map_unescape(key));
408+
}
409+
}
386410
}
387411
});
412+
for (var key in new d3_Set()) d3_map_builtin[key] = true;
388413
d3.behavior = {};
389414
d3.rebind = function(target, source) {
390415
var i = 1, n = arguments.length, method;

d3.min.js

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/arrays/map.js

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,55 +12,87 @@ function d3_Map() {}
1212
d3_class(d3_Map, {
1313
has: d3_map_has,
1414
get: function(key) {
15-
return this[d3_map_prefix + key];
15+
return this[d3_map_escape(key)];
1616
},
1717
set: function(key, value) {
18-
return this[d3_map_prefix + key] = value;
18+
return this[d3_map_escape(key)] = value;
1919
},
2020
remove: d3_map_remove,
2121
keys: d3_map_keys,
2222
values: function() {
2323
var values = [];
24-
this.forEach(function(key, value) { values.push(value); });
24+
for (var key in this) {
25+
if (d3_map_hasOwnProperty.call(this, key)) {
26+
values.push(this[key]);
27+
}
28+
}
2529
return values;
2630
},
2731
entries: function() {
2832
var entries = [];
29-
this.forEach(function(key, value) { entries.push({key: key, value: value}); });
33+
for (var key in this) {
34+
if (d3_map_hasOwnProperty.call(this, key)) {
35+
entries.push({key: d3_map_unescape(key), value: this[key]});
36+
}
37+
}
3038
return entries;
3139
},
3240
size: d3_map_size,
3341
empty: d3_map_empty,
3442
forEach: function(f) {
35-
for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) f.call(this, key.slice(1), this[key]);
43+
for (var key in this) {
44+
if (d3_map_hasOwnProperty.call(this, key)) {
45+
f.call(this, d3_map_unescape(key), this[key]);
46+
}
47+
}
3648
}
3749
});
3850

3951
var d3_map_prefix = "\0", // prevent collision with built-ins
40-
d3_map_prefixCode = d3_map_prefix.charCodeAt(0);
52+
d3_map_builtin = new d3_Map,
53+
d3_map_hasOwnProperty = Object.prototype.hasOwnProperty;
54+
55+
function d3_map_escape(key) {
56+
return (key += "") in d3_map_builtin || key[0] === d3_map_prefix ? d3_map_prefix + key : key;
57+
}
58+
59+
function d3_map_unescape(key) {
60+
return (key += "")[0] === d3_map_prefix ? key.slice(1) : key;
61+
}
4162

4263
function d3_map_has(key) {
43-
return d3_map_prefix + key in this;
64+
return d3_map_escape(key) in this;
4465
}
4566

4667
function d3_map_remove(key) {
47-
key = d3_map_prefix + key;
48-
return key in this && delete this[key];
68+
return (key = d3_map_escape(key)) in this && delete this[key];
4969
}
5070

5171
function d3_map_keys() {
5272
var keys = [];
53-
this.forEach(function(key) { keys.push(key); });
73+
for (var key in this) {
74+
if (d3_map_hasOwnProperty.call(this, key)) {
75+
keys.push(d3_map_unescape(key));
76+
}
77+
}
5478
return keys;
5579
}
5680

5781
function d3_map_size() {
5882
var size = 0;
59-
for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) ++size;
83+
for (var key in this) {
84+
if (d3_map_hasOwnProperty.call(this, key)) {
85+
++size;
86+
}
87+
}
6088
return size;
6189
}
6290

6391
function d3_map_empty() {
64-
for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) return false;
92+
for (var key in this) {
93+
if (d3_map_hasOwnProperty.call(this, key)) {
94+
return false;
95+
}
96+
}
6597
return true;
6698
}

src/arrays/set.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,22 @@ function d3_Set() {}
1111

1212
d3_class(d3_Set, {
1313
has: d3_map_has,
14-
add: function(value) {
15-
this[d3_map_prefix + value] = true;
16-
return value;
17-
},
18-
remove: function(value) {
19-
value = d3_map_prefix + value;
20-
return value in this && delete this[value];
14+
add: function(key) {
15+
this[d3_map_escape(key)] = true;
16+
return key;
2117
},
18+
remove: d3_map_remove,
2219
values: d3_map_keys,
2320
size: d3_map_size,
2421
empty: d3_map_empty,
2522
forEach: function(f) {
26-
for (var value in this) if (value.charCodeAt(0) === d3_map_prefixCode) f.call(this, value.slice(1));
23+
for (var key in this) {
24+
if (d3_map_hasOwnProperty.call(this, key)) {
25+
f.call(this, d3_map_unescape(key));
26+
}
27+
}
2728
}
2829
});
30+
31+
// In case Object.defineProperty is not supported…
32+
for (var key in new d3_Set) d3_map_builtin[key] = true;

test/arrays/map-test.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ suite.addBatch({
119119
"returns an array of string keys": function(map) {
120120
var m = map({foo: 1, bar: "42"});
121121
assert.deepEqual(m.keys().sort(), ["bar", "foo"]);
122+
},
123+
"properly unescapes zero-prefixed keys": function(map) {
124+
var m = map();
125+
m.set("__proto__", 42);
126+
m.set("\0weird", 42);
127+
assert.deepEqual(m.keys().sort(), ["\0weird", "__proto__"]);
122128
}
123129
},
124130
"values": {
@@ -246,6 +252,11 @@ suite.addBatch({
246252
m.set("__proto__", 42);
247253
assert.equal(m.get("__proto__"), 42);
248254
},
255+
"can set keys using zero-prefixed names": function(map) {
256+
var m = map();
257+
m.set("\0weird", 42);
258+
assert.equal(m.get("\0weird"), 42);
259+
},
249260
"coerces keys to strings": function(map) {
250261
var m = map();
251262
m.set(42, 1);

test/arrays/set-test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ suite.addBatch({
108108
var s = set(["bar", "foo"]);
109109
assert.deepEqual(s.values().sort(_.ascending), ["bar", "foo"]);
110110
},
111+
"properly unescapes zero-prefixed keys": function(set) {
112+
var s = set(["__proto__", "\0weird"]);
113+
assert.deepEqual(s.values().sort(_.ascending), ["\0weird", "__proto__"]);
114+
},
111115
"observes changes via add and remove": function(set) {
112116
var s = set(["foo", "bar"]);
113117
assert.deepEqual(s.values().sort(_.ascending), ["bar", "foo"]);
@@ -162,6 +166,11 @@ suite.addBatch({
162166
s.add("__proto__");
163167
assert.isTrue(s.has("__proto__"));
164168
},
169+
"can add values using zero-prefixed names": function(set) {
170+
var s = set();
171+
s.add("\0weird");
172+
assert.isTrue(s.has("\0weird"));
173+
},
165174
"coerces values to strings": function(set) {
166175
var s = set();
167176
s.add(42);

0 commit comments

Comments
 (0)