JavaScript Regular Expressions`da /g, /y, and .lastIndex


Bu blog yazısında, RegExp Bayrağı /g ve /y'nin nasıl çalıştığını ve RegExp özelliğinin .lastIndex'e nasıl bağlı olduğunu inceliyoruz. Ayrıca .lastIndex için henüz düşünmediğiniz ilginç bir kullanım durumu keşfedeceğiz.

/g ve /y Bayrağı

Bu bayraklar aşağıdaki gibi özetlenebilir:

  • /g (.global) birkaç normal ifade işlemi için çoklu eşleme modlarını etkinleştirir.
  • /y (.sticky) /g'ye benzer, ancak eşleşmeler arasında boşluk olamaz.

Aşağıdaki iki normal ifade işlemi /g ve /yyi tamamen yok sayar:

  • String.prototype.search(regExp)
  • String.prototype.split(regExp)

Diğer tüm işlemler bazı yönlerden onlardan etkilenir.

/g Bayrağı (.global

Çok eşleme modlarının nasıl göründüğüne bakalım.

.exec() ve /g  

/g olmadan .exec() her zaman ilk eşleşme için bir eşleşme nesnesi döndürür:

> const re = /#/;
> re.exec('##-#')
{ 0: '#', index: 0, input: '##-#' }
> re.exec('##-#')
{ 0: '#', index: 0, input: '##-#' }

/g ile her çağrı için bir yeni maç ve başka eşleşme yoksa null değerini döndürür:

> const re = /#/g;
> re.exec('##-#')
{ 0: '#', index: 0, input: '##-#' }
> re.exec('##-#')
{ 0: '#', index: 1, input: '##-#' }
> re.exec('##-#')
{ 0: '#', index: 3, input: '##-#' }
> re.exec('##-#')
null

.replace() ve /g 

/g olmadan .replace() sadece ilk eşleşmenin yerini alır:

> '##-#'.replace(/#/, 'x')
'x#-#'

/g ile .replace() tüm eşleşmelerin yerini alır:

> '##-#'.replace(/#/g, 'x')
'xx-x'

.matchAll() ve /g

.matchAll() sadece /g varsa çalışır ve tüm eşleşen nesneleri döndürür:

> const re = /#/g;
> [...'##-#'.matchAll(re)]
[
  { 0: '#', index: 0, input: '##-#' },
  { 0: '#', index: 1, input: '##-#' },
  { 0: '#', index: 3, input: '##-#' },
]

/y Bayrağı (.sticky)

Şimdilik /y ile birlikte /g kullanacağız (“/g boşluk olmadan düşünün”). Kendi başına /y'yi anlamak için, yakında ele alacağımız RegExp özelliği .lastIndex hakkında bilgi edinmemiz gerekecek.

.exec() ve /gy

/gy ile .exec() tarafından döndürülen her eşleşme hemen bir önceki eşleşmeyi takip etmelidir. Bu nedenle, aşağıdaki örnekte yalnızca iki eşleşme döndürür:

> const re = /#/gy;
> re.exec('##-#')
{ 0: '#', index: 0, input: '##-#' }
> re.exec('##-#')
{ 0: '#', index: 1, input: '##-#' }
> re.exec('##-#')
null

.replace() ve /gy

/gy ile .replace(), aralarında boşluk olmadığı sürece tüm eşleşmelerin yerini alır:

> '##-#'.replace(/#/gy, 'x')
'xx-#'

.matchAll() ve `/gy'

/gy ile .matchAll() yalnızca bitişik eşleşmeler için eşleşme nesneleri döndürür:

> const re = /#/gy;
> [...'##-#'.matchAll(re)]
[
  { 0: '#', index: 0, input: '##-#' },
  { 0: '#', index: 1, input: '##-#' },
]

Regular expression .lastIndex Özelliği

Regular expression .lastIndex Özelliği yalnızca /g ve /y bayraklarından en az biri kullanıldığında bir etkiye sahiptir.

Etkilenen Regular Expression işlemleri için eşleşmenin nerede başladığını kontrol eder.

.lastIndex ve /g

Örneğin, .exec() yöntemi şu anda giriş dizesinde nerede olduğunu hatırlamak için .lastIndex kullanır:

