PDA

View Full Version : [OPEN] [FIXED][3.0rc3] Cannot use native JSON without patching ExtJS



papandreou
2 Jul 2009, 3:43 AM
The way Ext.util.JSON is declared makes it useless to set Ext.USE_NATIVE_JSON to true (and expect it to have an effect) after Ext.util.JSON has been defined:



Ext.util.JSON = new (function(){
var useHasOwn = !!{}.hasOwnProperty,
isNative = Ext.USE_NATIVE_JSON && JSON && JSON.toString() == '[object JSON]';

[...]

this.encode = isNative ? JSON.stringify : function(o){[...]};

[...]

this.decode = isNative ? JSON.parse : function(json){
return eval("(" + json + ')');
};
})();
The documentation of Ext.USE_NATIVE_JSON makes no notice of this limitation, so I guess this is a bug.

Best regards,
Papandreou

evant
2 Jul 2009, 4:00 AM
I don't know how common it would be for people to be regularly changing the json encoding/decode inside an application. The reason it's done that way is to stop repeated calculation over and over when 99.9% of the time it will be the same.

Condor
2 Jul 2009, 4:03 AM
The docs should mention that you need to set Ext.USE_NATIVE_JSON before including ext-all.js (in contrast to any other global Ext setting).


<script type="text/javascript" src="adapter/ext/ext-base.js"></script>
<script type="text/javascript">
Ext.USE_NATIVE_JSON = true;
</script>
<script type="text/javascript" src="ext-all.js"></script>

ps. Shouldn't USE_NATIVE_JSON be the default?

var useHasOwn = !!{}.hasOwnProperty,
isNative = (Ext.USE_NATIVE_JSON !== false) && JSON && JSON.toString() == '[object JSON]';

papandreou
2 Jul 2009, 4:09 AM
I don't know how common it would be for people to be regularly changing the json encoding/decode inside an application. The reason it's done that way is to stop repeated calculation over and over when 99.9% of the time it will be the same.

I appreciate the efficiency of the singleton = new (function(){[...]})(); construct in cases like this. I'm just looking for a way to enable the use of the native JSON object that doesn't involve patching the ExtJS code or shoehorning a script in between the inclusions of ext-base and ext-all (that would seem rather inelegant).

And you're right -- of course I don't need to be able to change my mind afterwards.

Best regards,
Papandreou

evant
2 Jul 2009, 4:27 AM
@Condor

No, for a few reasons, mainly that native JSON encode/decode require property values to be quoted, which I know isn't necessarily common amongst users.

@papandreou

You make a good point, we'll look into it.

mystix
2 Jul 2009, 4:41 AM
how about changing isNative, JSON.encode and JSON.decode to functions which set themselves up on initial access?


Ext.util.JSON = new (function(){
var useHasOwn = !!{}.hasOwnProperty,
isNative = function() {
var native;

return function() {
if (native === null) {
native = Ext.USE_NATIVE_JSON && JSON && JSON.toString() == '[object JSON]';
}

return native;
};
}();

// ... SNIP

this.encode = function() {
var ec;

return function(obj) {
if (!ec) {
// setup encoding function on first access
ec = isNative() ? JSON.stringify : function(o) { /* ... SNIP ... */ };
}

return ec(obj);
};
}();

// ... SNIP

this.decode = function() {
var dc;

return function(str) {
if (!dc) {
// setup decoding function on first access
dc = isNative() ? JSON.parse : function(json) {
return eval("(" + json + ')');
};
}

return dc(str);
};
}();
})();

papandreou
2 Jul 2009, 5:07 AM
@mystix: Did you mean like this?


Ext.util.JSON = new (function() {
var useHasOwn = !!{}.hasOwnProperty,
isNative = function() {return Ext.USE_NATIVE_JSON && window.JSON && JSON.toString() == '[object JSON]';};

[...]

this.encode = function(o) {
return (this.encode = isNative() ? JSON.stringify : function(o) {
[...]
})(o);
};

[...]

this.decode = function(o) {
return (this.decode = isNative() ? JSON.parse : function(json) {
return eval("(" + json + ')');
})(o);
};
})();

Edit: Ah, I see your edit and raise you one clever, but unreadable hack :)

mystix
2 Jul 2009, 5:09 AM
ah, yes. that's the idea.

p.s. does your code work? :-?

papandreou
2 Jul 2009, 5:14 AM
p.s. does your code work? :-?

