程式本質上,就是由資料和處理資料的方法構成。函式和方法,這兩個名詞雖然字面不同,但意義上其實沒有區別。只是因為它們出現的地方有異,給予了不同的名稱,比如在全域性環境中,叫函式,在物件或類中,叫方法。而C#沒有全域性的概念,所以絕大多數時候,都叫方法。本節內容比較多,列一下目錄:
- 基本概念
- 函式的宣告和呼叫
- 函式表示式
- 通過Function的建構函式來宣告函式(JS/TS)
- 函式/變數提升(JS)
- 值引數
- 引用引數/輸出引數
- 可選引數/預設值引數
- 陣列引數/剩餘引數
- 擴充套件方法
- 閉包和委託捕獲
一、基本概念
JS:
①一等公民,和number、string、bool等一樣,是一種值;
②是object的子型別;
③可以全域性定義和使用;
④當函式作為物件的屬性時,稱之為方法
TS:
和JS一樣,只是多了型別約束
C #
①類中有兩個成員:資料成員和方法成員,方法規定了類的行為
②只能在類裡申明、類裡使用;
③方法可否視為一種值?C#中,方法不是一種值,但使用委託時,有類似表現
二、函式宣告和呼叫
JS/TS的函式可以宣告在全域性、物件和類中,C#只能在類中宣告。三者的引數和返回值等概念及用法沒有什麼不同,只是JS沒有型別約束,TS在JS基礎上增加了型別約束(還可以通過介面約束),而C#本身就是強型別。C#中多了一些修飾符,比如public等訪問修飾符,以及static、abtract等,TS中,當方法在類中申明時,也引入了部分修飾符。
//==========JS==========
//全域性宣告和呼叫 function sum(x,y){ return x + y; }
sum(1,2);
//物件中宣告和呼叫 const mc = { name: 'MC', sayHi: function(){console.log('Hi,i am MC');} , //還可以寫成 sayHi(){console.log('Hi,i am MC');} }
mc.sayHi(); //類中宣告(暫略,比較類時再具體談)
//==========TS==========
//全域性宣告和呼叫,多了型別約束 function sum(x:number,y:number):number{ return x + y; }
sum(1,2); //物件中宣告和呼叫,通過介面約束 interface{ name: string, sayHi():void } const person = { name: 'MC', sayHi(){console.log('Hi,i am MC');} }
person.sayHi(); //類中宣告(暫略,比較類時再具體談)
//==========C#========== //需要在類中宣告,如果不是靜態方法,需要建立物件後,才能呼叫 public class Person { public string Name{get;set} public int Sum(int x, int y){return x + y;} public void SayHi(){Console.WriteLine("Hi, i am MC");} }
var p1 = new Person();
p1.SayHi();
三、函式表示式(JS/TS)
僅限於JS/TS,使用非常靈活,是函式作為一種值的突出表現。C#中沒有此概念,但是通過委託貌似能實現類似功能。
//==========JS========== //將函式賦值給變數,變數sum也是方法的名稱 let sum = function(x,y){ return x + y; } //方法呼叫 sum(5,6)
//==========TS========== //通過型別推斷來宣告 let sum = function(x:number,y:number):number{ return x + y; } //完整的寫法應該是這樣 let sum1:(x:number,y:number) => number = function(x:number,y:number):number{ return x + y; } //也可以通過介面來約束申明 interface ISum{ (x:number,y:number):number } let sum2:ISum = function(x:number,y:number):number{ return x + y; }
//==========C#========== //定義一個委託型別 delegate int DeleSum(int x,int y); class HelloWorld { static void Main(string[] args) { //定義一個委託物件,並將匿名函式“賦值”給委託“變數” DeleSum deleSum = delegate(int x,int y){return x + y;}; Console.WriteLine(deleSum(2,2)); } } //對比一下,都使用Lambda表示式,像不像? //TS中:let sum = (int x,int y)=>{return x + y;} //C#中,使用自定義委託型別:DeleSum deleSum = (int x,int y)=>{return x + y;} //C#中,使用內建泛型委託:Func<int,int,int> sum = (int x,int y)=>{return x + y;}
四、通過Function的建構函式來宣告函式/有點拗口(JS/TS)
僅限於JS/TS,極少使用。JS中,幾個型別都有相對應的包裝類,都有對應的構造方法,如number>Number,string>String,array>Array,function>Function,object>Object等。所以函式也可以通過建構函式建立。
//==========JS========== //建構函式的引數,最後一個為返回值,前面的均為引數 let sum = new Function('x','y','return x+y'); console.log(sum(1,2)); //TS?不知道咋搞,型別約束放在哪?
五、函式提升(JS/TS)
僅限於JS/TS,在全域性或一個作用域中,編譯時,變數和函式的定義會先執行,函式定義優先於變數定義。函式提升僅限於通過“函式宣告”定義的方法,函式表示式定義的方法,不存在變數提升;變數提升僅限var定義的變數。let和const定義的變數,不存在變數提升。
//==========JS========== //全域性中,雖然函式宣告在後面,但先執行了 console.log(sum1(1,2)); function sum1(x,y){return x+y;} //函式作用域中,函式宣告也提前到了作用域的頂部 function f1(){ console.log(sum2(1,2)); function sum2(x,y){return x+y;} } //TS中有一樣的表現 //C#中不存在變數提升
六、值引數
形參和實參是值複製關係,呼叫方法時,實參的值複製給了形參。如果是基本型別,直接複製值,如果是引用型別,則複製引用地址。C#和JS/TS,基本一致。
//==========JS========== //引數為值型別(複製值) function sum(x,y){ return x + y; } //呼叫時分別將1和2的值,複製給了形參x和y sum(1,2); //引數為引用型別(複製引用地址) function sayName(x){ console.log(x.name); x.name = 'functionMC'; } let p1 = { name: 'MC', age: 18 } //呼叫時將p1的引用地址複製給了形參x,兩者指向的堆中的值是同一個 sayName(p1);//輸出MC console.log(p1.name);//輸出functionMC
//==========TS========== //引數為值型別(複製值) function sum(x:number,y:number):number{ return x + y; } //呼叫時分別將1和2的值,複製給了x和y sum(1,2); //引數為引用型別。注:此處使用介面來約束形參和實參 interface IPerson{ name: string, age: number } function sayName(x:IPerson):void{ console.log(x.name); x.name = 'functionMC'; } let p1:IPerson = { name: 'MC', age: 18 } sayName(p1);//輸出MC console.log(p1.name);//輸出functionMC
//==========C#========== public class Program { public static void Main() { //靜態方法中,不能直接呼叫例項成員,所以先將自己例項化 Program program = new Program(); //值型別引數,方法呼叫時,直接將值複製給形參 program.Sum(1, 2); //結果為3 //引用型別引數,方法呼叫時,將引用地址複製給形參 //形參和實參指向的堆中的資料,是同一個 var p1 = new Person() { Name = "MC", Age = 18 }; program.SayName(p1); //輸入MC Console.WriteLine(p1.Name); //輸出functionMC } //定義一個使用值型別引數的方法 public int Sum(int x, int y) { return x + y; } //定義一個使用引用型別引數的方法 public void SayName(Person p) { Console.WriteLine(p.Name); p.Name = "functionMC"; } } //自定義類,用來測試引用型別引數 public class Person { public string? Name { get; set; } public int? Age { get; set; } }
七、引用引數和輸出引數
引用參和輸出參,是C#中的概念。和值引數不同的是,實參作為形參的別名直接進入方法體中運算。所以,在方法體中如果改變了形參,也會同時改變實參。JS/TS中,因為var的作用域問題,也會產生類似結果。
//==========C#========== //引用引數使用ref,輸出引數用out,原理和用法參不多 //在申明和呼叫的時候都要用ref或out關鍵詞 //呼叫時,只能使用變數 //out的特殊在於,在呼叫的方法體中,在給輸出引數賦值 //out在方法呼叫裡,變數可以不用賦值,賦值也沒有意義,因為方法體中需要賦值 class HelloWorld { static void Main(string[] args) { Count a1 = new Count(); int a2 = 10; //呼叫時,也要用ref關鍵詞修飾實參,且實參只能用變數 RefMethod(ref a1, ref a2); Console.WriteLine($"a1值變成了{a1.Val},a2值變成了{a2}"); } //方法定義時,使用ref關鍵詞修飾形參 static void RefMethod(ref Count c1, ref int i1) { //形參和實參是同一個,形參值變了,實參值也會變 c1.Val += 2; i1 += 2; } } class Count { public int Val = 20; }
//==========JS/TS========== //方法體中,直接找到全域性的變數count修改值 var count = 10; function Method(){ count += 2; } Method(); console.log(count);
八、可選引數/預設值引數
C#和TS都是強型別,所以方法引數要受到一定約束,可選引數、陣列引數等,都是在可約束條件下的增加靈活性。而JS的引數則不受任務約束,愛傳不傳,愛傳啥就傳啥。
//==========JS========== //JS中沒有可選引數的概念,因為它不受約束 function f1(a,b){ return a + b; } //愛咋咋滴 f1(1,2,3); f1(1); f1(1,'MC'); f1();
//==========TS========== //“?”號定義可選引數 function f1(a:string,b?:string):void{ console.log(a +'-'+ b); } //可傳可不傳,不傳時預設為undefined f1('function','MC');//結果function-MC f1('function');//結果function-undefined //設定引數預設值 function f2(a:string,b:string='MC'):void{ console.log(a +'-'+ b); } f2('function','MC');//結果function-MC f2('function');//結果function-MC
//==========C#========== public class Program { public static void Main() { f1("function", "MC");//輸出結果function-MC f1("function");//輸出結果function-MC f2("function", "MC");//輸出結果function-MC f2("function");//輸出結果function- } //可選引數,設定預設值 static void f1(string a, string b = "MC") { Console.WriteLine(a + "-" + b); } //可空引數,如果不傳,則為null static void f2(string a, string? b = null) { Console.WriteLine(a + "-" + b); } }
九、陣列引數/剩餘引數
C#和TS都是強型別,所以方法引數要受到一定約束,可選引數、陣列引數等,都是在可約束條件下的增加靈活性。而JS的引數則不受任務約束,愛傳不傳,愛傳啥就傳啥。
//==========C#========== public class Program { public static void Main() { //呼叫方式一 f1(1, 2, 3, 4); f1(1, 2, 3); f1(1, 2); f1(); //呼叫方式二 var a1 = new int[] { 1, 2, 3, 4, 5, 6 }; f1(a1); } //使用關鍵詞params定義陣列引數 static void f1(params int[] intVals) { if ((intVals != null) && (intVals.Length != 0)) { foreach (var item in intVals) { Console.WriteLine(item); } } } }
//==========TS========== //TS中用“...”定義剩餘引數 function push(array: any[], ...items: any[]):void { items.forEach(function(item) { array.push(item); }); } let a = []; push(a, 1, 2, 3);//結果[1,2,3] push(a, 2, 3, 4);//結果[1,2,3,2,3,4]
十、擴充套件方法
擴充套件方法是C#中的概念,通過新類擴充套件定義新的方法,呼叫時,直接用原物件呼叫,就好像這個方法屬於原類一樣。JS和TS中,不動類,一樣也可以擴充套件,直接粗魯的“.”符號就可以,即使是引入了類,也能通過原型隨意擴充套件,和C#不一樣的是,實質上這個方法是新增到了原對像裡。
//==========C#========== public class Program { public static void Main() { var cal = new Cal(2, 4); cal.Sum();//結果為6,原類的方法 cal.Avg();//結果為3,新類的擴充套件方法 } } //原類 internal class Cal { private int d1; private int d2; public Cal(int d1, int d2) { this.d1 = d1; this.d2 = d2; } public int Sum() { return d1 + d2; } } //在一個靜態的新類裡,"靜靜的"增加了一個新的方法 internal static class CalExtend { //公開的靜態方法 //引數為原型別,且使用this關鍵詞修飾 static public int Avg(this Cal c1) { return c1.Sum() / 2; } }
//==========JS========== let a = { name:'MC', sayHi(){console.log('HI,MC');} }; a.sayHi(); //隨意的擴充套件一個方法 a.sayHello = ()=>{console.log('Hello,MC')}; a.sayHello();
//==========TS========== //下面這個案例,無法執行,提示Object沒有assign方法,TS中去掉這個方法了? //下面的程式碼,去掉型別,可以在JS中執行 class Cal{ x:number; y:number; constructor(x:number,y:number){ this.x = x; this.y = y; } sum():number{ return this.x + this.y; } } //隨意的新增一個擴充套件方法 Object.assign(Cal.prototype, { avg():number{ return 2; } }); let cal = new Cal(2,4); cal.sum(); cal.avg();
十一、閉包和委託捕獲
JS/TS中,對於作用域的巢狀,內層作用域可以看到和使用外層作用域的東西,就像一個隱私玻璃,裡面可以看外面,外面看不到裡面。閉包可以簡單的類比為,外層作用域派出的,混入內層作用域的一個函式間諜,通過它將內層作用域的東西“偷出來”,這樣外層作用域也能看到和使用內層作用域的東西,而且這個函式間諜還很敬業,把內層的環境也一起打包帶了出來,使得內層環境不會塌陷。巧得是,C#中也有類似的功能,叫匿名方法的捕獲。
//==========JS/TS========== function outF(){ const x = '我是內層的x'; function inF(){ return x; } return inF(); } //inF就是我們派出的間諜函式-閉包 //他不僅帶出了x,還把他潛入的作用域,整個都一鍋端了出來 console.log(outF());//結果為'我是內層的x' console.log(x);//報錯,提示x沒有定義,外層直接向內層要是要不到的
public class Program { public static void Main() { //定義一個委託物件 Func<int> f1; //下面是內層作用域 { int x = 3; f1 = () => { return x; };//捕獲了變數x } //委託物件f1將捕獲到的x帶到了外層作用域,獲得變數x Console.WriteLine(f1()); Console.WriteLine(x);//報錯提示當前上下文不存在x } }