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


137

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

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

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

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

คำตอบ:


208

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

.forEachมันถูกวางไว้บนหน้าเว็บเพราะใช้อาร์เรย์ช่วยให้คุณเข้าถึงต้นแบบอาร์เรย์เช่น

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

ถัดไปforEachเป็นฟังก์ชันที่รับฟังก์ชันเป็นอินพุต ...

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

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

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

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

[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

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

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

ทำความสะอาดขึ้น

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

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

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

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) และอื่น ๆ

ไม่เพียง แต่วิธีตัวช่วยa slice( )/ array( )/ etc จะทำให้ชีวิตง่ายขึ้นสำหรับผู้ที่ต้องการใช้รายการเช่นเดียวกับที่ใช้อาร์เรย์ (เท่าที่ควร) แต่สำหรับผู้ที่มีความหรูหราในการใช้งานเบราว์เซอร์ ES6 + ที่ค่อนข้างใกล้ อนาคตหรือ "การถ่ายทอด" ใน 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 ];

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


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


ตอนนี้ฉันรู้สึกสับสนเล็กน้อยถ้าฉันทำ: console.log (this); ฉันจะได้รับวัตถุหน้าต่างเสมอและฉันคิดว่าการโทรเปลี่ยนค่าของสิ่งนี้
มูฮัมหมัดซาเลห์

2
@MuhammadSaleh .callไม่เปลี่ยนค่าของthisภายในของสิ่งที่คุณส่งผ่านไปforEach, .callเปลี่ยนค่าของthis ภายในของ forEach หากคุณสับสนว่าเหตุใดจึงthisไม่ถูกส่งต่อจากforEachไปยังฟังก์ชันที่คุณต้องการเรียกคุณควรค้นหาลักษณะการทำงานของthisJavaScript ในฐานะที่เป็นภาคผนวก, บังคับเฉพาะที่นี่ (และแผนที่ / กรอง / ลด) มีอาร์กิวเมนต์ที่สองไปforEachซึ่งชุดthisภายในฟังก์ชั่นarr.forEach(fn, thisInFn)หรือ[].forEach.call(arrLike, fn, thisInFn);หรือการใช้เพียง.bind; arr.forEach(fn.bind(thisInFn));
Norguard

1
@thetrystero นั่นคือประเด็น []เป็นเพียงอาร์เรย์เพื่อให้คุณสามารถ.callกำหนด.forEachวิธีการและเปลี่ยนthisเป็นชุดที่คุณต้องการใช้งานได้ .callลักษณะเช่นนี้function (thisArg, a, b, c) { var method = this; method(a, b, c); }ยกเว้นว่ามีภายในสีดำมายากลชุดthisภายในเท่ากับสิ่งที่คุณส่งเป็นmethod thisArg
Norguard

1
ดังนั้นในระยะสั้น ... Array.from ()?
zakius

1
@LukeHutchison เพื่อปัญญา[].forEach.call(arr, fn)จะวิ่งตามความยาวarrผ่านเข้าไปcall; ไม่ใช่อาร์เรย์ที่ถูกใช้เมื่อเริ่มต้นforEach(ตามที่ระบุไว้ในสองประโยคแรกและอธิบายเพิ่มเติมหลังจากนั้น) ความยาวของอาร์เรย์เริ่มต้นไม่สำคัญทั้งหมด เมื่อคุณใช้อาร์กิวเมนต์ที่สองของforEachฟังก์ชันคุณกำลังใช้ดัชนี เช่นเดียวกับถ้าคุณผ่านฟังก์ชั่นโดยตรงแทนforEach callหากคุณบันทึกอาร์กิวเมนต์แรกคุณจะได้รับ"a", "b", "c"ไม่ใช่1, 2, 3เนื่องจากวิธีการcallแทนที่thisและวิธีการforEachทำงาน
การ์ด

55

querySelectorAllวิธีการส่งกลับNodeListซึ่งจะคล้ายกับอาร์เรย์ แต่มันไม่ได้ค่อนข้างอาร์เรย์ ดังนั้นจึงไม่มีforEachเมธอด (ซึ่งวัตถุอาร์เรย์สืบทอดผ่านArray.prototype)

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

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

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

