Вот разложенный подход к проблеме с использованием функциональных приемов.
Сначала реализуйте общую функцию files
-
const emptyTree =
{ dir: "", files: [] }
const files = (tree = emptyTree, path = "") =>
Object(tree) === tree
? tree.files.flatMap(f => files(f, `${path}/${tree.dir}`))
: [ `${path}/${tree}` ]
files(fileData)
// [ "/app/index.html"
// , "/app/js/main.js"
// , "/app/js/app.js"
// , "/app/js/misc.js"
// , "/app/js/vendor/jquery.js"
// , "/app/js/vendor/underscore.js"
// , "/app/css/reset.css"
// , "/app/css/main.css"
// ]
Затем реализуйте search
как функцию более высокого порядка, например Array.prototype.filter
-
const identity = x =>
x
const search = (test = identity, tree = emptyTree) =>
files(tree).filter(test)
Наконец, мы можем использовать search
интуитивно понятным способом -
search(f => f.endsWith(".js"), fileData)
// [ "/app/js/main.js"
// , "/app/js/app.js"
// , "/app/js/misc.js"
// , "/app/js/vendor/jquery.js"
// , "/app/js/vendor/underscore.js"
// ]
search(f => f.startsWith("/app/css"), fileData)
// [ "/app/css/reset.css"
// , "/app/css/main.css"
// ]
Разделив сложную проблему на изолированные части, каждую функцию проще писать, тестировать и обслуживать. Дополнительным преимуществом является то, что files
и search
предлагают даже больше функциональных возможностей, чем ваша первоначальная функция, и их можно легче повторно использовать в других областях вашей программы. Надеемся, что это демонстрирует, как функции более высокого порядка дают вам повышенную гибкость и с меньшим количеством кода тоже.
Запустите полную программу в своем браузере, расширив фрагмент кода ниже -
const fileData =
{ dir: 'app', files: [ 'index.html', { dir: 'js', files: [ 'main.js','app.js','misc.js', { dir: 'vendor', files: [ 'jquery.js','underscore.js' ] } ] }, { dir: 'css', files: [ 'reset.css','main.css' ] } ] }
const emptyTree =
{ dir: "", files: [] }
const files = (tree = emptyTree, path = "") =>
Object(tree) === tree
? tree.files.flatMap(f => files(f, `${path}/${tree.dir}`))
: [ `${path}/${tree}` ]
console.log(files(fileData))
// [ "/app/index.html"
// , "/app/js/main.js"
// , "/app/js/app.js"
// , "/app/js/misc.js"
// , "/app/js/vendor/jquery.js"
// , "/app/js/vendor/underscore.js"
// , "/app/css/reset.css"
// , "/app/css/main.css"
// ]
const identity = x =>
x
const search = (test = identity, tree = emptyTree) =>
files(tree).filter(test)
console.log(search(f => f.endsWith(".js"), fileData))
// [ "/app/js/main.js"
// , "/app/js/app.js"
// , "/app/js/misc.js"
// , "/app/js/vendor/jquery.js"
// , "/app/js/vendor/underscore.js"
// ]
console.log(search(f => f.startsWith("/app/css"), fileData))
// [ "/app/css/reset.css"
// , "/app/css/main.css"
// ]
Другой подход заключается в использовании генераторов. Обратите внимание на сходство между этой и предыдущей программами -
const files = function* (tree = emptyTree, path = "")
{ if (Object(tree) === tree)
for (const f of tree.files)
yield* files(f, `${path}/${tree.dir}`)
else
yield `${path}/${tree}`
}
const search = function* (test = identity, tree = emptyTree)
{ for (const f of files(tree))
if (test(f))
yield f
}
Теперь files
и search
возвращают ленивый результат, где результаты могут обрабатываться один за другим, когда они выходят из генератора. Или мы можем собрать все результаты, используя Array.from
. Результаты одинаковы -
Array.from(search(f => f.endsWith(".js"), fileData))
// [ "/app/js/main.js"
// , "/app/js/app.js"
// , "/app/js/misc.js"
// , "/app/js/vendor/jquery.js"
// , "/app/js/vendor/underscore.js"
// ]
Array.from(search(f => f.startsWith("/app/css"), fileData))
// [ "/app/css/reset.css"
// , "/app/css/main.css"
// ]
Поскольку генераторы могут приостанавливать и возобновлять работу, эта программа имеет дополнительное преимущество, заключающееся в том, что вычисления могут быть остановлены на ранней стадии. Напротив, первая программа, использующая Array.prototype.flatMap
и Array.prototype.filter
, всегда будет перебирать все tree
.
Разверните фрагмент ниже, чтобы проверить результаты в вашем собственном браузере
const fileData =
{ dir: 'app', files: [ 'index.html', { dir: 'js', files: [ 'main.js','app.js','misc.js', { dir: 'vendor', files: [ 'jquery.js','underscore.js' ] } ] }, { dir: 'css', files: [ 'reset.css','main.css' ] } ] }
const emptyTree =
{ dir: "", files: [] }
const files = function* (tree = emptyTree, path = "")
{ if (Object(tree) === tree)
for (const f of tree.files)
yield* files(f, `${path}/${tree.dir}`)
else
yield `${path}/${tree}`
}
console.log(Array.from(files(fileData)))
// [ "/app/index.html"
// , "/app/js/main.js"
// , "/app/js/app.js"
// , "/app/js/misc.js"
// , "/app/js/vendor/jquery.js"
// , "/app/js/vendor/underscore.js"
// , "/app/css/reset.css"
// , "/app/css/main.css"
// ]
const identity = x =>
x
const search = function* (test = identity, tree = emptyTree)
{ for (const f of files(tree))
if (test(f))
yield f
}
console.log(Array.from(search(f => f.endsWith(".js"), fileData)))
// [ "/app/js/main.js"
// , "/app/js/app.js"
// , "/app/js/misc.js"
// , "/app/js/vendor/jquery.js"
// , "/app/js/vendor/underscore.js"
// ]
console.log(Array.from(search(f => f.startsWith("/app/css"), fileData)))
// [ "/app/css/reset.css"
// , "/app/css/main.css"
// ]