Làm thế nào để mở rộng Chức năng với các lớp ES6?


105

ES6 cho phép mở rộng các đối tượng đặc biệt. Vì vậy, nó có thể kế thừa từ hàm. Đối tượng như vậy có thể được gọi là một hàm, nhưng làm thế nào tôi có thể triển khai logic cho lệnh gọi như vậy?

class Smth extends Function {
  constructor (x) {
    // What should be done here
    super();
  }
}

(new Smth(256))() // to get 256 at this call?

Bất kỳ phương thức nào của lớp cũng được tham chiếu đến thể hiện của lớp thông qua this. Nhưng khi nó được gọi là một hàm, thistham chiếu đến window. Làm cách nào tôi có thể lấy tham chiếu đến cá thể lớp khi nó được gọi là một hàm?

Tái bút: Câu hỏi tương tự bằng tiếng Nga.


17
À, cuối cùng cũng có người hỏi nhiệm vụ này :-)
Bergi

1
Chỉ cần làm super(x)(tức là chuyển nó cùng với Function)? Không chắc chắn nếu Functionthực sự có thể được gia hạn.
Felix Kling

Hãy nhớ rằng vẫn có vấn đề với việc mở rộng các lớp tích hợp sẵn. Thông số cho thấy điều đó có thể xảy ra, nhưng tôi đã gặp phải các vấn đề mở rộng Error, trong số những vấn đề khác.
ssube

1
Hãy nhớ rằng đó Functionchỉ đơn giản là một hàm tạo hàm. Việc thực hiện hàm phải được chuyển cho hàm tạo. Nếu bạn không muốn Smthchấp nhận một triển khai, bạn phải cung cấp nó trong phương thức khởi tạo, tức là super('function implementation here').
Felix Kling

1
@Qwertiy: Tôi cho rằng đây là trường hợp ngoại lệ , không phải trường hợp chung. Điều này cũng rất cụ thể đối với biểu thức hàm , nhưng bạn đang sử dụng hàm Functiontạo (thời gian chạy) rất khác với biểu thức hàm (cú pháp).
Felix Kling

Câu trả lời:


49

Lời supergọi sẽ gọi phương thức Functionkhởi tạo, phương thức này mong đợi một chuỗi mã. Nếu bạn muốn truy cập dữ liệu phiên bản của mình, bạn có thể mã hóa nó:

class Smth extends Function {
  constructor(x) {
    super("return "+JSON.stringify(x)+";");
  }
}

nhưng điều đó không thực sự thỏa mãn. Chúng tôi muốn sử dụng một đóng cửa.

Có thể để hàm được trả về là một bao đóng có thể truy cập các biến cá thể của bạn , nhưng không dễ dàng. Điều tốt là bạn không phải gọi supernếu bạn không muốn - bạn vẫn có thể có returncác đối tượng tùy ý từ các hàm tạo lớp ES6 của mình. Trong trường hợp này, chúng tôi sẽ làm

class Smth extends Function {
  constructor(x) {
    // refer to `smth` instead of `this`
    function smth() { return x; };
    Object.setPrototypeOf(smth, Smth.prototype);
    return smth;
  }
}

Nhưng chúng tôi có thể làm tốt hơn nữa và tóm tắt điều này ra khỏi Smth:

class ExtensibleFunction extends Function {
  constructor(f) {
    return Object.setPrototypeOf(f, new.target.prototype);
  }
}

class Smth extends ExtensibleFunction {
  constructor(x) {
    super(function() { return x; }); // closure
    // console.log(this); // function() { return x; }
    // console.log(this.prototype); // {constructor: …}
  }
}
class Anth extends ExtensibleFunction {
  constructor(x) {
    super(() => { return this.x; }); // arrow function, no prototype object created
    this.x = x;
  }
}
class Evth extends ExtensibleFunction {
  constructor(x) {
    super(function f() { return f.x; }); // named function
    this.x = x;
  }
}

Phải thừa nhận rằng điều này tạo ra một mức độ hướng dẫn bổ sung trong chuỗi kế thừa, nhưng đó không nhất thiết là một điều xấu (bạn có thể mở rộng nó thay vì bản gốc Function). Nếu bạn muốn tránh nó, hãy sử dụng

function ExtensibleFunction(f) {
  return Object.setPrototypeOf(f, new.target.prototype);
}
ExtensibleFunction.prototype = Function.prototype;

