ECMAScript函数详解

作者: 2857b61e8161 | 来源:发表于2017-06-05 14:18 被阅读143次

    函数对任何语言来说都是一个核心的概念,通过函数可以封装任意多条语句,而且在任何时候调用执行。

    一、语法
    ECMAScript中的函数使用function关键字来声明,后跟一组参数以及函数体,函数表达式后面会讲。

    //语法
    function funcName(arg0,arg1,...argN){
      statements
    }
    //举个栗子:
    function sayHi(name,message){
      alert("hello "+name+","+message);
    }
    

    这个函数可以通过其函数名加一对圆括号和参数来调用:

    sayHi("liping","how are you today?");
    

    这个函数的输出结果是hello liping,how are you today?。函数中定义的命名参数name和message被用做了字符串拼接的两个操作数。

    ECMAScript中的函数在定义的时候不必指定是否有返回值。任何函数在任何时候我们可以通过return语句后跟要返回的值来实现返回值:

    function sum(num1,num2){
      return num1+num2;
    }
    var result = sum(5,1);
    console.log(result);//6
    

    这个函数会在执行完return语句之后停止并立即退出,因此位于return语句之后的任何代码都永远不会执行。

    function sum(num1,num2){
      return num1+num2;
      alert("hello");//永远不会执行
    }
    

    一个函数中也可以包含多return语句,如下面这个例子中所示:

    function diff(num1,num2){
      if(num1<num2){
        return num2-num1;
      }else{
        return num1-num2;
      }
    }
    

    另外,return语句也可以不带任何返回值,在这种情况下,函数在停止执行后将返回undefined值,这种用法一般用在需要提前停止函数执行而又不需要返回值的情况下。

    注意:严格模式下对函数有一些限制:

    1、不能把函数名命名为`eval`或`arguments`;
    2、不能把参数命名为`eval`或`arguments`;
    3、不能出现两个命名参数同名的情况;
    

    如果发生以上情况,就会导致语法错误,代码无法执行。

    二、理解参数
    ECMAScript函数的参数与大多数其他语言中函数的参数有所不同,ECMAScript函数不介意传递进来多少个参数,也不在乎传进来参数是什么数据类型,也就说,即便你定义的函数只接收两个参数,在调用这个函数时也未必一定要传递两个参数,可以传递一个、三个甚至不传递参数。之所以这样,原因是ECMAScript中的参数在内部是用一个数组来表示的,函数接收到的始终是这个数组,而不关心数组中包含哪些参数,在函数体内我们可以通过arguments对象来访问这个参数数组,从而获取传递给函数的每一个参数

    arguments对象只是与数组类似(它并不是Array的实例),因为可以使用方括号语法访问它的每一个元素(第一个元素arguments[0],第二个元素arguments[1]),使用length属性来确定传递进来多少个参数。

    //之前的例子还可以这样写
    function sayHi(){
      alert("hello "+ arguments[0] + "," + arguments[1]);
    }
    

    这个重写后的函数中不包含命名的参数。虽然没有使用namemessage标识符,但函数的功能依旧。这个事实说明了ECMAScript函数的一个重要特点:命名的参数只提供便利,但不是必需的。

    通过arguments对象的length属性可以知道有多少个参数传递给了函数,下面这个函数会在每次调用时,输出传入其中的参数个数:

    function howManyArgs(args){
            console.log(arguments.length);
        }
        howManyArgs("string",45);//2
        howManyArgs();//0
        howManyArgs(12);//1
    

    执行以上代码控制台依次出现2,0,1,我们可以利用这个特点让函数能够接受任意个参数并分别实现适当的功能:

    function doAdd(){
            if(arguments.length == 1){
                alert(arguments[0]+10);
            }else if(arguments.length == 2){
                alert(arguments[0]+arguments[1]);
            }
        }
        doAdd(10);//20
        doAdd(30,20);//50
    

    另一个与参数相关的重要方面,是arguments对象可以与命名参数一起使用,举个栗子:

    function doAdd(num1,num2){
            if(arguments.length == 1){
                alert(num1+10);
            }else if(arguments.length == 2){
                alert(arguments[0]+num2);
            }
        }
        doAdd(10);//20
        doAdd(20,30);//50
    

    arguments的值永远与对应命名参数的值保持同步。举个栗子:

    function doAdd(num1,num2){
            arguments[1] = 10;
            alert(arguments[0]+num2);
        }
        doAdd(5,2);//15
    

    每次执行doAdd()函数都会重写第二个参数,将第二个参数的值修改为10。因为arguments对象中的值会自动反应到对应的命名参数,所以修改了arguments[1],也就修改了num2,结果他们的值都会变成10。

    不过这并不是说读取这两个值会访问相同的内存空间;它的内存空间是独立的,但他们的值会同步,注意的是,如果只传入了一个参数,那么为arguments[1]设置的值不会反映到命名参数中。因为arugments对象的长度是由传入的参数个数决定的,不是由定义函数时命名的参数的个数决定的。

    没有传递值的命名参数将自动被赋予undefined值。

    严格模式下,重写arguments的值会导致语法错误。

    三、没有重载

    ECMAScript函数没有重载,而在其他语言(如Java)中,可以为一个函数编写两个定义,只要这两个定义的签名(接受的参数的类型和数量)不同即可。

    ECMAScript函数没有签名,因为其参数是由包含零或多个值的数组来表示的,没有函数签名,真的函数重载是不能做到的。

    如果在ECMAScript中定义了两个名字相同的函数,则后面的函数会覆盖前面的函数,

    function addNum(num){
            return num + 100
        }
        function addNum(num){
            return num + 200;
        }
        var result = addNum(100);
        console.log(result);//300
    

    四、Function类型(函数深入理解)

    函数实际上是对象,每个函数都是Function类型的实例,而且与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上是一个指向函数对象的指针,不会与某个函数绑定。

    函数声明:

    function sum(num1,num2){
            return num1 + num2;
        }
    
        var sum = function(num1,num2){
            return num1 + num2;
        };
    var sum = new Function("num1","num2","return num1 + num2");//不推荐
    

    函数名仅仅是指向函数对象的指针,因此函数名与包含对象指针的其他变量没有什么不同,一个函数可能会有多个名字:

    function sum(num1,num2){
            return num1 + num2;
        }
        alert(sum(10,10));//20
        var anotherSum = sum;
        alert(anotherSum(10,10));//20
        alert(sum(5,5));//10
        sum = null;
        alert(anotherSum(10,10));//20
        alert(sum(2,4));//会报错
    

    以上代码首先定义了一个名为sum()的函数,用于求两个值的和,然后,又声明了变量anotherSum,并将其设置为sum相等。

    注意:使用不带圆括号的函数名是访问函数指针,而非调用函数。此时,anotherSumsum指向同一个函数,因此anotherSum()也可以调用并返回结果。

    我们在重新理解一下为什么没有函数重载,

    function addNum(num){
            return num + 100
        }
        function addNum(num){
            return num + 200;
        }
        var result = addNum(100);
        console.log(result);//300
    
    //以上代码和下面代码没有什么区别
    var  addNum = function(num){
      return num + 100;
    };
    addNum = function(num){
      return num + 200;
    };
    var result = addNum(100);//300
    

    我们现在可以明白,在创建第二个相同的函数时,实际上覆盖了引用第一个函数的变量addNum

    五、函数声明和函数表达式的区别

    解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解析执行。我们看下栗子:

    sum(2,3);//5
        function sum(num1,num2){
             alert(num1 + num2);
        }
    
        sum2(3,4);//会报错
        var sum2 = function(num1,num2){
            alert(num1 + num2);
        }
    

    六、作为值的函数

    ECMAScript中的函数名本身就是变量,所以函数也可以作为值来使用,也就是说,不仅可以像传递参数一样把一个函数传递给另一个函数,而且也可以将一个函数作为另个函数的结果返回。

    function callSomeFunction(someFunction,someArgument){
            return someFunction(someArgument);
        }
        function addNum(num){
            return num + 10;
        }
        var result1 = callSomeFunction(addNum,10);
        alert(result1);//20
    
        function getGreeing(name){
            return "hello " + name;
        }
        var result2 = callSomeFunction(getGreeing,"liping");
        alert(result2);//hello liping
    

    也可以从一个函数中返回另一个函数,例如,假设有一个对象数组,我们想要根据某个对象属性对数组进行排序。而传递给数组sort()方法的比较函数要接受两个参数,既要比较的值。可是我们需要一种方式来指明按照哪个属性来排序。

    function createComparisonFunction(propertyName){
            return function(object1,object2){
                //要比较的值
                var val1 = object1[propertyName];
                var val2 = object2[propertyName];
                if(val1>val2){
                    return 1;
                }else if(val1<val2){
                    return -1;
                }else{
                    return 0;
                }
            }
        }
        var data = [{"name":"zhangsan",age:18},{"name":"lisi",age:19}];
        data.sort(createComparisonFunction("name"));
        console.log(data[0].name);//lisi
    
        data.sort(createComparisonFunction("age"));
        console.log(data[0].age);//18
    

    七、函数内部属性

    在函数内部有两个特殊的对象,argumentsthis

    arguments:是一个类数组对象,包含着传入函数中的所有参数,虽然arguments的主要用途是保存函数参数,但这个对象还有一个名叫callee的属性。

    callee:该属性是一个指针,指向拥有这个arguments对象的函数

    举个栗子:

    function factorial(num){
            if(num<=1){
                return 1;
            }else{
            return num * factorial(num-1);
            }
        }
    
        function factorial(num){
            if(num<=1){
                return 1;
            }else{
            return num * arguments.callee(num-1);
            }
        }
        var result1 = factorial(5);
        console.log(result1);//120
    

    上面的栗子,第一种写法,在函数有名字,而且名字以后也不会变的情况下,这样定义是没有问题的。但是这个函数的执行与函数名factorial紧紧耦合在了一起。

    为了消除这种紧密耦合的现象,可以像第二种写法。没有再引用函数名factorial,这样,无论引用函数时使用的是什么名字,都可以保证正常完成递归调用,栗子如下。

    var trueFactorial = factorial;
        factorial = function(){
            return 0;
        }
        function factorial(num){
            if(num<=1){
                return 1;
            }else{
            return num * arguments.callee(num-1);
            }
        }
        var result1 = trueFactorial(5);
        console.log(result1);//120
        console.log(factorial(5));//0
    

    this:

    window.color = "red";
        var o = {color:"blue"};
    
        function sayColor(){
            alert(this.color);
        }
        sayColor();//red
        o.sayColor =sayColor;
        o.sayColor();//blue
    

    注意:函数的名字仅仅是一个包含指针的变量而已,因此,即使是在不同的环境中执行,全局的sayColor()函数与o.sayColor()指向的仍然是同一个函数。

    函数对象的属性 caller

    function outer(){
            inner();
        }
        function inner(){
            alert(inner.caller);
            alert(arguments.callee.caller);
        }
        outer();
    

    以上代码会导致警告框中显示outer()函数的源代码。因为outer()调用了inner(),所以inner.caller指向了outer().

    八、函数方法

    每个函数都包含两个非继承而来的方法:apply()call()。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值。

    apply()方法:接受两个参数,一个是在其中运行函数的作用域,另一个是参数数组。其中第二个参数可以是Array的实例也可是arguments对象。举个栗子:

    function sum(num1,num2){
            return num1 + num2;
        }
        function callSum1(num1,num2){
            return sum.apply(this,arguments);
        }
        function callSum2(num1,num2){
            return sum.apply(this,[num1,num2]);
        }
    
        alert(callSum1(10,20));//30
        alert(callSum2(10,10));//20
    

    call()方法:第一个参数是this值,没有变化,变化的是其余参数都直接传递给函数,就是说,在使用call()方法时,传递给函数的参数必须逐个列举出来。

    function sum(num1,num2){
            return num1 + num2;
        }
        function callSum1(num1,num2){
            return sum.call(this,num1,num2);
        }
    
        alert(callSum1(10,20));//30
    

    使用call()方法时,必须明确地传入每一个参数。至于使用哪种方式比较好,取决于给函数传递参数的方式最方便。

    以上两种方法,最强大的地方在于是能够扩充函数的作用域。

    window.color = "red";
        var o = {color:"blue"};
        function sayColor(){
            alert(this.color);
        }
        sayColor();//red
        sayColor.call(this);//red
        sayColor.call(window);//red
        sayColor.call(o);//blue
    

    bind()方法:这个方法会创建一个函数的实例,其this值会被绑定到传给`bind()函数的值。例如:

    window.color = "red";
        var o = {color:"blue"};
        function sayColor(){
            alert(this.color);
        }
        var objectSayColor = sayColor.bind(o);
        objectSayColor();//blue
    

    关于函数先就记录到这里,最近在看《JavaScript高级程序设计》这本书,收获很多,文章内容摘自这本书第三章和第五章。感兴趣的小伙伴可以买这本书看看。

    相关文章

      网友评论

      本文标题:ECMAScript函数详解

      本文链接:https://www.haomeiwen.com/subject/lksvfxtx.html