φυβλαςのβλογ
phyblas的博客



javascript เบื้องต้น บทที่ ๓๘: อิเทอเรเตอร์
เขียนเมื่อ 2020/02/06 18:01
แก้ไขล่าสุด 2021/09/28 16:42


ในบทนี้จะแนะนำเกี่ยวกับสิ่งที่เรียกว่าอิเทอเรเตอร์ (iterator)




ความหมายของอิเทอเรเตอร์

ในบทที่ ๓๗ ได้อธิบายถึงการใช้ for๛of กับแถวลำดับ, สายอักขระ, แม็ป, เซ็ต ไปแล้ว แต่นอกจากนี้แล้วยังมีสิ่งที่เรียกว่า "อิเทอเรเตอร์" ก็สามารถใช้ for๛of ได้เช่นกัน

อิเทอเรเตอร์ (iterator) มาจากคำว่า iterate ที่แปลว่า "การทำซ้ำ" ดังนั้นอิเทอเรเตอร์จึงหมายถึงอะไรบางอย่างที่มีไว้เพื่อทำอะไรซ้ำๆกันไปเรื่อยๆ

อิเทอเรเตอร์ทำงานเหมือนกับแถวลำดับเมื่อเจอ for๛of นั่นคือจะวนซ้ำโดยไล่เรียงค่าออกมาทีละตัวตามลำดับ

แต่ข้อแตกต่างก็คืออิเทอเรเตอร์จะไม่ได้เตรียมข้อมูลไว้ล่วงหน้า แต่สร้างข้อมูลออกมาเรื่อยๆระหว่างการวนซ้ำ

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

  • และอาจต้องใช้เวลาในการเตรียมข้อมูลนาน กว่าจะได้เริ่มใช้

  • การวนซ้ำบางทีเราอาจไม่ได้ต้องวนจนหมดตามสมาชิกที่มี แต่อาจมีการหลุดออกมาก่อน เช่นเจอ break แบบนี้เตรียมข้อมูลเยอะไว้ก่อนแล้วไม่ได้ใช้กลับจะเสียเวลาเปล่า
ด้วยเหตุผลนี้อิเทอเรเตอร์ซึ่งค่อยๆสร้างข้อมูลออกมาระหว่างวนซ้ำจึงมีความเหมาะสมสำหรับการใช้งานบางอย่าง แทนที่จะใช้แถวลำดับ

อิเทอเรเตอร์มีหลายชนิด บางครั้งก็เราก็อาจจะเคยใช้มาแล้วโดยไม่รู้ตัว เช่นเมธอด .keys() หรือ .values() ในออบเจ็กต์ชนิดแม็ปหรือเซ็ต (ในบทที่ ๓๗) นั้นจริงๆก็เป็นการสร้างอิเทอเรเตอร์เพื่อวนซ้ำ

ลองตรวจดูชนิดของข้อมูลได้ จะเห็นว่าเป็นอิเทอเรเตอร์
let mapu = new Map()
alert(mapu.values()) // ได้ [object Map Iterator]
alert(mapu.keys()) // ได้ [object Map Iterator]
let seto = new Set()
alert(seto.values()) // ได้ [object Set Iterator]

และเมื่อเป็นอิเทอเรเตอร์จึงถูกนำไปวนซ้ำด้วย for๛of ได้




เบื้องหลังการทำงานของอิเทอเรเตอร์

การที่อิเทอเรเตอร์เกิดการวนซ้ำเมื่อเจอ for๛of นั้น จริงๆแล้วเบื้องหลังคือมีการไปเรียกเมธอด .next() ขึ้นมา

เมธอด .next() คือเมธอดที่มีอยู่ในตัวอิเทอเรเตอร์ และเราสามารถเรียกใช้ได้โดยตรงก็ได้

