Iterators When you have a data structure that needs to be iterated over sometime, consider providing an iterator to make things easier and more abstract for consumers. That is, you don't really want to write
for(let x = this.first; x != null; x = x.next) {
every time, nor would you want consumers of SequentialSearchST
to have to do that if they wanted to examine the data manually. Instead, put an iterator on the class, and call that iterator by using for..of
whenever needed.
class SequentialSearchST {
*[Symbol.iterator]() {
for (let node = this.first; node; node = node.next) {
yield node;
}
}
Variable names x
is not an informative variable name. Like I did above, maybe call a node node
instead.
Semicolons Either use semicolons wherever appropriate, or don't use semicolons. Anything in-between looks inconsistent and can result in you eventually getting tripped up by the rules of Automatic Semicolon Insertion. If you're a beginner in JS, I'd highly recommend using semicolons. You can use a linter to enforce a particular style - to warn you of potential bugs before they turn into runtime errors that you need to spend time tracking down.
Declare your variables before using them. If you don't, you'll either implicitly create a global variable (an easy source of bugs), or an error will be thrown (in strict mode, and strict mode is recommended). Change
st = new SequentialSearchST();
to
const st = new SequentialSearchST();
Class usage A class is useful when you need to tie together data with methods that operate on that data. If you have only data, a class doesn't provide any benefit, and is simply noise, IMO. If I were you, I'd remove the Node
class and change
this.first = new Node(key, val, this.first);
to
this.first = { key, val, next: this.first };
null Rather than assigning null
as the initial value, and returning null when no matching node is found, you might consider if simply omitting a return statement entirely would do the trick. It's perfectly fine for a function call to return undefined
; omitting null
requires less code, less potential for inconsistencies, and one less thing to think about. (If this is for an exercise that requires null
to be used, that's fine)
(If you do use null, make sure to verify it by using !== null
, not !=
null; sloppy comparison requires more cognitive overhead than strict comparison)
This is how I'd refactor it:
class SequentialSearchST {
*[Symbol.iterator]() {
for (let node = this.first; node; node = node.next) {
yield node;
}
}
get(key) {
for (const node of this) {
if (node.key === key) {
return node.val;
}
}
}
put(key, val) {
for (const node of this) {
if (node.key === key) {
node.val = val;
return;
}
}
this.first = { key, val, next: this.first };
}
}
const st = new SequentialSearchST();
st.put('s', 0)
st.put('e', 1)
st.put('a', 2)
st.put('r', 3)
st.put('c', 4)
st.put('h', 5)
st.put('e', 6)
st.put('x', 7)
st.put('a', 9)
st.put('m', 10)
st.put('p', 11)
st.put('l', 12)
st.put('e', 13)
console.dir(st.get('s'));
Another approach Maps hold key-value pairs. (Objects do too, but objects can only contain string and symbol keys, which is a limitation.) If this was a real-world problem I had to solve, I'd just use a Map:
const st = new Map();
st.set('s', 0)
st.set('e', 1)
st.set('a', 2)
st.set('r', 3)
st.set('c', 4)
st.set('h', 5)
st.set('e', 6)
st.set('x', 7)
st.set('a', 9)
st.set('m', 10)
st.set('p', 11)
st.set('l', 12)
st.set('e', 13)
console.dir(st.get('s'));