Reputation:
How to get the reference to a slice of an array?
var A = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'];
A.mySlice = function(l, h){return this.slice(l,h)};
var B = A.mySlice(1,5); // ["b", "c", "d", "e"]
It works for direct slices derived from A. But, how to get it for all slices derived? (in this case for B)
B.mySlice = function(l, h){return this.slice(l,h)};
A[3] = 33;
A.mySlice(1,5) // ["b", "c", 33, "e"] => ok
B.mySlice(0,3) // ["b", "c", "d"] => I would want ["b", "c", 33]
Upvotes: 4
Views: 3217
Reputation: 14318
I overengineered a solution to this using a Proxy
. This will behave more or less like a pointer to some position within an int[]
in C++
would. It does this by proxying the original array, keeping track of the start and end indices, and trapping get
and set
with versions that reference the relative index within the original array.
Array.prototype.slicePtr = function(startIdx, endIdx = this.length) {
if (startIdx < 0) {
startIdx = this.length - startIdx - 1;
}
if (endIdx < 0) {
endIdx = this.length - endIdx;
}
if (startIdx > endIdx) {
throw new Error('startIdx must not be greater than endIdx');
}
endIdx = endIdx == null ? this.length : endIdx;
const self = this;
function contains(idx) {
return idx > 0 && idx < endIdx;
}
const proxy = new Proxy(this, {
get(target, prop, receiver) {
// symbols can't be converted to numbers, so handle them first
if (typeof prop === 'symbol') {
switch (prop) {
case Symbol.iterator:
return function*() {
for (let i = startIdx; i < endIdx; i++) {
yield self[i];
}
}
}
}
const idx = startIdx + Number(prop);
if (!isNaN(idx)) {
if (!contains(idx)) {
return undefined;
}
return Reflect.get(target, idx, receiver);
}
switch (prop) {
case 'splice':
return function splice(start, delCount, ...items) {
endIdx += items.length - delCount;
return self.splice(start + startIdx, delCount, ...items);
}
case 'length':
return endIdx - startIdx;
case 'pop':
return function pop() {
return proxy.splice(proxy.length - 1, 1)[0];
}
case 'push':
return function push(...items) {
proxy.splice(proxy.length, 0, ...items);
return proxy[proxy.length - 1];
}
case 'shift':
return function shift() {
return proxy.splice(0, 1)[0];
}
case 'unshift':
return function unshift(...items) {
proxy.splice(startIdx, 0, ...items);
return proxy[proxy.length - 1];
}
}
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
if (typeof prop !== 'symbol') {
const idx = startIdx + Number(prop);
if (!isNaN(idx)) {
if (Number.isFinite(idx) && Number.isInteger(idx) && Number(prop) >= proxy.length) {
endIdx = idx + 1;
}
return Reflect.set(target, idx, value, receiver);
}
}
switch (prop) {
case 'length':
endIdx = startIdx + value;
if (endIdx > self.length) {
self.length = endIdx;
}
return value;
}
return Reflect.set(target, prop, value, receiver);
},
});
return proxy;
}
/////////////
// TESTS //
/////////////
let array = [];
let slice = [];
const fnRegex = /\(?\w*\)? => (?:\{\}|(.*)?);?$/;
const resultsTbl = $('#results tbody');
const makeRow = (fn) => {
const row = $('<tr>')
.append($('<td>').append($('<code>').text(fn.toString().replace(fnRegex, '$1'))))
.append($('<td>').append($('<code>').text(JSON.stringify(fn(), null, 2))))
.append($('<td>').append($('<code>').text(JSON.stringify(array, null, 2))))
.append($('<td>').append($('<code>').text(JSON.stringify(slice, null, 2))));
resultsTbl.append(row);
};
[
() => array = [ 0, 1, 2, 3, 4, 5, 6, 7 ],
() => slice = array.slicePtr(2, 5),
() => slice[2],
() => slice[0] = 1000,
() => slice.length = 4,
() => slice.push(20),
() => slice.shift(),
() => slice.indexOf(4),
() => slice.filter(v => !(v % 2)),
() => slice.map(v => v / 2),
() => slice.splice(1, 2, 30, 40, 50),
() => slice = slice.slicePtr(1, 4),
() => slice[1] = 60,
() => slice.fill(0),
() => JSON.stringify(slice),
() => slice.toString(),
() => slice[7] = 'end',
].forEach(fn => makeRow(fn));
th {
text-align: left;
border-bottom: 1px solid darkgray;
padding: 4px 2px;
}
td {
padding-left: 2px;
white-space: nowrap;
}
td:not(:last-child) {
padding-right: 3em;
}
tr:nth-child(2n) {
background: #eee;
}
table {
border: 1px solid black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<table id="results">
<thead><tr><th>Code</th><th>Result</th><th><code>array</code></th><th><code>[ ...slice ]</code></th></tr></thead>
<tbody></tbody>
</table>
The only bit I don't have (I think) is console.log(slice)
; it always prints array
. As far as I know, there is no way to override what console.log()
and console.dir()
will print for a given object.
It's quite possible I have left some things out that I just didn't think of, and I never tested copyWithin()
because I still can't quite wrap my head around what it actually does. Might work though. 🤷♀️
Upvotes: 0
Reputation: 69934
I don't think you can do this with native JS arrays (well, not in a straightforward manner anyway).
I think the cleanest approach would be going back and using custom objects to represent the slices. Perhaps something along these lines:
function ArraySlice(arr, lo, hi){
this.arr = arr;
this.lo = lo;
this.hi = hi;
this.length = hi - lo;
};
ArraySlice.prototype._contains = function(ix){
return this.lo + ix < this.hi;
};
ArraySlice.prototype.get = function(ix){
if (this._contains(ix)){
return this.arr[this.lo + ix];
}else{
return; //undefined
}
};
ArraySlice.prototype.set = function(ix, value){
if (this._contains(ix)){
return (this.arr[this.lo + ix] = value);
}else{
return; //undefined
}
};
var a = [0,1,2,3,4,5];
var b = new ArraySlice(a, 1, 3);
a[2] = 17;
console.log( b.get(1) );
Of course, this loses the convenient []
syntax and the objects aren't arrays anymore but subclassing Array is annoying and error prone and I don't believe there is a cross-browser way to do operator overloading.
Upvotes: 3
Reputation: 5187
It's not quite as clean, since you'd be dealing with function objects, but you could wrap the calls to chain back up the original array using closures:
var A = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'];
A.mySlice = function (l, h) { return function() { return this.slice(l, h); }; };
var B = A.mySlice(1, 5); // B() will resolve to ["b", "c", "d", "e"]
B.mySlice = function (l, h) { return function() { return this().slice(1, h) }; };
A[3] = 33;
A.mySlice(1, 5)() // ["b", "c", 33, "e"]
B.mySlice(0, 3)() // ["b", "c", 33]
Upvotes: 0
Reputation: 2310
I'm pretty sure this isn't possible in Javascript. This line var B = A.mySlice(1,5);
sets B to an array object containting a slice of A's numbers. It has no reference to A. It's just an array object.
Upvotes: 0
Reputation: 5417
Here is the only possible solution I see:
This may however break lots of your code.
Upvotes: 0
Reputation: 230326
slice()
copies elements to a new array.
So, in your example, A
and B
are two completely different arrays. So, changing elements in one does not affect another.
Upvotes: 3