nhưng lưu ý rằng Smthsẽ không thừa kế động các Functionthuộc tính tĩnh .


Tôi muốn truy cập vào trạng thái lớp từ hàm.
Qwertiy

2
@Qwertiy: Sau đó sử dụng gợi ý thứ hai của Bergi.
Felix Kling

@ AlexanderO'Mara: Bạn không cần phải thay đổi nguyên mẫu của hàm nếu bạn muốn các Smthphiên bản của mình instanceof Smthgiống như mọi người mong đợi. Bạn có thể bỏ qua Object.setPrototypeOfcuộc gọi nếu bạn không cần điều này hoặc bất kỳ phương thức nguyên mẫu nào được khai báo trong lớp của bạn.
Bergi

@ AlexanderO'Mara: Cũng Object.setPrototypeOfkhông có nhiều nguy cơ tối ưu hóa miễn là nó được thực hiện ngay sau khi tạo đối tượng. Chỉ là nếu bạn biến đổi [[nguyên mẫu]] của một đối tượng qua lại trong thời gian tồn tại của nó thì nó sẽ rất tệ.
Bergi

1
@amn Không, bạn không, khi bạn không sử dụng thisreturnmột đối tượng.
Bergi

32

Đây là một cách tiếp cận để tạo các đối tượng có thể gọi tham chiếu chính xác đến các thành viên đối tượng của chúng và duy trì tính kế thừa chính xác mà không gây rối với các nguyên mẫu.

Đơn giản:

class ExFunc extends Function {
  constructor() {
    super('...args', 'return this.__self__.__call__(...args)')
    var self = this.bind(this)
    this.__self__ = self
    return self
  }

  // Example `__call__` method.
  __call__(a, b, c) {
    return [a, b, c];
  }
}

Mở rộng lớp này và thêm một __call__phương thức, thêm vào bên dưới ...

Giải thích trong mã và nhận xét:

// This is an approach to creating callable objects
// that correctly reference their own object and object members,
// without messing with prototypes.

// A Class that extends Function so we can create
// objects that also behave like functions, i.e. callable objects.
class ExFunc extends Function {
  constructor() {
    super('...args', 'return this.__self__.__call__(...args)');
    // Here we create a function dynamically using `super`, which calls
    // the `Function` constructor which we are inheriting from. Our aim is to create
    // a `Function` object that, when called, will pass the call along to an internal
    // method `__call__`, to appear as though the object is callable. Our problem is
    // that the code inside our function can't find the `__call__` method, because it
    // has no reference to itself, the `this` object we just created.
    // The `this` reference inside a function is called its context. We need to give
    // our new `Function` object a `this` context of itself, so that it can access
    // the `__call__` method and any other properties/methods attached to it.
    // We can do this with `bind`:
    var self = this.bind(this);
    // We've wrapped our function object `this` in a bound function object, that
    // provides a fixed context to the function, in this case itself.
    this.__self__ = self;
    // Now we have a new wrinkle, our function has a context of our `this` object but
    // we are going to return the bound function from our constructor instead of the
    // original `this`, so that it is callable. But the bound function is a wrapper
    // around our original `this`, so anything we add to it won't be seen by the
    // code running inside our function. An easy fix is to add a reference to the
    // new `this` stored in `self` to the old `this` as `__self__`. Now our functions
    // context can find the bound version of itself by following `this.__self__`.
    self.person = 'Hank'
    return self;
  }
  
  // An example property to demonstrate member access.
  get venture() {
    return this.person;
  }
  
  // Override this method in subclasses of ExFunc to take whatever arguments
  // you want and perform whatever logic you like. It will be called whenever
  // you use the obj as a function.
  __call__(a, b, c) {
    return [this.venture, a, b, c];
  }
}

// A subclass of ExFunc with an overridden __call__ method.
class DaFunc extends ExFunc {
  constructor() {
    super()
    this.a = 'a1'
    this.b = 'b2'
    this.person = 'Dean'
  }

  ab() {
    return this.a + this.b
  }
  
  __call__(ans) {
    return [this.ab(), this.venture, ans];
  }
}

// Create objects from ExFunc and its subclass.
var callable1 = new ExFunc();
var callable2 = new DaFunc();

