TypeScript Dynamically หรือ Programmatically Chain Functions
การเชื่อมโยงฟังก์ชัน TypeScript แต่ฉันต้องการเชื่อมโยงด้วยโปรแกรม
ตัวอย่างคลาส : chain.ts
class MyChain {
value: number = 0;
constructor() {
this.value = 0;
}
sum(args: number[]) {
this.value = args.reduce((s, c) => s + c, 0);
return this;
}
add(v: number) {
this.value = this.value + v;
return this;
}
subtract(v: number) {
this.value = this.value - v;
return this;
}
}
const mc = new MyChain();
console.log(mc.sum([1, 2, 3, 4]).subtract(5).value);
ฉันเห็นหมายเลข5บนคอนโซล
ตอนนี้ฉันยังค่อนข้างใหม่กับ JavaScript และ TypeScript ดังนั้นฉันจึงพบว่าฟังก์ชันภายในคลาสนี้เป็นองค์ประกอบของอาร์เรย์ของอินสแตนซ์ของคลาส ดังนั้นฉันสามารถทำได้:
console.log(mc["sum"]([1, 2, 3, 4]).value);
10นี้แน่นอนส่งกลับจำนวน
ตอนนี้ฉันสับสนว่าฉันจะเชื่อมโยงสิ่งนี้แบบเป็นโปรแกรมได้อย่างไร ตัวอย่างเช่น (เห็นได้ชัดว่านี่ไม่ใช่สิ่งที่ฉันต้องการจะทำต่อไปและแสดงให้เห็นว่าฉันขาดความเข้าใจ JavaScript:
console.log(mc["sum"]([1, 2, 3, 4]).mc["subtract"](5).value);
ข้อผิดพลาด:
ไม่มีคุณสมบัติ 'mc' ในประเภท 'MyChain'.ts (2339)
เอาล่ะพูดตามตรงฉันรู้โดยสัญชาตญาณว่าจะไม่ได้ผล อย่างไรก็ตามเมื่อลองคิดดูฉันจะเข้าถึงองค์ประกอบของอาร์เรย์หลายมิติในภาษาที่สมเหตุสมผลได้อย่างไร
console.log(mc["sum"]([1, 2, 3, 4])["subtract"](5).value);
บิงโก นี่เป็นเคล็ดลับ แต่นี่ไม่ใช่วิธีแก้ปัญหาที่ฉันต้องการ สิ่งที่ฉันต้องการมีดังนี้:
interface IChainObject {
action: string;
operand: number | number[];
}
const chainObj: IChainObject[] = [
{ action: "sum", operand: [1, 2, 3, 4] },
{ action: "subtract", operand: 5 },
];
และในการเริ่มต้นฉันต้องการลองสิ่งนี้:
console.log(mc[chainObj[0].action](chainObj[0].operand).value);
และด้วยเหตุนี้การสร้างกลไกที่จะสร้างสิ่งนี้ในที่สุด:
console.log(
mc[chainObj[0].action](chainObj[0].operand)[chainObj[1].action](
chainObj[1].operand
).value
);
ดังนั้นสำหรับฉันแล้วดูเหมือนว่าสิ่งที่ฉันต้องการคือวิธีการสร้างสิ่งนี้:
[chainObj[0].action](chainObj[0].operand)[chainObj[1].action](chainObj[1].operand)
จากสิ่งนี้ด้วยวัตถุ chain ของฉันที่มีชุดวัตถุการกระทำ / ตัวถูกดำเนินการหนึ่งชุดหรือหลายชุด:
const chainObj: IChainObject[] = [
{ action: "sum", operand: [1, 2, 3, 4] },
{ action: "subtract", operand: 5 },
];
ตอนนี้นี่คือจุดที่สมองของฉันปิดลงไม่มากก็น้อย ฉันคิดว่าฉันต้องสร้างชุดของค่าสตริง แต่มันจะเป็นสตริงและจะไม่ทำงานเป็นดัชนีอาร์เรย์ในฟังก์ชันตามที่ฉันต้องการ
ทำไมฉันถึงอยากทำสิ่งนี้ ในที่สุดฉันต้องการสร้างวัตถุ Yup schema ที่ซับซ้อนจากออบเจ็กต์ JSON ฉันพบโพสต์ที่ยอดเยี่ยมนี้แต่ปัญหาหลักของฉันคือฉันไม่เข้าใจว่าโค้ดนี้ทำงานอย่างไร
ในตอนนี้ฉันสามารถแยกวิเคราะห์วิธีที่วีเจย์สามารถแก้ปัญหาของเขาและเลียนแบบได้ นี่คือรหัสการทำงานสำหรับตัวอย่างของฉัน:
const mc = new MyChain();
interface IChainObject {
action: string;
operand: number | number[];
}
const chainObj: IChainObject[] = [
{ action: "sum", operand: [1, 2, 3, 4, 5] },
{ action: "subtract", operand: 5 },
];
let myChain = {};
chainObj.forEach((o) => {
myChain = mc[o.action](o.operand);
});
console.log("myChain is", myChain["value"]);
ผลลัพธ์ใน: myChain is 10
คุณอาจกำลังถามตัวเองว่า "แดนมีปัญหาอะไรตอนนี้คุณมีทางออกแล้ว" ใช่ฉันเดาว่าฉันทำ แต่ฉันไม่เข้าใจ โดยพื้นฐานแล้วฉันกำลังคัดลอกและวางโค้ดทำความเข้าใจเล็กน้อยและทำการเปลี่ยนแปลงที่ทำให้ใช้งานได้
ปัญหาพื้นฐานของฉันคือฉันไม่เข้าใจว่าโค้ดบรรทัดนี้ทำงานอย่างไร: myChain = mc[o.action](o.operand);
ฉันได้รับความสำคัญทั่วไปว่ามันเรียกใช้ฟังก์ชันตามการกระทำและให้ข้อมูลกับฟังก์ชันผ่านตัวถูกดำเนินการ ฉันเป็นลิงคัดลอกและวางโค้ด ฉันอยากเป็นมากกว่าลิง อาจจะเป็นลิงบาบูนหรือแม้แต่ลิง ดังนั้นฉันต้องการเข้าใจสิ่งที่ฉันทำ สิ่งที่ไม่สมเหตุสมผลสำหรับฉันคือการผูกมัดมันไว้อย่างไร
ฉันคิดว่าความลับอาจอยู่ในฟังก์ชันforEachแต่ดูเหมือนจะไม่เป็นเช่นนั้น นี่คือการทดสอบง่ายๆ:
let p = 0;
const x = [1, 2, 3, 4];
x.forEach((y) => {
p = y;
});
console.log("p is", p); p is 4
อะไรคือความลับของจาวาสคริปต์ที่เกิดขึ้นภายใต้ประทุนที่ทำให้myChain = mc[o.action](o.operand);โค้ดเชื่อมโยงฟังก์ชันของฉันเข้าด้วยกันแทนที่จะทำงานอย่างเดียวและทำงานอย่างอื่น ฉันไม่เห็นมัน
คำตอบ
เริ่มจากความเข้าใจผิดแรกที่ฉันพบ:
ตอนนี้ฉันยังค่อนข้างใหม่กับ JavaScript และ TypeScript ดังนั้นฉันจึงพบว่าฟังก์ชันภายในคลาสนี้เป็นองค์ประกอบของอาร์เรย์ของอินสแตนซ์ของคลาส
กรณีนี้ไม่ได้. วงเล็บเหลี่ยมใน Javascript ใช้สำหรับการค้นหาคุณสมบัติทั้งหมดไม่ใช่แค่การทำดัชนีอาร์เรย์เท่านั้น x.fooเป็นจริงเทียบเท่ากับx["foo"]และการทำงานที่เหมือนกันสำหรับไวยากรณ์อาร์เรย์ตั้งแต่อาร์เรย์เป็นวัตถุเพียง คลาสใน Javascript เป็นเพียงอ็อบเจ็กต์ที่มีคุณสมบัติต้นแบบซึ่งก็คืออ็อบเจ็กต์ ประกอบด้วยรายการแอตทริบิวต์เริ่มต้นและถ้าคุณสร้างอินสแตนซ์คลาสและค้นหาคุณสมบัติที่ไม่ได้อยู่ในออบเจ็กต์ก็จะค้นหาคุณสมบัตินั้นในต้นแบบ ดังนั้นดูรหัส:
mc["sum"]([1, 2, 3])
มันค้นหาพร็อพเพอร์ตี้ "sum" mcและไม่พบเลยเนื่องจากคุณยังไม่ได้กำหนดไว้มันจึงค้นหาในprototypeของMyChainและค้นหาmcวิธีการ ดังนั้นจึงmc["sum"]เป็นวิธีการของsum mcตอนนี้รหัสนี้:
console.log(mc["sum"]([1, 2, 3, 4]).mc["subtract"](5).value);
ไม่ได้ผลและดูเหมือนจะไม่เหมาะสมด้วยเหตุผล mc["sum"]([1, 2, 3, 4])ผลตอบแทนmcดังนั้นทำไมคุณต้องเข้าถึงmcทรัพย์สิน (ไม่ใช่ว่ามีmcทรัพย์สินอยู่) นั่นเป็นเหตุผลที่ตัวอย่างที่สองของคุณซึ่งเป็นตัวอย่างที่โทรsubtractโดยตรงได้ผล:
console.log(mc["sum"]([1, 2, 3, 4])["subtract"](5).value);
ตอนนี้มาดูรหัสการทำงาน:
const mc = new MyChain();
interface IChainObject {
action: string;
operand: number | number[];
}
const chainObj: IChainObject[] = [
{ action: "sum", operand: [1, 2, 3, 4, 5] },
{ action: "subtract", operand: 5 },
];
let myChain = {};
chainObj.forEach((o) => {
myChain = mc[o.action](o.operand);
});
console.log("myChain is", myChain["value"]);
คุณไม่จำเป็นต้องใช้รหัสนี้มากนัก สามารถทำให้ง่ายขึ้นเป็น:
const mc = new MyChain();
interface IChainObject {
action: keyof MyChain;
operand: number | number[];
}
const chainObj: IChainObject[] = [
{ action: "sum", operand: [1, 2, 3, 4, 5] },
{ action: "subtract", operand: 5 },
];
chainObj.forEach((o) => {
// bypass typescript type checking with cast
(mc[o.action] as Function)(o.operand);
});
console.log("myChain is", mc.value);
โดยพื้นฐานแล้วforEachลูปผ่านองค์ประกอบchainObjตามลำดับ oมูลค่าขององค์ประกอบที่ถูกเก็บไว้ในตัวแปร mc[o.action]รับชื่อเมธอดที่เก็บไว้o.actionและเข้าถึงโดยใช้วงเล็บเหลี่ยม นี่คือการค้นหาวิธีการโดยทั่วไป จากนั้นจึงเรียกเมธอดด้วย(o.operand)(ในฟังก์ชัน Javascript เป็นเพียงค่าและคุณสามารถเรียกค่าใดก็ได้เช่นฟังก์ชัน แต่ถ้าไม่ใช่ฟังก์ชันก็จะเกิดข้อผิดพลาด) mcจากนั้นปรับเปลี่ยนตัวเองและคุณไปยังลูปถัดไป หากเราแทรกdebuggerคำสั่งในฟังก์ชันแล้วแตกในลูปแรกเราสามารถตรวจสอบตัวแปร:
อย่างที่คุณเห็นค่าเริ่มต้นที่ 0 o.actionคือ "sum" และmc[o.action]เป็นวิธี sum จากนั้นเราสามารถเรียกเมธอด sum ด้วยo.operandซึ่งจะเพิ่มองค์ประกอบและตั้งค่าเป็น 15 จากนั้นในลูปที่สอง:
mc[o.action]คือวิธีการลบและเราเรียกมันด้วยo.operandซึ่งก็คือ 5 ลดค่าเป็น 10
สิ่งที่มากที่สุดใน Javascript เช่นมีพื้นเพียงclasses 1objects
อะไรที่ว่าเป็นคุณลักษณะนั้นหรือในกรณีนี้ - ฟังก์ชั่นที่สามารถเข้าถึงได้ผ่านทางสัญกรณ์จุดหรือสัญกรณ์วงเล็บ
ลองดูตัวอย่างที่อาจช่วยคำอธิบาย:
class MyClass {
myFunction(x) {
console.log(x);
}
}
const x = new MyClass();
// attribute accessed via the dot notation
x.myFunction("Hello World!");
// attribute accessed via the bracket notation and a string
x['myFunction']("Hello World, again!");
// attribute accessed via a variable that is a string
const functionName = 'myFunction';
x[functionName]("Well uh, Hello World again?");
// attribute accessed via a variable that is a string, and passing in an argument
const argument = "This is " + "an argument";
x[functionName](argument);
เพื่อแสดงประเด็นเพิ่มเติม:
class MyClass {
myFunction(x) {
console.log(x);
}
}
const x = new MyClass();
console.log(x.myFunction) // returns a function
console.log(x["myFunction"]) // returns a function
// executing the function
x.myFunction("Method One");
x["myFunction"]("Method Two")
เราจะเห็นว่าฟังก์ชันที่ส่งคืนสามารถเรียกใช้ได้
ลองกลับไปที่ตัวอย่างของคุณ
chainObj.forEach((o) => {
myChain = mc[o.action](o.operand);
});
o.actionคือชื่อฟังก์ชันo.operandคือการโต้แย้งดังนั้นสิ่งที่แปลได้คร่าวๆคือ:
chainObj.forEach((o) => {
myChain = mc[functionName](arugment);
});
เช่นเดียวกับตัวอย่างก่อนหน้านี้ของเรา
1 "คลาสเป็นเพียงวัตถุ"
มีหลายชิ้นส่วนนี้; ฉันจะเน้นที่ " อะไรคือความลับที่ทำให้forEach()โค้ดทำงาน "
"ความลับ" คืออินสแตนซ์ของMyChainมีคุณสมบัติชื่อvalueที่ได้รับการอัปเดตหลังจากเรียกแต่ละวิธี รหัสที่มีforEach()อยู่ในไม่ได้จริงๆผูกมัดโทรกัน ; มันทำงานกับMyChainตัวแปรดั้งเดิมที่ตั้งชื่อmcในแต่ละครั้ง
เนื่องจากวิธีการทั้งหมดของการMyChainอัปเดตthis.valueนั้นกลับมาthisเช่นกันจึงไม่สำคัญว่าคุณจะโทรแบบเชนจริงๆหรือไม่ (ดำเนินการกับค่าส่งคืนของการเรียกแต่ละวิธี):
const chaining = new MyChain();
console.log(chaining.add(3).subtract(1).value); // 2
หรือถ้าคุณเรียก method บนวัตถุดั้งเดิมแบบต่อเนื่อง:
const notChaining = new MyChain();
notChaining.add(3);
notChaining.subtract(1);
console.log(notChaining.value) // 2
หากคุณต้องการที่จะมีความแตกต่างระหว่างที่คุณสามารถแสดงได้โดยการทำสองรุ่นMyChain; หนึ่งที่ใช้งานได้โดยการผูกมัดและอีกแบบที่ทำงานต่อเนื่องกันเท่านั้น
สิ่งต่อไปนี้ต้องมีการผูกมัดเนื่องจากไม่เคยอัพเดตอ็อบเจ็กต์ดั้งเดิมและการเรียกเมธอดส่งคืนอ็อบเจ็กต์ใหม่พร้อมผลลัพธ์ของการเรียกเมธอด:
class RealChain {
constructor(public value: number = 0) { }
sum(args: number[]) {
return new RealChain(args.reduce((s, c) => s + c, 0));
}
add(v: number) {
return new RealChain(this.value + v);
}
subtract(v: number) {
return new RealChain(this.value - v);
}
}
const realChaining = new RealChain();
console.log(realChaining.add(3).subtract(1).value); // 2
const notRealChaining = new RealChain();
notRealChaining.add(3);
notRealChaining.subtract(1);
console.log(notRealChaining.value) // 0
และสิ่งต่อไปนี้ห้ามไม่ให้มีการผูกมัดเนื่องจากจะอัปเดตเฉพาะออบเจ็กต์ดั้งเดิมและวิธีการจะไม่ส่งคืนอะไรเลย:
class NotChain {
value: number = 0;
constructor() {
this.value = 0;
}
sum(args: number[]) {
this.value = args.reduce((s, c) => s + c, 0);
}
add(v: number) {
this.value = this.value + v;
}
subtract(v: number) {
this.value = this.value - v;
}
}
const realNotChaining = new NotChain();
realNotChaining.add(3);
realNotChaining.subtract(1);
console.log(realNotChaining.value) // 2
const badNotChaining = new NotChain();
console.log(badNotChaining.add(3).subtract(1).value); // error!
// badNotChaining.add(3) is undefined so you can't call subtract() on it
โค้ดที่มีforEach()จะใช้ได้กับNotChainอินสแตนซ์เท่านั้นไม่ใช่กับRealChainอินสแตนซ์
หากคุณต้องการสิ่งที่คล้ายการวนซ้ำแบบเป็นโปรแกรมที่ใช้งานได้จริงกับการเชื่อมโยงและไม่เรียกใช้เมธอดบนออบเจ็กต์ดั้งเดิมคุณควรใช้reduce()แทนforEach():
const realChainReduced = chainObj.reduce(
(mc, o) => mc[o.action](o.operand),
new RealChain() // or MyChain, doesn't matter
);
console.log("realChainReduced is", realChainReduced.value); // 10
โปรดทราบว่าฉันไม่ได้กล่าวถึงส่วนอื่น ๆ รวมถึงข้อมูลจำเพาะของ TypeScript (การพิมพ์ที่ใช้ที่นี่ทำให้เกิดข้อผิดพลาดของคอมไพเลอร์) ดังนั้นขอเตือน
ลิงก์สนามเด็กเล่นไปยังรหัส