Can method chaining be implemented the way built-in functions in Javascript are implemented? - Stack Overflow

I think there is something that i'm missing about method chaining. To me it feels inplete.Method

I think there is something that i'm missing about method chaining. To me it feels inplete.

Method chaining works by having each method return this so that another method on that object can be called. However, the fact that the return value is this and not the result of the function seems inconvenient to me.

Here is a simple example.

const Obj = {
    result: 0,
    addNumber: function (a, b) {
        this.result = a + b;
        return this;
    },

    multiplyNumber: function (a) {
        this.result = this.result * a;
        return this;
    },
}

const operation = Obj.addNumber(10, 20).multiplyNumber(10).result
console.log(operation)

key points:

  1. Every method in the chain Obj.addNumber(10, 20).multiplyNumber(10) returns this.
  2. The last part of the chain .result is the one that returns a value other than this.

The problem with this approach is that it require you to tack on a property / method to get a value at the end other thanthis.

Compare this with built-in functions in JavaScript.

const str = "  SomE RandoM StRIng  "

console.log(str.toUpperCase()) // "  SOME RANDOM STRING  "
console.log(str.toUpperCase().trim()) // "SOME RANDOM STRING"
console.log(str.toUpperCase().trim().length) // 18

key points:

  1. Each function in the chain returns the result of the function not this (maybe this is done under the hood)
  2. No property / method is required at the end of the chain just to get the result.

Can we implement method chaining to behave the way built-in functions in Javascript behave?

I think there is something that i'm missing about method chaining. To me it feels inplete.

Method chaining works by having each method return this so that another method on that object can be called. However, the fact that the return value is this and not the result of the function seems inconvenient to me.

Here is a simple example.

const Obj = {
    result: 0,
    addNumber: function (a, b) {
        this.result = a + b;
        return this;
    },

    multiplyNumber: function (a) {
        this.result = this.result * a;
        return this;
    },
}

const operation = Obj.addNumber(10, 20).multiplyNumber(10).result
console.log(operation)

key points:

  1. Every method in the chain Obj.addNumber(10, 20).multiplyNumber(10) returns this.
  2. The last part of the chain .result is the one that returns a value other than this.

The problem with this approach is that it require you to tack on a property / method to get a value at the end other thanthis.

Compare this with built-in functions in JavaScript.

const str = "  SomE RandoM StRIng  "

console.log(str.toUpperCase()) // "  SOME RANDOM STRING  "
console.log(str.toUpperCase().trim()) // "SOME RANDOM STRING"
console.log(str.toUpperCase().trim().length) // 18

key points:

  1. Each function in the chain returns the result of the function not this (maybe this is done under the hood)
  2. No property / method is required at the end of the chain just to get the result.

Can we implement method chaining to behave the way built-in functions in Javascript behave?

Share Improve this question edited Oct 16, 2021 at 14:03 Personal Information asked Oct 16, 2021 at 13:15 Personal InformationPersonal Information 5811 gold badge8 silver badges20 bronze badges 7
  • 4 string values do not have methods; the string primitives are "boxed" by String instances implicitly, which effectively does the same thing as chaining with explicit this – Pointy Commented Oct 16, 2021 at 13:17
  • I know they don't have methods and that they are casted as "string objects" (if i recall correctly) in cases like this. It's just used as an example to show what I want to achieve with method chaining. – Personal Information Commented Oct 16, 2021 at 13:23
  • 3 The point is that new String instances are (at least conceptually) created for each of the string primitive values when a . operator is encountered. That does not happen when you have actual objects, thus the need to return this from methods intended to be used in chains of method calls. – Pointy Commented Oct 16, 2021 at 13:25
  • @Pointy I know here is a need to return this for chaining. I just don't get how built in functions have return values at any point in the chain. for example const array = [["hellow","pastas"],["travel", "militarie"],["oranges","mint"]]; const arrayOne = array.map(e1 => e1.filter(e2 => e2.length > 6)).flat(); array.map has a return value which is then used in the chain for filter, and again by flat. Also, if the chain is reduced and you log that to the console the "this' object isn't logged but a result. (I know 'this' is being implemented... but it's not what we get back) – Personal Information Commented Oct 16, 2021 at 13:54
  • 2 String.prototype.toUpperCase() returns a string, a primitive value. The subsequent . operator causes that string to be implicitly cast to a new String instance. It's not magic, it's just the way expression evaluation works with . and primitive values. – Pointy Commented Oct 16, 2021 at 14:01
 |  Show 2 more ments