// Inheritance is correctly maintained.
console.log('\nInheritance maintained:');
console.log(callable2 instanceof Function);  // true
console.log(callable2 instanceof ExFunc);  // true
console.log(callable2 instanceof DaFunc);  // true

// Test ExFunc and its subclass objects by calling them like functions.
console.log('\nCallable objects:');
console.log( callable1(1, 2, 3) );  // [ 'Hank', 1, 2, 3 ]
console.log( callable2(42) );  // [ 'a1b2', Dean', 42 ]

// Test property and method access
console.log(callable2.a, callable2.b, callable2.ab())

Xem trên repl.it

Giải thích thêm về bind:

function.bind()hoạt động giống như vậy function.call()và chúng có chung một chữ ký phương pháp tương tự:

fn.call(this, arg1, arg2, arg3, ...);thêm trên mdn

fn.bind(this, arg1, arg2, arg3, ...);thêm trên mdn

Trong cả hai đối số đầu tiên xác định lại thisngữ cảnh bên trong hàm. Các đối số bổ sung cũng có thể được liên kết với một giá trị. Nhưng nơi callngay lập tức gọi hàm với các giá trị được ràng buộc, bindtrả về một đối tượng hàm "kỳ lạ" bao bọc trong suốt đối tượng gốc, với thisvà bất kỳ đối số nào được đặt trước.

Vì vậy, khi bạn xác định một hàm thì bindmột số đối số của nó:

var foo = function(a, b) {
  console.log(this);
  return a * b;
}

foo = foo.bind(['hello'], 2);

Bạn gọi hàm ràng buộc chỉ với các đối số còn lại, ngữ cảnh của nó được đặt trước, trong trường hợp này là ['hello'].

// We pass in arg `b` only because arg `a` is already set.
foo(2);  // returns 4, logs `['hello']`

Bạn có thể vui lòng thêm một lời giải thích tại sao bindhoạt động (tức là tại sao nó trả về một thể hiện của ExFunc)?
Bergi

@Bergi bindtrả về một đối tượng hàm trong suốt bao bọc đối tượng hàm mà nó được gọi, là đối tượng có thể gọi của chúng ta, chỉ với sự thisphục hồi ngữ cảnh. Vì vậy, nó thực sự trả về một phiên bản được bao bọc trong suốt của ExFunc. Đã cập nhật bài đăng với nhiều thông tin hơn bind.
Adrien

1
@Bergi Tất cả getters / setters và phương pháp có thể truy cập, tính / thuộc tính phải được chỉ định trong constructorkhi bindtrong ExFunc. Trong các lớp con của ExFunc, tất cả các thành viên đều có thể truy cập được. Đối với instanceof; trong es6 các hàm ràng buộc được gọi là ngoại lai, vì vậy hoạt động bên trong của chúng không rõ ràng, nhưng tôi nghĩ rằng nó chuyển lệnh gọi đến mục tiêu được bao bọc của nó, thông qua Symbol.hasInstance. Nó giống như một Proxy, nhưng nó là một phương pháp đơn giản để đạt được hiệu quả mong muốn. Chữ ký của họ giống nhau không giống nhau.
Adrien

1
@Adrien nhưng từ bên trong __call__tôi không thể truy cập this.ahoặc this.ab(). ví dụ: repl.it/repls/FelineFinishingDesktopenosystem
cướp

1
@rob được phát hiện tốt, có một lỗi tham chiếu, tôi đã cập nhật câu trả lời và mã với bản sửa lỗi và giải thích mới.
Adrien

20

Bạn có thể bọc cá thể Smth trong một Proxy với một apply(và có thể construct) bẫy:

class Smth extends Function {
  constructor (x) {
    super();
    return new Proxy(this, {
      apply: function(target, thisArg, argumentsList) {
        return x;
      }
    });
  }
}
new Smth(256)(); // 256

Ý tưởng tuyệt vời. Như thế này. Tôi có nên triển khai logic nào đó thay vì đặt vào bên trong ứng dụng không?
Qwertiy

4
Một proxy sẽ phải chịu khá nhiều chi phí, phải không? Ngoài ra, thisvẫn là một chức năng trống (kiểm tra new Smth().toString()).
Bergi

2
@Bergi Không có ý tưởng về hiệu suất. MDN có một cảnh báo lớn màu đỏ đậm về setPrototypeOfvà không nói gì về proxy. Nhưng tôi đoán proxy cũng có thể có vấn đề như vậy setPrototypeOf. Và về toString, nó có thể được phủ bóng bằng một phương thức tùy chỉnh trong Smth.prototype. Bản gốc luôn phụ thuộc vào việc triển khai.
Oriol