เพื่อให้เห็นภาพจะยกตัวอย่างการวนซ้ำโดยใช้เมธอด .next() โดยตรง โดยใช้อิเทอเรเตอร์ที่มาจากเมธอด .keys() ของแม็ปเป็นตัวอย่าง
let shoufu = new Map([
  ["เหอเป่ย์", "สือเจียจวาง"],
  ["เหอหนาน", "เจิ้งโจว"],
  ["หูหนาน", "ฉางซา"]
]);

let kitr = shoufu.keys()
let nex1 = kitr.next()
alert(nex1) // ได้ [object Object]
alert(nex1.value) // ได้ เหอเป่ย์
alert(nex1.done) // ได้ false

let nex2 = kitr.next()
alert(nex2.value) // ได้ เหอหนาน
alert(nex2.done) // ได้ false

let nex3 = kitr.next()
alert(nex3.value) // ได้ หูหนาน
alert(nex3.done) // ได้ false

let nex4 = kitr.next()
alert(nex4.value) // ได้ undefined
alert(nex4.done) // ได้ true

let nex5 = kitr.next()
alert(nex5.value) // ได้ undefined
alert(nex5.done) // ได้ true

แม็ปในตัวอย่างนี้มีสมาชิกอยู่ ๓ ตัว เมื่อใช้เมธอด .keys() ก็ได้อิเทอเรเตอร์ kitr มาใช้

จากนั้นก็ใช้เมธอด .next() กับตัว kitr เพิ่มบังคับให้เริ่มการวนรอบแรก ก็จะได้ค่าคืนกลับมาเป็นออบเจ็กต์ตัวหนึ่ง ซึ่งออบเจ็กต์นี้มีค่าอยู่ ๒ อย่างที่จะดูได้ก็คือ .value กับ .done

.value คือค่าที่ดึงมาได้ในรอบนั้น ส่วน .done เป็นตัวบอกว่าการวนซ้ำจะสิ้นสุดลงหรือยัง ซึ่งรอบแรกจะได้ false เพราะยังวนต่อได้อีก

จากนั้นก็ใช้ .next() กับตัว kitr อีกรอบเพื่อวนซ้ำครั้งที่ ๒ ก็จะได้ออบเจ็กต์ที่มีค่า .value กับ .done มาเหมือนเดิม และวนรอบ ๓ ก็เช่นกัน

แต่พอถึงรอบ ๔ .value จะได้เป็น undefined และ .done จะได้ true นั่นหมายความว่าสิ้นสุดการวนซ้ำแล้ว ในที่นี้เป็นเพราะสมาชิกมีแค่ ๓ ตัว จึงจบที่รอบที่ ๔

หลังจากนั้นถึงใช้ .next() ซ้ำรอบที่ ๕ ขึ้นไปก็จะได้ผลไม่ต่างจากรอบ ๔ เพราะการวนควรจะสิ้นสุดไปแล้ว

อาจเขียนในรูปวนซ้ำโดยมีเงื่อนไขว่าถ้า .done เป็น true เมื่อไหร่ก็ให้หยุด ดังนี้
let shoufu = new Map([
  ["เจียงซู", "หนานจิง"],
  ["เจียงซี", "หนานชาง"],
  ["ส่านซี", "ซีอาน"]
]);

let kitr = shoufu.keys();
let x;
let nex = kitr.next(); // เริ่มรอบแรก
while (nex.done == false){ // ดูว่าสิ้นสุดหรือยัง
  x = nex.value; // ได้ค่าในแต่ละรอบ
  alert(x);
  nex = kitr.next(); // เริ่มรอบต่อไป
}

ซึ่งตรงนี้จะเทียบเท่ากับการใช้ for๛of แบบนี้
let kitr = shoufu.keys();
for (let x of kitr){
  alert(x);
}

จะเห็นว่าเมื่อใช้ for๛of จะดูแล้วสั้นและเรียบง่ายกว่ามาก แต่จริงๆแล้วเบื้องหลังการทำงานของ for๛of ก็คือการเรียก .next() และตรวจดูค่า .done ซ้ำไปมาในแต่ละรอบนั่นเอง