Of course it does -- does it seem too good to be true? :)

Here's the complete JSON.js with my patch:



/**
* @class Ext.util.JSON
* Modified version of Douglas Crockford's json.js that doesn't
* mess with the Object prototype
* http://www.json.org/js.html
* @singleton
*/
Ext.util.JSON = new (function() {
var useHasOwn = !!{}.hasOwnProperty,
isNative = function() {return Ext.USE_NATIVE_JSON && window.JSON && JSON.toString() == '[object JSON]';};

// crashes Safari in some instances
//var validRE = /^("(\\.|[^"\\\n\r])*?"|[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t])+?$/;

var pad = function(n) {
return n < 10 ? "0" + n : n;
};

var m = {
"\b": '\\b',
"\t": '\\t',
"\n": '\\n',
"\f": '\\f',
"\r": '\\r',
'"' : '\\"',
"\\": '\\\\'
};

var encodeString = function(s){
if (/["\\\x00-\x1f]/.test(s)) {
return '"' + s.replace(/([\x00-\x1f\\"])/g, function(a, b) {
var c = m[b];
if(c){
return c;
}
c = b.charCodeAt();
return "\\u00" +
Math.floor(c / 16).toString(16) +
(c % 16).toString(16);
}) + '"';
}
return '"' + s + '"';
};

var encodeArray = function(o){
var a = ["["], b, i, l = o.length, v;
for (i = 0; i < l; i += 1) {
v = o[i];
switch (typeof v) {
case "undefined":
case "function":
case "unknown":
break;
default:
if (b) {
a.push(',');
}
a.push(v === null ? "null" : Ext.util.JSON.encode(v));
b = true;
}
}
a.push("]");
return a.join("");
};

this.encodeDate = function(o){
return '"' + o.getFullYear() + "-" +
pad(o.getMonth() + 1) + "-" +
pad(o.getDate()) + "T" +
pad(o.getHours()) + ":" +
pad(o.getMinutes()) + ":" +
pad(o.getSeconds()) + '"';
};

/**
* Encodes an Object, Array or other value
* @param {Mixed} o The variable to encode
* @return {String} The JSON string
*/
this.encode = function(o) {
return (this.encode = isNative() ? JSON.stringify : function(o) {
if(typeof o == "undefined" || o === null){
return "null";
}else if(Ext.isArray(o)){
return encodeArray(o);
}else if(Object.prototype.toString.apply(o) === '[object Date]'){
return Ext.util.JSON.encodeDate(o);
}else if(typeof o == "string"){
return encodeString(o);
}else if(typeof o == "number"){
return isFinite(o) ? String(o) : "null";
}else if(typeof o == "boolean"){
return String(o);
}else {
var a = ["{"], b, i, v;
for (i in o) {
if(!useHasOwn || o.hasOwnProperty(i)) {
v = o[i];
switch (typeof v) {
case "undefined":
case "function":
case "unknown":
break;
default:
if(b){
a.push(',');
}
a.push(this.encode(i), ":",
v === null ? "null" : this.encode(v));
b = true;
}
}
}
a.push("}");
return a.join("");
}
})(o);
};

/**
* Decodes (parses) a JSON string to an object. If the JSON is invalid, this function throws a SyntaxError unless the safe option is set.
* @param {String} json The JSON string
* @return {Object} The resulting object
*/
this.decode = function(o) {
return (this.decode = isNative() ? JSON.parse : function(json) {
return eval("(" + json + ')');
})(o);
};
})();
/**
* Shorthand for {@link Ext.util.JSON#encode}
* @param {Mixed} o The variable to encode
* @return {String} The JSON string
* @member Ext
* @method encode
*/
Ext.encode = Ext.util.JSON.encode;
/**
* Shorthand for {@link Ext.util.JSON#decode}
* @param {String} json The JSON string
* @param {Boolean} safe (optional) Whether to return null or throw an exception if the JSON is invalid.
* @return {Object} The resulting object
* @member Ext
* @method decode
*/
Ext.decode = Ext.util.JSON.decode;

mystix
2 Jul 2009, 5:24 AM
great! learnt something new today. thanks for that :)

p.s. you might want to highlight your changes in red for easy reading ;)

papandreou
2 Jul 2009, 5:29 AM
Of course it does -- does it seem too good to be true? :)

Whoops, actually there's one noteworty detail about my code caused by these shortcut methods:



