With this new slice notation you’ll stop writing code like this:
const decisions = [
'maybe',
'HELL YEAH!',
'No.',
'never',
'are you fr',
'uh, okay?',
'never',
'let me think about it',
];
const some = decisions.slice(1, 4);
console.log(some);
// [ 'HELL YEAH!', 'No.', 'are you fr' ]
And start writing code like this:
const decisions = [
'maybe',
'HELL YEAH!',
'No.',
'never',
'are you fr',
'uh, okay?',
'never',
'let me think about it',
];
const some = decisions[1:4];
console.log(some);
// [ 'HELL YEAH!', 'No.', 'are you fr' ]
Much shorter, readable and intuitive.
And we don’t even have to wait till it officially arrives — we can have it right now.
By extending the Array
class:
Array.prototype.r = function (str) {
const [start, end] = str.split(':').map(Number);
return this.slice(start, end);
}
const decisions = [
'maybe',
'HELL YEAH!',
'No.',
'never',
'are you fr',
'uh, okay?',
'never',
'let me think about it',
];
const some = decisions.r('1:4');
console.log(some);
// [ 'HELL YEAH!', 'No.', 'are you fr' ]
Slice it right to the end
Will it slice to the last item if we leave out the second number?
Array.prototype.r = function (str) {
const [start, end] = str.split(':').map(Number);
return this.slice(start, end);
};
const yumFruits = [
'apple🍎',
'banana🍌',
'orange🍊',
'strawberry🍓',
'mango🥭',
];
const some = yumFruits.r('1:');
console.log(some);
It doesn’t?
Because end
is empty string and Number('')
is 0
, so we have arr.slice(n, 0)
which is always an empty array.
Let’s upgrade r()
with this new ability:
Array.prototype.r = function (str) {
const [startStr, endStr] = str.split(':');
// 👇 Slice from start too
const start = startStr === '' ? 0 : Number(startStr); // ✅
const end = endStr === '' ? this.length : Number(endStr); // ✅
return this.slice(start, end);
};
const yumFruits = [
'apple🍎',
'banana🍌',
'orange🍊',
'strawberry🍓',
'mango🥭',
];
console.log(yumFruits.r('1:'));
console.log(yumFruits.r(':2'));
console.log(yumFruits.r('1:3'));
Dealing with negativity
Can it handle negative indices?
const yumFruits = [
'apple🍎',
'banana🍌',
'orange🍊',
'strawberry🍓',
'mango🥭',
];
console.log(yumFruits.r(':-2'));
console.log(yumFruits.r('2:-1'));
It surely can:
The negative start
or end
is passed straight to slice()
which already has built-in support for them.
Start-stop-step
We upgrade again to array[start:stop:step
] – step
for jumping across the array in constant intervals.
Like we see in Python (again):
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// Index 2 to 7, every 2 elements
print(arr[2:7:2])
This time slice()
has no built-in stepping support, so we use a good old for loop to quickly leap through the array.
Array.prototype.r = function (str) {
const [startStr, endStr, stepStr] = str.split(':');
const start = startStr === '' ? 0 : Number(startStr);
// ⚒️ negative indexes
const absStart = start < 0 ? this.length + start : start;
const end = endStr === '' ? this.length : Number(endStr);
const absEnd = end < 0 ? this.length + end : end;
const step = stepStr === '' ? 1 : Number(stepStr);
const result = [];
for (let i = absStart; i < absEnd; i += step) {
result.push(this[i]);
}
return result;
};
const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(nums.r('2:7:2'));
console.log(nums.r('8::1'));
console.log(nums.r('-6::2'));
console.log(nums.r('::3'));
Perfect:
Array
reduce()
does the exact same job elegant immutably.
I think there’s something about the function flow of data transformation that makes it elegant.
Readability
Array.prototype.r = function (str) {
const [startStr, endStr, stepStr] = str.split(':');
const start = startStr === '' ? 0 : Number(startStr);
const absStart = start < 0 ? this.length + start : start;
const end = endStr === '' ? this.length : Number(endStr);
const absEnd = end < 0 ? this.length + end : end;
const step = stepStr === '' ? 1 : Number(stepStr);
const result = this.reduce(
(
acc,
cur,
index
) =>
index >= absStart &&
index < absEnd &&
(index - absStart) % step === 0
? [...acc, cur]
: acc,
[]
);
return result;
};
Flip the script
What about stepping backwards?
Of course Python has it:
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(arr[7:3:-1]) # [8, 7, 6, 5]
One thing you instantly notice here is start
is greater than stop
. This is a requirement for backward stepping.
print(arr[3:7:-1]) # []
print(arr[7:3:1]) # []
Which makes sense: if you’re counting backwards you’re going from right to left so start should be more.
What do we do? Once again slice()
does some of the heavy lifting for us…
We simply swap absStart
and absEnd
when step is negative
const [realStart, realEnd] =
step > 0 ? [absStart, absEnd] : [absEnd, absStart];
// ❌ start > end (4:8), step: -1 -> (8:4)
// ✅ end > start (7:3), step: -1 -> (3:7)
slice()
returns an empty array when end
> start
:
const color = [
'cream🟡',
'cobalt blue🔵',
'cherry🔴',
'celadon🟢',
];
console.log(color.slice(1, 3));
// [ 'cobalt blue🔵', 'cherry🔴' ]
console.log(color.slice(3, 0));
// []
Now let’s combine everything together:
Array.prototype.r = function (str) {
const [startStr, endStr, stepStr] = str.split(':');
const start = startStr === '' ? 0 : Number(startStr);
const step = stepStr === '' ? 1 : Number(stepStr);
const absStart = start < 0 ? this.length + start : start;
// 👇 count to start for empty end when step is negative
const end =
endStr === '' ? (step > 0 ? this.length : 0) : Number(endStr);
const absEnd = end < 0 ? this.length + end : end;
const [realStart, realEnd] =
step > 0 ? [absStart, absEnd] : [absEnd, absStart];
const slice = this.slice(realStart, realEnd); // 👈
if (slice.length === 0) return []; // 👈
const result = [];
// 👇
for (
let i = absStart;
step > 0 ? i < absEnd : i > absEnd;
i += step
) {
result.push(this[i]);
}
return result;
};
const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(nums.r('2:7:2'));
console.log(nums.r('-1:-7:-1'));
console.log(nums.r('-7::-1'));
console.log(nums.r('-5:9:-2'));
console.log(nums.r('::3'));
We’ve come a long way! Remember how we started?
Array.prototype.r = function (str) {
const [start, end] = str.split(':').map(Number);
return this.slice(start, end);
}
Yeah, and we didn’t even add any checks for wrong types and edge cases. And it goes without saying that I spent more than a few minutes debugging this…
And just imagine how it would be if we add multi-dimensional array support like in numpy
:
import numpy as np
sensor_data = np.array([
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
])
temperatures = sensor_data[:, 1]
print(temperatures) # [20 50 80]
But with our new Array
r()
method, we’ve successfully brought Python’s cool array slicing notation to JavaScript.
Every Crazy Thing JavaScript Does
A captivating guide to the subtle caveats and lesser-known parts of JavaScript.