前言
近日,Mac 下著名軟體 Homebrew 的作者,因為沒解出來二叉樹翻轉的白板演算法題,慘遭 Google 拒絕,繼而引發推特熱議。
在 JavaScript 中也有很多樹形結構。比如 DOM 樹,省市區地址聯動,檔案目錄等; JSON 本身就是樹形結構。
很多前端面試題也跟樹形結構的有關,比如在瀏覽器端寫遍歷 DOM 樹的函式,比如在 nodejs 執行時遍歷檔案目錄等。
這裡演示用 JavaScript 遍歷樹形結構的幾種策略。
場景1:遍歷 DOM 樹
方案1:遞迴模式
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function walkDom(node, callback) { if (node === null) { //判斷node是否為null return } callback(node) //將node自身傳入callback node = node.firstElementChild //改變node為其子元素節點 while (node) { walkDom(node, callback) //如果存在子元素,則遞迴呼叫walkDom node = node.nextElementSibling //從頭到尾遍歷元素節點 } } walkDom(document, function(node) {console.count()}) //包含document節點 document.querySelectorAll('*').length //數量比上面輸出的少1,因為不包含document節點 |
將上述程式碼黏貼到任意頁面的控制檯 console 中執行。
方案2:迴圈模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function walkDom(node, callback) { if (node === null) { return } var stack = [node] //存入陣列 var target while(stack.length) { //陣列長度不為0,繼續迴圈 target = stack.shift() //取出元素 callback(target) //傳入callback Array.prototype.push.apply(stack, target.children) //將其子元素一股腦推入stack,增加長度 } } walkDom(document, function(node) {console.count()}) //包含document節點 document.querySelectorAll('*').length //數量比上面輸出的少1,因為不包含document節點 |
在迴圈模式中,shift方法可以換成pop,從尾部取出元素;push方法可以換成unshift從頭部新增元素。不同的順序,影響了是「廣度優先」還是「深度優先」。
場景2:在 nodejs 執行時裡遍歷檔案目錄
子場景1:同步模式
方案1:遞迴
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
var fs = require('fs') var Path = require('path') function readdirs(path) { var result = { //構造資料夾資料 path: path, name: Path.basename(path), type: 'directory' } var files = fs.readdirSync(path) //拿到檔案目錄下的所有檔名 result.children = files.map(function(file) { var subPath = Path.resolve(path, file) //拼接為絕對路徑 var stats = fs.statSync(subPath) //拿到檔案資訊物件 if (stats.isDirectory()) { //判斷是否為資料夾型別 return readdirs(subPath) //遞迴讀取資料夾 } return { //構造檔案資料 path: subPath, name: file, type: 'file' } }) return result //返回資料 } var cwd = process.cwd() var tree = readdirs(cwd) fs.writeFileSync(Path.join(cwd, 'tree.json'), JSON.stringify(tree)) //儲存在tree.json中,去檢視吧 |
將上面的程式碼儲存在 tree.js 中,然後在當前資料夾開啟命令列,輸入node tree.js,目錄資訊儲存在生成tree.json檔案中。
方案2:迴圈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
var fs = require('fs') var Path = require('path') function readdirs(path) { var result = { //構造資料夾資料 path: path, name: Path.basename(path), type: 'directory' } var stack = [result] //生成一個棧陣列 while (stack.length) { //如果陣列不為空,讀取children var target = stack.pop() //取出資料夾物件 var files = fs.readdirSync(target.path) //拿到檔名陣列 target.children = files.map(function(file) { var subPath = Path.resolve(target.path, file) //轉化為絕對路徑 var stats = fs.statSync(subPath) //拿到檔案資訊物件 var model = { //構造檔案資料結構 path: subPath, name: file, type: stats.isDirectory() ? 'directory' : 'file' } if (model.type === 'directory') { stack.push(model) //如果是資料夾,推入棧 } return model //返回資料模型 }) } return result //返回整個資料結果 } var cwd = process.cwd() var tree = readdirs(cwd) fs.writeFileSync(Path.join(cwd, 'tree.json'), JSON.stringify(tree)) //儲存在tree.json中,去檢視吧 |
迴圈策略中的pop跟shift,push跟unshift也可以互換以調整優先順序,甚至用可以用splice方法更精細的控制stack陣列。迴圈模式比遞迴模式更可控。
子場景2:非同步模式
方案1:過程式 Promise
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
var fs = require('fs') var Path = require('path') //promise包裝的fs.stat方法 var stat = function(path) { return new Promise(function(resolve, reject) { fs.stat(path, function(err, stats) { err ? reject(err) : resolve(stats) }) }) } //promise包裝的fs.readdir方法 var readdir = function(path) { return new Promise(function(resolve, reject) { fs.readdir(path, function(err, files) { err ? reject(err) : resolve(files) }) }) } //promise包裝的fs.writeFile var writeFile = function(path, data) { return new Promise(function(resolve, reject) { fs.writeFile(path, JSON.stringify(data || ''), function(err) { err ? reject(err) : resolve }) }) } function readdirs(path) { return readdir(path) //非同步讀取資料夾 .then(function(files) { //拿到檔名列表 var promiseList = files.map(function(file) { //遍歷列表 var subPath = Path.resolve(path, file) //拼接為絕對路徑 return stat(subPath) //非同步讀取檔案資訊 .then(function(stats) { //拿到檔案資訊 //是資料夾型別的,繼續讀取目錄,否則返回資料 return stats.isDirectory() ? readdirs(subPath) : { path: subPath, name: file, type: 'file' } }) }) return Promise.all(promiseList) //等待所有promise完成 }) .then(function(children) { //拿到包含所有資料的children陣列 return { //返回結果 path: path, name: Path.basename(path), type: 'directory', children: children } }) } var cwd = process.cwd() readdirs(cwd) .then(writeFile.bind(null, Path.join(cwd, 'tree.json'))) //儲存在tree.json中,去檢視吧 .catch(console.error.bind(console)) //出錯了就輸出錯誤日誌檢視原因 |
上面的函式都能工作,但都是一個個function的呼叫,顯得太「過程式」;
能不能用物件導向的方式來寫呢?
當然可以。
其實物件導向的寫法,更清晰。
為了更加語義化,以及增顯逼格。
我們用 ES6 的 class 來寫這個樹形結構類。
方案2:ES6-class + ES6-Promise
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
import fs from 'fs' import {join, resolve, isAbsolute, basename, extname, dirname, sep} from 'path' /** * 獲取目錄下的所有檔案 * @param {string} path * @return {promise} resolve files || reject error */ let readdir = (path) => { return new Promise((resolve, reject) => { fs.readdir(path, (err, files) => { err ? reject(err) : resolve(files) }) }) } /** * 將data寫入檔案 * @param {string} path 路徑 * @param {data} data * @return {promise} resolve path || reject error */ let writeFile = (path, data) => { return new Promise((resolve, reject) => { fs.writeFile(path, data, (err) => { err ? reject(err) : resolve(path) }) }) } /** * 獲取檔案屬性 * @param {string} path * @return {promise} resolve stats || reject error */ let stat = (path) => { return new Promise((resolve, reject) => { fs.stat(path, (err, stats) => { err ? reject(err) : resolve(stats) }) }) } /** * 判斷path是否存在 * @param {string} path 路徑 * @return {promise} resolve exists */ let exists = (path) => { return new Promise((resolve) => fs.exists(path, resolve)) } //文件類 class Document { constructor(path) { this.path = path this.name = basename(path) } //存在性判斷 exists() { return exists(this.path) } //非同步獲取檔案資訊 stat() { return stat(this.path) } //輸出基本資料 json() { return JSON.stringify(this) } //將基本資料儲存在path路徑的檔案中 saveTo(path) { if (isAbsolute(path)) { return writeFile(path, this.json()) } return writeFile(resolve(this.path, path), this.json()) } } //檔案類,繼承自文件類 class File extends Document { constructor(path) { super(path) //必須先呼叫超類構造方法 this.type = 'file' //type 為 file this.extname = extname(path) //新增副檔名 } //寫入資料 write(data = '') { return writeFile(this.path, data) } //其他檔案特有方法如 read unlink 等 } //資料夾類,繼承自文件類 class Directory extends Document { constructor(path) { super(path) //必須先呼叫超類構造方法 this.type = 'directory' } //讀取當前資料夾 readdir() { return readdir(this.path) //讀取目錄 .then((files) => { //拿到檔名列表 let promiseList = files.map((file) => { let subPath = resolve(this.path, file) //拼接為絕對路徑 return stat(subPath) //獲取檔案資訊 .then((stats) => { //根據檔案資訊,歸類為Directory或File型別 return stats.isDirectory() ? new Directory(subPath) : new File(subPath) }) }) return Promise.all(promiseList) }) .then((children) => { //拿到children陣列 this.children = children //儲存children屬性 return children //返回children }) } //深度讀取檔案目錄 readdirs() { return this.readdir() //讀取當前資料夾 .then((children) => { //拿到children let promiseList = [] children.map((child) => { if (child instanceof Directory) { //是資料夾例項,繼續深度讀取檔案目錄 promiseList.push(child.readdirs()) } }) return Promise.all(promiseList) //等待所有子元素深度讀取目錄完畢 }) .then(() => this) //返回this } //其他資料夾特有方法如 addFile removeFile addDir remveDir 等 } let cwd = process.cwd() new Directory(cwd) .readdirs() .then((tree) => { tree.saveTo('tree.json') //讓它自己儲存在tree.json裡 }) .catch(console.error.bind(console)) //輸出錯誤日誌 |
因為當前 JavaScript 引擎對 ES6 的支援度還不夠,所以上述程式碼不能直接執行。可以通過以下兩種方式來驗證程式碼能不能跑起來。
第一種,先 npm install -g bable 全域性安裝 babel 工具,再以 babel-node tree.js的方式取代 node tree.js 來執行上述程式碼。
第二種,將上述程式碼黏貼到 https://babeljs.io/repl/,拿到編譯為 ES5 的程式碼,將程式碼儲存在 tree.js檔案中,以 ES5 的形式執行。
結語
以上就是我知道的一些用 JavaScript 處理樹形結構的幾種方法,希望看過後對你有幫助。