@Qwertiy Bạn có thể thêm một constructcái bẫy để chỉ định hành vi của new new Smth(256)(). Và thêm các phương thức tùy chỉnh phủ bóng các phương thức gốc truy cập mã của một hàm, như toStringBergi đã lưu ý.
Oriol

Tôi đề cập là applyphương pháp của bạn được thực hiện theo cách nó được cho là được sử dụng, hay nó chỉ là một minh chứng và tôi cần xem qua thêm thông tin về ProxyReflectsử dụng nó theo cách thích hợp?
Qwertiy

3

Tôi lấy lời khuyên từ câu trả lời của Bergi và gói nó vào một mô-đun NPM .

var CallableInstance = require('callable-instance');

class ExampleClass extends CallableInstance {
  constructor() {
    // CallableInstance accepts the name of the property to use as the callable
    // method.
    super('instanceMethod');
  }

  instanceMethod() {
    console.log("instanceMethod called!");
  }
}

var test = new ExampleClass();
// Invoke the method normally
test.instanceMethod();
// Call the instance itself, redirects to instanceMethod
test();
// The instance is actually a closure bound to itself and can be used like a
// normal function.
test.apply(null, [ 1, 2, 3 ]);

3

Cập nhật:

Thật không may, điều này không hoàn toàn hoạt động vì nó bây giờ trả về một đối tượng hàm thay vì một lớp, vì vậy có vẻ như điều này thực sự không thể được thực hiện nếu không sửa đổi nguyên mẫu. Què.


Về cơ bản, vấn đề là không có cách nào để đặt thisgiá trị cho hàm Functiontạo. Cách duy nhất để thực sự làm điều này là sử dụng .bindphương pháp này sau đó, tuy nhiên phương pháp này không thân thiện lắm với lớp học.

Chúng ta có thể làm điều này trong một lớp cơ sở của trình trợ giúp, tuy nhiên thisnó không khả dụng cho đến sau supercuộc gọi đầu tiên , vì vậy nó hơi phức tạp.

Ví dụ làm việc:

'use strict';

class ClassFunction extends function() {
    const func = Function.apply(null, arguments);
    let bound;
    return function() {
        if (!bound) {
            bound = arguments[0];
            return;
        }
        return func.apply(bound, arguments);
    }
} {
    constructor(...args) {
        (super(...args))(this);
    }
}

class Smth extends ClassFunction {
    constructor(x) {
        super('return this.x');
        this.x = x;
    }
}

console.log((new Smth(90))());

(Ví dụ yêu cầu trình duyệt hiện đại hoặc node --harmony.)

Về cơ bản, hàm cơ sở ClassFunctionmở rộng sẽ bao bọc Functionlời gọi phương thức khởi tạo với một hàm tùy chỉnh tương tự như .bind, nhưng cho phép ràng buộc sau đó, trong lần gọi đầu tiên. Sau đó, trong chính phương thức ClassFunctionkhởi tạo, nó gọi hàm trả về supermà từ đó bây giờ là hàm liên kết, chuyển thisđể kết thúc việc thiết lập hàm liên kết tùy chỉnh.

(super(...))(this);

Tất cả điều này khá phức tạp, nhưng nó tránh làm thay đổi nguyên mẫu, được coi là dạng xấu vì lý do tối ưu hóa và có thể tạo ra cảnh báo trong bảng điều khiển trình duyệt.


1
Bạn là những thứ quá phức tạp. boundsẽ tham chiếu đến hàm mà bạn returntừ lớp ẩn danh đó. Chỉ cần đặt tên và tham khảo trực tiếp. Tôi cũng khuyên bạn nên tránh chuyển các chuỗi mã xung quanh, chúng chỉ là một mớ hỗn độn để làm việc (trong mọi bước của quá trình phát triển).
Bergi

Điều đó extendsdường như không thực sự hoạt động như mong đợi, Function.isPrototypeOf(Smth)và cũng new Smth instanceof Functionlà sai.
Bergi

@Bergi Bạn đang sử dụng công cụ JS nào? console.log((new Smth) instanceof Function);trueđối với tôi trong Node v5.11.0 và Firefox mới nhất.
Alexander O'Mara,

