web-dev-qa-db-fra.com

Différer l'exécution pour les littéraux de modèle ES6

Je joue avec la nouvelle fonctionnalité ES6 Template Literals et la première chose qui m’est venue à l’esprit était un String.format pour Javascript; j’ai donc entrepris de mettre en œuvre un prototype:

String.prototype.format = function() {
  var self = this;
  arguments.forEach(function(val,idx) {
    self["p"+idx] = val;
  });
  return this.toString();
};
console.log(`Hello, ${p0}. This is a ${p1}`.format("world", "test"));

ES6Fiddle

Cependant, le gabarit de modèle est évalué avant il est transmis à ma méthode prototype. Est-il possible d'écrire le code ci-dessus pour différer le résultat après avoir créé dynamiquement les éléments?

48
CodingIntrigue

Je peux voir trois façons de contourner cela:

  • Utilisez des chaînes de modèle telles qu'elles ont été conçues pour être utilisées, sans aucune fonction format:

    console.log(`Hello, ${"world"}. This is a ${"test"}`);
    // might make more sense with variables:
    var p0 = "world", p1 = "test";
    console.log(`Hello, ${p0}. This is a ${p1}`);
    // or even function parameters for actual deferral of the evaluation:
    const welcome = (p0, p1) => `Hello, ${p0}. This is a ${p1}`;
    console.log(welcome("world", "test"));
    
  • N'utilisez pas de chaîne de modèle, mais un littéral de chaîne simple:

    String.prototype.format = function() {
        var args = arguments;
        return this.replace(/\$\{p(\d)\}/g, function(match, id) {
            return args[id];
        });
    };
    console.log("Hello, ${p0}. This is a ${p1}".format("world", "test"));
    
  • Utilisez un littéral de modèle marqué. Notez que les substitutions seront toujours évaluées sans interception par le gestionnaire, vous ne pouvez donc pas utiliser d'identificateurs tels que p0 sans avoir une variable nommée so. Ce comportement peut changer si une proposition différente corps de substitution syntaxe est acceptée (Update: ce n'était pas le cas).

    function formatter(literals, ...substitutions) {
        return {
            format: function() {
                var out = [];
                for(var i=0, k=0; i < literals.length; i++) {
                    out[k++] = literals[i];
                    out[k++] = arguments[substitutions[i]];
                }
                out[k] = literals[i];
                return out.join("");
            }
        };
    }
    console.log(formatter`Hello, ${0}. This is a ${1}`.format("world", "test"));
    // Notice the number literals: ^               ^
    
58
Bergi

J'aime aussi l'idée de la fonction String.format et la possibilité de définir explicitement les variables à résoudre.

C’est ce que j’ai trouvé avec… en gros une méthode String.replace avec une recherche deepObject.

const isUndefined = o => typeof o === 'undefined'

const nvl = (o, valueIfUndefined) => isUndefined(o) ? valueIfUndefined : o

// gets a deep value from an object, given a 'path'.
const getDeepValue = (obj, path) =>
  path
    .replace(/\[|\]\.?/g, '.')
    .split('.')
    .filter(s => s)
    .reduce((acc, val) => acc && acc[val], obj)

// given a string, resolves all template variables.
const resolveTemplate = (str, variables) => {
  return str.replace(/\$\{([^\}]+)\}/g, (m, g1) =>
            nvl(getDeepValue(variables, g1), m))
}

// add a 'format' method to the String prototype.
String.prototype.format = function(variables) {
  return resolveTemplate(this, variables)
}

// setup variables for resolution...
var variables = {}
variables['top level'] = 'Foo'
variables['deep object'] = {text:'Bar'}
var aGlobalVariable = 'Dog'

// ==> Foo Bar <==
console.log('==> ${top level} ${deep object.text} <=='.format(variables))

// ==> Dog Dog <==
console.log('==> ${aGlobalVariable} ${aGlobalVariable} <=='.format(this))

// ==> ${not an object.text} <==
console.log('==> ${not an object.text} <=='.format(variables))

Si vous souhaitez une résolution autre que variable (par exemple, le comportement des littéraux de modèle), vous pouvez également utiliser ce qui suit. 

N.B.eval est considéré comme 'diabolique' - envisagez d'utiliser un safe-eval alternative.

// evalutes with a provided 'this' context.
const evalWithContext = (string, context) => function(s){
    return eval(s);
  }.call(context, string)

// given a string, resolves all template variables.
const resolveTemplate = function(str, variables) {
  return str.replace(/\$\{([^\}]+)\}/g, (m, g1) => evalWithContext(g1, variables))
}

// add a 'format' method to the String prototype.
String.prototype.format = function(variables) {
  return resolveTemplate(this, variables)
}