> const re = /[a-z]/g;
> re.lastIndex
0
> [re.exec('a-b'), re.lastIndex]
[{ 0: 'a', index: 0, input: 'a-b' }, 1]
> [re.exec('a-b'), re.lastIndex]
[{ 0: 'b', index: 2, input: 'a-b' }, 3]
> [re.exec('a-b'), re.lastIndex]
[ null, 0 ]

.matchAll() .lastIndex'i değiştirmez:

> const re = /#/g; re.lastIndex = 1;
> [...'##-#'.matchAll(re)]
[
  { 0: '#', index: 1, input: '##-#' },
  { 0: '#', index: 3, input: '##-#' },
]
> re.lastIndex
1

.replace() .lastIndex öğesini yok sayar ve sıfırlar:

> const re = /#/g; re.lastIndex = 1;
> '##-#'.replace(re, 'x')
'xx-x'
> re.lastIndex
0

Özetlemek gerekirse, birkaç işlem için /g şu anlama gelir: .lastIndex veya daha yenisinde eşleştir.

.lastIndex ve /y

/y için .lastIndex şu anlama gelir: Tam olarak .lastIndex ile eşleştirin. Normal ifadenin başlangıcı .lastIndex'e bağlanmış gibi çalışır.

^ ve $ öğelerinin genellikle olduğu gibi çalışmaya devam ettiğini unutmayın: .Multiline ayarlanmadığı sürece, giriş dizesinin başına veya sonuna eşleşirler. Sonra çizgilerin başlangıcına veya sonlarına sabitlenirler.

.exec(), /y ayarlanmışsa (/g ayarlanmamış olsa bile) birden çok kez eşleşir:

> const re = /#/y; re.lastIndex = 1;
> [re.exec('##-#'), re.lastIndex]
[{0: '#', index: 1, input: '##-#'}, 2]
> [re.exec('##-#'), re.lastIndex]
[ null, 0 ]

/y, /g olmadan kullanılırsa, .replace() yöntemi, .lastIndex öğesinde (doğrudan) bulunan ilk oluşumun yerini alır. .lastIndex'i günceller.

> const re = /#/y; re.lastIndex = 1;
> ['##-#'.replace(re, 'x'), re.lastIndex]
[ '#x-#', 2 ]
> ['##-#'.replace(re, 'x'), re.lastIndex] // no match
[ '##-#', 0 ]
> ['##-#'.replace(re, 'x'), re.lastIndex]
[ 'x#-#', 1 ]

/g ve /y'nin Tuzakları   

Tuzak: /g veya /y ile düzenli bir ifadeyi satır içi yapamayız

/g ile normal bir ifade satır içine alınamaz. Örneğin, aşağıdaki while döngüsünde, koşul her kontrol edildiğinde düzenli ifade yeni oluşturulur. Bu nedenle, .lastIndex değeri her zaman sıfırdır ve döngü hiçbir zaman sona ermez.

let matchObj;
// Infinite loop
while (matchObj = /a+/g.exec('bbbaabaaa')) {
  console.log(matchObj[0]);
}

Aynı şey /y ile de olacaktır.

Tuzak: /g veya /y öğesini kaldırmak kodu bozabilir.

Kod /g ile normal bir ifade bekliyorsa ve .exec() veya .test() sonuçları üzerinde bir döngüye sahipse, /giçermeyen normal bir ifade sonsuz döngüye neden olabilir:

function collectMatches(regExp, str) {
  const matches = [];
  let matchObj;
  // Infinite loop
  while (matchObj = regExp.exec(str)) {
    matches.push(matchObj[0]);
  }
  return matches;
}
collectMatches(/a+/, 'bbbaabaaa'); // /g 'yi unuttuk

Neden sonsuzluk döngüsü var? Çünkü .exec() her zaman ilk sonucu, bir eşleşme nesnesini döndürür ve asla null değerini döndürmez.

Aynı şey /y ile de olacaktır.

Tuzak: /g veya /y eklemek kodu bozabilir

.test() ile başka bir uyarı daha vardır: .lastIndex tarafından etkilenir. Bu nedenle, normal ifadenin bir dizeyle eşleşip eşleşmediğini tam olarak bir kez kontrol etmek istiyorsak, normal ifadede /g bulunmamalıdır. Aksi takdirde, .test() öğesini her çağırdığımızda genellikle farklı bir sonuç alırız:

