You might think direct interpolation is enough… but there are a few nasty corner cases.
The most well-known among them is __proto__:
var obj0 = {"__proto__": {"a": 5}};
var obj1 = JSON.parse('{"__proto__": {"a": 5}}');
console.log(obj0.a);
console.log(Object.getPrototypeOf(obj0) === Object.prototype);
console.log(obj1.a);
console.log(Object.getPrototypeOf(obj1) === Object.prototype);
In an object literal, a "__proto__": key is required to set the prototype of the object being constructed, unless it is written as ["__proto__"]:; this rule is suppressed when parsing JSON. This is specified in ECMA-262 13th Ed., §13.2.5.5, 4th variant.
You may never run into this problem if your JSON objects are always fixed, class-like structures, but if you ever use JSON objects to represent arbitrary key-value mappings, this may be a nasty surprise for you.
When interpolating into the <script> element in HTML (which you probably will do most of the time, as opposed to having PHP directly emit a bare text/javascript response), there are a few other dangers as well. For example, the character sequence </script> embedded within a string literal may terminate the tag prematurely:
<script>
console.log(["</script>"]);
</script>
Fortunately, PHP takes care of this, as by default it emits the slash character escaped; JSON_UNESCAPED_SLASHES can be used to suppress this when unnecessary. (In fact, this is why JSON allows the \/ escape in the first place.) But there is another hazard that PHP does not take care of by default (thanks to Jon Surrell for describing the issue):
<script>
document.addEventListener('DOMContentLoaded', () => {
console.log(
"contents of the <script> element: %O",
document.querySelector(
'body > script:nth-of-type(2)').innerHTML);
});
</script>
<script>
console.log("<!--<script>");
</script>
<p> Oh no, where did the paragraph go?
This is because, as per HTML5 parsing rules, a comment introducer found within a script element will make the parser skip through any </script> tags until the corresponding comment terminator sequence -->.
<script>
var text = `<!--<script>
</script>
<p> Oh no, where did the paragraph go?
<script>
//-->`;
console.log("ah, there it is: %O", text);
</script>
So the really safe way to serialize JSON into JavaScript source code would be to encode the string twice into a string literal, taking care to escape < characters, and parse that literal on the JS side:
var testarray = JSON.parse(<?=(
json_encode(
json_encode($testarray, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
JSON_UNESCAPED_SLASHES | JSON_HEX_TAGS)); ?>);
If for some reason you are emitting code into a plain text/javascript response, normal double-encoding will be sufficient:
var testarray = JSON.parse(<?=(
json_encode(
json_encode($testarray, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
JSON_UNESCAPED_SLASHES)); ?>);
(Flags other than JSON_HEX_TAGS are only added above to avoid adding more escape characters than necessary.)
Surprisingly, this technique can also come with some performance benefits. The JavaScript grammar is sufficiently complicated that it can actually pay off to defer parsing of the object literal to runtime, in order to be able to use the much simpler and more performant JSON parser. But as always, performance can vary over engines, versions and amount of data, and is hardly guaranteed. Worry about correctness first, and performance second.