สรุปก็คือ อิเทอเรเตอร์จะต้องมีเมธอด .next และในเมธอด .next นี้จะต้องให้ออบเจ็กต์ที่มีพรอเพอร์ตีเป็น .value และ .done คืนออกมา




ออบเจ็กต์ไหนนำมาไล่เรียงวนซ้ำได้บ้าง

สำหรับออบเจ็กต์ที่เป็นอิเทอเรเตอร์อยู่แล้วเมื่อใช้ for๛of จะทำงานโดยใช้เมธอด .next ในลักษณะดังที่กล่าวมาแล้ว แต่สำหรับออบเจ็กต์อื่นที่สามารถใช้ใน for๛of ได้เช่นแถวลำดับ, แม็ป, เซ็ต ที่จริงแล้วเมื่อเจอ for๛of ก็จะมีการสร้างอิเทอเรเตอร์ขึ้นโดยอัตโนมัติ

การสร้างอิเทอเรเตอร์ขึ้นมานั้นเกิดขึ้นโดยเมธอด [Symbol.iterator] ซึ่งอยู่ในตัวออบเจ็กต์นั้นๆ

เพื่อแสดงให้เห็นตรงนี้ อาจลองสร้างแถวลำดับขึ้นมาแล้วใช้เมธอด [Symbol.iterator] เองโดยตรงดู
let arr = [6,4,3];
alert(arr[Symbol.iterator]()) // [object Array Iterator]

[Symbol.iterator] คือเมธอดที่ใช้คีย์เป็นข้อมูลชนิดซิมโบล ตัว Symbol.iterator ก็เป็นซิมโบลตัวหนึ่งซึ่งเอาไว้แทนชื่อของเมธอดที่ใช้ในการสร้างอิเทอเรเตอร์ของออบเจ็กต์ (เกี่ยวกับซิมโบลและการใช้ซิมโบลแทนชื่อพรอเพอร์ตีดูในบทที่ ๓๕)
alert(typeof Symbol.iterator); // ได้ symbol
alert(Symbol.iterator.toString()); // ได้ Symbol(Symbol.iterator)

หมายความว่าออบเจ็กต์ที่สามารถใช้ใน for๛of ได้คือออบเจ็กต์ที่มีเมธอด [Symbol.iterator] อยู่ในตัว ในขณะที่ออบเจ็กต์ที่ไล่เรียงไม่ได้จะไม่มีเมธอดนี้อยู่

ลองดูว่าออบเจ็กต์ชนิดไหนมีหรือไม่มี [Symbol.iterator]
let object = {};
alert(typeof object[Symbol.iterator]); // ได้ undefined
let date = new Date();
alert(typeof date[Symbol.iterator]); // ได้ undefined
let array = [];
alert(typeof array[Symbol.iterator]); // ได้ function
let map = new Map();
alert(typeof map[Symbol.iterator]); // ได้ function
let set = new Set();
alert(typeof set[Symbol.iterator]); // ได้ function

จากตรงนี้จะเห็นว่าแถวลำดับ, แม็ป และเซ็ต มีเมธอด [Symbol.iterator] อยู่ ส่วนออบเจ็กต์เปล่าและออบเจ็กต์วันเวลา (date) ไม่มีเมธอด [Symbol.iterator] อยู่

ออบเจกต์ที่ไม่มีเมธอด [Symbol.iterator] อยู่นั้นเมื่อนำมาใช้ for๛of ก็จะเกิดข้อผิดพลาด โดยเตือนว่าออบเจ็กต์นั้นไม่ iterable
let object = {};
for (let x of object){} // ได้ TypeError: object is not iterable

iterable หมายถึงสามารถนำมาไล่เรียงวนซ้ำได้

