摘要:本文主要介紹解析生成的抽象語法樹節(jié)點(diǎn),的實(shí)現(xiàn)也是基于的。原文地址解析器是把源碼轉(zhuǎn)化為抽象語法樹的解析器。參考文獻(xiàn)前端進(jìn)階之抽象語法樹抽象語法樹
前言
Babel為當(dāng)前最流行的代碼JavaScript編譯器了,其使用的JavaScript解析器為babel-parser,最初是從Acorn 項(xiàng)目fork出來的。Acorn 非常快,易于使用,并且針對(duì)非標(biāo)準(zhǔn)特性(以及那些未來的標(biāo)準(zhǔn)特性) 設(shè)計(jì)了一個(gè)基于插件的架構(gòu)。本文主要介紹esprima解析生成的抽象語法樹節(jié)點(diǎn),esprima的實(shí)現(xiàn)也是基于Acorn的。
原文地址
解析器 ParserJavaScript Parser 是把js源碼轉(zhuǎn)化為抽象語法樹(AST)的解析器。這個(gè)步驟分為兩個(gè)階段:詞法分析(Lexical Analysis) 和 語法分析(Syntactic Analysis)。
常用的JavaScript Parser:
esprima
uglifyJS2
traceur
acorn
espree
@babel/parser
詞法分析詞法分析階段把字符串形式的代碼轉(zhuǎn)換為 令牌(tokens)流。你可以把令牌看作是一個(gè)扁平的語法片段數(shù)組。
n * n;
例如上面n*n的詞法分析得到結(jié)果如下:
[ { type: { ... }, value: "n", start: 0, end: 1, loc: { ... } }, { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } }, { type: { ... }, value: "n", start: 4, end: 5, loc: { ... } }, ]
每一個(gè) type 有一組屬性來描述該令牌:
{ type: { label: "name", keyword: undefined, beforeExpr: false, startsExpr: true, rightAssociative: false, isLoop: false, isAssign: false, prefix: false, postfix: false, binop: null, updateContext: null }, ... }
和 AST 節(jié)點(diǎn)一樣它們也有 start,end,loc 屬性。
語法分析語法分析就是根據(jù)詞法分析的結(jié)果,也就是令牌tokens,將其轉(zhuǎn)換成AST。
function square(n) { return n * n; }
如上面代碼,生成的AST結(jié)構(gòu)如下:
{ type: "FunctionDeclaration", id: { type: "Identifier", name: "square" }, params: [{ type: "Identifier", name: "n" }], body: { type: "BlockStatement", body: [{ type: "ReturnStatement", argument: { type: "BinaryExpression", operator: "*", left: { type: "Identifier", name: "n" }, right: { type: "Identifier", name: "n" } } }] } }
下文將對(duì)AST各個(gè)類型節(jié)點(diǎn)做解釋。更多AST生成,入口如下:
eslint
AST Explorer
esprima
結(jié)合可視化工具,舉個(gè)例子
如下代碼:
var a = 42; var b = 5; function addA(d) { return a + d; } var c = addA(2) + b;
第一步詞法分析之后長成如下圖所示:
語法分析,生產(chǎn)抽象語法樹,生成的抽象語法樹如下圖所示
Base Node所有節(jié)點(diǎn)類型都實(shí)現(xiàn)以下接口:
interface Node { type: string; range?: [number, number]; loc?: SourceLocation; }
該type字段是表示AST變體類型的字符串。該loc字段表示節(jié)點(diǎn)的源位置信息。如果解析器沒有生成有關(guān)節(jié)點(diǎn)源位置的信息,則該字段為null;否則它是一個(gè)對(duì)象,包括一個(gè)起始位置(被解析的源區(qū)域的第一個(gè)字符的位置)和一個(gè)結(jié)束位置.
interface SourceLocation { start: Position; end: Position; source?: string | null; }
每個(gè)Position對(duì)象由一個(gè)line數(shù)字(1索引)和一個(gè)column數(shù)字(0索引)組成:
interface Position { line: uint32 >= 1; column: uint32 >= 0; }Programs
interface Program <: Node { type: "Program"; sourceType: "script" | "module"; body: StatementListItem[] | ModuleItem[]; }
表示一個(gè)完整的源代碼樹。
Scripts and Modules源代碼數(shù)的來源包括兩種,一種是script腳本,一種是modules模塊
當(dāng)為script時(shí),body為StatementListItem。
當(dāng)為modules時(shí),body為ModuleItem。
類型StatementListItem和ModuleItem類型如下。
type StatementListItem = Declaration | Statement; type ModuleItem = ImportDeclaration | ExportDeclaration | StatementListItem;ImportDeclaration
import語法,導(dǎo)入模塊
type ImportDeclaration { type: "ImportDeclaration"; specifiers: ImportSpecifier[]; source: Literal; }
ImportSpecifier類型如下:
interface ImportSpecifier { type: "ImportSpecifier" | "ImportDefaultSpecifier" | "ImportNamespaceSpecifier"; local: Identifier; imported?: Identifier; }
ImportSpecifier語法如下:
import { foo } from "./foo";
ImportDefaultSpecifier語法如下:
import foo from "./foo";
ImportNamespaceSpecifier語法如下
import * as foo from "./foo";ExportDeclaration
export類型如下
type ExportDeclaration = ExportAllDeclaration | ExportDefaultDeclaration | ExportNamedDeclaration;
ExportAllDeclaration從指定模塊中導(dǎo)出
interface ExportAllDeclaration { type: "ExportAllDeclaration"; source: Literal; }
語法如下:
export * from "./foo";
ExportDefaultDeclaration導(dǎo)出默認(rèn)模塊
interface ExportDefaultDeclaration { type: "ExportDefaultDeclaration"; declaration: Identifier | BindingPattern | ClassDeclaration | Expression | FunctionDeclaration; }
語法如下:
export default "foo";
ExportNamedDeclaration導(dǎo)出部分模塊
interface ExportNamedDeclaration { type: "ExportNamedDeclaration"; declaration: ClassDeclaration | FunctionDeclaration | VariableDeclaration; specifiers: ExportSpecifier[]; source: Literal; }
語法如下:
export const foo = "foo";Declarations and Statements
declaration,即聲明,類型如下:
type Declaration = VariableDeclaration | FunctionDeclaration | ClassDeclaration;
statements,即語句,類型如下:
type Statement = BlockStatement | BreakStatement | ContinueStatement | DebuggerStatement | DoWhileStatement | EmptyStatement | ExpressionStatement | ForStatement | ForInStatement | ForOfStatement | FunctionDeclaration | IfStatement | LabeledStatement | ReturnStatement | SwitchStatement | ThrowStatement | TryStatement | VariableDeclaration | WhileStatement | WithStatement;VariableDeclarator
變量聲明,kind 屬性表示是什么類型的聲明,因?yàn)?ES6 引入了 const/let。
interface VariableDeclaration <: Declaration { type: "VariableDeclaration"; declarations: [ VariableDeclarator ]; kind: "var" | "let" | "const"; }FunctionDeclaration
函數(shù)聲明(非函數(shù)表達(dá)式)
interface FunctionDeclaration { type: "FunctionDeclaration"; id: Identifier | null; params: FunctionParameter[]; body: BlockStatement; generator: boolean; async: boolean; expression: false; }
例如:
function foo() {} function *bar() { yield "44"; } async function noop() { await new Promise(function(resolve, reject) { resolve("55"); }) }ClassDeclaration
類聲明(非類表達(dá)式)
interface ClassDeclaration { type: "ClassDeclaration"; id: Identifier | null; superClass: Identifier | null; body: ClassBody; }
ClassBody聲明如下:
interface ClassBody { type: "ClassBody"; body: MethodDefinition[]; }
MethodDefinition表示方法聲明;
interface MethodDefinition { type: "MethodDefinition"; key: Expression | null; computed: boolean; value: FunctionExpression | null; kind: "method" | "constructor"; static: boolean; }
class foo { constructor() {} method() {} };ContinueStatement
continue語句
interface ContinueStatement { type: "ContinueStatement"; label: Identifier | null; }
例如:
for (var i = 0; i < 10; i++) { if (i === 0) { continue; } }DebuggerStatement
debugger語句
interface DebuggerStatement { type: "DebuggerStatement"; }
例如
while(true) { debugger; }DoWhileStatement
do-while語句
interface DoWhileStatement { type: "DoWhileStatement"; body: Statement; test: Expression; }
test表示while條件
例如:
var i = 0; do { i++; } while(i = 2)EmptyStatement
空語句
interface EmptyStatement { type: "EmptyStatement"; }
例如:
if(true); var a = []; for(i = 0; i < a.length; a[i++] = 0);ExpressionStatement
表達(dá)式語句,即,由單個(gè)表達(dá)式組成的語句。
interface ExpressionStatement { type: "ExpressionStatement"; expression: Expression; directive?: string; }
當(dāng)表達(dá)式語句表示一個(gè)指令(例如“use strict”)時(shí),directive屬性將包含該指令字符串。
例如:
(function(){});ForStatement
for語句
interface ForStatement { type: "ForStatement"; init: Expression | VariableDeclaration | null; test: Expression | null; update: Expression | null; body: Statement; }ForInStatement
for...in語句
interface ForInStatement { type: "ForInStatement"; left: Expression; right: Expression; body: Statement; each: false; }ForOfStatement
for...of語句
interface ForOfStatement { type: "ForOfStatement"; left: Expression; right: Expression; body: Statement; }IfStatement
if 語句
interface IfStatement { type: "IfStatement"; test: Expression; consequent: Statement; alternate?: Statement; }
consequent表示if命中后內(nèi)容,alternate表示else或者else if的內(nèi)容。
LabeledStatementlabel語句,多用于精確的使用嵌套循環(huán)中的continue和break。
interface LabeledStatement { type: "LabeledStatement"; label: Identifier; body: Statement; }
如:
var num = 0; outPoint: for (var i = 0 ; i < 10 ; i++){ for (var j = 0 ; j < 10 ; j++){ if( i == 5 && j == 5 ){ break outPoint; } num++; } }ReturnStatement
return 語句
interface ReturnStatement { type: "ReturnStatement"; argument: Expression | null; }SwitchStatement
Switch語句
interface SwitchStatement { type: "SwitchStatement"; discriminant: Expression; cases: SwitchCase[]; }
discriminant表示switch的變量。
SwitchCase類型如下
interface SwitchCase { type: "SwitchCase"; test: Expression | null; consequent: Statement[]; }ThrowStatement
throw語句
interface ThrowStatement { type: "ThrowStatement"; argument: Expression; }TryStatement
try...catch語句
interface TryStatement { type: "TryStatement"; block: BlockStatement; handler: CatchClause | null; finalizer: BlockStatement | null; }
handler為catch處理聲明內(nèi)容,finalizer為finally內(nèi)容。
CatchClaus 類型如下
interface CatchClause { type: "CatchClause"; param: Identifier | BindingPattern; body: BlockStatement; }
例如:
try { foo(); } catch (e) { console.erroe(e); } finally { bar(); }WhileStatement
while語句
interface WhileStatement { type: "WhileStatement"; test: Expression; body: Statement; }
test為判定表達(dá)式
WithStatementwith語句(指定塊語句的作用域的作用域)
interface WithStatement { type: "WithStatement"; object: Expression; body: Statement; }
如:
var a = {}; with(a) { name = "xiao.ming"; } console.log(a); // {name: "xiao.ming"}Expressions and Patterns
Expressions可用類型如下:
type Expression = ThisExpression | Identifier | Literal | ArrayExpression | ObjectExpression | FunctionExpression | ArrowFunctionExpression | ClassExpression | TaggedTemplateExpression | MemberExpression | Super | MetaProperty | NewExpression | CallExpression | UpdateExpression | AwaitExpression | UnaryExpression | BinaryExpression | LogicalExpression | ConditionalExpression | YieldExpression | AssignmentExpression | SequenceExpression;
Patterns可用有兩種類型,函數(shù)模式和對(duì)象模式如下:
type BindingPattern = ArrayPattern | ObjectPattern;ThisExpression
this 表達(dá)式
interface ThisExpression { type: "ThisExpression"; }Identifier
標(biāo)識(shí)符,就是我們寫 JS 時(shí)自定義的名稱,如變量名,函數(shù)名,屬性名,都?xì)w為標(biāo)識(shí)符。相應(yīng)的接口是這樣的:
interface Identifier { type: "Identifier"; name: string; }Literal
字面量,這里不是指 [] 或者 {} 這些,而是本身語義就代表了一個(gè)值的字面量,如 1,“hello”, true 這些,還有正則表達(dá)式(有一個(gè)擴(kuò)展的 Node 來表示正則表達(dá)式),如 /d?/。
interface Literal { type: "Literal"; value: boolean | number | string | RegExp | null; raw: string; regex?: { pattern: string, flags: string }; }
例如:
var a = 1; var b = "b"; var c = false; var d = /d/;ArrayExpression
數(shù)組表達(dá)式
interface ArrayExpression { type: "ArrayExpression"; elements: ArrayExpressionElement[]; }
例:
[1, 2, 3, 4];ArrayExpressionElement
數(shù)組表達(dá)式的節(jié)點(diǎn),類型如下
type ArrayExpressionElement = Expression | SpreadElement;
Expression包含所有表達(dá)式,SpreadElement為擴(kuò)展運(yùn)算符語法。
SpreadElement擴(kuò)展運(yùn)算符
interface SpreadElement { type: "SpreadElement"; argument: Expression; }
如:
var a = [3, 4]; var b = [1, 2, ...a]; var c = {foo: 1}; var b = {bar: 2, ...c};ObjectExpression
對(duì)象表達(dá)式
interface ObjectExpression { type: "ObjectExpression"; properties: Property[]; }
Property代表為對(duì)象的屬性描述
類型如下
interface Property { type: "Property"; key: Expression; computed: boolean; value: Expression | null; kind: "get" | "set" | "init"; method: false; shorthand: boolean; }
kind用來表示是普通的初始化,或者是 get/set。
例如:
var obj = { foo: "foo", bar: function() {}, noop() {}, // method 為 true ["computed"]: "computed" // computed 為 true }FunctionExpression
函數(shù)表達(dá)式
interface FunctionExpression { type: "FunctionExpression"; id: Identifier | null; params: FunctionParameter[]; body: BlockStatement; generator: boolean; async: boolean; expression: boolean; }
例如:
var foo = function () {}ArrowFunctionExpression
箭頭函數(shù)表達(dá)式
interface ArrowFunctionExpression { type: "ArrowFunctionExpression"; id: Identifier | null; params: FunctionParameter[]; body: BlockStatement | Expression; generator: boolean; async: boolean; expression: false; }
generator表示是否為generator函數(shù),async表示是否為async/await函數(shù),params為參數(shù)定義。
FunctionParameter類型如下
type FunctionParameter = AssignmentPattern | Identifier | BindingPattern;
例:
var foo = () => {};ClassExpression
類表達(dá)式
interface ClassExpression { type: "ClassExpression"; id: Identifier | null; superClass: Identifier | null; body: ClassBody; }
例如:
var foo = class { constructor() {} method() {} };TaggedTemplateExpression
標(biāo)記模板文字函數(shù)
interface TaggedTemplateExpression { type: "TaggedTemplateExpression"; readonly tag: Expression; readonly quasi: TemplateLiteral; }
TemplateLiteral類型如下
interface TemplateLiteral { type: "TemplateLiteral"; quasis: TemplateElement[]; expressions: Expression[]; }
TemplateElement類型如下
interface TemplateElement { type: "TemplateElement"; value: { cooked: string; raw: string }; tail: boolean; }
例如
var foo = function(a){ console.log(a); } foo`test`;MemberExpression
屬性成員表達(dá)式
interface MemberExpression { type: "MemberExpression"; computed: boolean; object: Expression; property: Expression; }
例如:
const foo = {bar: "bar"}; foo.bar; foo["bar"]; // computed 為 trueSuper
父類關(guān)鍵字
interface Super { type: "Super"; }
例如:
class foo {}; class bar extends foo { constructor() { super(); } }MetaProperty
(這個(gè)不知道干嘛用的)
interface MetaProperty { type: "MetaProperty"; meta: Identifier; property: Identifier; }
例如:
new.target // 通過new 聲明的對(duì)象,new.target會(huì)存在 import.metaCallExpression
函數(shù)執(zhí)行表達(dá)式
interface CallExpression { type: "CallExpression"; callee: Expression | Import; arguments: ArgumentListElement[]; }
Import類型,沒搞懂。
interface Import { type: "Import" }
ArgumentListElement類型
type ArgumentListElement = Expression | SpreadElement;
如:
var foo = function (){}; foo();NewExpression
new 表達(dá)式
interface NewExpression { type: "NewExpression"; callee: Expression; arguments: ArgumentListElement[]; }UpdateExpression
更新操作符表達(dá)式,如++、--;
interface UpdateExpression { type: "UpdateExpression"; operator: "++" | "--"; argument: Expression; prefix: boolean; }
如:
var i = 0; i++; ++i; // prefix為trueAwaitExpression
await表達(dá)式,會(huì)與async連用。
interface AwaitExpression { type: "AwaitExpression"; argument: Expression; }
如
async function foo() { var bar = function() { new Primise(function(resolve, reject) { setTimeout(function() { resove("foo") }, 1000); }); } return await bar(); } foo() // fooUnaryExpression
一元操作符表達(dá)式
interface UnaryExpression { type: "UnaryExpression"; operator: UnaryOperator; prefix: boolean; argument: Expression; }
枚舉UnaryOperator
enum UnaryOperator { "-" | "+" | "!" | "~" | "typeof" | "void" | "delete" | "throw" }BinaryExpression
二元操作符表達(dá)式
interface BinaryExpression { type: "BinaryExpression"; operator: BinaryOperator; left: Expression; right: Expression; }
枚舉BinaryOperator
enum BinaryOperator { "==" | "!=" | "===" | "!==" | "<" | "<=" | ">" | ">=" | "<<" | ">>" | ">>>" | "+" | "-" | "*" | "/" | "%" | "**" | "|" | "^" | "&" | "in" | "instanceof" | "|>" }LogicalExpression
邏輯運(yùn)算符表達(dá)式
interface LogicalExpression { type: "LogicalExpression"; operator: "||" | "&&"; left: Expression; right: Expression; }
如:
var a = "-"; var b = a || "-"; if (a && b) {}ConditionalExpression
條件運(yùn)算符
interface ConditionalExpression { type: "ConditionalExpression"; test: Expression; consequent: Expression; alternate: Expression; }
例如:
var a = true; var b = a ? "consequent" : "alternate";YieldExpression
yield表達(dá)式
interface YieldExpression { type: "YieldExpression"; argument: Expression | null; delegate: boolean; }
例如:
function* gen(x) { var y = yield x + 2; return y; }AssignmentExpression
賦值表達(dá)式。
interface AssignmentExpression { type: "AssignmentExpression"; operator: "=" | "*=" | "**=" | "/=" | "%=" | "+=" | "-=" | "<<=" | ">>=" | ">>>=" | "&=" | "^=" | "|="; left: Expression; right: Expression; }
operator屬性表示一個(gè)賦值運(yùn)算符,left和right是賦值運(yùn)算符左右的表達(dá)式。
SequenceExpression序列表達(dá)式(使用逗號(hào))。
interface SequenceExpression { type: "SequenceExpression"; expressions: Expression[]; }
var a, b; a = 1, b = 2ArrayPattern
數(shù)組解析模式
interface ArrayPattern { type: "ArrayPattern"; elements: ArrayPatternElement[]; }
例:
const [a, b] = [1,3];
elements代表數(shù)組節(jié)點(diǎn)
ArrayPatternElement如下
type ArrayPatternElement = AssignmentPattern | Identifier | BindingPattern | RestElement | null;AssignmentPattern
默認(rèn)賦值模式,數(shù)組解析、對(duì)象解析、函數(shù)參數(shù)默認(rèn)值使用。
interface AssignmentPattern { type: "AssignmentPattern"; left: Identifier | BindingPattern; right: Expression; }
例:
const [a, b = 4] = [1,3];RestElement
剩余參數(shù)模式,語法與擴(kuò)展運(yùn)算符相近。
interface RestElement { type: "RestElement"; argument: Identifier | BindingPattern; }
例:
const [a, b, ...c] = [1, 2, 3, 4];ObjectPatterns
對(duì)象解析模式
interface ObjectPattern { type: "ObjectPattern"; properties: Property[]; }
例:
const object = {a: 1, b: 2}; const { a, b } = object;結(jié)束
AST的作用大致分為幾類
IDE使用,如代碼風(fēng)格檢測(eslint等)、代碼的格式化,代碼高亮,代碼錯(cuò)誤等等
代碼的混淆壓縮
轉(zhuǎn)換代碼的工具。如webpack,rollup,各種代碼規(guī)范之間的轉(zhuǎn)換,ts,jsx等轉(zhuǎn)換為原生js
了解AST,最終還是為了讓我們了解我們使用的工具,當(dāng)然也讓我們更了解JavaScript,更靠近JavaScript。
參考文獻(xiàn)前端進(jìn)階之 Javascript 抽象語法樹
抽象語法樹(Abstract Syntax Tree)
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/102602.html
摘要:為什么要談抽象語法樹如果你查看目前任何主流的項(xiàng)目中的,會(huì)發(fā)現(xiàn)前些年的不計(jì)其數(shù)的插件誕生。什么是抽象語法樹估計(jì)很多同學(xué)會(huì)和圖中的喵一樣,看完這段官方的定義一臉懵逼。它讀取我們的代碼,然后把它們按照預(yù)定的規(guī)則合并成一個(gè)個(gè)的標(biāo)識(shí)。 前言 首先,先說明下該文章是譯文,原文出自《AST for JavaScript developers》。很少花時(shí)間特地翻譯一篇文章,咬文嚼字是件很累的事情,實(shí)在...
摘要:抽象語法樹,是一個(gè)非?;A(chǔ)而重要的知識(shí)點(diǎn),但國內(nèi)的文檔卻幾乎一片空白。事實(shí)上,在世界中,你可以認(rèn)為抽象語法樹是最底層。通過抽象語法樹解析,我們可以像童年時(shí)拆解玩具一樣,透視這臺(tái)機(jī)器的運(yùn)轉(zhuǎn),并且重新按著你的意愿來組裝。 抽象語法樹(AST),是一個(gè)非?;A(chǔ)而重要的知識(shí)點(diǎn),但國內(nèi)的文檔卻幾乎一片空白。本文將帶大家從底層了解AST,并且通過發(fā)布一個(gè)小型前端工具,來帶大家了解AST的強(qiáng)大功能 ...
摘要:語法樹與代碼轉(zhuǎn)化實(shí)踐歸納于筆者的現(xiàn)代開發(fā)語法基礎(chǔ)與實(shí)踐技巧系列文章中。抽象語法樹抽象語法樹的作用在于牢牢抓住程序的脈絡(luò),從而方便編譯過程的后續(xù)環(huán)節(jié)如代碼生成對(duì)程序進(jìn)行解讀。 JavaScript 語法樹與代碼轉(zhuǎn)化實(shí)踐 歸納于筆者的現(xiàn)代 JavaScript 開發(fā):語法基礎(chǔ)與實(shí)踐技巧系列文章中。本文引用的參考資料聲明于 JavaScript 學(xué)習(xí)與實(shí)踐資料索引中,特別需要聲明是部分代碼片...
摘要:例如會(huì)被分解成解析語法分析這個(gè)過程是將詞法單元流數(shù)組轉(zhuǎn)換成一個(gè)由元素逐級(jí)嵌套所組成的代表了程序語法結(jié)構(gòu)的樹,這個(gè)樹就叫抽象語法樹。常用的有使用生成并使用抽象語法樹。 一般來說,程序中的一段源代碼在執(zhí)行之前會(huì)經(jīng)歷下面三個(gè)步驟1 分詞/詞法分析這個(gè)過程會(huì)將由字符組成的字符串分解成有意義的代碼快,這些代碼塊被稱為詞法單元。例如 var a = 4;會(huì)被分解成 var、a、=、4、; 2 解析...
摘要:無論你使用的是解釋型語言還是編譯型語言,都有一個(gè)共同的部分將源代碼作為純文本解析為抽象語法樹的數(shù)據(jù)結(jié)構(gòu)。和抽象語法樹相對(duì)的是具體語法樹,通常稱作分析樹。這是引入字節(jié)碼緩存的原因。 這是專門探索 JavaScript 及其所構(gòu)建的組件的系列文章的第 14 篇。 想閱讀更多優(yōu)質(zhì)文章請猛戳GitHub博客,一年百來篇優(yōu)質(zhì)文章等著你! 如果你錯(cuò)過了前面的章節(jié),可以在這里找到它們: JavaS...
閱讀 2426·2021-10-09 09:41
閱讀 3228·2021-09-26 09:46
閱讀 866·2021-09-03 10:34
閱讀 3208·2021-08-11 11:22
閱讀 3399·2019-08-30 14:12
閱讀 745·2019-08-26 11:34
閱讀 3369·2019-08-26 11:00
閱讀 1810·2019-08-26 10:26