Ext.encode = Ext.util.JSON.encode;
Ext.decode = Ext.util.JSON.decode;
The shortcuts will initially refer to the version of encode/decode that replaces itself (on whatever the 'this' object happens to be) on the first invocation.

Luckily the Ext shortcut methods have the same names as the Ext.util.JSON ones, so it's still going to work, but the two versions of encode and decode will be replaced separately. If that's a problem, you'll have to go with your approach, but that seems to involve an extra function call.

Best regards,
Papandreou

mystix
2 Jul 2009, 5:50 AM
i think that's easily worked around:


this.encode = function(o) {
this.encode = isNative() ? JSON.stringify : function(o) {
// ... snip ...
});

Ext.encode = this.encode;

return this.encode(o);
};

and likewise for Ext.decode.

papandreou
2 Jul 2009, 6:06 AM
i think that's easily worked around:


this.encode = function(o) {
this.encode = isNative() ? JSON.stringify : function(o) {
// ... snip ...
});

Ext.encode = this.encode;

return this.encode(o);
};
and likewise for Ext.decode.

Not quite -- you're assuming that Ext.util.JSON.encode will be executed before Ext.encode, which won't necessarily be the case. Luckily we're only talking about an overhead of one function call, and only if both Ext.encode and Ext.util.JSON.encode are used by the application.

It would be much worse if the shortcut method was called something besides 'encode', e.g. Ext.encodeJSON. Then my code would still install (or overwrite if Ext.encode existed for a different purpose) Ext.encode on each call to Ext.encodeJSON. It would still return the correct result, though, but that would make the bug even tougher to find.

But I suppose Ext.encode and Ext.decode are there to stay :)

Best regards,
Papandreou

mystix
2 Jul 2009, 6:07 AM
oh well. it's back to my code then. :))

papandreou
2 Jul 2009, 7:29 AM
This fixes it:



Ext.util.JSON = new (function(){
var useHasOwn = !!{}.hasOwnProperty,
isNative = Ext.USE_NATIVE_JSON && window.JSON && JSON.toString() == '[object JSON]';
[...]
})();
Best regards,
Papandreou

evant
2 Jul 2009, 7:38 AM
I'll move this to the other JSON discussion.

evant
2 Jul 2009, 9:06 AM
Both of these fixed in SVN, used Marc's solution.

papandreou
3 Jul 2009, 12:18 AM
Both of these fixed in SVN, used Marc's solution.
Thank you very much!

Best regards,
Papandreou

papandreou
3 Jul 2009, 1:40 AM
Both of these fixed in SVN, used Marc's solution.

I really don't mean to pick nits, but on second thought it seems to me that Mystix' solution can be simplified quite a bit. I managed to shrink it by 183 bytes (yuicompressed) -- that's gotta be worth something in Ext Core :).

First I realized that since encode and decode first need to check that ec/dc is defined, then perform yet another function call, they might as well check (Ext.USE_NATIVE_JSON && <whether the native JSON object is available>) directly, then call the correct function.

Whether the native JSON object is available can be precomputed (nativeJSONAvailable in the below code), and then there's no need for isNative() anymore.

Next, the non-native deserialization function (doDecode) can be inlined -- and that actually saves a function call per decode call compared to Mystix' solution.

