ค้นหาเว็บไซต์

arrays - [].forEach.call() ทำอะไรใน JavaScript?


ฉันกำลังดูโค้ดบางส่วน และฉันพบหลายองค์ประกอบที่เรียกใช้ฟังก์ชันเหนือรายการโหนดโดยมี forEach ใช้กับอาเรย์ว่าง

ตัวอย่างเช่น ฉันมีบางอย่างเช่น:

[].forEach.call( document.querySelectorAll('a'), function(el) {
   // whatever with the current node
});

แต่ฉันไม่เข้าใจว่ามันทำงานอย่างไร มีใครสามารถอธิบายพฤติกรรมของอาร์เรย์ว่างหน้าสำหรับ forEach และวิธีการทำงานของ call ได้ไหม?

คำตอบ #1

[] คืออาร์เรย์
อาร์เรย์นี้ไม่ได้ถูกใช้งานเลย

มันถูกใส่ลงในหน้า เพราะการใช้แถวให้คุณเข้าถึงโพรโทไทป์ของแถวได้ เช่น .forEach

นี่เร็วกว่าแค่การพิมพ์ Array.prototype.forEach.call(...);

ต่อไป, forEach เป็นฟังก์ชันที่รับฟังก์ชันเป็นข้อมูลนำเข้า...

[1,2,3].forEach(function (num) { console.log(num); });

...และสำหรับแต่ละองค์ประกอบใน this (ซึ่ง this คล้ายกับอาเรย์ เพราะมันมี length และคุณสามารถเข้าถึงส่วนต่าง ๆ ของมันได้เหมือน this[1]) มันจะส่งต่อสามสิ่ง:

  1. องค์ประกอบในอาเรย์
  2. ดัชนีขององค์ประกอบ (องค์ประกอบที่สามจะผ่าน 2)
  3. การอ้างอิงถึงอาเรย์

สุดท้าย, .call เป็นพรอทไทป์ที่ฟังก์ชันมี (มันเป็นฟังก์ชันที่ถูกเรียกใช้กับฟังก์ชันอื่น ๆ)
.call จะเอาอาร์กิวเมนต์ตัวแรกและแทนที่ this ภายในฟังก์ชันปกติด้วยค่าที่คุณส่งให้ call เป็นอาร์กิวเมนต์ตัวแรก (undefined หรือ null จะใช้ window ใน JS ประจำวัน หรือจะเป็นค่าที่คุณส่งไปถ้าอยู่ใน "strict-mode") ส่วนอาร์กิวเมนต์ที่เหลือจะถูกส่งไปยังฟังก์ชันเดิม

[1, 2, 3].forEach.call(["a", "b", "c"], function (item, i, arr) {
    console.log(i + ": " + item);
});
// 0: "a"
// 1: "b"
// 2: "c"

ดังนั้น คุณกำลังสร้างวิธีการที่รวดเร็วในการเรียกฟังก์ชัน forEach และคุณกำลังเปลี่ยน this จากอาเรย์ว่างไปเป็นรายการของแท็ก <a> ทั้งหมด และสำหรับแต่ละ <a> ตามลำดับ คุณกำลังเรียกฟังก์ชันที่ให้ไว้

แก้ไข

ข้อสรุปเชิงตรรกะ/การทำความสะอาด

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

ผมจะบอกว่าแม้ว่า .forEach จะช่วยได้น้อยกว่าคู่แข่งของมันอย่าง .map(transformer), .filter(predicate), .reduce(combiner, initialValue) แต่ก็ยังมีประโยชน์เมื่อสิ่งที่คุณต้องการจริงๆ คือการแก้ไขโลกภายนอก (ไม่ใช่อาร์เรย์) n ครั้ง ในขณะที่ยังสามารถเข้าถึง arr[i] หรือ i ได้

แล้วเราจะจัดการกับความแตกต่างนี้อย่างไร เพราะชัดเจนว่าโมโต้เป็นคนที่มีความสามารถและมีความรู้ดี และผมอยากจะจินตนาการว่าผมรู้ว่าตัวเองกำลังทำอะไร/จะไปไหน (บางครั้ง... บางครั้งก็เป็นการเรียนรู้แบบลงไปก่อนเลย)?

คำตอบนั้นจริงๆ แล้วค่อนข้างง่าย และเป็นบางสิ่งที่ลุงบ็อบและเซอร์ครอคฟอร์ดทั้งคู่คงต้องผงกหน้าเพราะความพลาดนั้น:

ทำความสะอาดมัน.

function toArray (arrLike) { // or asArray(), or array(), or *whatever*
  return [].slice.call(arrLike);
}