> const regExp = /^X/g;
> [regExp.test('Xa'), regExp.lastIndex]
[ true, 1 ]
> [regExp.test('Xa'), regExp.lastIndex]
[ false, 0 ]
> [regExp.test('Xa'), regExp.lastIndex]
[ true, 1 ]

İlk çağırma bir eşleşme oluşturur ve .lastIndex'i günceller. İkinci çağırma bir eşleşme bulamaz ve .lastIndex öğesini sıfırlar.

Özellikle .test() için normal bir ifade oluşturursak, muhtemelen /g eklemeyeceğiz. Bununla birlikte, aynı normal ifadeyi değiştirmek ve test etmek için kullanırsak, /g ile karşılaşma olasılığı artar.

Aynı şey /y ile de olacaktır.

> const regExp = /^X/y;
> regExp.test('Xa')
true
> regExp.test('Xa')
false
> regExp.test('Xa')
true

Tuzak: .lastIndex sıfır değilse kod beklenmedik sonuçlar doğurabilir

.lastIndex tarafından etkilenen tüm normal ifade işlemleri göz önüne alındığında, .lastIndex'in başında sıfır olduğu birçok algoritmaya dikkat etmeliyiz. Aksi takdirde, beklenmedik sonuçlar elde edebiliriz:

function countMatches(regExp, str) {
  let count = 0;
  while (regExp.test(str)) {
    count++;
  }
  return count;
}

const myRegExp = /a/g;
myRegExp.lastIndex = 4;
assert.equal(
  countMatches(myRegExp, 'babaa'), 1); //  3 olmalı

Normalde, .lastIndex yeni oluşturulan normal ifadelerde sıfırdır ve örnekte yaptığımız gibi bunu açıkça değiştirmeyiz. Ancak, normal ifadeyi birden çok kez kullanırsak .lastIndex yine de sıfır olmayabilir.

/g, /y ve .lastIndex tuzaklarından kaçınmak için önlemler 

/g ve .lastIndex ile ilgili bir örnek olarak, önceki örnekten countMatches() yöntemini tekrar ziyaret ediyoruz. Yanlış bir regular expression kodumuzu bozulmasını nasıl önleyebiliriz? Üç yaklaşıma bakalım.

Hataları yakalama

İlk olarak, /g ayarlanmamışsa veya .lastIndex sıfır değilse bir istisna atabiliriz:

function countMatches(regExp, str) {
  if (!regExp.global) {
    throw new Error('Flag /g of regExp must be set');
  }
  if (regExp.lastIndex !== 0) {
    throw new Error('regExp.lastIndex must be zero');
  }
  
  let count = 0;
  while (regExp.test(str)) {
    count++;
  }
  return count;
}

Regular expressions'ı Kopyalama

İkincisi, parametreyi klonlayabiliriz. Bu, regExp'in değiştirilmeyeceği ek bir faydaya sahiptir.

function countMatches(regExp, str) {
  const cloneFlags = regExp.flags + (regExp.global ? '' : 'g');
  const clone = new RegExp(regExp, cloneFlags);

  let count = 0;
  while (clone.test(str)) {
    count++;
  }
  return count;
}

.lastIndex veya bayraklardan etkilenmeyen bir işlem kullanma

Bazı düzenli ifade işlemleri .lastIndex veya bayraklardan etkilenmez. Örneğin, ".match()", "/g varsa" .lastIndex "i yok sayar:

function countMatches(regExp, str) {
  if (!regExp.global) {
    throw new Error('Flag /g of regExp must be set');
  }
  return (str.match(regExp) || []).length;
}

const myRegExp = /a/g;
myRegExp.lastIndex = 4;
assert.equal(countMatches(myRegExp, 'babaa'), 3); // OK!

Burada, countMatches(), .lastIndex i kontrol etmemiş veya düzeltmemiş olsak da çalışır.

.lastIndex için use-case kullanın: belirli bir dizinde eşleştirmeye başlama


Depolama durumu dışında, .lastIndex de belirli bir dizinde eşleştirmeye başlamak için kullanılabilir. Bu bölümde nasıl yapılacağı açıklanmaktadır.