ออบเจ็กต์จะ iterable ได้ต้องมีเมธอด [Symbol.iterator] และในเมธอดก็จะต้องได้ผลลัพธ์เป็นอิเทอเรเตอร์สร้างออกมา ไม่เช่นนั้นก็จะไม่ iterable ก็หมายความว่าไม่สามารถใช้วนซ้ำใน for๛of ได้




การสร้างออบเจ็กต์ที่ไล่เรียงวนซ้ำได้

ถ้าออบเจ็กต์มีเมธอด [Symbol.iterator] ซึ่งสามารถคืนค่าออบเจ็กต์ที่มีเมธอด .next อยู่ก็จะสามารถนำมาวนซ้ำใน for๛of ได้

และเมธอดนี้ก็เป็นสิ่งที่เราสามารถเขียนใส่เข้าไปให้ตัวออบเจ็กต์ได้เองด้วย ดังนั้นเมื่อเข้าใจหลักการแล้ว เราก็สามารถมาลองสร้างออบเจ็กต์ที่สามารถวนซ้ำใน for๛of แบบนั้นกันดูเองได้

ตัวอย่างการสร้าง
let obje = {}
obje[Symbol.iterator] = function () {
  this.value = 1;
  this.done = false;
  let itera = {};
  itera.next = () => {
    this.value *= 2;
    if (this.value > 16) this.done = true;
    return { value: this.value, done: this.done };
  }
  return itera;
}

for (let x of obje) {
  alert(x) // วน ๔​ ครั้ง ได้ 2, 4, 8, 16 ตามลำดับ
}

โค้ดอาจจะออกมาดูซับซ้อนเข้าใจยากสักหน่อย ค่อยๆดูไปทีละขั้น

เริ่มแรกสร้างออบเจ็กต์ obje ขึ้นมา เป็นออบเจ็กต์เปล่าๆ จากนั้นก็ป้อนเมธอด [Symbol.iterator] เข้าไป

โดยในเมธอดนั้นมีการสร้างออบเจ็กต์ itera และในออบเจ็กต์ itera นี้ก็ถูกป้อนเมธอด .next ให้ โดยเมธอด .next ในที่นี้เป็นฟังก์ชันที่จะให้ออบเจ็กต์ที่มีพรอเพอร์ตี .value และ .done ออกมา

สุดท้าย itera ก็จะกลายเป็นผลลัพธ์ที่ได้ออกมาของเมธอด [Symbol.iterator] ของ obje นี้

เมื่อสร้างออบเจ็กต์แบบนี้ออกมาเสร็จแล้ว พอนำไปวนด้วย for๛of ก็จะสามารถทำงานได้ และเกิดการวนซ้ำตามที่ต้องการ

ด้วยวิธีการเช่นนี้ เราจึงสามารถสร้างออบเจ็กต์ที่นำมาวนซ้ำใน for๛of ขึ้นมาได้สำเร็จ

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

ถ้าเข้าใจดีแล้ว ตัวอย่างนี้อาจเขียนใหม่ให้กระชับขึ้นได้ในลักษณะนี้
let obje = {
  value: 1,
  done: false,
  [Symbol.iterator]() {
    return {
      next: () => {
        this.value *= 2;
        if (this.value > 16) this.done = true;
        return { value: this.value, done: this.done };
      }
    }
  }
}

อีกตัวอย่างหนึ่ง สร้างอิเทอเรเตอร์ที่ไล่เอาค่าที่อยู่ในแถวลำดับออกมาใช้
let array = [3, 50, 110];
let obji = {
  array,
  i: 0,
  [Symbol.iterator]() {
    return {
      next: () => {
        let x = this.array[this.i];
        if (!x) return { done: true };
        this.i++;
        return { value: x.toExponential(), done: false };
      }
    }
  }
}

for (let x of obji) {
  alert(x) // วน ๓​ ครั้ง ได้ 3e+0, 5e+1, 1.1e+2 ตามลำดับ
}




สร้างคลาสของออบเจ็กต์ที่วนซ้ำได้

