จะขยายฟังก์ชันด้วยคลาส ES6 ได้อย่างไร?


106

ES6 ช่วยให้สามารถขยายวัตถุพิเศษได้ ดังนั้นจึงเป็นไปได้ที่จะสืบทอดจากฟังก์ชัน สามารถเรียกวัตถุดังกล่าวเป็นฟังก์ชันได้ แต่ฉันจะใช้ตรรกะสำหรับการเรียกดังกล่าวได้อย่างไร

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

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

thisวิธีการเรียนที่ได้รับการอ้างอิงถึงเช่นชั้นผ่าน แต่เมื่อมันจะเรียกว่าเป็นฟังก์ชั่นหมายถึงthis windowฉันจะรับการอ้างอิงไปยังอินสแตนซ์คลาสได้อย่างไรเมื่อเรียกว่าเป็นฟังก์ชัน

PS: คำถามเดียวกันในภาษารัสเซีย


18
อาในที่สุดก็มีคนถาม
ภารกิจ

1
เพียงแค่ทำsuper(x)(เช่นส่งต่อไปยังFunction)? ไม่แน่ใจว่าFunctionสามารถขยายได้จริงหรือไม่
Felix Kling

โปรดทราบว่ายังคงมีปัญหาในการขยายคลาสในตัว ข้อมูลจำเพาะชี้ให้เห็นว่าควรจะเป็นไปได้ แต่ฉันพบปัญหาในการขยายErrorตัว
ssube

1
โปรดทราบว่าFunctionเป็นเพียงตัวสร้างฟังก์ชัน การใช้ฟังก์ชันจะต้องถูกส่งผ่านไปยังตัวสร้าง หากคุณไม่ต้องการที่จะยอมรับการดำเนินการที่คุณจะต้องให้มันในตัวสร้างคือSmth super('function implementation here')
Felix Kling

1
@Qwertiy: ฉันขอเถียงว่านี่เป็นข้อยกเว้นไม่ใช่กรณีทั่วไป นอกจากนี้ยังมีความเฉพาะเจาะจงมากสำหรับนิพจน์ฟังก์ชันแต่คุณกำลังใช้ตัวFunctionสร้าง (รันไทม์) ซึ่งแตกต่างจากนิพจน์ฟังก์ชัน (ไวยากรณ์) มาก
Felix Kling

คำตอบ:


49

การsuperเรียกจะเรียกตัวFunctionสร้างซึ่งคาดว่าจะมีสตริงรหัส หากคุณต้องการเข้าถึงข้อมูลอินสแตนซ์ของคุณคุณสามารถฮาร์ดโค้ดได้:

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

แต่นั่นไม่น่าพอใจจริงๆ เราต้องการใช้การปิด

การมีฟังก์ชันส่งคืนเป็นการปิดที่สามารถเข้าถึงตัวแปรอินสแตนซ์ของคุณเป็นไปได้ แต่ไม่ใช่เรื่องง่าย สิ่งที่ดีคือคุณไม่จำเป็นต้องโทรหาsuperถ้าคุณไม่ต้องการ - คุณยังสามารถreturnใช้วัตถุตามอำเภอใจจากตัวสร้างคลาส ES6 ของคุณได้ ในกรณีนี้เราจะทำ

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

แต่เราสามารถทำได้ดีกว่านี้และแยกสิ่งนี้ออกจาก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;
  }
}

เป็นที่ยอมรับว่าสิ่งนี้ทำให้เกิดการกำหนดทิศทางเพิ่มเติมในห่วงโซ่การสืบทอด แต่นั่นก็ไม่จำเป็นต้องเป็นเรื่องเลวร้าย (คุณสามารถขยายแทนเนทีฟได้Function) หากคุณต้องการหลีกเลี่ยงให้ใช้

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

แต่สังเกตว่าSmthจะไม่สืบทอดFunctionคุณสมบัติคงที่แบบไดนามิก


ฉันต้องการเข้าถึงสถานะคลาสจากฟังก์ชัน
Qwertiy

2
@Qwertiy: จากนั้นใช้คำแนะนำที่สองของ Bergi
Felix Kling

@ AlexanderO'Mara: คุณไม่ต้องเปลี่ยนต้นแบบของฟังก์ชันถ้าคุณต้องการให้Smthอินสแตนซ์ของคุณเป็นinstanceof Smth(อย่างที่ทุกคนคาดหวัง) คุณสามารถละเว้นการObject.setPrototypeOfโทรได้หากคุณไม่ต้องการสิ่งนี้หรือวิธีการต้นแบบใด ๆ ที่ประกาศไว้ในชั้นเรียน
Bergi

