程序本質上,就是由數據和處理數據的方法構成。函數和方法,這兩個名詞雖然字面不同,但意義上其實沒有區別。只是因為它們出現的地方有異,給予了不同的名稱,比如在全局環境中,叫函數,在對象或類中,叫方法。而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 } }