Array state will be cached in iOS 12 Safari. Is it a bug or feature?
I found a problem with Array's value state in the newly released iOS 12 Safari, for example, code like this:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<title>iOS 12 Safari bugs</title>
<script type="text/javascript">
window.addEventListener("load", function ()
{
let arr = [1, 2, 3, 4, 5];
alert(arr.join());
document.querySelector("button").addEventListener("click", function ()
{
arr.reverse();
});
});
</script>
</head>
<body>
<button>Array.reverse()</button>
<p style="color:red;">test: click button and refresh page, code:</p>
</body>
</html>
After refreshing the page, the array's value is still reversed. Is this a bug or a feature of new Safari?
Here is a demo page. Try to use it with iOS 12 Safari:
https://cdn.miss.cat/demo/ios12-safari-bug.html
It's definitely a BUG! And it's a very serious bug.
Per my testing, the bug is due to the optimization of array initializers in which all values are primitive literals. For example, () => [1, null, 'x'] will result in such an array; all returned array references from this lambda will link to the same memory address, and some method like toString() will be cached. Normally, any mutable operation on such arrays will copy the data to a separate memory space and link to it; this is called copy-on-write, or CoW for short.
The reverse() method mutates the array, so it should trigger a copy-on-write. Apparently, it no longer does so, which causes the bug you’re seeing.
On the other hand, all methods which do not modify the array should not trigger CoW, and I’ve found that even a.fill(value, 0, 0) or a.copyWithin(index, 0, 0) won't trigger CoW because such callings don't really mutate the array. But I noticed that a.slice() WILL trigger CoW. So, if I had to guess, I’d say the underlying cause of this bug may be that someone accidentally swapped the indices of slice and reverse.My guess is not correct, the original author (Keith Miller @ Apple) just simply miss the reverse() case though he already wrote many test cases. slice() is a separate story, may be related to fastSlice optimization though I still don't understand the reason. Anyway, unnecessary CoW won't cause bug.
I wrote a lib to fix the bug.
https://www.npmjs.com/package/array-reverse-polyfill
This is the code:
(function() {
function buggy() {
var a = [1, 2];
return String(a) === String(a.reverse());
}
if(!buggy()) return;
var r = Array.prototype.reverse;
Array.prototype.reverse = function reverse() {
if (Array.isArray(this)) this.length = this.length;
return r.call(this);
}
})();
This is a bug in webkit. Though this has been solved at their end but not yet shipped with iOS GM release. One of the solutions to this problem:
(function() {
function getReverseStr() {
return [1, 2].reverse();
}
var n1 = getReverseStr()[0];
var n2 = getReverseStr()[0];
// check if there is an issue
if(n1 != n2) {
var origReverseFunction = Array.prototype.reverse;
Array.prototype.reverse = function() {
var newArr = this.slice();
// use original reverse function so that edge cases are taken care of
origReverseFunction.apply(newArr, arguments);
var that = this;
// copy reversed array
newArr.forEach(function(value, index) {
that[index] = value;
});
return this;
}
}
})();
It seems not to be cached if the number of elements changes.
I was able to avoid this like this.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<title>iOS 12 Safari bugs</title>
<script type="text/javascript">
window.addEventListener("load", function ()
{
let arr = [1, 2, 3, 4, 5];
arr.push('');
arr.pop();
alert(arr.join());
document.querySelector("button").addEventListener("click", function ()
{
arr.reverse();
});
});
</script>
</head>
<body>
<button>Array.reverse()</button>
<p style="color:red;">test: click button and refresh page, code:</p>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<title>iOS 12 Safari bugs</title>
<script type="text/javascript">
window.addEventListener("load", function ()
{
let arr = [1, 2, 3, 4, 5];
alert(arr.join());
document.querySelector("button").addEventListener("click", function ()
{
arr.reverse();
});
});
</script>
</head>
<body>
<button>Array.reverse()</button>
<p style="color:red;">test: click button and refresh page, code:</p>
</body>
</html>
After refreshing the page, the array's value is still reversed. Is this a bug or a feature of new Safari?
Here is a demo page. Try to use it with iOS 12 Safari:
https://cdn.miss.cat/demo/ios12-safari-bug.html
It's definitely a BUG! And it's a very serious bug.
Per my testing, the bug is due to the optimization of array initializers in which all values are primitive literals. For example, () => [1, null, 'x'] will result in such an array; all returned array references from this lambda will link to the same memory address, and some method like toString() will be cached. Normally, any mutable operation on such arrays will copy the data to a separate memory space and link to it; this is called copy-on-write, or CoW for short.
The reverse() method mutates the array, so it should trigger a copy-on-write. Apparently, it no longer does so, which causes the bug you’re seeing.
On the other hand, all methods which do not modify the array should not trigger CoW, and I’ve found that even a.fill(value, 0, 0) or a.copyWithin(index, 0, 0) won't trigger CoW because such callings don't really mutate the array. But I noticed that a.slice() WILL trigger CoW. So, if I had to guess, I’d say the underlying cause of this bug may be that someone accidentally swapped the indices of slice and reverse.My guess is not correct, the original author (Keith Miller @ Apple) just simply miss the reverse() case though he already wrote many test cases. slice() is a separate story, may be related to fastSlice optimization though I still don't understand the reason. Anyway, unnecessary CoW won't cause bug.
I wrote a lib to fix the bug.
https://www.npmjs.com/package/array-reverse-polyfill
This is the code:
(function() {
function buggy() {
var a = [1, 2];
return String(a) === String(a.reverse());
}
if(!buggy()) return;
var r = Array.prototype.reverse;
Array.prototype.reverse = function reverse() {
if (Array.isArray(this)) this.length = this.length;
return r.call(this);
}
})();
This is a bug in webkit. Though this has been solved at their end but not yet shipped with iOS GM release. One of the solutions to this problem:
(function() {
function getReverseStr() {
return [1, 2].reverse();
}
var n1 = getReverseStr()[0];
var n2 = getReverseStr()[0];
// check if there is an issue
if(n1 != n2) {
var origReverseFunction = Array.prototype.reverse;
Array.prototype.reverse = function() {
var newArr = this.slice();
// use original reverse function so that edge cases are taken care of
origReverseFunction.apply(newArr, arguments);
var that = this;
// copy reversed array
newArr.forEach(function(value, index) {
that[index] = value;
});
return this;
}
}
})();
It seems not to be cached if the number of elements changes.
I was able to avoid this like this.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<title>iOS 12 Safari bugs</title>
<script type="text/javascript">
window.addEventListener("load", function ()
{
let arr = [1, 2, 3, 4, 5];
arr.push('');
arr.pop();
alert(arr.join());
document.querySelector("button").addEventListener("click", function ()
{
arr.reverse();
});
});
</script>
</head>
<body>
<button>Array.reverse()</button>
<p style="color:red;">test: click button and refresh page, code:</p>
</body>
</html>
Comments
Post a Comment