@ AlexanderO'Mara: นอกจากนี้ยังObject.setPrototypeOfมีอันตรายจากการเพิ่มประสิทธิภาพไม่มากนักตราบใดที่เสร็จสิ้นทันทีหลังจากสร้างวัตถุ มันก็แค่ถ้าคุณกลายพันธุ์ [[ต้นแบบ]] ของวัตถุไปมาในช่วงชีวิตของมันซึ่งมันจะไม่ดี
Bergi

1
@amn ไม่คุณไม่เมื่อคุณไม่ใช้thisและreturnวัตถุ
Bergi

32

นี่เป็นแนวทางในการสร้างอ็อบเจกต์ที่สามารถเรียกได้ซึ่งอ้างอิงสมาชิกอ็อบเจ็กต์อย่างถูกต้องและรักษาการสืบทอดที่ถูกต้องโดยไม่ต้องยุ่งกับต้นแบบ

เพียงแค่:

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

ขยายคลาสนี้และเพิ่ม__call__วิธีการเพิ่มเติมด้านล่าง ...

คำอธิบายในรหัสและความคิดเห็น:

// 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())

ดูใน repl.it

คำอธิบายเพิ่มเติมของbind:

function.bind()ทำงานได้ดีมากfunction.call()และพวกเขาแบ่งปันลายเซ็นวิธีการที่คล้ายกัน:

fn.call(this, arg1, arg2, arg3, ...);เพิ่มเติมเกี่ยวกับmdn

fn.bind(this, arg1, arg2, arg3, ...);เพิ่มเติมเกี่ยวกับmdn

ในอาร์กิวเมนต์แรกทั้งสองจะกำหนดthisบริบทใหม่ภายในฟังก์ชัน อาร์กิวเมนต์เพิ่มเติมสามารถผูกไว้กับค่าได้เช่นกัน แต่ในกรณีที่callเรียกใช้ฟังก์ชันด้วยค่าที่ถูกผูกไว้ทันทีbindจะส่งคืนอ็อบเจ็กต์ฟังก์ชัน "แปลกใหม่" ที่ล้อมรอบต้นฉบับอย่างโปร่งใสพร้อมด้วยthisและอาร์กิวเมนต์ที่ตั้งไว้ล่วงหน้า

ดังนั้นเมื่อคุณกำหนดฟังก์ชันแล้วbindอาร์กิวเมนต์บางส่วน:

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

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

คุณเรียกใช้ฟังก์ชันที่ถูกผูกไว้ด้วยอาร์กิวเมนต์ที่เหลือเท่านั้นบริบทของมันถูกตั้งค่าล่วงหน้าในกรณีนี้['hello']คือ

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

คุณช่วยเพิ่มคำอธิบายได้ไหมว่าทำไมถึงใช้bindงานได้ (เช่นทำไมจึงส่งคืนอินสแตนซ์ExFunc)
Bergi

@Bergi bindส่งคืนอ็อบเจ็กต์ฟังก์ชันโปร่งใสที่ล้อมรอบอ็อบเจ็กต์ฟังก์ชันที่ถูกเรียกใช้ซึ่งเป็นอ็อบเจ็กต์ที่เรียกได้ของเราพร้อมกับการthisตอบกลับบริบท ExFuncดังนั้นจริงๆมันส่งกลับเช่นห่อโปร่งใสของ bindการโพสต์การปรับปรุงด้วยข้อมูลเพิ่มเติมเกี่ยวกับ
Adrien

1
@Bergi ทั้งหมด getters / setters และวิธีการเข้าถึงคุณสมบัติ / คุณลักษณะที่จะต้องกำหนดในconstructorหลังจากที่ในbind ExFuncในคลาสย่อยของ ExFunc สมาชิกทุกคนสามารถเข้าถึงได้ สำหรับinstanceof; ในฟังก์ชั่นที่ถูกผูกไว้ของ es6 เรียกว่าแปลกใหม่ดังนั้นการทำงานภายในของพวกเขาจึงไม่ชัดเจน แต่ฉันคิดว่ามันส่งต่อไปยังเป้าหมายที่ห่อหุ้มไว้ผ่านทางSymbol.hasInstance. มันเหมือนกับ Proxy แต่เป็นวิธีง่ายๆในการทำให้ได้เอฟเฟกต์ที่ต้องการ ลายเซ็นของพวกเขาคล้ายกันไม่เหมือนกัน
Adrien

