2

I've been learning about method chaining and how, when a method returns this, the methods become chainable as each methods returns the object.

I'm wondering, how are Array methods (such as .map, .filter, .reduce, etc.) chainable, when they don't return this?

I'm guessing it has to do with the fact that Array.map() is in fact Array.prototype.map() and a new array, returned from Array.prototype.map(), has all of the Array methods on it's prototype, allowing it to be chainable?

How does this work under the hood? Does the Array.prototype.map() return a new array, with it's prototype set to this?

What I'd really like to learn, is how you could write an object that has methods, that return new arrays for example, that are chainable, without returning this? The same way that Array works.

This doesn't work but illustrates what I am thinking.

const generate = {
  method1(arr) {
    console.log(`Param: ${arr}`)
    const result = ['NameA', 'NameB']
    result.prototype = this
    return result
  },
  method2(arr) {
    console.log(`Param: ${arr}`)
    const result = ['NameC', 'NameD']
    result.prototype = this
    return result
  },
}

const example = generate.method1(['Paul']).method2(['Steve'])
console.log(example)
console.log(generate)

Another Attempt

const generate = {
  method1: (arr) => {
    console.log(`Param: ${arr}`)
    const result = ['NameA', 'NameB']
    return Object.assign(result, Object.create(this))
  },
  method2: (arr) => {
    console.log(`Param: ${arr}`)
    const result = ['NameC', 'NameD']
    return Object.assign(result, Object.create(this))
  },
}

const example = generate.method1(['Paul']).method2(['Steve'])
console.log(example)
console.log(generate)
Cazineer
  • 2,235
  • 4
  • 26
  • 44

2 Answers2

3

The array methods do not return this, but they return the array after being transformed by whichever method was used. You wouldn't want the array methods to return the original object it was called upon - that would be pointless, because then the array methods wouldn't be useful for anything at all.

You often want to chain one transformation after another transformation, and so on - the particular object each method is using is the result of the previous transformation, which is an array if you're using map or filter (or reduce in certain circumstances).

Because you can apply Array methods to Array objects, and since some of the Array methods return Array objects, they are chainable.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • So if I'm understanding you correctly, because each newly returned array has all of the `Array` methods on its prototype, you can continue to chain `Array` methods? I guess what I am trying to understanding is how does this really work? How could you write an object with methods that return an array for example, that are chainable, without returning `this`? – Cazineer Apr 14 '18 at 20:29
  • For example, `Array.map()` returns a new array. When Array.map()` returns the new array, how do the `Array methods get on the prototype of the newly created array? Are they being set to the prototype when the new array is created, prior to being returned? I hope that makes sense. Thanks for your time. – Cazineer Apr 14 '18 at 20:36
  • The Array methods have access to the Array constructor, or to array literals - so, they simply call that constructor again with the mapped or filtered elements. You don't need to add prototype methods to each new instantiation of an object - the whole purpose of prototypes is to have *just one* method shared among all instantiations. So when a new Array object is created, with the mapped/filtered elements, that Array object will automatically have all associated Array methods accessible on it without having to set anything manually. – CertainPerformance Apr 14 '18 at 21:05
2

When a method returns this, the method becomes chainable as each call returns the object

Returning the object that the method was called on (this) is just one of the many ways to make something chainable. All you need to return is any object which has the methods that should be callable in the chain.

The array methods map and filter satisfy that criterion by returning new array objects - which again have all the array methods of course.

I'm guessing that a new array, returned from Array.prototype.map(), has all of the Array methods on it's prototype, allowing it to be chainable?

Yes.

Does the Array.prototype.map() return a new array, with its prototype set to this?

No, it returns a plain, normal array, with its prototype set to Array.prototype. It uses this only in the iteration.

How could you write an object that has methods, that return new arrays for example, that are chainable, without returning this?

You shouldn't return arrays, as arrays have neither method1 nor method21. Return an object of your own kind, with the methods you like, that contains the array result.

function generate(result) {
  return {
    result,
    method1(arr) {
      console.log(`Method1 called with args ${arr} on object with ${this.result} result`);
      return generate(['NameA', 'NameB']);
    },
    method2(arr) {
      console.log(`Method1 called with args ${arr} on object with ${this.result} result`);
      return generate(['NameC', 'NameD']);
    }
  };
}
const start = generate();
const example = start.method1(['Paul']).method2(['Steve']);
console.log(example);
console.log(start);

1: Technically you could subclass Array to do that, but I cannot recommend it without knowing more about your use case.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I don't have a use case, I am just learning. I've read numerous articles on chaining and they all just talk about returning `this`. – Cazineer Apr 14 '18 at 21:11
  • @Kainan Yeah. I must assume that they didn't really understand chaining and just think that `this` is some kind of magic… – Bergi Apr 14 '18 at 21:12
  • Thank you for the answer and clarity. I totally understand how your example works, however I am still confused when it comes to how `Array.XXX` works. `Array.XXX` doesn't return an object with a `results` key. `Array.map()` returns a new array with `.map()` under its prototype (`__proto__`). Is this type of behavior not replicable? – Cazineer Apr 14 '18 at 21:29
  • "You shouldn't return arrays, as arrays have neither method1 nor method2." I understand this statement and this is why returning an array is not chainable, there would be no methods to call, but if the methods were on the prototype of said array (like `Array`), then they would be chainable. Unless I am missing something fundamental here. – Cazineer Apr 14 '18 at 21:33
  • @Kainan Do you really mean [`Array.map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#Array_generic_methods), or `Array.prototype.map` called on an array instance? – Bergi Apr 14 '18 at 21:34
  • @Kainan If you want to replicate a design where the methods are inherited from a prototype, just switch from the factory function `generate` to a constructor+prototype approach (e.g. with `class` syntax). It doesn't change anything for chainability though. – Bergi Apr 14 '18 at 21:35
  • I'd like to replicate the design of `Array.map, filter, etc.` where there are a group of chainable methods, where each method returns a new array, with the said chainable methods on the prototype of the new array, returned from each method. `const arr = [{id: 'name'}] const newArr = arr.filter(x => x.id === 'name').map(x => x.id)` Each returned value from `.filter` and `.map` is a new array with all the `Array` methods on the prototype of the new array, which is how these methods are chainable. :) – Cazineer Apr 14 '18 at 21:48
  • "*with the said chainable methods on the prototype of the new array*" means that you either have something that isn't an `Array` but your own type (which might look *like* an array), or that you have to [extend the builtin `Array.prototype`](https://stackoverflow.com/q/8859828/1048572) ([properly](https://stackoverflow.com/q/13296340/1048572), please), or that you would have to make a subclass of `Array` using ES6 `class MyArray extends Array …` – Bergi Apr 14 '18 at 21:58
  • If I'm understanding you correctly, you are saying that you can't add to an arrays prototype the way that you can with an object (constructor, `Object.create`, etc.? I am going to look at your subclass suggestions as I am not confident modifying Array.prototype. I've never extended the build in prototypes before. – Cazineer Apr 14 '18 at 22:46
  • @Kainan Yes, you cannot create arrays using `Object.create`, and arrays already have the builtin `Array` constructor not something that you can write yourself - the only way would be subclassing `Array`. But I think you really should ask a new question about that, it doesn't really have anything to do with chaining in particular. – Bergi Apr 15 '18 at 10:20
  • thanks for your help, you have put this in perspective for me. – Cazineer Apr 15 '18 at 21:40