7
ดังนั้นเพื่อชี้แจงมีประโยชน์ในการใช้[].forEach.call(['a','b'], cb)มากกว่า['a','b'].forEach(cb)ในการใช้งานในชีวิตประจำวันกับอาร์เรย์มาตรฐานเพียงเมื่อพยายามที่จะย้ำกว่าอาร์เรย์เหมือนโครงสร้างที่ไม่ได้มีforEachต้นแบบของตัวเอง? ถูกต้องหรือไม่
Matt Fletcher

2
@MattFletcher: ใช่ถูกต้อง ทั้งสองจะได้ผล แต่ทำไมสิ่งที่ซับซ้อนเกินไป? เพียงแค่เรียกใช้เมธอดโดยตรงบนอาร์เรย์เอง
James Allardice

ขอบคุณครับ และฉันไม่รู้ว่าทำไมอาจเป็นเพียงเพื่อความน่าเชื่อถือบนท้องถนนสร้างความประทับใจให้กับหญิงชราใน ALDI และอื่น ๆ ฉันจะยึดติด[].forEach():)
Matt Fletcher

var section = document.querySelectorAll(".section"); section.forEach((item)=>{ console.log(item.offsetTop) }) ทำงานได้ดี
daslicht

@daslicht ไม่ได้อยู่ใน IE เวอร์ชันเก่า! (ที่ทำเช่นนั้นรองรับforEachอาร์เรย์ แต่ไม่ใช่วัตถุ NodeList)
Djave

21

คำตอบอื่น ๆ อธิบายรหัสนี้ได้เป็นอย่างดีดังนั้นฉันจะเพิ่มข้อเสนอแนะ

นี่เป็นตัวอย่างที่ดีของโค้ดที่ควรปรับโครงสร้างใหม่เพื่อความเรียบง่ายและชัดเจน แทนที่จะใช้[].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
});

5

สามารถเขียนได้ดีขึ้นโดยใช้

Array.prototype.forEach.call( document.querySelectorAll('a'), function(el) {

});

คืออะไรคือdocument.querySelectorAll('a')ส่งคืนอ็อบเจ็กต์ที่คล้ายกับอาร์เรย์ แต่ไม่ได้รับมาจากArrayชนิด ดังนั้นเราจึงเรียกforEachเมธอดจากArray.prototypeวัตถุที่มีบริบทเป็นค่าที่ส่งกลับมาdocument.querySelectorAll('a')


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

โดยพื้นฐานแล้วจะเหมือนกับ:

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

2

ต้องการอัปเดตคำถามเก่านี้:

เหตุผลที่ต้องใช้[].foreach.call()ในการวนซ้ำองค์ประกอบในเบราว์เซอร์สมัยใหม่ส่วนใหญ่จบแล้ว เราสามารถใช้document.querySelectorAll("a").foreach()โดยตรง

อ็อบเจ็กต์ NodeList คือคอลเล็กชันของโหนดโดยปกติจะส่งคืนโดยคุณสมบัติเช่น Node.childNodes และวิธีการเช่น document.querySelectorAll ()

แม้ว่า NodeList ไม่ได้เป็นอาร์เรย์มันเป็นไปได้ที่จะย้ำไปกับ forEach () นอกจากนี้ยังสามารถแปลงเป็น Array จริงโดยใช้ Array.from ()

อย่างไรก็ตามเบราว์เซอร์รุ่นเก่าบางตัวยังไม่ได้ติดตั้ง NodeList.forEach () หรือ Array.from () สิ่งนี้สามารถหลีกเลี่ยงได้โดยใช้ Array.prototype.forEach () - ดูตัวอย่างของเอกสารนี้


1

อาร์เรย์ว่างมีคุณสมบัติforEachในต้นแบบซึ่งเป็นวัตถุฟังก์ชัน (อาร์เรย์ว่างเป็นเพียงวิธีง่ายๆในการรับการอ้างอิงถึงforEachฟังก์ชันที่Arrayอ็อบเจ็กต์ทั้งหมดมี) อ็อบเจ็กต์ฟังก์ชันก็มีcallคุณสมบัติซึ่งเป็นฟังก์ชันเช่นกัน เมื่อคุณเรียกใช้ฟังก์ชันของcallฟังก์ชันฟังก์ชันจะเรียกใช้ฟังก์ชันด้วยอาร์กิวเมนต์ที่กำหนด อาร์กิวเมนต์แรกจะกลายเป็นthisฟังก์ชันที่เรียกว่า

คุณสามารถค้นหาเอกสารสำหรับcallฟังก์ชั่นที่นี่ เอกสารสำหรับforEachเป็นที่นี่


