C#和TS/JS的对比学习02:函数与方法

程序本质上,就是由数据和处理数据的方法构成。函数和方法,这两个名词虽然字面不同,但意义上其实没有区别。只是因为它们出现的地方有异,给予了不同的名称,比如在全局环境中,叫函数,在对象或类中,叫方法。而C#没有全局的概念,所以绝大多数时候,都叫方法。本节内容比较多,列一下目录:

  1. 基本概念
  2. 函数的声明和调用
  3. 函数表达式
  4. 通过Function的构造函数来声明函数(JS/TS)
  5. 函数/变量提升(JS)
  6. 值参数
  7. 引用参数/输出参数
  8. 可选参数/默认值参数
  9. 数组参数/剩余参数
  10. 扩展方法
  11. 闭包和委托捕获

 

 

一、基本概念

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>Stringarray>Arrayfunction>Functionobject>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

    }

}