1
@Adrien แต่จากข้างใน__call__ฉันไม่สามารถเข้าถึงthis.aหรือthis.ab(). เช่นrepl.it/repls/FelineFinishedDesktopenvironment
rob

1
@ พบข้อผิดพลาดในการอ้างอิงฉันได้อัปเดตคำตอบและรหัสพร้อมการแก้ไขและคำอธิบายใหม่
Adrien

20

คุณสามารถรวมอินสแตนซ์ Smth ในProxyด้วยกับดักapply(และอาจจะconstruct):

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

ไอเดียเจ๋ง. แบบนี้. ฉันควรใช้ตรรกะ somemore แทนที่จะวางไว้ข้างในใช้หรือไม่?
Qwertiy

4
พร็อกซีจะมีค่าใช้จ่ายค่อนข้างมากใช่หรือไม่? นอกจากนี้thisยังคงเป็นฟังก์ชันว่าง (ตรวจสอบnew Smth().toString())
Bergi

2
@Bergi ไม่มีความคิดเกี่ยวกับประสิทธิภาพ MDN มีคำเตือนตัวหนาสีแดงขนาดใหญ่setPrototypeOfและไม่พูดอะไรเกี่ยวกับผู้รับมอบฉันทะ แต่ฉันเดาว่าพร็อกซีอาจเป็นปัญหาได้เช่นsetPrototypeOfกัน และเกี่ยวกับtoStringมันสามารถเงาด้วยวิธีการที่กำหนดเองในSmth.prototype. เนทีฟขึ้นอยู่กับการนำไปใช้งานอย่างไรก็ตาม
Oriol

@Qwertiy คุณสามารถเพิ่มconstructกับดักเพื่อระบุลักษณะการทำงานของnew new Smth(256)(). และเพิ่มวิธีการแบบกำหนดเองที่เงาคนพื้นเมืองที่เข้าถึงรหัสของฟังก์ชันเช่นเดียวtoStringกับที่ Bergi กล่าวไว้
Oriol

ฉันพูดถึงapplyวิธีการของคุณที่นำไปใช้ในลักษณะที่ควรจะใช้หรือเป็นเพียงการสาธิตและฉันต้องการดูข้อมูลเพิ่มเติมเกี่ยวกับProxyและReflectใช้อย่างถูกต้อง?
Qwertiy

3

ผมเอาคำแนะนำจากคำตอบ Bergi และห่อมันกลายเป็นโมดูล 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

อัปเดต:

น่าเสียดายที่สิ่งนี้ใช้ไม่ได้ผลเพราะตอนนี้ส่งคืนวัตถุฟังก์ชันแทนที่จะเป็นคลาสดังนั้นจึงดูเหมือนว่าจะไม่สามารถทำได้โดยไม่ต้องแก้ไขต้นแบบ กะพร่องกะแพร่ง.


โดยทั่วไปปัญหาคือไม่มีวิธีการตั้งthisค่าสำหรับตัวFunctionสร้าง วิธีเดียวที่จะทำได้จริง ๆ คือใช้.bindวิธีนี้ในภายหลังอย่างไรก็ตามวิธีนี้ไม่เหมาะกับคลาส

เราสามารถทำสิ่งนี้ได้ในคลาสฐานผู้ช่วย แต่thisจะไม่สามารถใช้งานได้จนกว่าจะมีการsuperโทรครั้งแรกดังนั้นจึงค่อนข้างยุ่งยาก

ตัวอย่างการทำงาน:

'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))());

(ตัวอย่างต้องใช้เบราว์เซอร์ที่ทันสมัยหรือnode --harmony.)

โดยพื้นฐานแล้วฟังก์ชันพื้นฐานที่ClassFunctionขยายออกไปจะตัดการFunctionเรียกตัวสร้างด้วยฟังก์ชันที่กำหนดเองซึ่งคล้ายกับ.bindแต่อนุญาตให้เชื่อมโยงในภายหลังในการโทรครั้งแรก จากนั้นในClassFunctionตัวสร้างเองมันจะเรียกใช้ฟังก์ชันที่ส่งคืนsuperซึ่งตอนนี้เป็นฟังก์ชันที่ถูกผูกไว้โดยส่งผ่านthisไปเพื่อสิ้นสุดการตั้งค่าฟังก์ชันการผูกแบบกำหนดเอง

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

ทั้งหมดนี้ค่อนข้างซับซ้อน แต่จะหลีกเลี่ยงการกลายพันธุ์ของต้นแบบซึ่งถือว่าเป็นรูปแบบที่ไม่ดีเนื่องจากเหตุผลในการปรับให้เหมาะสมและสามารถสร้างคำเตือนในคอนโซลเบราว์เซอร์


