前言
近日,Mac 下著名軟體 Homebrew 的作者,因為沒解出來二叉樹翻轉的白板演演算法題,慘遭 Google 拒絕,繼而引發推特熱議。
在 JavaScript 中也有很多樹形結構。比如 DOM 樹,省市區地址聯動,檔案目錄等; JSON 本身就是樹形結構。
很多前端面試題也跟樹形結構的有關,比如在瀏覽器端寫遍歷 DOM 樹的函式,比如在 nodejs 執行時遍歷檔案目錄等。
這裡演示用 JavaScript 遍歷樹形結構的幾種策略。
場景1:遍歷 DOM 樹
方案1:遞迴樣式
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:迴圈樣式
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:遞迴
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:迴圈
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
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
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 處理樹形結構的幾種方法,希望看過後對你有幫助。
來自:工業聚(@工業聚)
連結:https://github.com/Lucifier129/Lucifier129.github.io/issues/4
 知識星球
知識星球