My solution has the added bonus that Ext.USE_NATIVE_JSON can be changed at any moment. Pretty useless (as we've discussed), but it saves the explanation in the docs :).



/**
* @class Ext.util.JSON
* Modified version of Douglas Crockford's json.js that doesn't
* mess with the Object prototype
* http://www.json.org/js.html
* @singleton
*/
Ext.util.JSON = new (function(){
var useHasOwn = !!{}.hasOwnProperty,
nativeJSONAvailable = window.JSON && JSON.toString() == '[object JSON]',
pad = function(n) {
return n < 10 ? "0" + n : n;
},
doEncode = function(o){
if(typeof o == "undefined" || o === null){
return "null";
}else if(Ext.isArray(o)){
return encodeArray(o);
}else if(Object.prototype.toString.apply(o) === '[object Date]'){
return Ext.util.JSON.encodeDate(o);
}else if(typeof o == "string"){
return encodeString(o);
}else if(typeof o == "number"){
return isFinite(o) ? String(o) : "null";
}else if(typeof o == "boolean"){
return String(o);
}else {
var a = ["{"], b, i, v;
for (i in o) {
if(!useHasOwn || o.hasOwnProperty(i)) {
v = o[i];
switch (typeof v) {
case "undefined":
case "function":
case "unknown":
break;
default:
if(b){
a.push(',');
}
a.push(doEncode(i), ":",
v === null ? "null" : doEncode(v));
b = true;
}
}
}
a.push("}");
return a.join("");
}
},
m = {
"\b": '\\b',
"\t": '\\t',
"\n": '\\n',
"\f": '\\f',
"\r": '\\r',
'"' : '\\"',
"\\": '\\\\'
},
encodeString = function(s){
if (/["\\\x00-\x1f]/.test(s)) {
return '"' + s.replace(/([\x00-\x1f\\"])/g, function(a, b) {
var c = m[b];
if(c){
return c;
}
c = b.charCodeAt();
return "\\u00" +
Math.floor(c / 16).toString(16) +
(c % 16).toString(16);
}) + '"';
}
return '"' + s + '"';
},
encodeArray = function(o){
var a = ["["], b, i, l = o.length, v;
for (i = 0; i < l; i += 1) {
v = o[i];
switch (typeof v) {
case "undefined":
case "function":
case "unknown":
break;
default:
if (b) {
a.push(',');
}
a.push(v === null ? "null" : Ext.util.JSON.encode(v));
b = true;
}
}
a.push("]");
return a.join("");
};

this.encodeDate = function(o){
return '"' + o.getFullYear() + "-" +
pad(o.getMonth() + 1) + "-" +
pad(o.getDate()) + "T" +
pad(o.getHours()) + ":" +
pad(o.getMinutes()) + ":" +
pad(o.getSeconds()) + '"';
};

/**
* Encodes an Object, Array or other value
* @param {Mixed} o The variable to encode
* @return {String} The JSON string
*/
this.encode = function(o) {
return (Ext.USE_NATIVE_JSON && nativeJSONAvailable ? JSON.stringify : doEncode)(o);
};

/**
* Decodes (parses) a JSON string to an object. If the JSON is invalid, this function throws a SyntaxError unless the safe option is set.
* @param {String} json The JSON string
* @return {Object} The resulting object
*/
this.decode = function(o) {
return Ext.USE_NATIVE_JSON && nativeJSONAvailable ? JSON.parse(o) : eval("("+o+")");
};
})();
/**
* Shorthand for {@link Ext.util.JSON#encode}
* @param {Mixed} o The variable to encode
* @return {String} The JSON string
* @member Ext
* @method encode
*/
Ext.encode = Ext.util.JSON.encode;
/**
* Shorthand for {@link Ext.util.JSON#decode}
* @param {String} json The JSON string
* @param {Boolean} safe (optional) Whether to return null or throw an exception if the JSON is invalid.
* @return {Object} The resulting object
* @member Ext
* @method decode
*/
Ext.decode = Ext.util.JSON.decode;


Best regards,
Papandreou

mystix
3 Jul 2009, 1:52 AM
he shoots, he scores! after the final whistle... :>

let's see what @evan has to say about this. :)

papandreou
5 Oct 2009, 12:13 PM
he shoots, he scores! after the final whistle... :>

let's see what @evan has to say about this. :)

@evant: Well? How about those 183 bytes? :)

Best regards,
Papandreou

art.home.ext
5 Oct 2009, 12:56 PM
Can someone help me understand how the post#15 fixes the problem described from #11 to #13 please ?


@evant: Well? How about those 183 bytes? :)he's still looking for better than those 183 bytes ;)

papandreou
6 Oct 2009, 1:07 AM
Can someone help me understand how the post#15 fixes the problem described from #11 to #13 please ?

Well, it doesn't. Post #15 was actually a separate bug report that I posted (includnig a fix), and evant moved it in here so that all the native JSON issues could be resolved at once.

The problems I described in #11 and #13 were solved by not doing it the smart-ass way I proposed (replacing the encode/decode methods on the first invocation). Had that been possible, a function call could have been saved on subsequent encode/decode calls compared to Mystix' solution.

Best regards,
Papandreou

art.home.ext
6 Oct 2009, 1:19 AM
Thanks for your answer.

mystix
6 Oct 2009, 7:38 AM
@papandreou, since this thread has already been marked [FIXED], it will not be monitored as closely. might i suggest you copy the relevant posts of interest to a new thread in the Feature Requests forum instead? it'll likely get more attention there since it's an enhancement and no longer a bug.