JavaScript: 在.replace中使用Async/await

39

我正在以下方式使用 async/await 函数

async function(){
  let output = await string.replace(regex, async (match)=>{
    let data = await someFunction(match)
    console.log(data); //gives correct data
    return data
  })
  return output;
}

但返回的数据是一个 Promise 对象。对于这样带有回调函数的函数,应该如何实现有些困惑。


async 函数的返回值始终是一个 Promise 对象,该对象通过返回的 output 值解决(resolve),或者通过抛出的错误拒绝(reject)。 - Madara's Ghost
你是否想知道为什么output是一个promise?我不清楚你遇到了什么问题。请注意,如果string.replace就是String.prototype.replace,那么它将无法正常工作。.replace期望回调是一个普通函数,而不是一个异步函数。 - Felix Kling
7个回答

50

一个易于使用和理解的函数,用于一些异步替换:

async function replaceAsync(str, regex, asyncFn) {
    const promises = [];
    str.replace(regex, (match, ...args) => {
        const promise = asyncFn(match, ...args);
        promises.push(promise);
    });
    const data = await Promise.all(promises);
    return str.replace(regex, () => data.shift());
}

如果您进行某些较复杂的处理,则需要注意它会执行两次替换函数。但对于大多数情况,它非常方便。

使用方法如下:

replaceAsync(myString, /someregex/g, myAsyncFn)
    .then(replacedString => console.log(replacedString))

或者这个:

const replacedString = await replaceAsync(myString, /someregex/g, myAsyncFn);

请不要忘记,您的myAsyncFn需要返回一个Promise。

异步函数的示例:

async function myAsyncFn(match) {
    // match is an url for example.
    const fetchedJson = await fetch(match).then(r => r.json());
    return fetchedJson['date'];
}

function myAsyncFn(match) {
    // match is a file
    return new Promise((resolve, reject) => {
        fs.readFile(match, (err, data) => {
            if (err) return reject(err);
            resolve(data.toString())
        });
    });
}

只有在使用替换迭代匹配时才有效。这对于替换无效。 - Jack G
3
可以的。它确实会这样做。它会迭代并替换。 - Overcl9ck
1
我真的很喜欢这个解决方案,简单而不失优雅! - Livewire
可以确认,它可以替换多个匹配项。 - GavinBelson

8
原生的 replace 方法 无法处理异步回调,因此您不能使用返回 Promise 的替换器。不过,我们可以编写自己的 `replace` 函数来处理 Promise:
async function(){
  return string.replace(regex, async (match)=>{
    let data = await someFunction(match)
    console.log(data); //gives correct data
    return data;
  })
}

function replaceAsync(str, re, callback) {
    // http://es5.github.io/#x15.5.4.11
    str = String(str);
    var parts = [],
        i = 0;
    if (Object.prototype.toString.call(re) == "[object RegExp]") {
        if (re.global)
            re.lastIndex = i;
        var m;
        while (m = re.exec(str)) {
            var args = m.concat([m.index, m.input]);
            parts.push(str.slice(i, m.index), callback.apply(null, args));
            i = re.lastIndex;
            if (!re.global)
                break; // for non-global regexes only take the first match
            if (m[0].length == 0)
                re.lastIndex++;
        }
    } else {
        re = String(re);
        i = str.indexOf(re);
        parts.push(str.slice(0, i), callback.apply(null, [re, i, str]));
        i += re.length;
    }
    parts.push(str.slice(i));
    return Promise.all(parts).then(function(strings) {
        return strings.join("");
    });
}

7

这是Overcl9ck的回答的改进和现代化版本:

async function replaceAsync(string, regexp, replacerFunction) {
    const replacements = await Promise.all(
        Array.from(string.matchAll(regexp),
            match => replacerFunction(...match)));
    let i = 0;
    return string.replace(regexp, () => replacements[i++]);
}

由于 String.prototype.matchAll,这需要一个更新的浏览器基线,在2019年全面推出(除了基于Chromium的Edge,在2020年初才获得)。但它至少与原来一样简单,同时更加高效,只有第一次匹配时才会进行匹配,而不是创建无用的字符串,并且不会以昂贵的方式改变替换数组。


