【问题标题】:Can method chaining be implemented the way built-in functions in Javascript are implemented?方法链可以像 Javascript 中内置函数的实现方式那样实现吗?
【发布时间】:2021-10-16 13:15:06
【问题描述】:

我认为我在方法链接方面缺少一些东西。对我来说感觉不完整。

方法链接的工作原理是让每个方法返回this,以便可以调用该对象上的另一个方法。但是,返回值是this 而不是函数的结果这一事实对我来说似乎很不方便。

这是一个简单的例子。

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)

要点:

  1. Obj.addNumber(10, 20).multiplyNumber(10) 链中的每个方法都返回 this
  2. 链的最后一部分.result 是返回this 以外的值的部分。

这种方法的问题在于,它需要您附加一个属性/方法才能在末尾获得一个值而不是this

将此与 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

要点:

  1. 链中的每个函数都返回函数的结果,而不是this(也许这是在幕后完成的)
  2. 在链的末尾不需要任何属性/方法来获得结果。

我们可以实现方法链接以像 Javascript 中的内置函数一样运行吗?

【问题讨论】:

  • 字符串值没有方法;字符串原语被 String 实例隐式“装箱”,这与使用显式链接 this 有效地做同样的事情
  • 我知道他们没有方法,并且在这种情况下它们被转换为“字符串对象”(如果我没记错的话)。它只是作为一个例子来展示我想要通过方法链接实现的目标。
  • 重点是,当遇到. 运算符时,会(至少在概念上)为每个字符串原始值创建新的 String 实例。当您拥有实际对象时不会发生这种情况,因此需要从旨在用于方法调用链中的方法返回 this
  • @Pointy 我知道这里需要返回this 进行链接。我只是不明白内置函数如何在链中的任何点都有返回值。例如 const array = [["hellow","pastas"],["travel", "militarie"],["oranges","mint"]]; const arrayOne = array.map(e1 => e1.filter(e2 => e2.length > 6)).flat(); array.map 有一个返回值,然后在链中用于过滤器,并再次被平面使用。此外,如果链减少并且您将其记录到控制台,则“this”对象不会被记录,而是一个结果。(我知道“this”正在实施......但这不是我们得到的)
  • String.prototype.toUpperCase() 返回一个字符串,一个原始值。随后的. 运算符导致该字符串被隐式转换为new String 实例。这不是魔法,它只是表达式评估与 . 和原始值一起工作的方式。

标签: javascript method-chaining


【解决方案1】:

首先,您的每个 console.log 都没有正确返回:

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

它返回 undefined 因为 str.toUpperCase 返回函数对象并且不执行函数本身所以它不会工作
唯一正确的用法是

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

现在关于您的问题,没有结果很容易做到,而且效率更高。
javascript 中的所有内容都有一个名为 valueOf() 的方法,这是我为数字调用类似的所有内容的示例,尽管我更喜欢只创建函数而不是对象。

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 becomes (nr+0)*1
console.log(nr.addNumber(3).multiplyNumber(2)) // Prints out 10;

【讨论】:

  • 我不知道您为什么使用循环将方法添加到对象的原型中,而不是仅仅执行 Obj.Prototype.addNumber,但这适用于链中的任何点。您能否详细说明 valueOf() 是如何将这一切联系起来的?
  • 所以 nr 不是 Obj 的一个实例,而是一个数字。我做了循环,以防有多种方法要添加。我所做的是将方法从 Obj 复制到 Number,因为这些方法返回一个数字,它们可以在 Number 类中一个接一个地调用。如果你想使用字符串,你只需输入String.prototype['myfunc'] = Obj.['myfunc']。 valueOf() 是在 javascript 中获取几乎任何类型的值的默认方法,除了需要执行 values() 的自定义对象或数组。
  • 虽然这可行,但修改内置对象通常不是一个好主意。在这里,基础Number 原型被污染了。如果有一天 JS 标准碰巧添加了 addNumbermultiplyNumber 方法(尽管不太可能),那么您将覆盖本机实现。真正适合修改内置函数的情况相对较少。如果这样做,您应该检查以确保您添加的内容尚未定义。在这种情况下,你最好创建自己的新类而不是更改Number的原型
  • @rfestag 这就是为什么我更喜欢使用普通函数而不是面向对象的原因。定义和使用函数更简洁更好。
  • @FurrySenko 当然,绝对同意。但是如果使用这样的解决方案,最好不要修改内置的原型,因为不这样做也可以实现相同的基本解决方案。
【解决方案2】:

我认为您误解了方法链接实际上是什么。它只是调用多个方法而不将每个中间结果存储在变量中的简写。换句话说,这是一种表达方式:

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

这样

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

没有什么特别的事情发生。 trim 方法只是在" bob ".toUpperCase() 的结果上调用。从根本上说,这归结为运算符优先级和操作顺序。 . 运算符是一个访问器,从左到右计算。这使得上面的表达式等同于这个(用于显示评估顺序的括号):

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

无论每个单独的方法返回什么,都会发生这种情况。例如,我可以这样做:

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

相当于

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

那么,回到你的例子。你有一个对象。该对象在内部封装了一个值,并向该对象添加了用于操作结果的方法。为了使这些方法有用,您需要不断返回具有这些方法可用的对象。最简单的机制是返回this。如果您实际上正在尝试使对象可变,这也可能是最合适的方法。但是,如果不变性是一个选项,则可以改为实例化要返回的新对象,每个对象都具有原型中所需的方法。一个例子是:

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

要注意的关键是您仍在使用您的对象,但是原型上的valueOf 允许您直接使用number 作为对象的值,同时仍然制作方法可用的。这在最后一个示例中显示,我们直接将 3 添加到它(不访问number)。通过将this 直接添加到方法的数字参数,在整个实现过程中都可以使用它。

【讨论】:

    【解决方案3】:

    方法链是一种在同一对象的另一个方法上调用方法的机制,以获得更清晰易读的代码。

    在 JavaScript 方法链中,大多数在对象的类中使用 this 关键字来访问其方法(因为 this 关键字指的是调用它的当前对象)

    当某个方法返回 this 时,它只是返回返回它的对象的一个​​实例,所以换句话说,要将方法链接在一起,我们必须确保我们定义的每个方法都有一个返回值,以便我们可以调用其他方法。

    在上面的代码中,函数 addNumber 从函数调用返回当前正在执行的上下文。然后下一个函数在这个上下文中执行(引用同一个对象),并调用与该对象关联的其他函数。这是这个链接工作的必要条件。函数链中的每个函数都返回当前的执行上下文。这些函数可以链接在一起,因为之前的执行返回的结果可以进一步处理。

    这是 JavaScript 的魔力和独特性的一部分,如果您来自 Java 或 C# 等其他语言,您可能会觉得这很奇怪,但 JavaScript 中的 this 关键字表现不同。

    【讨论】:

    • 方法链接不一定是在 same 对象的另一个方法上调用方法的机制。它是一种根据另一个方法的 result 调用方法的机制,而无需将结果放入中间变量中。如果一个方法返回一个不同的对象,它仍然可以被链接(调用的可用方法不同)。如果返回相同的对象(this),它只是一种在相同对象上调用方法的机制。
    【解决方案4】:

    您可以避免this 的必要性并且能够使用Proxy objectget-trap 隐式返回值。

    Here 你会为它找到一个更通用的工厂。

    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;
    }

    【讨论】:

    • 这不会改变第一个实现。它仍然需要您附加 result 以获取对象 this 以外的值
    • @PersonalInformation 我找到了一种链接方法的方法,而不是使用this 并返回隐式值。请参阅编辑后的答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多