var checked = toArray(checkboxes).filter(isChecked);
checked.forEach(listValues);

ตอนนี้ ถ้าคุณกำลังสงสัยว่าคุณจำเป็นต้องทำสิ่งนี้ด้วยตัวเองหรือไม่ คำตอบอาจจะไม่จำเป็น...
สิ่งนี้โดยตรงถูกทำโดย... ...ทุก(?) ไลบรารีที่มีฟีเจอร์สูงในปัจจุบัน
ถ้าคุณกำลังใช้ lodash หรือ underscore หรือแม้แต่ jQuery พวกมันทั้งหมดจะมีวิธีรับชุดขององค์ประกอบ และทำการกระทำซ้ำ n ครั้ง
ถ้าคุณไม่ได้ใช้สิ่งเหล่านี้ ก็แน่นอนว่าคุณสามารถเขียนเองได้

lib.array = (arrLike, start, end) => [].slice.call(arrLike, start, end);
lib.extend = function (subject) {
  var others = lib.array(arguments, 1);
  return others.reduce(appendKeys, subject);
};

อัปเดตสำหรับ ES6(ES2015) และต่อไป

ไม่เพียงแต่เมธอดช่วยเหลือ slice( )/array( )/อื่น ๆ จะทำให้ชีวิตง่ายขึ้นสำหรับคนที่อยากใช้ลิสต์เหมือนกับที่ใช้แอเรย์ (ตามที่ควรจะเป็น) แต่สำหรับคนที่มีโอกาสใช้เบราว์เซอร์ ES6+ ในอนาคตอันใกล้ หรือทำการ "transpiling" ใน Babel วันนี้ คุณมีคุณสมบัติของภาษาในตัว ซึ่งทำให้สิ่งประเภทนี้ไม่จำเป็น

function countArgs (...allArgs) {
  return allArgs.length;
}

function logArgs (...allArgs) {
  return allArgs.forEach(arg => console.log(arg));
}

function extend (subject, ...others) { /* return ... */ }


var nodeArray = [ ...nodeList1, ...nodeList2 ];

สะอาดมาก และมีประโยชน์มาก
ลองดูตัวดำเนินการ Rest และ Spread; ทดลองใช้ที่เว็บไซต์ BabelJS; ถ้าชุดเทคโนโลยีของคุณพร้อม ใช้พวกมันในงานจริงกับ Babel และขั้นตอนการสร้าง

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

คำตอบ #2

เมทอด querySelectorAll จะคืนค่า NodeList ซึ่งคล้ายกับอาร์เรย์ แต่ไม่ใช่อาร์เรย์โดยตรง ดังนั้นมันจึงไม่มีเมทอด forEach (ซึ่งออบเจ็กต์อาร์เรย์สืบทอดมาผ่าน Array.prototype)

เนื่องจาก NodeList มีลักษณะคล้ายกับอาเรย์ วิธีของอาเรย์จึงสามารถใช้งานกับมันได้ ดังนั้นโดยการใช้ [].forEach.call คุณกำลังเรียกใช้วิธี Array.prototype.forEach ในบริบทของ NodeList ราวกับว่าคุณสามารถทำได้โดยตรงอย่าง yourNodeList.forEach(/*...*/)

ทราบว่าลิเทอรัลอาร์เรย์ว่างเป็นเพียงทางลัดไปยังเวอร์ชันที่ขยายออก ซึ่งคุณน่าจะเห็นค่อนข้างบ่อยเช่นกัน:

Array.prototype.forEach.call(/*...*/);

คำตอบ #3

คำตอบอื่นได้อธิบายโค้ดนี้อย่างดีแล้ว ดังนั้นฉันจะเพียงแค่เพิ่มคำแนะนำเล็กน้อย

นี่คือตัวอย่างที่ดีของโค้ดที่ควรถูกปรับโครงสร้างใหม่เพื่อความเรียบง่ายและชัดเจน แทนที่จะใช้ [].forEach.call() หรือ Array.prototype.forEach.call() ทุกครั้งที่คุณทำแบบนี้ ให้สร้างฟังก์ชันง่ายๆ ออกมาจากมัน:

function forEach( list, callback ) {
    Array.prototype.forEach.call( list, callback );
}

ตอนนี้คุณสามารถเรียกใช้ฟังก์ชันนี้แทนโค้ดที่ซับซ้อนและเข้าใจยากกว่าได้:

forEach( document.querySelectorAll('a'), function( el ) {
   // whatever with the current node
});

บทความที่เกี่ยวข้อง