1
คุณมีความซับซ้อนมากเกินไป boundจะอ้างถึงฟังก์ชันที่คุณreturnจากคลาสที่ไม่ระบุชื่อนั้น เพียงแค่ตั้งชื่อและอ้างอิงโดยตรง ฉันขอแนะนำให้หลีกเลี่ยงการส่งสตริงรหัสไปรอบ ๆ ซึ่งเป็นเพียงความยุ่งเหยิงในการทำงาน (ในทุกขั้นตอนของกระบวนการพัฒนา)
Bergi

นั่นextendsไม่ได้จริงๆดูเหมือนจะทำงานตามที่คาดไว้เป็นFunction.isPrototypeOf(Smth)และยังnew Smth instanceof Functionเป็นเท็จ
Bergi

@Bergi คุณใช้เอ็นจิ้น JS อะไร? console.log((new Smth) instanceof Function);คือtrueสำหรับผมในโหนด v5.11.0 และล่าสุด Firefox
Alexander O'Mara

อ๊ะตัวอย่างผิด มันใช้new Smth instanceof Smthไม่ได้กับโซลูชันของคุณ นอกจากนี้ยังไม่มีวิธีการใดที่Smthสามารถใช้ได้กับอินสแตนซ์ของคุณเนื่องจากคุณเพิ่งส่งคืนมาตรฐานFunctionไม่ใช่ไฟล์Smth.
Bergi

1
@Bergi Darn ดูเหมือนว่าคุณจะพูดถูก อย่างไรก็ตามการขยายประเภทเนทีฟใด ๆ ดูเหมือนจะมีปัญหาเหมือนกัน extend Functionยังทำให้new Smth instanceof Smthเป็นเท็จ
Alexander O'Mara

1

ประการแรกฉันมาถึงวิธีแก้ปัญหาarguments.calleeแต่มันแย่มาก
ฉันคาดว่ามันจะพังในโหมดเข้มงวดระดับโลก แต่ดูเหมือนว่ามันจะใช้งานได้

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

(new Smth(90))()

เป็นวิธีที่ไม่ดีเนื่องจากการใช้การarguments.calleeส่งรหัสเป็นสตริงและบังคับให้ดำเนินการในโหมดที่ไม่เข้มงวด แต่กว่าความคิดที่จะลบล้างapplyก็ปรากฏขึ้น

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

และการทดสอบแสดงให้เห็นว่าฉันสามารถเรียกใช้สิ่งนี้เป็นฟังก์ชันได้หลายวิธี:

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"

เวอร์ชันด้วย

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

ในความเป็นจริงมีbindฟังก์ชัน:

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

เวอร์ชันด้วย

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

ทำให้callและapplyในwindowไม่สอดคล้องกัน:

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

ดังนั้นควรย้ายเช็คไปที่apply:

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

1
คุณกำลังพยายามทำอะไรอยู่?
ขอบคุณ

2
ฉันคิดว่าชั้นเรียนอยู่ในโหมดเข้มงวดเสมอ: stackoverflow.com/questions/29283935/…
Alexander O'Mara

@ AlexanderO'Mara thisเป็นหน้าต่างไม่ได้กำหนดดังนั้นฟังก์ชันที่สร้างขึ้นจึงไม่อยู่ในโหมดเข้มงวด (อย่างน้อยในโครเมี่ยม)
Qwertiy

ได้โปรดหยุดการใช้คำตอบนี้ ฉันเคยเขียนไปแล้วว่ามันเป็นวิธีที่ไม่ดี แต่มันเป็นคำตอบจริงๆ - ใช้งานได้ทั้งใน FF และ Chrome (ไม่มี Edge ให้ตรวจสอบ)
Qwertiy

ฉันคาดเดาว่าจะได้ผลเพราะFunctionไม่ได้อยู่ในโหมดเข้มงวด แม้ว่าจะแย่ แต่ก็น่าสนใจ +1 คุณอาจจะไม่สามารถเดินโซ่ได้อีกต่อไป
Alexander O'Mara

1

