Skip to content

Commit 2d5c259

Browse files
committed
Merge pull request #17 from yahoo/perf
Improve perf via `isJSON` option
2 parents c5b0b93 + e7015c3 commit 2d5c259

File tree

6 files changed

+123
-20
lines changed

6 files changed

+123
-20
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ node_js:
44
- "0.12"
55
- "4"
66
- "5"
7+
- "6"

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,28 @@ The above will produce the following string, HTML-escaped output which is safe t
6666
'{"haxorXSS":"\\u003C\\u002Fscript\\u003E"}'
6767
```
6868

69+
### Options
70+
71+
The `serialize()` function accepts `options` as its second argument. There are two options, both default to being `undefined`:
72+
73+
#### `options.space`
74+
75+
This option is the same as the `space` argument that can be passed to [`JSON.stringify`][JSON.stringify]. It can be used to add whitespace and indentation to the serialized output to make it more readable.
76+
77+
```js
78+
serialize(obj, {space: 2});
79+
```
80+
81+
#### `options.isJSON`
82+
83+
This option is a signal to `serialize()` that the object being serialized does not contain any function or regexps values. This enables a hot-path that allows serialization to be over 3x faster. If you're serializing a lot of data, and know its pure JSON, then you can enable this option for a speed-up.
84+
85+
**Note:** That when using this option, the output will still be escaped to protect against XSS.
86+
87+
```js
88+
serialize(obj, {isJSON: true});
89+
```
90+
6991
## License
7092

7193
This software is free to use under the Yahoo! Inc. BSD license.
@@ -79,4 +101,5 @@ See the [LICENSE file][LICENSE] for license text and copyright information.
79101
[travis]: https://travis-ci.org/yahoo/serialize-javascript
80102
[travis-badge]: https://img.shields.io/travis/yahoo/serialize-javascript.svg?style=flat-square
81103
[express-state]: https://github.com/yahoo/express-state
104+
[JSON.stringify]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
82105
[LICENSE]: https://github.com/yahoo/serialize-javascript/blob/master/LICENSE

index.js

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,40 +10,69 @@ var isRegExp = require('util').isRegExp;
1010

1111
// Generate an internal UID to make the regexp pattern harder to guess.
1212
var UID = Math.floor(Math.random() * 0x10000000000).toString(16);
13-
var PLACE_HOLDER_REGEXP = new RegExp('"@__(FUNCTION|REGEXP)-' + UID + '-(\\d+)__@"', 'g');
13+
var PLACE_HOLDER_REGEXP = new RegExp('"@__(F|R)-' + UID + '-(\\d+)__@"', 'g');
1414

1515
var IS_NATIVE_CODE_REGEXP = /\{\s*\[native code\]\s*\}/g;
1616
var UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g;
1717

1818
// Mapping of unsafe HTML and invalid JavaScript line terminator chars to their
1919
// Unicode char counterparts which are safe to use in JavaScript strings.
20-
var UNICODE_CHARS = {
20+
var ESCAPED_CHARS = {
2121
'<' : '\\u003C',
2222
'>' : '\\u003E',
2323
'/' : '\\u002F',
2424
'\u2028': '\\u2028',
2525
'\u2029': '\\u2029'
2626
};
2727

28-
module.exports = function serialize(obj, space) {
28+
function escapeUnsafeChars(unsafeChar) {
29+
return ESCAPED_CHARS[unsafeChar];
30+
}
31+
32+
module.exports = function serialize(obj, options) {
33+
options || (options = {});
34+
35+
// Backwards-compatability for `space` as the second argument.
36+
if (typeof options === 'number' || typeof options === 'string') {
37+
options = {space: options};
38+
}
39+
2940
var functions = [];
3041
var regexps = [];
31-
var str;
3242

33-
// Creates a JSON string representation of the object and uses placeholders
34-
// for functions and regexps (identified by index) which are later
35-
// replaced.
36-
str = JSON.stringify(obj, function (key, value) {
37-
if (typeof value === 'function') {
38-
return '@__FUNCTION-' + UID + '-' + (functions.push(value) - 1) + '__@';
43+
// Returns placeholders for functions and regexps (identified by index)
44+
// which are later replaced by their string representation.
45+
function replacer(key, value) {
46+
if (!value) {
47+
return value;
48+
}
49+
50+
var type = typeof value;
51+
52+
if (type === 'object') {
53+
if (isRegExp(value)) {
54+
return '@__R-' + UID + '-' + (regexps.push(value) - 1) + '__@';
55+
}
56+
57+
return value;
3958
}
4059

41-
if (typeof value === 'object' && isRegExp(value)) {
42-
return '@__REGEXP-' + UID + '-' + (regexps.push(value) - 1) + '__@';
60+
if (type === 'function') {
61+
return '@__F-' + UID + '-' + (functions.push(value) - 1) + '__@';
4362
}
4463

4564
return value;
46-
}, space);
65+
}
66+
67+
var str;
68+
69+
// Creates a JSON string representation of the value.
70+
// NOTE: Node 0.12 goes into slow mode with extra JSON.stringify() args.
71+
if (options.isJSON && !options.space) {
72+
str = JSON.stringify(obj);
73+
} else {
74+
str = JSON.stringify(obj, options.isJSON ? null : replacer, options.space);
75+
}
4776

4877
// Protects against `JSON.stringify()` returning `undefined`, by serializing
4978
// to the literal string: "undefined".
@@ -54,9 +83,7 @@ module.exports = function serialize(obj, space) {
5483
// Replace unsafe HTML and invalid JavaScript line terminator chars with
5584
// their safe Unicode char counterpart. This _must_ happen before the
5685
// regexps and functions are serialized and added back to the string.
57-
str = str.replace(UNSAFE_CHARS_REGEXP, function (unsafeChar) {
58-
return UNICODE_CHARS[unsafeChar];
59-
});
86+
str = str.replace(UNSAFE_CHARS_REGEXP, escapeUnsafeChars);
6087

6188
if (functions.length === 0 && regexps.length === 0) {
6289
return str;
@@ -66,7 +93,7 @@ module.exports = function serialize(obj, space) {
6693
// string with their string representations. If the original value can not
6794
// be found, then `undefined` is used.
6895
return str.replace(PLACE_HOLDER_REGEXP, function (match, type, valueIndex) {
69-
if (type === 'REGEXP') {
96+
if (type === 'R') {
7097
return regexps[valueIndex].toString();
7198
}
7299

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "Serialize JavaScript to a superset of JSON that includes regular expressions and functions.",
55
"main": "index.js",
66
"scripts": {
7-
"benchmark": "node test/benchmark/serialize.js",
7+
"benchmark": "node -v && node test/benchmark/serialize.js",
88
"test": "istanbul cover -- ./node_modules/mocha/bin/_mocha test/unit/ --reporter spec"
99
},
1010
"repository": {

test/benchmark/serialize.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

3-
var Benchmark = require('benchmark'),
4-
serialize = require('../../');
3+
var Benchmark = require('benchmark');
4+
var serialize = require('../../');
55

66
var suiteConfig = {
77
onStart: function (e) {
@@ -31,6 +31,14 @@ new Benchmark.Suite('simpleObj', suiteConfig)
3131
.add('JSON.stringify( simpleObj )', function () {
3232
JSON.stringify(simpleObj);
3333
})
34+
.add('JSON.stringify( simpleObj ) with replacer', function () {
35+
JSON.stringify(simpleObj, function (key, value) {
36+
return value;
37+
});
38+
})
39+
.add('serialize( simpleObj, {isJSON: true} )', function () {
40+
serialize(simpleObj, {isJSON: true});
41+
})
3442
.add('serialize( simpleObj )', function () {
3543
serialize(simpleObj);
3644
})

test/unit/serialize.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,48 @@ describe('serialize( obj )', function () {
169169
expect(eval(serialize('</script>'))).to.equal('</script>');
170170
});
171171
});
172+
173+
describe('options', function () {
174+
it('should accept options as the second argument', function () {
175+
expect(serialize('foo', {})).to.equal('"foo"');
176+
});
177+
178+
it('should accept a `space` option', function () {
179+
expect(serialize([1], {space: 0})).to.equal('[1]');
180+
expect(serialize([1], {space: ''})).to.equal('[1]');
181+
expect(serialize([1], {space: undefined})).to.equal('[1]');
182+
expect(serialize([1], {space: null})).to.equal('[1]');
183+
expect(serialize([1], {space: false})).to.equal('[1]');
184+
185+
expect(serialize([1], {space: 1})).to.equal('[\n 1\n]');
186+
expect(serialize([1], {space: ' '})).to.equal('[\n 1\n]');
187+
expect(serialize([1], {space: 2})).to.equal('[\n 1\n]');
188+
});
189+
190+
it('should accept a `isJSON` option', function () {
191+
expect(serialize('foo', {isJSON: true})).to.equal('"foo"');
192+
expect(serialize('foo', {isJSON: false})).to.equal('"foo"');
193+
194+
function fn() { return true; }
195+
196+
expect(serialize(fn)).to.equal('function fn() { return true; }');
197+
expect(serialize(fn, {isJSON: false})).to.equal('function fn() { return true; }');
198+
199+
expect(serialize(fn, {isJSON: true})).to.equal('undefined');
200+
});
201+
});
202+
203+
describe('backwards-compatability', function () {
204+
it('should accept `space` as the second argument', function () {
205+
expect(serialize([1], 0)).to.equal('[1]');
206+
expect(serialize([1], '')).to.equal('[1]');
207+
expect(serialize([1], undefined)).to.equal('[1]');
208+
expect(serialize([1], null)).to.equal('[1]');
209+
expect(serialize([1], false)).to.equal('[1]');
210+
211+
expect(serialize([1], 1)).to.equal('[\n 1\n]');
212+
expect(serialize([1], ' ')).to.equal('[\n 1\n]');
213+
expect(serialize([1], 2)).to.equal('[\n 1\n]');
214+
});
215+
});
172216
});

0 commit comments

Comments
 (0)