你可以去掉 let i = 0 并使用 () => replacements.shift() 来减少一行代码。 - lapo
2
@lapo:我故意放弃了shift的使用,因为它对性能非常不利,将一些线性且只读的东西变成了二次方,并且没有任何好处。 (我在答案中提到了这一点。) - Chris Morgan
非常有道理,谢谢。我从来没有想过.shift()是昂贵的,但你很可能是对的;现在我必须进入那个兔子洞了...确实慢得多:https://jsbench.me/m4lclyqpsp/1 - lapo

6

所以,没有接受 Promise 的 replace 方法的重载。因此,只需重写您的代码:

async function(){
  let data = await someFunction();
  let output = string.replace(regex, data)
  return output;
}

当然,如果您需要使用匹配值来传递给异步函数,事情会变得更加复杂:
var sourceString = "sheepfoohelloworldgoocat";
var rx = /.o+/g;

var matches = [];
var mtch;
rx.lastIndex = 0; //play it safe... this regex might have state if it's reused
while((mtch = rx.exec(sourceString)) != null)
{
    //gather all of the matches up-front
    matches.push(mtch);
}
//now apply async function someFunction to each match
var promises = matches.map(m => someFunction(m));
//so we have an array of promises to wait for...
//you might prefer a loop with await in it so that
//you don't hit up your async resource with all
//these values in one big thrash...
var values = await Promise.all(promises);
//split the source string by the regex,
//so we have an array of the parts that weren't matched
var parts = sourceString.split(rx);
//now let's weave all the parts back together...
var outputArray = [];
outputArray.push(parts[0]);
values.forEach((v, i) => {
    outputArray.push(v);
    outputArray.push(parts[i + 1]);
});
//then join them back to a string... voila!
var result = outputArray.join("");

我已经更新了问题。我需要将匹配的元素传递给函数,这样就无法实现该方式。 - ritz078
@ritz078 我想你可能错过了那个。也许我的编辑更有用? - spender

3
这是Overcl9ck在TypeScript中实现的解决方案:
const replaceAsync = async (str: string, regex: RegExp, asyncFn: (match: any, ...args: any) => Promise<any>) => {
    const promises: Promise<any>[] = []
    str.replace(regex, (match, ...args) => {
        promises.push(asyncFn(match, ...args))
        return match
    })
    const data = await Promise.all(promises)
    return str.replace(regex, () => data.shift())
}

1

这里有一个使用递归函数的漂亮替代方法:

async function replaceAsync(str, regex, asyncFn) {
    const matches = str.match(regex);
    if (matches) {
        const replacement = await asyncFn(...matches);
        str = str.replace(matches[0], replacement);
        str = await replaceAsync(str, regex, asyncFn);
    }
    return str;
}

  1. str.replace 获取一个没有位置的字符串,可能会替换未匹配正则表达式的其他位置中的相同字符串。
  2. 递归假设替换后将不再匹配正则表达式,所以下一次它将找到“下一个”匹配项。如果它匹配,它会冒着永久循环的风险!
  3. 即使不是永久的,一个地方经历几次替换也会产生意想不到的结果。
  4. 如果替换不匹配或没有重叠,仍然可能通过 ^\b 边界或通用回溯(ES9)影响后续匹配的位置。
  5. O(n²) 性能见“施莱米尔画家”。
- Beni Cherniavsky-Paskin

1

还有另一种解决方案,这次是使用TypeScript。与Maxime的解决方案类似,它通过使用match()而不是在许多其他解决方案中使用“语义上不寻常”的初始replace()调用来避免问题。

async function replaceAsync(str: string, regex: RegExp, asyncFn: (match: string) => Promise<string>): Promise<string> {
  const promises = (str.match(regex) ?? []).map((match: string) => asyncFn(match));
  const data = await Promise.all(promises);
  return str.replace(regex, () => data.shift()!);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接