Rất tiếc, ví dụ sai. Nó new Smth instanceof Smthkhông hoạt động với giải pháp của bạn. Ngoài ra, sẽ không có phương thức nào Smthkhả dụng trên các phiên bản của bạn - vì bạn chỉ trả về một tiêu chuẩn Function, không phải a Smth.
Bergi

1
@Bergi Em yêu, có vẻ như anh nói đúng. Tuy nhiên mở rộng bất kỳ loại bản địa nào dường như có cùng một vấn đề. extend Functioncũng làm cho new Smth instanceof Smthsai.
Alexander O'Mara

1

Đầu tiên tôi đã đến giải pháp với arguments.callee, nhưng nó thật tồi tệ.
Tôi mong đợi nó sẽ phá vỡ ở chế độ nghiêm ngặt toàn cầu, nhưng có vẻ như nó hoạt động ngay cả ở đó.

class Smth extends Function {
  constructor (x) {
    super('return arguments.callee.x');
    this.x = x;
  }
}

(new Smth(90))()

Đó là một cách không tốt vì sử dụng arguments.callee, chuyển mã dưới dạng một chuỗi và buộc thực thi nó ở chế độ không nghiêm ngặt. Nhưng ý tưởng ghi đè đã applyxuất hiện.

var global = (1,eval)("this");

class Smth extends Function {
  constructor(x) {
    super('return arguments.callee.apply(this, arguments)');
    this.x = x;
  }
  apply(me, [y]) {
    me = me !== global && me || this;
    return me.x + y;
  }
}

Và bài kiểm tra, cho thấy tôi có thể chạy chức năng này theo những cách khác nhau:

var f = new Smth(100);

[
f instanceof Smth,
f(1),
f.call(f, 2),
f.apply(f, [3]),
f.call(null, 4),
f.apply(null, [5]),
Function.prototype.apply.call(f, f, [6]),
Function.prototype.apply.call(f, null, [7]),
f.bind(f)(8),
f.bind(null)(9),
(new Smth(200)).call(new Smth(300), 1),
(new Smth(200)).apply(new Smth(300), [2]),
isNaN(f.apply(window, [1])) === isNaN(f.call(window, 1)),
isNaN(f.apply(window, [1])) === isNaN(Function.prototype.apply.call(f, window, [1])),
] == "true,101,102,103,104,105,106,107,108,109,301,302,true,true"

Phiên bản với

super('return arguments.callee.apply(arguments.callee, arguments)');

trên thực tế có chứa bindchức năng:

(new Smth(200)).call(new Smth(300), 1) === 201

Phiên bản với

super('return arguments.callee.apply(this===(1,eval)("this") ? null : this, arguments)');
...
me = me || this;

làm cho callapplytrên windowkhông nhất quán:

isNaN(f.apply(window, [1])) === isNaN(f.call(window, 1)),
isNaN(f.apply(window, [1])) === isNaN(Function.prototype.apply.call(f, window, [1])),

vì vậy séc nên được chuyển vào apply:

super('return arguments.callee.apply(this, arguments)');
...
me = me !== global && me || this;

1
Bạn thực sự đang cố gắng làm gì?
Cảm ơn bạn

2
Tôi nghĩ rằng Lớp học luôn ở chế độ nghiêm ngặt: stackoverflow.com/questions/29283935/…
Alexander O'Mara,

@ AlexanderO'Mara, nhân tiện, thislà cửa sổ, không phải không xác định, vì vậy hàm được tạo không ở chế độ nghiêm ngặt (ít nhất là trong chrome).
Qwertiy

Làm ơn, hãy ngừng viết xuống câu trả lời này. Tôi đã viết rằng đó là một cách tồi tệ. Nhưng nó thực sự là một câu trả lời - nó hoạt động cả trong FF và Chrome (không có Edge để kiểm tra).
Qwertiy

Tôi đoán điều này hoạt động vì Functionkhông ở chế độ nghiêm ngặt. Mặc dù khủng khiếp, nó là +1 thú vị. Mặc dù vậy, bạn có thể sẽ không thể đi dây thêm nữa.
Alexander O'Mara

1