// ==> 5Foobar <==
console.log('==> ${1 + 4 + this.someVal} <=='.format({someVal: 'Foobar'}))

1
Nick Grealy

J'ai posté une réponse à une question similaire qui donne deux approches où l'exécution du modèle littéral est retardée. Lorsque le littéral de modèle est dans une fonction, il n'est évalué que lorsque la fonction est appelée et à l'aide de la portée de la fonction.

https://stackoverflow.com/a/49539260/188963

0
abalter

Vous pouvez injecter des valeurs dans une chaîne en utilisant la fonction ci-dessous

let inject = (str, obj) => str.replace(/\${(.*?)}/g, (x,g)=> obj[g]);
let inject = (str, obj) => str.replace(/\${(.*?)}/g, (x,g)=> obj[g]);


// --- test ---

// parameters in object
let t1 = 'My name is ${name}, I am ${age}. My brother name is also ${name}.';
let r1 = inject(t1, {name: 'JOHN',age: 23} );
console.log("OBJECT:", r1);


// parameters in array
let t2 = "Values ${0} are in ${2} array with ${1} values of ${0}."
let r2 = inject(t2, {...['A,B,C', 666, 'BIG']} );
console.log("ARRAY :", r2);
0

AFAIS, la fonctionnalité utile "exécution différée de modèles de chaîne" n'est toujours pas disponible. Utiliser un lambda est une solution expressive, lisible et courte, cependant:

var greetingTmpl = (...p)=>`Hello, ${p[0]}. This is a ${p[1]}`;

console.log( greetingTmpl("world","test") );
console.log( greetingTmpl("@CodingIntrigue","try") );
0
rplantiko

En prolongeant la réponse de @Bergi, la puissance des chaînes de modèles marqués se révèle dès que vous réalisez que vous pouvez renvoyer quoi que ce soit en conséquence, pas seulement les chaînes ordinaires. Dans son exemple, la balise construit et retourne un objet avec une propriété de fermeture et de fonction format.

Dans mon approche préférée, je renvoie une valeur de fonction par elle-même, que vous pouvez appeler plus tard et transmettre de nouveaux paramètres pour remplir le modèle. Comme ça:

function fmt([fisrt, ...rest], ...tags) {
  return values => rest.reduce((acc, curr, i) => {
    return acc + values[tags[i]] + curr;
  }, fisrt);
}

Ensuite, vous construisez vos modèles et différez les substitutions:

> fmt`Test with ${0}, ${1}, ${2} and ${0} again`(['A', 'B', 'C']);
// 'Test with A, B, C and A again'
> template = fmt`Test with ${'foo'}, ${'bar'}, ${'baz'} and ${'foo'} again`
> template({ foo:'FOO', bar:'BAR' })
// 'Test with FOO, BAR, undefined and FOO again'

Une autre option, plus proche de ce que vous avez écrit, serait de renvoyer un objet étendu à partir d'une chaîne de caractères, de sorte que le typage soit ouvert et l'interface respectée. Une extension du String.prototype ne fonctionnerait pas car vous auriez besoin de la fermeture de la balise de modèle pour résoudre les paramètres ultérieurement.

class FormatString extends String {
  // Some other custom extensions that don't need the template closure
}

function fmt([fisrt, ...rest], ...tags) {
  const str = new FormatString(rest.reduce((acc, curr, i) => `${acc}\${${tags[i]}}${curr}`, fisrt));
  str.format = values => rest.reduce((acc, curr, i) => {
    return acc + values[tags[i]] + curr;
  }, fisrt);
  return str;
}

Ensuite, dans le site d’appel:

> console.log(fmt`Hello, ${0}. This is a ${1}.`.format(["world", "test"]));
// Hello, world. This is a test.
> template = fmt`Hello, ${'foo'}. This is a ${'bar'}.`
> console.log(template)
// { [String: 'Hello, ${foo}. This is a ${bar}.'] format: [Function] }
> console.log(template.format({ foo: true, bar: null }))
// Hello, true. This is a null.

Vous pouvez vous référer à plus d'informations et applications dans cette autre réponse .

0
Rodrigo Rodrigues

Voici une solution que j'ai proposée avec le constructeur Function. Pas besoin de regex ou eval.

Fonction d'assistance:

const formatMessage = ({ message, values = {} }) => {
  return new Function(...Object.keys(values), `return \`${message}\``)(...Object.values(values));
};

Utilisation:

formatMessage({
  message: "This is a formatted message for ${NAME}.",
  values: { NAME: "Bob" }
});

// Output: This is a formatted message for Bob.

Vous pouvez essayer d'appliquer la même logique dans votre fonction de prototype.

0
Etienne Martin