suppose that, I've this Object for methods chain:
let list = {
numbers: [],
create: function(list) {
this.numbers = list;
return this;
},
asc: function() {
return this.numbers.sort((a, b) => a - b);
},
desc: function() {
return this.numbers.sort((a, b) => b - a);
},
unique: function() {
return this.numbers.filter((item, i, source) => source.indexOf(item) === i);
}
}
let new_list = list.create([10, 2, 2, 3, 1, 2, 4]).unique();
console.log(new_list);
but how can handle this chain:
list.create([10, 2, 2, 3, 1, 2, 4]).unique().asc();
I actually want to handle it for non-limited chain... but it works for only one method for now!
CodePudding user response:
You need to understand how state works in OOP, in addition to JavaScript's (somewhat unorthodox)
this
parameter.- As well as how some methods of
Array
(orArray.prototype
) in JS will mutate in-place (likesort
) while others treatthis
(theArray
) as immutable and instead always return a new array (e.g.filter
andmap
).
- As well as how some methods of
If you're defining a reusable object type (with properties and methods/functions) then you should define a
class
instead.- In JavaScript, a
class
is just shorthand for aprototype
constructor function.
- In JavaScript, a
BTW, I assume this is just a learning-exercise, otherwise there really isn't a reason to reimplment a list type in JavaScript as
Array
does it all already.As you want method-chaining (as a builder-pattern, I guess?), it's important to ensure you don't mutate
this.numbers
in-place, so only callArray.prototype.sort()
on a new copy:
Something like this:
class MyNumbersList {
constructor( arr ) {
if( !Array.isArray( arr ) ) throw new Error( "Argument `arr` must be an array." );
this.numbers = arr;
}
// Array.prototype.sort will mutate `this.numbers` in-place.
asc() {
const copy = Array.from( this.numbers );
copy.sort((a, b) => a - b);
return new MyNumbersList( copy );
}
desc() {
const copy = Array.from( this.numbers );
copy.sort((a, b) => a - b);
return new MyNumbersList( copy );
}
// But `filter` returns a new array.
// Use a `Set` instead of `indexOf`, otherwise you'll have `O(n*n)` complexity, which is horrible:
unique() {
const valuesSet = new Set( this.numbers );
const copy = this.numbers.filter( n => valuesSet.has( n ) );
return new MyNumbersList( copy );
}
// This is a property getter, so you don't need to use `()` to invoke it.
// To ensure encapsulation (i.e. to prevent unwanted mutation of `this.numbers`) always return a by-value copy (using `Array.from`), instead of exposing `this.numbers` directly:
get values() {
return Array.from( this.numbers );
}
}
Used like so:
const x = new MyNumbersList( [ 1, 2, 2, 3 ] );
const t = new MyNumbersList( [ 3, 2, 2, 1 ] );
console.log( x.values ); // "1, 2, 2, 3"
console.log( y.values ); // "3, 2, 2, 1"
console.log( x.desc() ); // "3, 2, 2, 1"
console.log( y.asc() ); // "1, 2, 2, 3"
console.log( x.desc().unique().values ); // "3, 2, 1"
All of the "modern" members of Array.prototype
do not mutate an array instance in-place (like map
and filter
), however certain older members dating back to the first editions of JS are considered "Mutator Methods" which require you to be careful.
They're listed in this page. MDN used to have a page listing them too but it seems to be gone now.
CodePudding user response:
but how can handle this chain:
list.create([10, 2, 2, 3, 1, 2, 4]).unique().asc();
In order to chain methods like that, each method (at least before the last method in the chain) must return an object that has the next method on it. Your unique
method returns an array, not the list
object, so it doesn't have an asc
method. In order to have that chain, you'd need unique
to return something with asc
.
In this specific example, you might use an augmented array. You could directly augment an array instance, but for chains like you're describing, you probably want multiple instances of list
(not just one), so that suggests an Array
subclass:
class List extends Array {
static fromList(list) { // Requires either no argument or an iterable argument
const fromList = new this();
if (list) {
fromList.push(...list);
}
return fromList;
}
asc() {
const fromList = this.constructor.fromList(this);
fromList.sort((a, b) => a - b);
return fromList;
}
desc() {
const fromList = this.constructor.fromList(this);
fromList.sort((a, b) => b - a);
return fromList;
}
unique() {
return this.constructor.fromList(new Set(this));
}
}
const new_list = List.fromList([10, 2, 2, 3, 1, 2, 4]).unique().asc();
console.log(new_list);
That example always creates a new instance of List
for each operation, but you can also do it where it mutates and returns the original list:
class List extends Array {
static fromList(list) { // Requires either no argument or an iterable argument
const fromList = new this();
if (list) {
fromList.push(...list);
}
return fromList;
}
asc() {
return this.sort((a, b) => a - b);
}
desc() {
return this.sort((a, b) => b - a);
}
unique() {
const set = new Set(this);
this.length = 0;
this.push(...set);
return this;
}
}
const new_list = List.fromList([10, 2, 2, 3, 1, 2, 4]).unique().asc();
console.log(new_list);
Alternatively, you might use a builder pattern where you store up the things to do and then have a "go" terminal operation that does all the steps.
Note: Creating augmented array classes like List
is fine, but beware that the subclass needs to handle all the signatures of the original Array
constructor, which is...eccentric. If you pass new Array
a single argument that's a number, it creates a sparse array with that length and no elements in it. If you pass it a single argument that isn't a number, it creates an array with that value as its only element. If you pass it multiple arguments, it creates an array with those elements in it. That's why I used a static fromList
method rather than writing our own List
constructor.
CodePudding user response:
You could implement a function which returns the numbers and all other functions to return this
for chaining.
This approach does not prevent the content to be overwritten from other calls, because of having only one object, with one data set at the same time.
let list = {
numbers: [],
create: function(list) {
this.numbers = list;
return this;
},
asc: function() {
this.numbers.sort((a, b) => a - b);
return this;
},
desc: function() {
this.numbers.sort((a, b) => b - a);
return this;
},
unique: function() {
this.numbers = this.numbers.filter((item, i, source) => source.indexOf(item) === i);
return this;
},
values: function() {
return this.numbers;
}
}
let new_list = list.create([10, 2, 2, 3, 1, 2, 4]).unique().values();
console.log(new_list);
To overcome this problem, you could use a class
with methods, where every instance keeps the own data.
class List {
constructor (numbers = []) {
this.numbers = numbers;
}
asc() {
this.numbers.sort((a, b) => a - b);
return this;
}
create(numbers) {
this.numbers = numbers;
return this;
}
desc() {
this.numbers.sort((a, b) => b - a);
return this;
}
unique() {
this.numbers = this.numbers.filter((item, i, source) => source.indexOf(item) === i);
return this;
}
values() {
return this.numbers;
}
}
let new_list = new List([10, 2, 2, 3, 1, 2, 4]).unique().values();
console.log(new_list);