ต่อไปลองสร้างคลาสที่มีเมธอด [Symbol.iterator] ขึ้นมาก็จะนำมาใช้วนซ้ำได้
class Itor {
  constructor(a, b, n) {
    this.a = a;
    this.b = b;
    this.n = n;
    this.i = 0;
  }
  [Symbol.iterator]() {
    return {
      next: () => {
        if (this.i >= this.n) return { done: true };
        let value = this.a + this.b * this.i;
        this.i++;
        return { value, done: false };
      }
    }
  }
}

let ito1 = new Itor(3, 4, 4);
for (let x of ito1) {
  alert(x); // วนซ้ำ ๔ รอบ ได้ 3, 7, 11, 15
}

let ito2 = new Itor(6, 7, 3);
for (let x of ito2) {
  alert(x); // วนซ้ำ ๓ รอบ ได้ 6, 13, 20
}

จะเห็นว่าอิเทอเรเตอร์เป็นสิ่งที่สามารถสร้างขึ้นเองได้ แต่ว่ายุ่งยากพอสมควร

ในบทต่อไปจะพูดถึงวิธีในการที่จะสร้างอิเทอเรเตอร์แบบง่ายๆ คือใช้เจเนอเรเตอร์ (generator)






-----------------------------------------

囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧

ดูสถิติของหน้านี้

หมวดหมู่

-- คอมพิวเตอร์ >> เขียนโปรแกรม >> javascript

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

目录

从日本来的名言
模块
-- numpy
-- matplotlib

-- pandas
-- manim
-- opencv
-- pyqt
-- pytorch
机器学习
-- 神经网络
javascript
蒙古语
语言学
maya
概率论
与日本相关的日记
与中国相关的日记
-- 与北京相关的日记
-- 与香港相关的日记
-- 与澳门相关的日记
与台湾相关的日记
与北欧相关的日记
与其他国家相关的日记
qiita
其他日志

按类别分日志



ติดตามอัปเดตของบล็อกได้ที่แฟนเพจ

  查看日志

  推荐日志

ตัวอักษรกรีกและเปรียบเทียบการใช้งานในภาษากรีกโบราณและกรีกสมัยใหม่
ที่มาของอักษรไทยและความเกี่ยวพันกับอักษรอื่นๆในตระกูลอักษรพราหมี
การสร้างแบบจำลองสามมิติเป็นไฟล์ .obj วิธีการอย่างง่ายที่ไม่ว่าใครก็ลองทำได้ทันที
รวมรายชื่อนักร้องเพลงกวางตุ้ง
ภาษาจีนแบ่งเป็นสำเนียงอะไรบ้าง มีความแตกต่างกันมากแค่ไหน
ทำความเข้าใจระบอบประชาธิปไตยจากประวัติศาสตร์ความเป็นมา
เรียนรู้วิธีการใช้ regular expression (regex)
การใช้ unix shell เบื้องต้น ใน linux และ mac
g ในภาษาญี่ปุ่นออกเสียง "ก" หรือ "ง" กันแน่
ทำความรู้จักกับปัญญาประดิษฐ์และการเรียนรู้ของเครื่อง
ค้นพบระบบดาวเคราะห์ ๘ ดวง เบื้องหลังความสำเร็จคือปัญญาประดิษฐ์ (AI)
หอดูดาวโบราณปักกิ่ง ตอนที่ ๑: แท่นสังเกตการณ์และสวนดอกไม้
พิพิธภัณฑ์สถาปัตยกรรมโบราณปักกิ่ง
เที่ยวเมืองตานตง ล่องเรือในน่านน้ำเกาหลีเหนือ
ตระเวนเที่ยวตามรอยฉากของอนิเมะในญี่ปุ่น
เที่ยวชมหอดูดาวที่ฐานสังเกตการณ์ซิงหลง
ทำไมจึงไม่ควรเขียนวรรณยุกต์เวลาทับศัพท์ภาษาต่างประเทศ