- ActionScript 3.0從入門到精通(視頻實戰版)
- 蔣國強 岳元亞等編著
- 272字
- 2018-12-31 19:52:48
第4章 函數
函數在程序設計的過程中是一個“革命性”的創新。利用函數編程,可以避免冗長、雜亂的代碼;利用函數編程,可以重復利用代碼,提高程序效率;利用函數編程,可以便利地修改程序,提高編程效率。
函數(function)的準確定義為:執行特定任務,并可以在程序中重用的代碼塊。ActionScript 3.0中有兩類函數:“方法”和“函數閉包”。具體是將函數稱為方法還是函數閉包,取決于定義函數的上下文。
4.1 定義函數
在ActionScript 3.0中有兩種定義函數的方法:一種是常用的函數語句定義法,一種是ActionScript中獨有的函數表達式定義法。具體使用哪一種方法來定義,要根據編程習慣來選擇。一般的編程人員使用函數語句定義法,對于有特殊需求的編程人員,則使用函數表達式定義法。
4.1.1 函數語句定義法
函數語句定義法是程序語言中基本類似的定義方法,使用function關鍵字來定義,其格式如下所示:
function 函數名(參數1:參數類型,參數2:參數類型...):返回類型{ //函數體 }
代碼格式說明:
?function:定義函數使用的關鍵字,function關鍵字要以小寫字母開頭。
?函數名:定義函數的名稱。函數名要符合變量命名的規則,最好給函數取一個與其功能一致的名字。
?小括號:定義函數的必需格式,小括號內的參數和參數類型均為可選。
?返回類型:定義函數的返回類型,也是可選的。要設置返回類型,冒號和返回類型必須成對出現,而且返回類型必須是存在的類型。
?大括號:定義函數的必需格式,需要成對出現。括起來的是函數定義的程序內容,是調用函數時執行的代碼。
下面的代碼定義一個求和sum函數,并調用函數,輸出調用結果。代碼如下所示:
function sum(a:int,b:int):int { //return為返回關鍵字 return a + b; } trace(sum(1,2));//輸出:3
4.1.2 函數表達式定義法
函數表達式定義法有時也稱為函數字面值或匿名函數。這是一種較為繁雜的方法,在早期的ActionScript版本中廣為使用。其格式如下所示:
var 函數名:Function=function(參數1:參數類型,參數2:參數類型...):返回類型{ //函數體 }
代碼格式說明:
?var:定義函數名的關鍵字,var關鍵字要以小寫字母開頭。
?函數名:定義的函數名稱。
?Function:指示定義數據類型是Function類。注意Function為數據類型,需大寫字母開頭。
?=:賦值運算符,把匿名函數賦值給定義的函數名。
?function:定義函數的關鍵字,指明定義的是函數。
?小括號:定義函數的必需格式,小括號內的參數和參數類型均為可選。
?返回類型:定義函數的返回類型,可選參數。
?大括號:其中為函數要執行的代碼。
需要注意的是,該方法首先定義的是一個匿名函數,然后利用賦值運算符“=”把該函數賦值給定義的函數名。
下面是一個使用函數表達式定義法定義函數的示例,其代碼如下所示:
var sum:Function=function(a:int,b:int):int { return a + b; }
4.1.3 兩種定義方法的區別和選擇
原則上推薦使用函數語句定義法,因為這種方法更加簡潔,更有助于保持嚴格模式和標準模式的一致性。
下面要講解的是函數語句和函數表達式定義法之間存在的區別,具體內容如下所示。
1. 兩種方法的區別
函數語句和函數表達式定義法的作用域不同。函數語句作用于定義它的整個作用域(包括函數前面的語句)內,而函數表達式定義法的作用域只存在于其定義之后。簡單地說,函數語句定義法定義的函數,無論在函數語句之前調用函數還是之后調用函數,函數都可以被調用;而函數表達式定義法則必須先定義后調用,否則編譯就會報錯。
下面使用函數語句法定義一個函數hello(),并在定義之前調用,其代碼如下:
hello(); function hello() { var str:String="函數語句法定義前后都可以調用"; trace(str);//輸出:函數語句法定義前后都可以調用 }
下面使用函數語句法定義一個函數hello(),并在定義之后調用,其代碼如下:
function hello() {
var str:String="函數語句法定義前后都可以調用";
trace(str);
}
hello();
下面使用函數表達式法定義一個函數hello(),并在定義之前調用,其代碼如下:
hello();
var hello:Function=function(){
var str:String="函數表達式定義法不能提前調用";
trace(str);
};
注意 此段代碼不能正確編譯,輸出提示為“value不是函數”,說明此時函數hello()還沒有被定義。
下面使用函數表達式法定義一個函數hello(),并在定義之后調用,其代碼如下:
var hello:Function=function(){
var str:String="函數表達式定義法定義之后才能調用";
trace(str);
};
hello();
函數語句和函數表達式定義法對this關鍵字的指向不同。函數語句定義法,this關鍵字永遠指向當前函數定義的域;而表達式定義法由于是匿名函數定義后被賦值為定義的函數變量,所以this的指向會隨著依附對象的不同而不同。
函數語句和表達式定義法在內存管理和垃圾回收方面也存在不同。因為函數表達式不像對象那樣獨立存在,它是一個匿名函數。當引用這個函數的對象由于其他原因不再可用,那么將無法訪問該函數。
函數語句比包含函數表達式的賦值語句更便于閱讀。對比兩種定義方法,可以發現,表達式定義法要求的語法內容更多,代碼更加復雜,更容易造成編程時的混亂。
2. 兩種方法的選擇
在兩種定義方法的選擇上,一般使用函數語句定義法。函數表達式定義函數主要用于:一是適合關注運行時行為或動態行為的編程;二是用于那些使用一次后便丟棄的函數或者向原型屬性附加的函數。函數表達式更多地用在動態編程或標準模式編程中。
4.2 調用函數
函數只是一個編好的程序塊,在沒有被調用之前,什么也不會發生。只有通過調用函數,函數的功能才能夠實現,才能體現出函數的高效率。通過本節的學習,讀者將掌握一般的函數調用方法以及嵌套和遞歸調用函數的方法。
4.2.1 函數的一般調用
對于沒有參數的函數,可以直接使用該函數的名字,并后跟一個圓括號(它被稱為“函數調用運算符”)來調用。
下面定義一個不帶參數的函數HelloAS(),并在定義之后直接調用,其代碼如下:
function HelloAS() {
trace("AS 3.0世界歡迎你!");
}
HelloAS();
代碼運行后的輸出結果如下所示。
//輸出:AS 3.0世界歡迎你!
如果函數本身沒有參數,就不能在調用的括號中輸入參數,否則就會報錯。
下面定義一個不帶參數的函數HelloAS(),調用時輸入參數“我要走進AS 3.0世界”,其代碼如下:
function HelloAS() { trace("歡迎走進AS 3.0世界"); } HelloAS("我要走進AS 3.0世界");
注意 此段代碼不能正確編譯,輸出提示為“參數個數不正確,應該為0個”,說明該函數無參數,而調用輸入了參數。
對于含有參數的函數,如果參數本身設有默認值,則函數名后的括號內可以輸入參數,也可以不輸入參數。
下面定義一個含有默認參數的函數HelloAS(),調用時不輸入參數,其代碼如下:
function HelloAS(str:String="AS 3.0世界歡迎你!") {
trace(str);
}
HelloAS();
代碼運行后的輸出結果如下所示:
//輸出:AS 3.0世界歡迎你!
但是,如果參數沒有設置默認值,那么函數名后的括號內必須輸入參數,而且輸入的參數類型必須和默認的參數類型一致,否則就會報錯。
下面定義一個含有默認參數的函數HelloAS(),調用時輸入參數“我要走進AS 3.0世界”,其代碼如下:
function HelloAS(str:String="AS 3.0世界歡迎你!") { trace(str); } HelloAS("我要走進AS 3.0世界");
代碼運行后的輸出結果如下所示:
//輸出:我要走進AS 3.0世界
下面定義一個含有參數的函數HelloAS(),調用時未輸入參數,其代碼如下:
function HelloAS(str:String) { trace(str); } HelloAS();
注意 此段代碼不能正確編譯,輸出提示為“參數個數不正確,應該為1個”,說明該函數有一個參數,而調用沒有輸入參數。
下面定義一個含有參數的函數HelloAS(),調用時輸入參數的數據類型和默認參數的數據類型不同,其代碼如下:
function HelloAS(str:String) { trace(str); } HelloAS(1);
注意 此段代碼不能正確編譯,輸出提示為“int類型值的隱式強制指令的目標是非相關類型String”,說明輸入參數的數據類型是int,而不是默認的數據類型String。
當然,如果函數的參數沒有指定類型,則輸入的參數類型沒有強制要求。
另外,還可以通過函數的引用,把一個函數賦值給另一個變量。一旦將對一個函數的引用賦給一個變量,就可以將那個變量名與函數調用運算符結合起來調用那個函數。
下面的代碼把一個定義好的函數“求和”賦值給變量“sum”,并利用“sum”變量來調用函數“求和”:
function 求和(a:int,b:int) { trace(a+b); } var sum:Function=求和; sum(1,2);
代碼運行后的輸出結果如下所示:
3
4.2.2 嵌套和遞歸調用函數
嵌套和遞歸調用是兩種類似的函數調用方式,其本質都是在調用函數時用一個函數調用另一個函數。不同的是,嵌套調用是用一個函數去調用另一個函數,而遞歸調用是函數調用自身函數。
1. 嵌套調用函數
AS 3.0允許函數嵌套定義,即在一個函數體中再定義一個新函數。嵌套的層次可以是多層的,比如函數A可以調用函數B,而函數B又可以調用函數C??
下面先定義求整數和sum()函數,它接受兩個參數:一個字符型參數a和一個數字型參數b。為了求和,需要把參數a轉換為整數類型,把參數b轉換為整數類型。為此在函數體中定義STI()函數把字符型轉為整數型,定義NTR()函數把數字進行求整。程序代碼如下:
function sum(a:String,b:Number) { function STI(c:String):int { return int(c); } function NTR(d:Number):int { return Math.round(d); } return STI(a) + NTR(b); } var a:String="10"; var b:Number=5.56; trace(sum(a,b));
代碼運行后的輸出結果如下所示:
16
注意 在嵌套函數內部定義的函數,僅在主函數體內可用,除非將對嵌套函數的引用傳遞給外部代碼。
此外,嵌套函數也可以調用和自身函數并列的函數,即允許在一個函數體內調用另一個函數。同樣的功能,使用下面的代碼也可以實現。
function sum(a:String,b:Number) { return STI(a) + NTR(b); } var a:String="10"; var b:Number=5.56; trace(sum(a,b)); function STI(c:String):int { return int(c); } function NTR(d:Number):int { return Math.round(d); }
代碼運行后的輸出結果如下所示:
16
注意 使用此種方法嵌套調用函數,外部的函數對整個作用域都是可用的。
2. 遞歸調用函數
程序調用自身的編程技巧稱為遞歸。遞歸調用是一個函數在其定義或說明中直接或間接調用自身的方法,它能夠把復雜的、大型的程序轉化成和原問題相似的小問題來處理。
下面的代碼使用遞歸函數的方法來求1+2+3+?+n的值,其代碼如下:
function sum(n:int):int { if (n==1) { return 1; } else { return n + sum(n - 1); } } trace(sum(100));
代碼運行后的輸出結果如下所示:
5050
注意 遞歸必須是有條件的,在定義的時候必須有一個明確的遞歸結束條件,稱為遞歸出口。缺少遞歸出口的代碼將永遠地調用自身,形成死循環。
如下面的遞歸求階乘(n?。┑拇a是不正確的。
function fac(n:int):int { return n * fac(n - 1); } trace(fac(100));
但是,Flash中對此有一個保護措施,在一定的遞歸次數之后,系統會終止ActionScript代碼的運行,并給出提示信息。
4.3 函數的返回值
主調函數通過函數的調用得到一個確定的值,此值被稱為函數的返回值。利用函數的返回值,可以通過函數進行數據的處理、分析和轉換,并能最終獲取想要獲得的結果。在本節我們主要學習函數返回值的獲取方法和獲取過程中的注意事項。
4.3.1 return語句
AS從函數中獲取返回值,使用return語句來實現,語法格式如下:
return 返回值
格式說明如下:
?return:函數返回值的關鍵字,必需的。
?返回值:函數中返回的數據,既可以是字符串、數值等,也可以是對象,如數組、影片簡介等。
下面定義一個求圓形面積的函數,并返回圓面積的值,其代碼如下:
function 圓面積(r:Number):Number{ var s:Number=Math.PI*r*r; return s; } trace(圓面積(5));
代碼運行后的輸出結果如下所示:
78.53981633974483
上面的代碼中,return后的返回值是一個變量s,return后的返回值也可以是一個表達式。把上面的代碼修改如下,也可以實現相同的效果。
function 圓面積(r:Number):Number{ return Math.PI*r*r; } trace(圓面積(5));
代碼運行后的輸出結果如下所示:
78.53981633974483
return語句在使用時還要注意下面幾點:
?同一個函數中可以有多個return語句,執行到哪個return語句,哪一個return語句起作用。
下面的代碼在輸入不同的參數時,執行的返回語句不同。
function 選擇輸出(x:Number):Number { if (x>0) { return 1; } else if (x<0) { return -1; } else { return 0; } } trace(選擇輸出(10));
代碼運行后的輸出結果如下所示:
1
說明
在輸入的參數大于0時,執行return 1語句;參數小于0,執行return-1語句;參數等于0,執行return 0語句。
?return語句同時又是一個終止語句,只要執行到return語句,Flash就終止執行之后的代碼,跳出函數體。
從下面代碼的輸出結果,可發現return前的輸出語句可以執行,而return后的輸出語句沒有被執行。
function 輸出測試() { trace("這是return前的語句,能夠被執行"); return; trace("這是return后的語句,不能被執行"); } 輸出測試();
代碼運行后的輸出結果如下所示:
這是return前的語句,能夠被執行
?return語句后的返回值可以為空,此時返回為“undefined”。
下面的return語句沒有參數,其代碼如下:
function 輸出測試() { return; } trace(輸出測試());
代碼運行后的輸出結果如下所示:
undefined
4.3.2 返回值類型
函數的返回類型在函數的定義中屬于可選參數,如果沒有選擇,那么返回值的類型由return語句中返回值的數據類型來決定。
下面的代碼,return語句返回一個字符型數據,來驗證一下返回值的類型。
function 類型測試() { var a:String="這是一個字符串"; return a; } trace(typeof(類型測試()));
代碼運行后的輸出結果如下所示:
string
如果函數定義過程中設定了返回類型,則返回值的類型必須和設置的數據類型相同,否則編譯就會報錯。
把上面的函數增加一個返回類型Number,代碼如下:
function 類型測試():Number { var a:String="這是一個字符串"; return a; } trace(類型測試());
注意
此段代碼不能正確編譯,輸出提示為“String類型值的隱式強制指令的目標是非相關類型Number”,說明return返回的數據類型是String,而不是默認的返回類型Number。把返回類型Number改為String,代碼就能編譯通過。
若函數不需要返回數據,也就是函數體中不存在return語句,那么定義函數時不能設置返回類型,否則編譯會報錯。下面的代碼不能夠被編譯:
function 返回測試():String { var a:String="這是一個字符串"; trace(a); } trace(返回測試());
注意 此段代碼不能正確編譯,輸出提示為“函數沒有返回值”,說明函數體中沒有返回語句。
4.4 函數的參數
函數通過參數向函數體傳遞數據和信息。ActionScript 3.0對函數的參數增加了一些新功能,同時也增加了一些限制。有大多數程序員都熟悉的按值或按引用傳遞參數這一概念,也有很多人相對陌生的arguments對象和...(rest)參數。
4.4.1 傳遞參數的語法
函數中傳遞的參數都位于函數格式的括號中,語法格式如下:
(參數1:參數類型=默認值,參數2:參數類型=默認值)
下面定義一個個性化的歡迎語句,對不同的姓名給出對應的問候。代碼如下所示:
function Welcome(username:String):void { trace("歡迎你!"+username); } Welcome("張三"); Welcome("小明");
代碼運行后的輸出結果如下所示:
歡迎你!張三 歡迎你!小明
ActionScript函數支持傳遞多個參數。在定義函數時,使用半角逗號分隔開不同的參數即可。下面定義一個多個參數的函數,代碼如下所示:
function intro(username:String,age:int):void { trace("你的姓名:"+username+" 年齡是:"+age); } intro("小王",25);
代碼運行后的輸出結果如下所示:
你的姓名:小王 年齡是:25
注意 隔開參數的是半角逗號而不是分號。
在設置參數時不能使用var語法,否則編譯時就會報錯。下面的代碼使用var定義了參數,不能編譯通過。
function Hello(var username:String):void{ trace("Hello."+username) }
4.4.2 傳遞參數的兩種方法
許多編程語言中,參數的傳遞基本都是兩種類型:按值或者按引用傳遞。按值傳遞意味著將參數的值復制到局部變量中以便在函數內使用。按引用傳遞意味著將只傳遞對參數的引用,而不傳遞實際值。要了解任何一門編程語言中的函數,首先必須搞清楚參數的傳遞到底是按值還是按引用。
在ActionScript 3.0中,所有的參數均按引用傳遞,因為所有的值都存儲為對象?;蛿祿遣蛔兊膶ο?,按值還是按引用的效果一樣,通??梢钥醋鍪前粗祩鬟f。按值傳遞,是指參數被傳遞給函數后,被傳遞的變量就獨立了。若在函數中改變這個變量,原變量不會發生任何變化。
下面來測試一下,代碼如下所示:
function test(a:Number):Number { a++; return a; } var b:Number=5; trace("b引用前:"+b); var c=test(b); trace("b引用后:"+b); trace("c:"+c);
代碼運行后的輸出結果如下所示:
b引用前:5 b引用后:5 c:6
說明
從輸出的結果可以看出,變量b在引用前和引用后,其值沒有變化,變化的是變量b的一個引用。
復雜類型數據不但按引用傳遞,而且還保持一個引用。這樣,在函數內部參數的更改同樣會影響到函數外部數據。
下面先建立一個復雜型數據Array對象,然后在數組中給其追加一個數據,結果發現數組的數據發生了改變。代碼如下所示:
function TestArr(_arr:Array):void { var a:int=100; _arr.push(a); } var b:Array=[1,2,3]; trace("引用前:"+b); TestArr(b); trace("引用后:"+b);
代碼運行后的輸出結果如下所示:
引用前:1,2,3 引用后:1,2,3,100
說明
從輸出的結果可以看出,數組變量b的值在引用前后發生了變化。
4.4.3 給函數設置默認參數
在ActionScript 2.0中并不支持對函數設置默認參數,此為ActionScript 3.0的新功能。要給一個函數的參數設置默認值,語法格式如下:
function(參數1:參數類型=默認值,參數2:參數類型=默認值)
默認參數是可選項,可以設置默認參數,也可以不設置默認參數。若設置了默認參數,則在調用函數時,如果沒有寫明參數,系統將使用在函數定義中為該參數指定的值。
下面定義一個有3個參數的函數,其中兩個參數有默認值。代碼如下所示:
function Test(a:int,b:int=2,c:int=3):void { trace(a,b,c); } Test(1); //輸出:1,2,3 Test(1,4); //輸出:1,4,3 Test(1,4,0); //輸出:1,4,0
參數設置了默認值,那么在調用時即為可選參數,可以設置,也可以不設置,而沒有默認值的參數則必須輸入。
和上面相同的一個函數,若在調用時不設置參數,則編譯時會報錯。代碼如下所示:
function Test(a:int,b:int=2,c:int=3):void { trace(a,b,c); } Test();
注意 此段代碼不能正確編譯,輸出提示為“參數個數不正確,應該為1個”,說明默認參數在系統分析的時候并不計入必選參數之列。
4.4.4 arguments 對象和...(rest) 參數
ActionScript 3.0中有兩種函數調用時檢查參數數量的方法,分別為使用arguments對象和...(rest) 參數。
1. arguments對象
在函數中,可以使用arguments對象訪問有關傳遞給該函數的參數信息。arguments對象是一個數組,其中按順序保存著傳遞給函數的所有參數。可以使用數組的訪問方式來訪問傳入的參數。它有一個length屬性記錄當前傳入的參數數目,還有一個屬性callee提供對函數本身的引用,該引用可用于遞歸調用函數表達式。
在ActionScript 3.0中,函數調用中所包括的參數的數量可以大于在函數定義中所指定的參數數量,但是,如果參數的數量小于必需參數的數量,編譯時就會報錯??梢酝ㄟ^arguments對象的數組來訪問傳遞給函數的任何參數,不管是否在函數定義中定義了該參數。下面使用arguments數組及arguments.length屬性來輸出傳遞給TestArg()函數的所有參數。代碼如下所示:
function TestArg(a:int,b:int,c:int):void { trace("輸入的參數個數是:"+arguments.length); for (var i:uint = 0; i < arguments.length; i++) { trace("這是第"+i+"個參數,其值為:"+arguments[i]); } } TestArg(1,2,3);
代碼運行后的輸出結果如下所示:
輸入的參數個數是:3 這是第0個參數,其值為:1 這是第1個參數,其值為:2 這是第2個參數,其值為:3
注意 在ActionScript 3.0中,要遵守函數的嚴格定義。所以ActionScript 2.0中無視函數定義,傳入任意多個參數的做法在ActionScript 3.0中是非法的。
arguments.callee屬性通常用在匿名函數中以創建遞歸,以此來提高程序的靈活性。下面的函數表達式中,使用arguments.callee屬性來啟用遞歸。代碼如下所示:
var fac:Function = function (i:uint){
if(i == 1) {
return 1;
} else{
return (i + arguments.callee(i - 1));
}
};
trace(fac(100));// 輸出:5050
2. ...(rest)參數
...(rest)參數是ActionScript 3.0引入的新參數聲明。使用該參數可指定一個自己命名的數組參數來接受任意多個以逗號分隔的參數。其語法格式如下:
function(..args) function(參數1,參數2,...args)
...(rest)參數擁有arguments對象的儲存功能和length屬性,但是不再具有callee屬性。
下面用...(rest)參數來重寫TestArg()函數,代碼如下所示:
function TestArg(...args):void { trace("輸入的參數個數是:"+args.length); for (var i:uint = 0; i < args.length; i++) { trace("這是第"+i+"個參數,其值為:"+args[i]); } } TestArg(1,2,3);
代碼運行后的輸出結果如下所示:
輸入的參數個數是:3 這是第0個參數,其值為:1 這是第1個參數,其值為:2 這是第2個參數,其值為:3
...(rest)參數還可與其他參數一起使用,但是要注意其只能是最后一個列出的參數。下面修改TestArg()函數,增加一個int型參數x,第二個參數使用...(rest)參數,輸出的結果將忽略第一個值。代碼如下所示:
function TestArg(x:int,...args):void { trace("...(rest) 參數個數是:"+args.length); for (var i:uint = 0; i < args.length; i++) { trace("這是第"+i+"個...(rest)參數,其值為:"+args[i]); } } TestArg(1,2,3);
代碼運行后的輸出結果如下所示:
...(rest) 參數個數是:2 這是第0個...(rest)參數,其值為:2 這是第1個...(rest)參數,其值為:3
注意 此時...(rest) 參數中記錄的參數個數是總的參數個數減去定義過的參數。
arguments對象和...(rest)參數都可以作為一個數組儲存輸入的參數,但是在使用...(rest)參數時, arguments對象不可用。
4.5 練習題
1. 簡述傳遞參數時,值類型和引用類型的區別,以及字符串String和數組Array屬于哪種類型。
2. 請嘗試完成:把數字1~100隨機不重復放到一個數組里。
3. “String類型值的隱式強制指令的目標是非相關類型Number”,可能產生這個編譯錯誤的原因是什么?
4. 關于函數的返回值,說法正確的是( )。
A. AS 3.0必須聲明函數的返回類型
B. 可以不聲明返回類型,也可以用“:void”定義函數無返回值,定義為void時,可以return null
C. 可以將函數返回類型設置為“*”號,代表函數可以返回任意類型的數據
D. 函數test():Object不能返回字符串數據
5. 在函數中,可以使用arguments對象來訪問有關傳遞給該函數的參數的信息,arguments有個屬性callee,它的用途是什么?
- C#程序設計實訓指導書
- Java高并發核心編程(卷2):多線程、鎖、JMM、JUC、高并發設計模式
- Mastering Objectoriented Python
- Mastering Ember.js
- Getting Started with CreateJS
- Python零基礎快樂學習之旅(K12實戰訓練)
- INSTANT Weka How-to
- INSTANT CakePHP Starter
- Building Serverless Applications with Python
- AppInventor實踐教程:Android智能應用開發前傳
- Mastering Data Mining with Python:Find patterns hidden in your data
- Python程序設計教程
- LabVIEW數據采集
- Java語言程序設計實用教程(第2版)
- MySQL從入門到精通