+2

Creating Callable Objects in JavaScript

Bài viết được dịch từ nguồn: https://hackernoon.com/creating-callable-objects-in-javascript-d21l3te1

Callable object là cấu trúc dữ liệu hoạt động như cả objectfunction. Bạn có thể access và gán thuộc tính obj.bar, gọi hàm obj.foo (), gọi trực tiếp obj (), như thể đó là một function.

Cách gọi trực tiếp giống như gọi một function của obj thay đổi các thuộc tính của đối tượng thông qua context.

Nếu bạn có kinh nghiệm với Python, bạn sẽ nhận ra điều này, có một giao thức Python dựng sẵn bằng cách sử dụng __call__ class method. Bất kỳ phương pháp nào được gán cho __call__ có thể được gọi theo cách obj.__call __ () hoặc obj().

Callable Objects cũng có thể được coi là stateful function. Các chức năng vốn dĩ là instance statless procedures.

Trong JavaScript hầu hết mọi thứ đều là object, bao gồm các hàm, vì vậy chắc chắn chúng ta có thể làm điều này, nhưng bằng cách nào? Nó không được tích hợp sẵn trong ngôn ngữ như Python, nhưng có một số cách để làm cho nó hoạt động.

The Challenge

Lấy cảm hứng từ Python, tạo một Class constructor mà có thể sử dụng để tạo các đối tượng có thể gọi được để chuyển hướng sang một phương thức có tên _call. Chuyển hướng này để có thể kế thừa từ Class và dễ dàng overrideextend _call method với chức năng mới, mà không phải lo lắng về hoạt động bên trong của đối tượng có thể gọi được.

Để làm điều này, chúng tôi sẽ cần phải kế thừa từ hàm tạo Function, kế thừa từ Object và cho phép tạo cả objectdynamic function.

Trở ngại chính là đưa ra một đối tượng chức năng tham chiếu đến chính nó.

Để có reference cho __call method, phần function của đối tượng hàm được tạo bởi Callable class constructor, phải có reference cho chính nó.

The Solutions

Tạo một extensible Callable class duy trì sự kế thừa đúng và chính xác trong JavaScript và cho phép gọi các đối tượng mà nó xây dựng là các hàm, với tham chiếu đến chính chúng, chuyển hướng tới overridable method _call.

'use strict'

class Callable extends Function {
  constructor() {
    super('...args', 'return this._bound._call(...args)')
    // Or without the spread/rest operator:
    // super('return this._bound._call.apply(this._bound, arguments)')
    this._bound = this.bind(this)

    return this._bound
  }
  
  _call(...args) {
    console.log(this, args)
  }
}

Bởi vì thừa kế từ Function, có thể tạo dynamic function từ string, sử dụng super trong hàm tạo. Vì vậy, chuỗi được chuyển đến super sẽ là body của fucntion. Fucntion đó có thể truy cập vào đối tượng của chính nó và gọi một phương thức _call, truyền vào argument của nó. Làm điều này bằng cách sử dụng bind.

Bind method sẽ cho set this context của một hàm cho bất cứ điều gì chúng ta muốn, bằng cách gói hàm đó trong một hàm bị ràng buộc. Vì vậy, bind chức năng cho chính nó bằng cách this.bind(this).

Callable objectreference tới chính nó, ngoại trừ đối tượng trả về từ constructor , được trả về bởi this. Vì vậy, tất cả các thuộc tính sẽ được gắn vào nó, hàm của chúng ta có tham chiếu đến đối tượng cũ được truyền cho bind.

Một giải pháp dễ dàng cho vấn đề này là đính kèm một tham chiếu đến object mới được gói trên object cũ bởi _bound. Và body của function, trong string chuyển đến super, chỉ cần gọi _call, sử dụng this._bound reference.

The Callee Way

'use strict'

class Callable extends Function {
  constructor() {
    super('return arguments.callee._call.apply(arguments.callee, arguments)')
    // We can't use the rest operator because of the strict mode rules.
    // But we can use the spread operator instead of apply:
    // super('return arguments.callee._call(...arguments)')
  }
  
  _call(...args) {
    console.log(this, args)
  }
}

Một lần nữa sử dụng super gọi để tạo một hàm động, được tham chiếu đến chính hàm đó bằng cách tận dụng một biến ẩn khác bên trong một hàm.

Đối tượng đối số có thuộc tính argument.callee là tham chiếu đến hàm được gọi. Dùng reference này làm đối số đầu tiên để liên kết các chức năng.

Vì vậy, bên trong thân hàm, chuỗi được truyền cho super, chỉ cần gọi _call trong arguments.callee.

The Closure & Prototype Way

'use strict'

class Callable extends Function {
  constructor() {
    var closure = function(...args) { return closure._call(...args) }
    // Or without the spread/rest operator:
    // var closure = function() {
    //   return closure._call.apply(closure, arguments)
    // }
    return Object.setPrototypeOf(closure, new.target.prototype)
  }
  
  _call(...args) {
    console.log(this, args)
  }
}

Ở đây thay vì tạo một hàm động với siêu, ta loại bỏ đối tượng hàm được tạo bởi constructor (this object) và thay thế nó bằng closure, bằng cách trả lại thay vì this từ constructor.

closure cũng là một function object và có thể tham chiếu chính nó trong body thông qua biến closure. Sử dụng closure tham chiếu để chuyển hướng gọi đến phương thức __call của nó.

Nhưng chúng tôi đã phá vỡ chuỗi bằng cách thay thế this với closure, và gắn lại prototype của constructor để closure sử dụng Object.setPrototypeOfnew.target (là tham chiếu đến hàm tạo) để lấy prototype.

Bạn có thể dùng this.constructor.prototype thay vì new.target.prototype. Nhưng khi gọi lần đầu để tạo object, nó khá vo nghĩa.

The Proxy Way

'use strict'

class Callable extends Function {
  constructor() {
    super()    
    return new Proxy(this, {
      apply: (target, thisArg, args) => target._call(...args)
    })
  }
  
  _call(...args) {
    console.log(this, args)
  }
}

Sử dụng Proxy, chúng tôi có thể chặn gọi đến fucntion, bằng cách sử dụng apply, và chuyển hướng nó đến một chức năng khác. Các apply cho phép gọi nó là một tham chiếu đến chính nó như là target argument.

Vì vậy, chúng tôi tạo Class kế thừa Fucntion, Callable, gói các đối tượng có thể gọi được tạo trong một Proxy , bất kỳ việc gọi nào được thực hiện cho các đối tượng đó và chuyển hướng chúng đến hàm _call trên chính đối tượng, sử dụng target.

Cảm ơn và hi vọng bài viết có ích trong công việc của bạn.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí