คำนำ
เมื่อเร็ว ๆ นี้ฉันพบว่าประสิทธิภาพของระเบียบในจาวาสคริปต์ในบางสถานที่ค่อนข้างแตกต่างจากภาษาอื่น ๆ ในภาษาหรือเครื่องมืออื่น ๆ และเป็นทางเลือกที่ค่อนข้างทางเลือก แม้ว่ามันจะเป็นไปไม่ได้เลยที่คุณจะเขียนและคุณแทบจะไม่สามารถใช้กฎที่ฉันกล่าวถึงด้านล่าง แต่ก็เป็นการดีที่จะเข้าใจพวกเขาหลังจากทั้งหมด
ตัวอย่างรหัสในบทความนี้ดำเนินการในสภาพแวดล้อม JavaScript ที่เข้ากันได้กับ ES5 กล่าวคือประสิทธิภาพในเวอร์ชันก่อน IE9 รุ่นรอบ FX4 ฯลฯ มีแนวโน้มที่จะแตกต่างจากที่ฉันกล่าวถึงด้านล่าง
1. คลาสอักขระที่ว่างเปล่า
คลาสอักขระที่ไม่มี [] ใด ๆ เรียกว่าคลาส empty char class ที่ว่างเปล่า ฉันเชื่อว่าคุณไม่เคยได้ยินคนอื่นเรียกมันเพราะในภาษาอื่นวิธีการเขียนนี้ผิดกฎหมายและเอกสารและบทเรียนทั้งหมดไม่ได้พูดถึงไวยากรณ์ที่ผิดกฎหมาย ให้ฉันแสดงให้เห็นว่าภาษาหรือเครื่องมืออื่น ๆ รายงานข้อผิดพลาดนี้อย่างไร:
$ echo | grep '[]' grep: unmatched [หรือ [^$ echo | sed '/[]/' sed: -e expression #1, อักขระ 4: ที่อยู่ที่ไม่ได้ยกเลิกการแสดงออกปกติ $ echo | awk '/[]/' awk: cmd บรรทัด: 1: /[] /awk: cmd บรรทัด: 1: ^ regexpawk ที่ไม่ได้ยกเลิกการยกเลิก: cmd บรรทัด: 1: ข้อผิดพลาด: unmatched [หรือ [^:/[] // $ echo | perl -ne '/[]/' uncated [ใน regex; ทำเครื่องหมายโดย <-ที่นี่ใน m/ [<-ที่นี่]/ at -e line 1 $ echo | RUBY -NE '/[]/' -E: 1: char -class ว่างเปล่า:/[]/$ python -c 'นำเข้า re; re.match ("[]", "")' traceback (การโทรล่าสุดล่าสุด): ไฟล์ "<string>", บรรทัดที่ 1, ใน <โมดูล> ไฟล์ " "E: /python/lib/re.py", บรรทัด 244, ในข้อผิดพลาดการยกระดับ _compile, v # envalid expressionsre_constants.error: สิ้นสุดการแสดงออกปกติที่ไม่คาดคิด ใน JavaScript คลาสอักขระที่ว่างเปล่าเป็นองค์ประกอบปกติที่ถูกกฎหมาย แต่เอฟเฟกต์ของมันคือ "ไม่เคยจับคู่" นั่นคือทุกอย่างจะล้มเหลว มันเทียบเท่ากับผลกระทบของ (empty negative lookahead)(?!) :
js> "อะไรก็ได้/n" .match (/[]/g) // คลาสอักขระ null ไม่เคยตรงกับ nulljs> "อะไรก็ได้/n". -match (/(?!)/g) // null ลบไปข้างหน้ามองไปรอบ ๆ
เห็นได้ชัดว่าสิ่งนี้ไร้ประโยชน์ใน JavaScript
2. ลบล้างคลาสอักขระที่ว่างเปล่า
คลาสอักขระเชิงลบที่ไม่มีอักขระใด ๆ เรียกว่าคลาสถ่านที่ว่างเปล่าหรือคลาสถ่านลบที่ว่างเปล่าเช่นกันเพราะคำนามนี้เป็น "สร้างตัวเอง" และคล้ายกับคลาสอักขระที่ว่างเปล่าที่กล่าวถึงข้างต้น วิธีการเขียนนี้ยังผิดกฎหมายในภาษาอื่น ๆ :
$ echo | grep '[^]' grep: unmatched [หรือ [^$ echo | sed '/[^]/' sed: -e expression #1, อักขระ 5: ที่อยู่ที่ไม่ได้ยกเลิกการแสดงออกปกติ $ echo | awk '/[^]/' awk: cmd บรรทัด: 1: /[^] /awk: cmd บรรทัด: 1: ^ regexpawk ที่ไม่ได้ยกเลิกการยกเลิก: cmd บรรทัด: 1: ข้อผิดพลาด: unmatched [หรือ [^:/[^] // $ echo | perl -ne '/[^]/' unmatched [ใน regex; ทำเครื่องหมายโดย <-ที่นี่ใน m/ [<-ที่นี่ ^]/ at -e line 1 $ echo | RUBY -NE '/[^]/' -E: 1: char -class ว่างเปล่า:/[^]/$ python -c 'นำเข้า re; re.match ("[^]", "")' traceback (การโทรล่าสุดล่าสุด): ไฟล์ "<string>", line 1, ใน <โมดูล ไฟล์ "e: /python/lib/re.py", บรรทัด 244, ใน _compile Rause error, v # envalid expressionsre_constants.error: สิ้นสุดการแสดงออกปกติที่ไม่คาดคิด $ ใน JavaScript การคัดค้านคลาสอักขระ null เป็นองค์ประกอบปกติทางกฎหมาย เอฟเฟกต์ของมันเป็นสิ่งที่ตรงกันข้ามกับเอฟเฟกต์ของคลาสอักขระ null มันสามารถจับคู่อักขระใด ๆ รวมถึงใหม่ "/n" นั่นคือมันเทียบเท่ากับ [/s/S] ทั่วไปและ [/w/W] :
js> "อะไรก็ตาม/n". -match (/[^]/g) // คลาสอักขระ neizontal ตรงกับตัวละครใด ๆ ["W", "H", "A", "T", "E", "V", "E", "R", "/n" js> "t", "e", "v", "e", "r", "/n"]
ควรสังเกตว่ามันไม่สามารถเรียกได้ว่า หากสตริงเป้าหมายว่างเปล่าหรือถูกใช้โดยปกติด้านซ้ายการแข่งขันจะล้มเหลวเช่น:
js> /abc [ridment^ like/.test("abc ") // ไม่มีอักขระหลังจาก c และการจับคู่ล้มเหลว. falseหากคุณต้องการทราบ "กฎการจับคู่ถาวร" ที่แท้จริงคุณสามารถตรวจสอบบทความที่ฉันแปลก่อนหน้านี้: กฎ "ว่างเปล่า"
3. []] และ [^]]
นี่เป็นเรื่องง่ายนั่นคือ: ในการแสดงออกปกติของ Perl และคำสั่ง Linux อื่น ๆ หากคลาสอักขระ [] มีวงเล็บสี่เหลี่ยมจัตุรัสขวาทันทีหลังจาก []] ยึดสี่เหลี่ยมจัตุรัสด้านซ้ายตัวยึดสี่เหลี่ยมจัตุรัสด้านขวาจะถือเป็นตัวละครปกตินั่นคือมันสามารถจับคู่ได้เท่านั้น "]" ใน JavaScript ความสม่ำเสมอนี้จะได้รับการยอมรับว่าเป็นคลาสอักขระที่ว่างเปล่าตามด้วยวงเล็บสี่เหลี่ยมจัตุรัสด้านขวาและคลาสอักขระที่ว่างเปล่าจะไม่ตรงกับอะไร .[^]] มีความคล้ายคลึง "a]","b]" : ใน JavaScript มันตรงกับตัวละครโดยพลการ
$ perl -e 'print "]" = ~/[]]/' 1 $ js -e 'พิมพ์ (/[]]/. ทดสอบ ("]")' false $ perl -e 'พิมพ์ "x" = ~/[^]]/' 1 $ js -e 'พิมพ์ (/[^]4. $ anchor point
ผู้เริ่มต้นบางคนคิดว่า $ ตรงกับตัวละครใหม่ "/n" ซึ่งเป็นความผิดพลาดครั้งใหญ่ $ เป็นการยืนยันแบบไม่มีความกว้างเป็นไปไม่ได้ที่จะจับคู่ตัวละครจริงมันสามารถจับคู่ตำแหน่งเดียวเท่านั้น ความแตกต่างที่ฉันต้องการพูดคุยเกี่ยวกับการเกิดขึ้นในโหมดที่ไม่ใช่หลายสาย: คุณอาจคิดว่าในโหมดที่ไม่ใช่หลายบรรทัดไม่ตรงกับตำแหน่งหลังจากตัวละครตัวสุดท้ายหรือไม่? จริงๆแล้วมันไม่ง่ายเลย ในภาษาอื่น ๆ ส่วนใหญ่หากอักขระตัวสุดท้ายในสตริงเป้าหมายคืออักขระใหม่ "/n" $ จะตรงกับตำแหน่งก่อนหน้าใหม่นั่นคือจับคู่ตำแหน่งทั้งสองที่ด้านซ้ายและขวาของเส้นแบ่งบรรทัดในตอนท้าย หลายภาษามีสองสัญลักษณ์ /z และ /z หากคุณรู้ถึงความแตกต่างระหว่างพวกเขาคุณควรเข้าใจว่าในภาษาอื่น ๆ (Perl, Python, PHP, Java, C#... ), $ ในโหมดที่ไม่ใช่หลายสายพันธุ์นั้นเทียบเท่ากับ /z ในขณะที่ JavaScript, $ ในโหมดที่ไม่ใช่หลายสาย ทับทิมเป็นกรณีพิเศษเนื่องจากค่าเริ่มต้นเป็นโหมดหลายบรรทัด $ ในโหมด multi-line จะตรงกับตำแหน่งก่อนแต่ละบรรทัดใหม่และแน่นอนว่ามันจะรวมถึงการแบ่งบรรทัดที่อาจปรากฏในตอนท้าย หนังสือ "แนวทางปกติ" ของ Yu Sheng ยังพูดถึงประเด็นเหล่านี้ด้วย
$ perl -e 'print "อะไรก็ได้/n" = ~ s/$/แทนที่อักขระ/rg' // การแทนที่ทั่วโลกไม่ว่าอักขระอะไรก็ตาม // ตำแหน่งก่อนที่การแบ่งบรรทัดจะถูกแทนที่ด้วยอักขระทดแทน // ตำแหน่งหลังจากการแบ่งบรรทัดจะถูกแทนที่ด้วยการพิมพ์ $ js -e '
5. Dot Metacharacter "."
ในการแสดงออกปกติใน JavaScript, dot metacharacter " สามารถจับคู่อักขระทั้งหมดยกเว้นสี่บรรทัด terminators ( /r-carriage return, /n-line newline, /u2028-line separator, /ตัวแยก U2029-paragraph) ในขณะที่ในภาษาทั่วไปอื่น ๆ จะไม่รวมบรรทัดใหม่ /n เท่านั้น
6. อ้างไปข้างหน้า
เราทุกคนรู้ว่ามีการอ้างอิงกลับในปกตินั่นคือการอ้างอิง backslash + หมายเลขกับสตริงที่ตรงกับในกลุ่มจับภาพก่อนหน้านี้ วัตถุประสงค์คือการจับคู่อีกครั้งหรือเป็นผลการแทนที่ (/ กลายเป็น $) แต่มีกรณีพิเศษว่าหากกลุ่มจับภาพอ้างอิงยังไม่ได้เริ่มต้น (วงเล็บด้านซ้ายถูกล้อมรอบ) จะใช้การอ้างอิงด้านหลังจะเกิดอะไรขึ้น ตัวอย่างเช่นปกติ /(/2(a)){2}/ , (a) เป็นกลุ่มการจับภาพที่สอง แต่ผลลัพธ์ที่ตรงกันของมันถูกใช้ทางด้านซ้าย เรารู้ว่าการจับคู่ปกติจากซ้ายไปขวา นี่คือที่มาของการอ้างอิงชื่อเรื่องไปข้างหน้าในส่วนนี้ มันไม่ใช่แนวคิดที่เข้มงวด ดังนั้นตอนนี้คุณคิดเกี่ยวกับมันรหัส JavaScript ต่อไปนี้จะส่งคืนอะไร:
js>/(/2 (a)) {2}/. exec ("aaa") ???ก่อนที่จะตอบคำถามนี้ลองมาดูการแสดงในภาษาอื่น ๆ ในทำนองเดียวกันในภาษาอื่น ๆ การเขียนด้วยวิธีนี้ไม่ถูกต้อง:
$ echo AAA | grep '(/2 (a)) {2}' grep: การอ้างอิงย้อนกลับที่ไม่ถูกต้อง $ echo aaa | sed -r '/(/2 (a)) {2}/' sed: -e นิพจน์ #1, อักขระ 12: การอ้างอิงกลับที่ผิดกฎหมาย $ echo aaa | awk '/(/2 (a)) {2}/' $ echo aaa | perl -ne 'print/(/2 (a)) {2}/' $ echo aaa | Ruby -ne 'พิมพ์ $ _ = ~/(/2 (a)) {2}/' $ python -c 'นำเข้าอีกครั้งพิมพ์ re.match ("(/2 (a)) {2}", "aaa")'ไม่มีข้อผิดพลาดใน AWK เนื่องจาก AWK ไม่รองรับ backreference นี้และ /2 ถูกตีความว่าเป็นอักขระที่มีรหัส ASCII 2 อย่างไรก็ตามไม่มีข้อผิดพลาดใน Perl Ruby Python ฉันไม่รู้ว่าทำไมการออกแบบนี้ควรได้รับการเรียนรู้โดย Perl แต่เอฟเฟกต์เหมือนกัน ในกรณีนี้มันเป็นไปไม่ได้ที่จะจับคู่ได้สำเร็จ
ใน JavaScript ไม่เพียง แต่รายงานข้อผิดพลาด แต่ยังสามารถจับคู่ได้สำเร็จ มาดูกันว่าคำตอบนั้นเหมือนกับที่คุณคิดว่า:
js> /(/2(a)) )/.exec("aaa") ["aa "," a "," a " เพื่อป้องกันไม่ให้คุณลืมสิ่งที่ผลลัพธ์ถูกส่งคืนโดยวิธีการ exec ให้ฉันพูด องค์ประกอบแรกคือสตริงการจับคู่ที่สมบูรณ์นั่นคือ RegExp["$&"] ตามด้วยเนื้อหาของการจับคู่กลุ่มการจับภาพแต่ละครั้งนั่นคือ RegExp.$1 และ RegExp.$2. ทำไมการจับคู่จึงประสบความสำเร็จ? กระบวนการจับคู่คืออะไร? ความเข้าใจของฉันคือ:
ก่อนอื่นเราเข้าสู่กลุ่มจับภาพแรก (วงเล็บซ้ายสุดซ้ายสุด) ซึ่งการจับคู่ที่ถูกต้องครั้งแรกคือ /2 แต่ในเวลานี้กลุ่มจับกุมครั้งที่สอง (A) ยังไม่ได้อยู่ในรอบดังนั้นค่าของ RegExp.$2 ยังคง undefined ประเด็นก็คือการแข่งขันประสบความสำเร็จ ดำเนินการต่อไปและจากนั้นกลุ่มการจับภาพที่สอง (a) ตรงกับ A แรกในสตริงเป้าหมายและค่าของ RegExp.$2 ก็ถูกกำหนดให้กับ "A" และจากนั้นกลุ่ม RegExp.$1 ครั้งแรกจะสิ้นสุดลง จากนั้นก็มี quantifier {2} นั่นคือหลังจาก A แรกในสตริงเป้าหมายจะเริ่มรอบใหม่ของการจับคู่ปกติ (/2(a)) จุดสำคัญคือที่นี่: ค่าของ RegExp.$2 คือค่าของ /2 จับคู่หรือเป็นค่าที่กำหนดเมื่อสิ้นสุดรอบแรกของการจับคู่ "A" คำตอบคือ: "ไม่" ค่า RegExp.$1 และ RegExp.$2 จะถูกล้างเป็น undefined และ /1 และ /2 จะเหมือนกับครั้งแรกที่ประสบความสำเร็จในการจับคู่อักขระที่ว่างเปล่า A ที่สองในสตริงเป้าหมายได้รับการจับคู่สำเร็จและค่าของ RegExp.$1 และ RegExp.$2 กลายเป็น "A" อีกครั้งค่าของ RegExp["$&"] กลายเป็นสตริงการจับคู่ที่สมบูรณ์สองครั้งแรก A: "AA"
ในเวอร์ชันก่อนหน้าของ Firefox (3.6) การจับคู่ของปริมาณอีกครั้งจะไม่ล้างค่าของกลุ่มที่ถูกจับที่มีอยู่ดังนั้นในรอบที่สองของการแข่งขัน /2 จะตรงกับ A ที่สองดังนั้น:
js> /(/2(a)) )/.exec("aaa") [Commaaa "," a "]นอกจากนี้การสิ้นสุดของกลุ่มจับกุมขึ้นอยู่กับว่าวงเล็บปิดปิดอยู่หรือไม่ ตัวอย่างเช่น/(a/1) {3}/ แม้ว่ากลุ่มการจับภาพครั้งแรกจะเริ่มจับคู่เมื่อใช้ /1 แต่ยังไม่สิ้นสุด นี่เป็นข้อมูลอ้างอิงไปข้างหน้าดังนั้นการจับคู่ระหว่าง /1 ยังคงว่างเปล่า:
js> /(a/1) {3 }/.exec("aaa") [Commaaa "," a "]อีกตัวอย่างหนึ่ง:
js> /(?:(f-)(o)(o)|(B)(a)(r))-/.exec("foobar") ที่เธอ * เป็นปริมาณ หลังจากรอบแรกของการจับคู่: $ 1 คือ "F", $ 2 คือ "O", $ 3 คือ "O", $ 4 ไม่ได้กำหนด, $ 5 undefined และ $ 6 undefined
ที่จุดเริ่มต้นของรอบที่สองของการแข่งขัน: ค่าที่จับได้ทั้งหมดจะถูกรีเซ็ตเป็น undefined
หลังจากการแข่งขันรอบที่สอง: $ 1 undefined $ 2 จะ undefined $ 3 จะ undefined $ 4 คือ "B", $ 5 คือ "A" และ $ 6 คือ "R"
& ได้รับมอบหมายให้เป็น "foobar" และการแข่งขันสิ้นสุดลง
สรุป
ข้างต้นเป็นเนื้อหาทั้งหมดที่สรุปความแตกต่างระหว่างความสม่ำเสมอของ JavaScript และภาษาอื่น ๆ ฉันหวังว่าเนื้อหาของบทความนี้จะเป็นประโยชน์ต่อการศึกษาและการทำงานของทุกคน