Đây là giải pháp tôi đã tìm ra để phục vụ mọi nhu cầu của tôi về việc mở rộng các chức năng và đã phục vụ tôi khá tốt. Lợi ích của kỹ thuật này là:

  • Khi mở rộng ExtensibleFunction, mã có nghĩa là mở rộng bất kỳ lớp ES6 nào (không, liên quan đến các hàm tạo hoặc proxy giả vờ).
  • Chuỗi nguyên mẫu được giữ lại thông qua tất cả các lớp con và instanceof/ .constructortrả về các giá trị mong đợi.
  • .bind() .apply().call()tất cả hoạt động như mong đợi. Điều này được thực hiện bằng cách ghi đè các phương thức này để thay đổi ngữ cảnh của hàm "bên trong" trái ngược với thể hiện ExtensibleFunction(hoặc lớp con của nó ').
  • .bind()trả về một thể hiện mới của hàm tạo hàm (có thể là nó ExtensibleFunctionhoặc một lớp con). Nó sử dụng Object.assign()để đảm bảo các thuộc tính được lưu trữ trên hàm ràng buộc nhất quán với các thuộc tính của hàm gốc.
  • Đóng cửa được tôn trọng và các chức năng mũi tên tiếp tục duy trì ngữ cảnh thích hợp.
  • Hàm "bên trong" được lưu trữ thông qua a Symbol, có thể được làm xáo trộn bởi các mô-đun hoặc IIFE (hoặc bất kỳ kỹ thuật phổ biến nào khác để tư nhân hóa các tham chiếu).

Và không cần quảng cáo thêm, mã:

// The Symbol that becomes the key to the "inner" function 
const EFN_KEY = Symbol('ExtensibleFunctionKey');

// Here it is, the `ExtensibleFunction`!!!
class ExtensibleFunction extends Function {
  // Just pass in your function. 
  constructor (fn) {
    // This essentially calls Function() making this function look like:
    // `function (EFN_KEY, ...args) { return this[EFN_KEY](...args); }`
    // `EFN_KEY` is passed in because this function will escape the closure
    super('EFN_KEY, ...args','return this[EFN_KEY](...args)');
    // Create a new function from `this` that binds to `this` as the context
    // and `EFN_KEY` as the first argument.
    let ret = Function.prototype.bind.apply(this, [this, EFN_KEY]);
    // For both the original and bound funcitons, we need to set the `[EFN_KEY]`
    // property to the "inner" function. This is done with a getter to avoid
    // potential overwrites/enumeration
    Object.defineProperty(this, EFN_KEY, {get: ()=>fn});
    Object.defineProperty(ret, EFN_KEY, {get: ()=>fn});
    // Return the bound function
    return ret;
  }

  // We'll make `bind()` work just like it does normally
  bind (...args) {
    // We don't want to bind `this` because `this` doesn't have the execution context
    // It's the "inner" function that has the execution context.
    let fn = this[EFN_KEY].bind(...args);
    // Now we want to return a new instance of `this.constructor` with the newly bound
    // "inner" function. We also use `Object.assign` so the instance properties of `this`
    // are copied to the bound function.
    return Object.assign(new this.constructor(fn), this);
  }

  // Pretty much the same as `bind()`
  apply (...args) {
    // Self explanatory
    return this[EFN_KEY].apply(...args);
  }

  // Definitely the same as `apply()`
  call (...args) {
    return this[EFN_KEY].call(...args);
  }
}

/**
 * Below is just a bunch of code that tests many scenarios.
 * If you run this snippet and check your console (provided all ES6 features
 * and console.table are available in your browser [Chrome, Firefox?, Edge?])
 * you should get a fancy printout of the test results.
 */

// Just a couple constants so I don't have to type my strings out twice (or thrice).
const CONSTRUCTED_PROPERTY_VALUE = `Hi, I'm a property set during construction`;
const ADDITIONAL_PROPERTY_VALUE = `Hi, I'm a property added after construction`;

// Lets extend our `ExtensibleFunction` into an `ExtendedFunction`
class ExtendedFunction extends ExtensibleFunction {
  constructor (fn, ...args) {
    // Just use `super()` like any other class
    // You don't need to pass ...args here, but if you used them
    // in the super class, you might want to.
    super(fn, ...args);
    // Just use `this` like any other class. No more messing with fake return values!
    let [constructedPropertyValue, ...rest] = args;
    this.constructedProperty = constructedPropertyValue;
  }
}

// An instance of the extended function that can test both context and arguments
// It would work with arrow functions as well, but that would make testing `this` impossible.
// We pass in CONSTRUCTED_PROPERTY_VALUE just to prove that arguments can be passed
// into the constructor and used as normal
let fn = new ExtendedFunction(function (x) {
  // Add `this.y` to `x`
  // If either value isn't a number, coax it to one, else it's `0`
  return (this.y>>0) + (x>>0)
}, CONSTRUCTED_PROPERTY_VALUE);

// Add an additional property outside of the constructor
// to see if it works as expected
fn.additionalProperty = ADDITIONAL_PROPERTY_VALUE;

// Queue up my tests in a handy array of functions
// All of these should return true if it works
let tests = [
  ()=> fn instanceof Function, // true
  ()=> fn instanceof ExtensibleFunction, // true
  ()=> fn instanceof ExtendedFunction, // true
  ()=> fn.bind() instanceof Function, // true
  ()=> fn.bind() instanceof ExtensibleFunction, // true
  ()=> fn.bind() instanceof ExtendedFunction, // true
  ()=> fn.constructedProperty == CONSTRUCTED_PROPERTY_VALUE, // true
  ()=> fn.additionalProperty == ADDITIONAL_PROPERTY_VALUE, // true
  ()=> fn.constructor == ExtendedFunction, // true
  ()=> fn.constructedProperty == fn.bind().constructedProperty, // true
  ()=> fn.additionalProperty == fn.bind().additionalProperty, // true
  ()=> fn() == 0, // true
  ()=> fn(10) == 10, // true
  ()=> fn.apply({y:10}, [10]) == 20, // true
  ()=> fn.call({y:10}, 20) == 30, // true
  ()=> fn.bind({y:30})(10) == 40, // true
];

// Turn the tests / results into a printable object
let table = tests.map((test)=>(
  {test: test+'', result: test()}
));

// Print the test and result in a fancy table in the console.
// F12 much?
console.table(table);

Biên tập

Vì tôi đang có tâm trạng, tôi nghĩ rằng tôi sẽ xuất bản một gói cho điều này vào npm.


1

Có một giải pháp đơn giản tận dụng các khả năng chức năng của JavaScript: Chuyển "logic" làm đối số hàm cho hàm tạo của lớp bạn, gán các phương thức của lớp đó cho hàm đó, sau đó trả về hàm đó từ hàm tạo. :

class Funk
{
    constructor (f)
    { let proto       = Funk.prototype;
      let methodNames = Object.getOwnPropertyNames (proto);
      methodNames.map (k => f[k] = this[k]);
      return f;
    }

    methodX () {return 3}
}

let myFunk  = new Funk (x => x + 1);
let two     = myFunk(1);         // == 2
let three   = myFunk.methodX();  // == 3

Phần trên đã được thử nghiệm trên Node.js 8.

Một thiếu sót của ví dụ trên là nó không hỗ trợ các phương thức kế thừa từ chuỗi lớp cha. Để hỗ trợ điều đó, chỉ cần thay thế "Object. GetOwnPropertyNames (...)" bằng một thứ gì đó cũng trả về tên của các phương thức kế thừa. Cách thực hiện điều đó mà tôi tin rằng sẽ được giải thích trong một số câu trả lời câu hỏi khác trên Stack Overflow :-). BTW. Sẽ thật tuyệt nếu ES7 thêm một phương thức để tạo tên các phương thức kế thừa ;-).

Nếu bạn cần hỗ trợ các phương thức kế thừa, một khả năng là thêm một phương thức tĩnh vào lớp trên để trả về tất cả các tên phương thức được kế thừa và cục bộ. Sau đó gọi nó từ hàm tạo. Nếu sau đó bạn mở rộng lớp Funk đó, bạn cũng sẽ nhận được phương thức tĩnh đó kế thừa.


Tôi nghĩ ví dụ này đưa ra câu trả lời đơn giản cho câu hỏi ban đầu "... làm thế nào tôi có thể triển khai logic cho cuộc gọi như vậy". Chỉ cần chuyển nó dưới dạng một đối số có giá trị hàm cho hàm tạo. Trong đoạn mã trên, lớp Funk không mở rộng Hàm một cách rõ ràng mặc dù có thể, nhưng nó không thực sự cần thiết. Như bạn có thể thấy, bạn có thể gọi các "thể hiện" của nó như cách bạn gọi bất kỳ hàm thông thường nào.
Panu Logic
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.