Örnek: Normal bir ifadenin belirli bir dizinde eşleşip eşleşmediğini kontrol etme

.test(), /y ve .lastIndex den etkilendiğinde, düzenli bir ifade olan regExp ifadesinin belirli bir indexde bir string str ile eşleşip eşleşmediğini kontrol etmek için kullanabiliriz:

function matchesStringAt(regExp, str, index) {
  if (!regExp.sticky) {
    throw new Error('Flag /y of regExp must be set');
  }
  regExp.lastIndex = index;
  return regExp.test(str);
}
assert.equal(
  matchesStringAt(/x+/y, 'aaxxx', 0), false);
assert.equal(
  matchesStringAt(/x+/y, 'aaxxx', 2), true);

regExp, /y nedeniyle .lastIndex e bağlanır.

Giriş dizesinin başına regExp sabitleyecek ^ ifadesini kullanmamamız gerektiğini unutmayın.

Örnek: Belirli bir dizinden başlayarak bir eşleşmenin yerini bulma

.search(), normal ifadenin eşleştiği konumu bulmamızı sağlar:

> '#--#'.search(/#/)
0

Ne yazık ki, .search() nin maç aramaya başladığı yeri değiştiremiyoruz. Geçici çözüm olarak, aramak için .exec() i kullanabiliriz:

function searchAt(regExp, str, index) {
  if (!regExp.global && !regExp.sticky) {
    throw new Error('Either flag /g or flag /y of regExp must be set');
  }
  regExp.lastIndex = index;
  const match = regExp.exec(str);
  if (match) {
    return match.index;
  } else {
    return -1;
  }
}

assert.equal(
  searchAt(/#/g, '#--#', 0), 0);
assert.equal(
  searchAt(/#/g, '#--#', 1), 3);

Örnek: Belirli bir dizindeki bir oluşumun değiştirilmesi

/g olmadan ve /y ile birlikte kullanıldığında, eğer .lastIndexde bir eşleşme varsa .replace() bir değiştirme yapar:

function replaceOnceAt(str, regExp, replacement, index) {
  if (!(regExp.sticky && !regExp.global)) {
    throw new Error('Flag /y must be set, flag /g must not be set');
  }
  regExp.lastIndex = index;
  return str.replace(regExp, replacement);
}
assert.equal(
  replaceOnceAt('aa aaaa a', /a+/y, 'X', 0), 'X aaaa a');
assert.equal(
  replaceOnceAt('aa aaaa a', /a+/y, 'X', 3), 'aa X a');
assert.equal(
  replaceOnceAt('aa aaaa a', /a+/y, 'X', 8), 'aa aaaa X');

Otomatik olarak oluşturulan sonuç tablosu

Aşağıdaki sonuç tablosunu yazdıran küçük bir Node.js komut dosyası yazdım:

const s='##-#';

const r=/#/g; r.lastIndex=1;
r.exec(s)             .index=1       .lastIndex updated  
r.test(s)             true           .lastIndex updated  
s.replace(r, 'x')     "xx-x"         .lastIndex reset    
s.replaceAll(r, 'x')  "xx-x"         .lastIndex reset    
s.match(r)            ["#","#","#"]  .lastIndex reset    
s.matchAll(r)         [["#"],["#"]]  .lastIndex unchanged

const r=/#/y; r.lastIndex=1;
r.exec(s)             .index=1   .lastIndex updated  
r.test(s)             true       .lastIndex updated  
s.replace(r, 'x')     "#x-#"     .lastIndex updated  
s.replaceAll(r, 'x')  TypeError
s.match(r)            .index=1   .lastIndex updated  
s.matchAll(r)         TypeError

const r=/#/yg; r.lastIndex=1;
r.exec(s)             .index=1   .lastIndex updated  
r.test(s)             true       .lastIndex updated  
s.replace(r, 'x')     "xx-#"     .lastIndex reset    
s.replaceAll(r, 'x')  "xx-#"     .lastIndex reset    
s.match(r)            ["#","#"]  .lastIndex reset    
s.matchAll(r)         [["#"]]    .lastIndex unchanged

(.matchAll() 'in eski sürümleri, /g eksikse TypeError atmaz.)