Преобразование строки в строку шаблона
можно ли создать строку шаблона как обычную строку
let a="b:${b}";
а затем преобразовать его в строку шаблона
let b=10;
console.log(a.template());//b:10
без eval
,new Function
и другие средства генерации динамического кода?
17 ответов:
В качестве строки шаблона необходимо получить ссылку на
b
переменной динамично (во время выполнения), поэтому ответ: нет, это невозможно сделать без динамической генерации кода.но с
eval
это довольно просто:let tpl = eval('`'+a+'`');
то, что вы просите здесь:
//non working code quoted from the question let b=10; console.log(a.template());//b:10
точно эквивалентно (с точки зрения мощности и, э-э, безопасности)
eval
: возможность взять строку, содержащую код и выполнить этот код, а также возможность для исполняемого кода, чтобы увидеть локальные переменные в среде абонента.в JS функция не может видеть локальные переменные в своем вызывающем объекте, если только эта функция не
eval()
. ДажеFunction()
не могу сделать оно.
когда вы слышите, что в JavaScript есть что-то под названием "строки шаблона", естественно предположить, что это встроенная библиотека шаблонов, например, усы. Это не так. Это в основном просто строку интерполяции и многострочные строки для JS. Я думаю, что это будет распространенное заблуждение на некоторое время, хотя. : (
нет, нет способа сделать это без динамической генерации кода.
тем не менее, я создал функцию, которая превратит обычную строку в функцию, которая может быть предоставлена с картой значений, используя строки шаблона внутри.
Генерировать Строку Шаблона Gist
/** * Produces a function which uses template strings to do simple interpolation from objects. * * Usage: * var makeMeKing = generateTemplateString('${name} is now the king of ${country}!'); * * console.log(makeMeKing({ name: 'Bryan', country: 'Scotland'})); * // Logs 'Bryan is now the king of Scotland!' */ var generateTemplateString = (function(){ var cache = {}; function generateTemplate(template){ var fn = cache[template]; if (!fn){ // Replace ${expressions} (etc) with ${map.expressions}. var sanitized = template .replace(/$\{([\s]*[^;\s\{]+[\s]*)\}/g, function(_, match){ return `$\{map.${match.trim()}\}`; }) // Afterwards, replace anything that's not ${map.expressions}' (etc) with a blank string. .replace(/($\{(?!map\.)[^}]+\})/g, ''); fn = Function('map', `return \`${sanitized}\``); } return fn; } return generateTemplate; })();
использование:
var kingMaker = generateTemplateString('${name} is king!'); console.log(kingMaker({name: 'Bryan'})); // Logs 'Bryan is king!' to the console.
надеюсь, это кому-то поможет. Если вы обнаружите проблему с кодом, пожалуйста, будьте добры, чтобы обновить суть.
в моем проекте я создал что-то вроде этого с ES6:
String.prototype.interpolate = function(params) { const names = Object.keys(params); const vals = Object.values(params); return new Function(...names, `return \`${this}\`;`)(...vals); } const template = 'Example text: ${text}'; const result = template.interpolate({ text: 'Foo Boo' }); console.log(result);
обновление Я удалил зависимость lodash, ES6 имеет эквивалентные методы для получения ключей и значений.
TLDR: https://jsfiddle.net/w3jx07vt/
все, кажется, беспокоятся о доступе к переменным, почему бы просто не передать их? Я уверен, что это не будет слишком трудно, чтобы получить переменный контекст в вызывающем абоненте и передать его вниз. Используйте это https://stackoverflow.com/a/6394168/6563504 чтобы получить реквизит от obj. Я не могу проверить прямо сейчас, но это должно работать.
function renderString(str,obj){ return str.replace(/$\{(.+?)\}/g,(match,p1)=>{return index(obj,p1)}) }
проверено. Вот полный код.
function index(obj,is,value) { if (typeof is == 'string') is=is.split('.'); if (is.length==1 && value!==undefined) return obj[is[0]] = value; else if (is.length==0) return obj; else return index(obj[is[0]],is.slice(1), value); } function renderString(str,obj){ return str.replace(/$\{.+?\}/g,(match)=>{return index(obj,match)}) } renderString('abc${a}asdas',{a:23,b:44}) //abc23asdas renderString('abc${a.c}asdas',{a:{c:22,d:55},b:44}) //abc22asdas
проблема здесь в том, чтобы иметь функцию, которая имеет доступ к переменным, его вызвавшему. Вот почему мы видим прямой
eval
используется для обработки шаблона. Возможным решением было бы создать функцию, берущую формальные параметры, названные свойствами словаря, и вызывающую ее с соответствующими значениями в том же порядке. Альтернативным способом было бы иметь что-то простое, как это:var name = "John Smith"; var message = "Hello, my name is ${name}"; console.log(new Function('return `' + message + '`;')());
и для всех, кто использует компилятор Babel, нам нужно создать закрытие который помнит среду, в которой он был создан:
console.log(new Function('name', 'return `' + message + '`;')(name));
вы можете использовать прототип строки, например
String.prototype.toTemplate=function(){ return eval('`'+this+'`'); } //... var a="b:${b}"; var b=10; console.log(a.toTemplate());//b:10
но ответ на исходный вопрос не является способом.
мне требовался этот метод с поддержкой Internet Explorer. Оказалось, что задние Тики не поддерживаются даже IE11. Также, используя
eval
или его эквивалентFunction
не так.для того, кто замечает; Я также использую обратные ссылки, но эти удаляются компиляторами, такими как babel. Методы, предложенные другими, зависят от них во время выполнения. Как было сказано ранее, это проблема в IE11 и ниже.
так вот что я придумал с:
function get(path, obj, fb = `$\{${path}}`) { return path.split('.').reduce((res, key) => res[key] || fb, obj); } function parseTpl(template, map, fallback) { return template.replace(/$\{.+?}/g, (match) => { const path = match.substr(2, match.length - 3).trim(); return get(path, map, fallback); }); }
пример:
const data = { person: { name: 'John', age: 18 } }; parseTpl('Hi ${person.name} (${person.age})', data); // output: Hi John (18) parseTpl('Hello ${person.name} from ${person.city}', data); // output: Hello John from ${person.city} parseTpl('Hello ${person.name} from ${person.city}', data, '-'); // output: Hello John from -
в настоящее время я не могу комментировать существующие ответы, поэтому я не могу напрямую прокомментировать отличный ответ Брайана Рейнора. Таким образом, этот ответ собирается обновить свой ответ с небольшой поправкой.
короче говоря, его функция не может фактически кэшировать созданную функцию, поэтому она всегда будет воссоздавать, независимо от того, видел ли он шаблон раньше. Вот исправленный код:
/** * Produces a function which uses template strings to do simple interpolation from objects. * * Usage: * var makeMeKing = generateTemplateString('${name} is now the king of ${country}!'); * * console.log(makeMeKing({ name: 'Bryan', country: 'Scotland'})); * // Logs 'Bryan is now the king of Scotland!' */ var generateTemplateString = (function(){ var cache = {}; function generateTemplate(template){ var fn = cache[template]; if (!fn){ // Replace ${expressions} (etc) with ${map.expressions}. var sanitized = template .replace(/$\{([\s]*[^;\s\{]+[\s]*)\}/g, function(_, match){ return `$\{map.${match.trim()}\}`; }) // Afterwards, replace anything that's not ${map.expressions}' (etc) with a blank string. .replace(/($\{(?!map\.)[^}]+\})/g, ''); fn = cache[template] = Function('map', `return \`${sanitized}\``); } return fn; }; return generateTemplate; })();
@Mateusz Moska, решение отлично работает, но когда я использовал его в React Native (режим сборки), он выдает ошибку:недопустимый символ "', хотя он работает, когда я запустить его в режиме отладки.
поэтому я записал свое собственное решение, используя регулярное выражение.
String.prototype.interpolate = function(params) { let template = this for (let key in params) { template = template.replace(new RegExp('\$\{' + key + '\}', 'g'), params[key]) } return template } const template = 'Example text: ${text}', result = template.interpolate({ text: 'Foo Boo' }) console.log(result)
демо: https://es6console.com/j31pqx1p/
Примечание: поскольку я не знаю основной причины проблемы, я поднял билет в react-native repo, https://github.com/facebook/react-native/issues/14107, так что как только они смогут исправить / направить меня примерно так же:)
все еще динамично, но кажется более контролируемым, чем просто использование голого eval:
const vm = require('vm') const moment = require('moment') let template = '### ${context.hours_worked[0].value} \n Hours worked \n #### ${Math.abs(context.hours_worked_avg_diff[0].value)}% ${fns.gt0(context.hours_worked_avg_diff[0].value, "more", "less")} than usual on ${fns.getDOW(new Date())}' let context = { hours_worked:[{value:10}], hours_worked_avg_diff:[{value:10}], } function getDOW(now) { return moment(now).locale('es').format('dddd') } function gt0(_in, tVal, fVal) { return _in >0 ? tVal: fVal } function templateIt(context, template) { const script = new vm.Script('`'+template+'`') return script.runInNewContext({context, fns:{getDOW, gt0 }}) } console.log(templateIt(context, template))
похоже на ответ Даниила (и С. Мейера суть), но более читабельным:
const regex = /${[^{]+}/g; export default function interpolate(template, variables, fallback) { return template.replace(regex, (match) => { const path = match.slice(2, -1).trim(); return getObjPath(path, variables, fallback); }); } //get the specified property or nested property of an object function getObjPath(path, obj, fallback = '') { return path.split('.').reduce((res, key) => res[key] || fallback, obj); }
Примечание: это немного улучшает оригинал s. meijer, так как он не будет соответствовать таким вещам, как
${foo{bar}
(регулярное выражение допускает только символы без фигурных скобок внутри${
и}
).
обновление: меня попросили пример использования этого, так что здесь вы идете:
const replacements = { name: 'Bob', age: 37 } interpolate('My name is ${name}, and I am ${age}.', replacements)
Мне понравился ответ С. Мейера и написал свою версию на его основе:
function parseTemplate(template, map, fallback) { return template.replace(/$\{[^}]+\}/g, (match) => match .slice(2, -1) .trim() .split(".") .reduce( (searchObject, key) => searchObject[key] || fallback || match, map ) ); }
Это решение работает без ES6:
function render(template, opts) { return new Function( 'return new Function (' + Object.keys(opts).reduce((args, arg) => args += '\'' + arg + '\',', '') + '\'return `' + template.replace(/(^|[^\])'/g, '\\'') + '`;\'' + ').apply(null, ' + JSON.stringify(Object.keys(opts).reduce((vals, key) => vals.push(opts[key]) && vals, [])) + ');' )(); } render("hello ${ name }", {name:'mo'}); // "hello mo"
Примечание:
Function
конструктор всегда создается в глобальной области видимости, что потенциально может привести к перезаписи глобальных переменных шаблоном, напримерrender("hello ${ someGlobalVar = 'some new value' }", {name:'mo'});
так как мы изобретаем колесо на что-то, что было бы прекрасной функцией в javascript.
Я использую
eval()
, который не является безопасным, но javascript не является безопасным. Я с готовностью признаю, что я не очень хорошо разбираюсь в javascript, но у меня была потребность, и мне нужен был ответ, поэтому я его сделал.я решил стилизовать свои переменные с помощью
@
, а не$
, особенно потому, что я хочу использовать многострочную функцию литералов без оценка пока он не будет готов. Таким образом, синтаксис переменной@{OptionalObject.OptionalObjectN.VARIABLE_NAME}
Я не специалист по javascript, поэтому я бы с удовольствием взял совет по улучшению, но...
var prsLiteral, prsRegex = /\@\{(.*?)(?!\@\{)\}/g for(i = 0; i < myResultSet.length; i++) { prsLiteral = rt.replace(prsRegex,function (match,varname) { return eval(varname + "[" + i + "]"); // you could instead use return eval(varname) if you're not looping. }) console.log(prsLiteral); }
очень простая реализация следующим образом
myResultSet = {totalrecords: 2, Name: ["Bob", "Stephanie"], Age: [37,22]}; rt = `My name is @{myResultSet.Name}, and I am @{myResultSet.Age}.` var prsLiteral, prsRegex = /\@\{(.*?)(?!\@\{)\}/g for(i = 0; i < myResultSet.totalrecords; i++) { prsLiteral = rt.replace(prsRegex,function (match,varname) { return eval(varname + "[" + i + "]"); // you could instead use return eval(varname) if you're not looping. }) console.log(prsLiteral); }
в моей фактической реализации, я решил использовать
@{{variable}}
. Еще один набор скобок. Абсурдно маловероятно столкнуться с этим неожиданно. Регулярное выражение для этого будет выглядеть как/\@\{\{(.*?)(?!\@\{\{)\}\}/g
чтобы сделать это легче читайте
\@\{\{ # opening sequence, @{{ literally. (.*?) # capturing the variable name # ^ captures only until it reaches the closing sequence (?! # negative lookahead, making sure the following # ^ pattern is not found ahead of the current character \@\{\{ # same as opening sequence, if you change that, change this ) \}\} # closing sequence.
если у вас нет опыта работы с регулярным выражением, довольно безопасное правило-избегать каждого не буквенно-цифрового символа и не когда-нибудь напрасно убегают буквы, так как многие уцелевшие буквы имеют особое значение практически для всех вкусов регулярных выражений.
вы должны попробовать этот крошечный модуль JS, Андреа Giammarchi, от github : https://github.com/WebReflection/backtick-template
/*! (C) 2017 Andrea Giammarchi - MIT Style License */ function template(fn, $str, $object) {'use strict'; var stringify = JSON.stringify, hasTransformer = typeof fn === 'function', str = hasTransformer ? $str : fn, object = hasTransformer ? $object : $str, i = 0, length = str.length, strings = i < length ? [] : ['""'], values = hasTransformer ? [] : strings, open, close, counter ; while (i < length) { open = str.indexOf('${', i); if (-1 < open) { strings.push(stringify(str.slice(i, open))); open += 2; close = open; counter = 1; while (close < length) { switch (str.charAt(close++)) { case '}': counter -= 1; break; case '{': counter += 1; break; } if (counter < 1) { values.push('(' + str.slice(open, close - 1) + ')'); break; } } i = close; } else { strings.push(stringify(str.slice(i))); i = length; } } if (hasTransformer) { str = 'function' + (Math.random() * 1e5 | 0); if (strings.length === values.length) strings.push('""'); strings = [ str, 'with(this)return ' + str + '([' + strings + ']' + ( values.length ? (',' + values.join(',')) : '' ) + ')' ]; } else { strings = ['with(this)return ' + strings.join('+')]; } return Function.apply(null, strings).apply( object, hasTransformer ? [fn] : [] ); } template.asMethod = function (fn, object) {'use strict'; return typeof fn === 'function' ? template(fn, this, object) : template(this, fn); };
демо (все следующие тесты возвращают true):
const info = 'template'; // just string `some ${info}` === template('some ${info}', {info}); // passing through a transformer transform `some ${info}` === template(transform, 'some ${info}', {info}); // using it as String method String.prototype.template = template.asMethod; `some ${info}` === 'some ${info}'.template({info}); transform `some ${info}` === 'some ${info}'.template(transform, {info});
Иногда самая простая вещь работает лучше всего. Просто оберните шаблон в функцию, которая возвращает оценку. Вы по-прежнему получаете все преимущества шаблонных литералов без регулярных выражений и дополнительных функций:
const formatter = (options) => `${options.title}: ${options.subtitle}` formatter({title: 'Hey There', subtitle: 'Return of the Sequel'}); // Produces 'Hey There: Return of the Sequel'
все обычные предостережения о шаблонных литералах все еще применяются:https://caniuse.com/#feat=template-literals