1

เพียงเพิ่มหนึ่งบรรทัด:

NodeList.prototype.forEach = HTMLCollection.prototype.forEach = Array.prototype.forEach;

และ voila!

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

สนุก :-)

คำเตือน: NodeList เป็นคลาสระดับโลก อย่าใช้คำแนะนำนี้หากคุณเขียนห้องสมุดสาธารณะ อย่างไรก็ตามมันเป็นวิธีที่สะดวกมากในการเพิ่มประสิทธิภาพในตนเองเมื่อคุณทำงานบนเว็บไซต์หรือแอป node.js


1

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

const forEach = (array, callback) => {
  if (!array || !array.length || !callback) return
  for (var i = 0; i < array.length; i++) {
    callback(array[i], i);
  }
}

forEach(document.querySelectorAll('.a-class'), (item, index) => {
  console.log(`Item: ${item}, index: ${index}`);
});

0

[]ส่งคืนอาร์เรย์ใหม่เสมอซึ่งเทียบเท่ากับnew Array()แต่รับประกันว่าจะส่งคืนอาร์เรย์เนื่องจากผู้ใช้Arrayอาจเขียนทับได้ในขณะที่[]ไม่สามารถทำได้ ดังนั้นนี่เป็นวิธีที่ปลอดภัยในการรับต้นแบบจากArrayนั้นตามที่อธิบายไว้callคือใช้เพื่อเรียกใช้ฟังก์ชันบนตัวสร้างแบบอาร์เรย์ (สิ่งนี้)

เรียกใช้ฟังก์ชันด้วยค่าและอาร์กิวเมนต์ที่กำหนดให้เป็นรายบุคคล mdn


แม้ว่า[]ในความเป็นจริงจะส่งคืนอาร์เรย์เสมอในขณะที่window.Arrayสามารถเขียนทับด้วยอะไรก็ได้ (ในเบราว์เซอร์เก่าทั้งหมด) ดังนั้นก็สามารถwindow.Array.prototype.forEachเขียนทับด้วยอะไรก็ได้ ไม่มีการรับประกันความปลอดภัยไม่ว่าในกรณีใดในเบราว์เซอร์ที่ไม่รองรับ getters / setters หรือการปิดผนึก / การแช่แข็งของวัตถุ (การแช่แข็งทำให้เกิดปัญหาการขยายของตัวเอง)
Norguard

รู้สึกอิสระที่จะแก้ไขคำตอบเพื่อที่จะเพิ่มข้อมูลเพิ่มเติมเกี่ยวกับความเป็นไปได้ของการเขียนทับที่หรือวิธีการของมัน:prototype forEach
Xotic750

0

Norguardอธิบายว่าทำอะไร [].forEach.call()และJames Allardice ทำไมเราถึงทำ: เพราะ querySelectorAll ส่งคืนค่าNodeListที่ไม่มี forEach method ...

เว้นแต่คุณจะมีเบราว์เซอร์รุ่นใหม่เช่น Chrome 51+, Firefox 50+, Opera 38, Safari 10

หากไม่สามารถเพิ่มPolyfill ได้ :

if (window.NodeList && !NodeList.prototype.forEach) {
    NodeList.prototype.forEach = function (callback, thisArg) {
        thisArg = thisArg || window;
        for (var i = 0; i < this.length; i++) {
            callback.call(thisArg, this[i], i, this);
        }
    };
}

0

ข้อมูลดีๆมากมายในหน้านี้ (ดูคำตอบ + คำตอบ + ความคิดเห็น ) แต่เมื่อเร็ว ๆ นี้ฉันมีคำถามเดียวกันกับ OP และต้องใช้เวลาขุดเพื่อให้ได้ภาพรวม ดังนั้นนี่คือเวอร์ชันสั้น ๆ :

เป้าหมายคือการใช้Arrayเมธอดบนอาร์เรย์NodeListที่ไม่มีเมธอดนั้นเอง

รูปแบบที่เก่ากว่าเลือกใช้วิธีการของ Array ผ่านFunction.call()และใช้ array literal ( []) มากกว่าArray.prototypeเนื่องจากพิมพ์สั้นกว่า:

[].forEach.call(document.querySelectorAll('a'), a => {})

รูปแบบที่ใหม่กว่า (หลัง ECMAScript 2015) จะใช้Array.from():

Array.from(document.querySelectorAll('a')).forEach(a => {})
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.