นี่เป็นวิธีแก้ปัญหาที่ฉันได้ทำซึ่งตอบสนองทุกความต้องการของฉันในการขยายฟังก์ชันและให้บริการฉันได้ดีทีเดียว ประโยชน์ของเทคนิคนี้คือ:

  • เมื่อขยายExtensibleFunctionรหัสเป็นสำนวนของการขยายคลาส ES6 ใด ๆ (ไม่ใช้การหลอกลวงด้วยตัวสร้างหรือพร็อกซีหลอก)
  • ห่วงโซ่ต้นแบบถูกเก็บไว้ในคลาสย่อยทั้งหมดและinstanceof/ .constructorส่งคืนค่าที่คาดไว้
  • .bind() .apply()และ.call()ฟังก์ชั่นทั้งหมดตามที่คาดไว้ ทำได้โดยการลบล้างเมธอดเหล่านี้เพื่อเปลี่ยนบริบทของฟังก์ชัน "inner" ซึ่งตรงข้ามกับExtensibleFunctionอินสแตนซ์ (หรือคลาสย่อย ')
  • .bind()ส่งคืนอินสแตนซ์ใหม่ของตัวสร้างฟังก์ชัน (ไม่ว่าจะExtensibleFunctionเป็นคลาสย่อย) ใช้Object.assign()เพื่อให้แน่ใจว่าคุณสมบัติที่เก็บไว้ในฟังก์ชันที่ถูกผูกไว้นั้นสอดคล้องกับคุณสมบัติของฟังก์ชันต้นทาง
  • การปิดได้รับเกียรติและฟังก์ชันลูกศรยังคงรักษาบริบทที่เหมาะสม
  • ฟังก์ชัน "inner" ถูกจัดเก็บผ่าน a Symbolซึ่งอาจทำให้โมดูลหรือ IIFE ทำให้สับสนได้ (หรือเทคนิคทั่วไปอื่น ๆ ในการอ้างอิงการแปรรูป)

และโดยไม่ต้องกังวลใจอีกต่อไปรหัส:

// 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);

แก้ไข

เนื่องจากฉันอยู่ในอารมณ์ฉันคิดว่าจะเผยแพร่แพ็คเกจสำหรับสิ่งนี้ใน npm


1

มีวิธีง่ายๆที่ใช้ประโยชน์จากความสามารถในการทำงานของ JavaScript: ส่ง "ตรรกะ" เป็นอาร์กิวเมนต์ของฟังก์ชันไปยังตัวสร้างของคลาสของคุณกำหนดวิธีการของคลาสนั้นให้กับฟังก์ชันนั้นจากนั้นส่งคืนฟังก์ชันนั้นจากตัวสร้างเป็นผลลัพธ์ :

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

ข้างต้นได้รับการทดสอบบน Node.js 8

ข้อบกพร่องของตัวอย่างข้างต้นคือไม่สนับสนุนวิธีการที่สืบทอดมาจาก superclass-chain เพื่อสนับสนุนสิ่งนั้นเพียงแค่แทนที่ "Object. getOwnPropertyNames (... )" ด้วยสิ่งที่ส่งคืนชื่อของวิธีการที่สืบทอดมาด้วย จะทำอย่างไรให้ฉันเชื่อว่ามีการอธิบายไว้ในคำถาม - คำตอบอื่น ๆ ใน Stack Overflow :-) BTW. คงจะดีไม่น้อยถ้า ES7 เพิ่มวิธีการสร้างชื่อของเมธอดที่สืบทอดมาด้วย ;-)

หากคุณต้องการสนับสนุนวิธีการที่สืบทอดมาความเป็นไปได้อย่างหนึ่งคือการเพิ่มวิธีการแบบคงที่ไปยังคลาสด้านบนซึ่งจะส่งคืนชื่อเมธอดที่สืบทอดและโลคัลทั้งหมด จากนั้นเรียกสิ่งนั้นจากตัวสร้าง หากคุณขยายคลาส Funk นั้นคุณจะได้รับวิธีการคงที่ที่สืบทอดมาเช่นกัน


ฉันคิดว่าตัวอย่างนี้ให้คำตอบง่ายๆสำหรับคำถามเดิม "... ฉันจะใช้ตรรกะสำหรับการโทรดังกล่าวได้อย่างไร" เพียงแค่ส่งมันเป็นอาร์กิวเมนต์ที่มีมูลค่าฟังก์ชันไปยังตัวสร้าง ในโค้ดด้านบนคลาส Funk ไม่ได้ขยายฟังก์ชันอย่างชัดเจนแม้ว่าจะทำได้ แต่ก็ไม่จำเป็นต้องทำ อย่างที่คุณเห็นคุณสามารถเรียกมันว่า "อินสแตนซ์" ได้เหมือนกับที่คุณเรียกใช้ฟังก์ชันธรรมดาทั่วไป
Panu Logic
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.