4 Answers 4

Reset to default 3

First of all, each of your console.log doesn't return properly:

console.log(str.toUpperCase.trim) //undefined

It returns undefined because str.toUpperCase returns the function object and does not execute the function itself so it won't work
The only correct usage is

console.log(str.toUpperCase().trim()

Now about your question, it is pretty easy to do it without a result and it is much more efficient.
Everything in javascript has a method called valueOf(), here is my example of calling everything like that for numbers, though I prefer just making functions instead of Objects.

const Obj = {
    addNumber: function (a = 0) {
        return a + this.valueOf();
    },

    multiplyNumber: function (a = 1) {
        return a*this.valueOf();
    },
}
const nr = 2;
Object.keys(Obj).forEach(method => {
    Number.prototype[method] = Obj[method];
})
console.log(Number.prototype); // will print out addNumber and multiplyNumber
// Now You can call it like this
console.log(nr.addNumber().multiplyNumber()); // Prints out 2 because it bees (nr+0)*1
console.log(nr.addNumber(3).multiplyNumber(2)) // Prints out 10;

I think you are misunderstanding what method chaining actually is. It is simply a shorthand for invoking multiple methods without storing each intermediate result in a variable. In other words, it is a way of expressing this:

const uppercase = " bob ".toUpperCase()
const trimmed = uppercase.trim()

as this

const result = " bob ".toUpperCase().trim()

Nothing special is happening. The trim method is simply being called on the result of " bob ".toUpperCase(). Fundamentally, this boils down to operator precedence and the order of operations. The . operator is an accessor, and is evaluated from left to right. This makes the above expression equivalent to this (parens used to show order of evaluation):

const result = (" bob ".toUpperCase()).trim()

This happens regardless of what is returned by each individual method. For instance, I could do something like this:

const result = " bob ".trim().split().map((v,i) => i)

Which is equivalent to

const trimmed = " bob ".trim()
const array = trimmed.split() //Note that we now have an array
const indexes = array.map((v,i) => i) //and can call array methods

So, back to your example. You have an object. That object has encapsulated a value internally, and adds methods to the object for manipulating the results. In order for those methods to be useful, you need to keep returning an object that has those methods available. The simplest mechanism is to return this. It also may be the most appropriate way to do this, if you actually are trying to make the object mutable. However, if immutability is an option, you can instead instantiate new objects to return, each of which have the methods you want in the prototype. An example would be:

function MyType(n) {
  this.number = n
}
MyType.prototype.valueOf = function() {
  return this.number
}
MyType.prototype.add = function(a = 0) {
  return new MyType(a + this)
}
MyType.prototype.multiply = function(a = 1) {
  return new MyType(a * this)
}

const x = new MyType(1)
console.log(x.add(1))                 // { number: 2 }
console.log(x.multiply(2))            // { number: 2 }
console.log(x.add(1).multiply(2))     // { number: 4 }
console.log(x.add(1).multiply(2) + 3) // 7

The key thing to note about this is that you are still using your object, but the valueOf on the prototype is what allows you to directly utilize the number as the value of the object, while still making the methods available. This is shown in the last example, where we directly add 3 to it (without accessing number). It is leveraged throughout the implementation by adding this directly to the numeric argument of the method.

Method chaining is the mechanism of calling a method on another method of the same object in order to get a cleaner and readable code.

In JavaScript method chaining most use the this keyword in the object's class in order to access its method (because the this keyword refers to the current object in which it is called)

When a certain method returns this, it simply returns an instance of the object in which it is returned, so in another words, to chain methods together, we must make sure that each method we define has a return value so that we can call another method on it.

In your code above, the function addNumber returns the current executing context back from the function call. The next function then executes on this context (referring to the same object), and invokes the other functions associated with the object. it's is a must for this chaining to work. each of the functions in the function chaining returns the current Execution Context. the functions can be chained together because the previous execution returns results that can be processed further on.

This is part of the magic and uniqueness of JavaScript, if you're ing from another language like Java or C# it may look weird for you, but the this keyword in JavaScript behaves differently.

You can avoid the necessity of this and be able to return a value implicitly, using a Proxy object with a get-trap.

Here you find a more generic factory for it.

const log = Logger();

log(`<code>myNum(42)
  .add(3)
  .multiply(5)
  .divide(3)
  .roundUp()
  .multiply(7)
  .divide(12)
  .add(-1.75)</code> => ${
    myNum(42)
    .add(3)
    .multiply(5)
    .divide(3)
    .roundUp()
    .multiply(7)
    .divide(12)
    .add(-1.75)}`,
  );

log(`\n<code>myString(\`hello world\`)
  .upper()
  .trim()
  .insertAt(6, \`cruel coding \`)
  .upper()</code> => ${
    myString(`hello world`)
        .upper()
        .trim()
        .insertAt(6, `cruel coding `)
        .upper()
    }`);

log(`<br><code>myString(\`border-top-left-radius\`).toUndashed()</code> => ${
  myString(`border-top-left-radius`).toUndashed()}`);

// the proxy handling
function proxyHandlerFactory() {
  return {
    get: (target, prop) => {
      if (prop && target[prop]) {
        return target[prop];
      } 
      return target.valueOf;
    }
  };
}

// a wrapped string with chainable methods
function myString(str = ``) {
  const proxyHandler = proxyHandlerFactory();
  const obj2Proxy = {
    trim: () => nwProxy(str.trim()),
    upper: () => nwProxy(str.toUpperCase()),
    lower: () => nwProxy(str.toLowerCase()),
    insertAt: (at, insertStr) => 
      nwProxy(str.slice(0, at) + insertStr + str.slice(at)),
    toDashed: () => 
      nwProxy(str.replace(/[A-Z]/g, a => `-${a.toLowerCase()}`.toLowerCase())),
    toUndashed: () => nwProxy([...str.toLowerCase()]
      .reduce((acc, v) => {
        const isDash = v === `-`;
        acc = { ...acc,
          s: acc.s.concat(isDash ? `` : acc.nextUpcase ? v.toUpperCase() : v)
        };
        acc.nextUpcase = isDash;
        return acc;
      }, {
        s: '',
        nextUpcase: false
      }).s),
    valueOf: () => str,
  };

  function nwProxy(nwStr) {
    str = nwStr || str;
    return new Proxy(obj2Proxy, proxyHandler);
  }

  return nwProxy();
}

// a wrapped number with chainable methods
function myNum(n = 1) {
  const proxyHandler = proxyHandlerFactory();
  const obj2Proxy = {
    add: x => nwProxy(n + x),
    divide: x => nwProxy(n / x),
    multiply: x => nwProxy(n * x),
    roundDown: () => nwProxy(Math.floor(n)),
    roundUp: () => nwProxy(Math.ceil(n)),
    valueOf: () => n,
  };

  function nwProxy(nwN) {
    n = nwN || n;
    return new Proxy(obj2Proxy, proxyHandler);
  }

  return nwProxy();
}

// ---- for demo ---- //
function Logger() {
  const report =
    document.querySelector("#report") ||
    document.body.insertAdjacentElement(
      "beforeend",
      Object.assign(document.createElement("pre"), {
        id: "report"
      })
    );

  return (...args) => {
    if (!args.length) {
      return report.textContent = ``;
    }

    args.forEach(arg =>
      report.insertAdjacentHTML(`beforeEnd`,
        `<div>${arg.replace(/\n/g, `<br>`)}</div>`)
    );
  };
}
body {
  font: 12px/15px verdana, arial;
  margin: 0.6rem;
}

code {
  color: green;
}

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744286723a4566834.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信