Cocos Creator_JavaScript权威指南(第六版)_第8章_函数

函数是这样一段JavaScript代码,它只定义一次,但可能被执行或调用任意次。你可能已经从诸如子例程或者过程这些名字里对函数的概念有所了解。JavaScript函数是参数化的:函数的定义会包括一个称为形参的标识符列表,这些参数在函数体中像局部变量一样工作。函数调用会为形参提供实参的值。函数使用它们实参的只来计算返回值,成为该函数调用表达式的值。除了实参之外,每次调用还会拥有另一个——本次调用的上下文——这就是this关键字的值。
如果函数挂载在一个对象上,作为对象的一个属性,就称为他为对象的方法。当通过这个对象来调用函数时,该对象就是此次调用的上下文,也就是该函数的this的值。用于初始化一个新创建的对象的函数称为构造函数。
在JavaScript里,函数即对象,程序可以随意控制他们。比如,JavaScript可以把函数赋值给变量,或者作为参数传递给其他函数。因为函数就是对象,所以可以给他们设置属性,甚至调用它们的方法。
JavaScript的函数可以嵌套在其他函数中定义,这样它们就可以访问它们被定义时所处的作用域中的任何变量。这意味着JavaScript函数构成了一个闭包,他给JavaScript带来了非常强劲的编程能力。

8.1 函数定义
函数使用function关键字来定义,它可以用在函数定义表达式(4.3)或者函数声明语句里。在两种形式中,函数定义都从function关键字开始,其后跟随这些组成部分:
·函数名称标识符。函数名称是函数声明语句必需的部分。它的用途就像变量的名字,新定义的函数对象会赋值给这个变量。对函数定义表达式来说,这个名字是可选的:如果存在,该名字只存在于函数体中,并指代该函数对象本身。
·一对圆括号,其中包含由0个或者多个用逗号隔开的标识符组成的列表。这些标识符是函数的参数名称,它们就像函数体中的局部变量一样。
·一对花括号,其中包含0条或多条JavaScript语句。这些语句构成了函数体:一旦调用函数,就会执行这些语句。
下例分别展示了函数语句和表达式两种方式的函数定义。注意,以表达式来定义函数只适用于它作为一个大的表达式的一部分,比如在赋值和调用过程中定义函数:

   //输出o的每个属性的名称和值,返回undefined
    printprops:function (o) {
           
        for (var p in o) {
           console.log(p+":"+o[p]+"\n");         
        }
    },
     var o = {a:1,b:2,c:3,d:4,e:5,f:6};
     console.log(this.printprops(o));

    //计算两个笛卡尔坐标(x1,y1)和(x2,y2)之间的距离
    distance:function(x1,y1,x2,y2){
   
        var dx = x2 -x1;
        var dy = y2 -y1;
        return Math.sqrt(dx*dx +dy*dy);
    },
    console.log(this.distance(0,1,0,1));

    //计算阶乘的递归函数(调用自身的函数)
    //x!的值是从x到x递减(步长为1)的值的累乘
    factorial:function (x) {
   
        if (x<=1)   return 1;
        return x*this.factorial(x-1);              
    },
     console.log(this.factorial(3));

    //这个函数表达式定义了一个函数用来求传入参数的平方
    //注意我们把它赋值给一个变量 
     var square = function (x) {
    return x*x;}
     console.log(square(9));

    //函数表达式可以包含名称,这在递归时很有用
     var f = function fact(x) {
   
         if (x<=1) return 1;
         else return x*fact(x-1);           
      };
      console.log(f(3));

    //函数表达式有时定义后立即调用
     var tensquared = (function (x) {
   return x*x;}(10));
     console.log(tensquared);       

    //函数表达式也可以作为参数传给其他函数     
     console.log( data.sort(function (a,b) {
    return a-b;}));

注意:以表达式方式定义的函数,函数的名称是可选的。一条函数声明语句实际上声明了一个变量,并把一个函数对象赋值给它。相对而言,定义函数表达式时并没有声明一个变量。函数可以命名,就像上面的阶乘函数,他需要一个名称来指代自己。如果一个函数定义表达式包含名称,函数的局部作用域将会包含一个绑定到函数对象的名称。实际上,函数的名称将成为函数内部的一个局部变量。通常而言,以表达式方式定义函数时都不需要名称,这会让定义它们的代码更为紧凑。函数定义表达式特别适合用来定义那些只会用到一次的函数,比如上面展示的最后两个例子。

 函数命名:
 任何合法的JavaScript标识符都可以用做一个函数的名称。命名时要尽量选择描述性强而又简洁的函数名。在这两者之间做到恰到好处是一门艺术,需要丰富的经验。有效的函数名可以极大地改善代码的可读性(从而也提高了可维护性)。
  函数名通常是动词或以动词为前缀的词语。通常函数名的第一个字符为小写,这是一种编程约定。当函数名包含多个单词时,一种约定是将单词以下划线分隔,就像like_this()。还有另外一种约定,就是除了第一个单词之外的单词首写字母使用大写字母,就像likeThis()。有一些函数就是用做内部函数或私有函数(不是作为公用API的一部分),这种函数名通常以一条下划线为前缀。
  在一些编程风格中,或者编程框架里,通常为那些经常调用的函数指定短名称,比如客户端JavaScript框架jQuery就将最常用的方法重命名为$()(一个美元符号)(2.4中提到,美元符号和下划线是除了字母和数字之外的两个合法的JavaScript标识符)。

如5.3.2所述,函数声明语句“被提前”到外部脚本或外部函数作用域的顶部,所以以这种方式声明的函数,可以被再它定义之前出现的代码所调用。不过,以表达式定义的的函数就另当别论了,为了调用一个函数,必须要能引用它,而要使用一个以表达式方式定义的函数之前,必须把它赋值给一个变量。变量的声明提前了(3.10.1),但给变量赋值是不会提前的,所以,以表达式方式定义的函数在定义值之前无法调用。
上文所述大多数函数(但不是全部)包含一条return语句(5.6.4)。return语句导致函数停止执行,并返回它的表达式(如果有的话)的值给调用者。如果return语句没有一个与之相关的表达式,则它返回undefined值。如果一个函数不包含return语句,那它就只执行函数体中的每条语句,并返回undefined值给调用者。
上文中大多数函数都是用来计算出一个值的,他们使用return把值返回给调用者。而printprops()函数的不用之处在于,它的任务是输出对象各属性的名称和值。
没有必要返回值,该函数不包含return语句。printprops()函数的返回值始终是undefined。(没有返回值的函数有时候称为过程)

嵌套函数
在JavaScript里,函数可以嵌套在其他函数里。例如

      hypotenuse:function (a,b) {
   
        function ab(x) {
   
            return x*x;
        };
        console.log(ab(9));        
        return Math.sqrt(ab(a)+ab(b));       
    },

嵌套函数的有趣之处在于它的变量作用域规则:他们可以访问嵌套他们(或多重嵌套)的函数的参数和变量。例如,在上面的代码里,内部函数square()可以读写外部函数hypotenuse()定义的参数a和b。这些作用域规则对内嵌函数非常重要。
5.3.2曾提到,函数声明语句并非真正的语句,ECMAScript规范只是允许他们作为顶级语句。他们可以出现在全局代码里,或者内嵌在其他函数中,但他们不能出现在循环、条件判断,或者try/cache/finally以及with语句中。注意,此限制仅适用于语句声明形式定义的函数。函数定义表达式可以出现在JavaScript代码的任何地方。

8.2 函数调用
构成函数主体的JavaScript代码在定义之时并不会执行,只有调用该函数时,他们才会执行。有4种方式来调用JavaScript函数:
·作为函数
·作为方法
·作为构造函数
·通过它们的call()和apply()方法间接调用
有些JavaScript的实现并未严格遵守这条规则,比如,Firefox就允许在if语句中出现条件函数声明。

8.2.1 函数调用
使用调用表达式可以进行普通的函数调用也可进行方法调用(4.5)。一个调用表达式由多个函数表达式组成,每个函数表达式都是有一个函数对象和左圆括号、参数列表和右圆括号组成,参数列表是由逗号分隔的零个或多个参数表达式组成。如果函数表达式是一个属性访问表达式,即该函数是一个对象的属性或数组中的一个元素,那么他就是一个方法调用表达式。如下代码展示了一些普通的函数调用表达式:

        this.printprops({x:1});
        var total = this.distance(0,0,2,1)+this.distance(2,1,3,5);
        var probability = this.factorial(5)/this.factorial(13);

在一个调用中,每个参数表达式(圆括号之间的部分)都会计算出一个值,计算的结果作为参数传递给另外一个函数。这些值作为实参传递给声明函数时定义的形参。在函数体中存在一个形参的引用,指向当前传入的实参列表,通过它可以获得参数的值。
对于普通的函数调用,函数的返回值成为调用表达式的值。如果该函数返回是因为解释器到达结尾,返回值就是undefined。如果函数返回是因为解释器执行到一条return语句,返回值就是return之后的表达式的值,如果return语句没有值,则返回undefined。
根据ECMAScript 3和非严格的ECMAScript 5对函数调用的规定,调用上下文(this的值)是全局对象。然而,在严格模式下,调用上下文则是undefined。
以函数形式调用的函数通常不使用this关键字。不过,“this”可以用来判断当前是否是严格模式。

       //定义并调用一个函数来确定当前脚本运行时是否为严格模式
       var strict = (function () {
   return !this;}());

8.2.2 方法调用
一个方法无非是个保存在一个对象的属性里的JavaScript函数。如果有一个函数f和一个对象o,则可以用下面的代码给o定义一个名为m()的方法:

   o.m = f;

给对象o定义了方法m(),调用它时就像这样:

   o.m();

或者,如果m()需要两个实参,调用起来则像这样:

   o.m(x,y);

上面的代码是一个调用表达式:它包括一个函数表达式o.m,以及两个实参表达式x和y,函数表达式本身就是一个属性访问表达式(4.4),这意味着该函数被当做一个方法,而不是作为一个普通函数来调用。
对方法调用的参数和返回值的处理,和上面所描述的普通函数调用完全一致。但是,方法调用和函数调用有一个重要的区别,即:调用上下文。属性访问表达式由两部分组成:一个对象(o)和属性名称(m)。在像这样的方法调用表达式里,对象o成为调用上下文,函数体可以使用关键字this引用该对象。下面是一个具体的例子:

            var calcuator = {  //对象直接量
            operand1:1,
            operand2:1,
            add:function () {
   
                //注意this关键字的用法,this指代当前对象
                this.result = this.operand1 + this.operand2;              
            }
        };
        calcuator.add(); //这个方法调用计算1+1的结果
        calcuator.result // 2

大多数方法调用使用点符号来访问属性,使用方括号(的属性访问表达式)也可以进行属性访问操作。下面两个例子都是函数调用:

 o["m"](x,y)  //o.m(x,y)的另一种写法
 a[o][z]    //同样是一个方法调用(这里假设a[o]是一个函数)

方法调用可能包括更复杂的属性访问表达式:

customer.surname.toUpperCase(); //调用customer.surname的方法
f().m();//在f()调用结束后继续调用返回值中的方法m()

方法和this关键字是面对对象编程范例的核心。任何函数只要作为方法调用实际上都会传入一个隐式的实参——这个实参是一个对象,方法调用的母体就是这个对象。通常来讲,基于那个对象的方法可以执行多种操作,方法调用的语法已经很清晰地表明了函数将基于一个对象进行操作,比较下面两行代码:

        rect.setSize(width,height);
        setRectSize(rect,width,height);

我们假设这两行代码的功能完全一样,它们都作用于一个假定的对象rect。可以看出,第一行的方法调用语法非常清晰地表明这个函数执行的载体是rect对象,函数中的所有操作都将基于这个对象。

方法链
当方法的返回值是一个对象,这个对象还可以再调用它的方法。这种方法调用序列中(通常称为“链”或者“级联”)每次的调用结果都是另外一个表达式的组成部分。比如,基于jQuery库(19),我们常常会这样写代码:

  //找到所有的header,取得他们id的映射,转换为数组并对它们进行排序
  $(":header").map(function(){
   return this.id}).get().sort();

当方法并不需要返回值是,最好直接返回this。如果在设计的API中一直采用这种方式(每个方法都返回this),使用API就可以进行“链式调用”风格的编程,在这阵编程风格中,只要指定一次要调用的对象即可,余下的方法都可以基于此进行调用:

 shape.setX(100).setY(100).setSize(50).setOutline("red").setFill("blue").draw();

不要将方法的链式调用和构造函数的链式调用混为一谈。

需要注意的是,this是一个关键字,不是变量,也不是属性名。JavaScript语法不允许给this赋值。
和变量不同,关键字this没有作用域的限制,嵌套的函数不会从调用它的函数继承this。如果嵌套函数作为方法调用,其this的值指向调用它的对象。如果嵌套函数作为函数调用,其this值不是全局对象(非严格模式下)就是undefined(严格模式下)。很多人误以为调用嵌套函数是this的值保存在一个变量里,这个变量和内部函数都同在一个作用域内。通常使用变量self来保存this,比如:

        var o = {            //对象o
            m:function () {
     //对象中的方法m()
                var self = this; //将this的值保存至一个变量中
                console.log(this === 0);//输出true,this就是这个对象o
                f();         //调用辅助函数f()

                function f() {
    //定义一个嵌套函数f()
                    console.log(this === 0);//false:this的值是全局对象或undefined
                    console.log(self === 0);//true:self指外部函数的this值               
                } 
            }
        }
    o.m(); //调用对象o的方法m(); 返回undefined;

在8.7.4节的例8-5中有var self = this 更切合实际的用法。

8.2.3 构造函数调用
如果函数或者方法调用之前带有关键字new,他就构成构造函数调用(构造函数调用在4.6和6.1.2有简单介绍)。构造函数调用和普通的函数调用以及方法调用在实参处理、调用上下文和返回值方面都有不同。
如果构造函数调用在圆括号内包含一组实参列表,先计算这些实参表达式,然后传入函数内,这和函数调用和方法调用是一致的。但如果构造函数没有形参,JavaScript构造函数调用的语法是允许省略实参列表和圆括号的。凡是没有形参的构造函数调用都可以省略圆括号,比如,下面这两行代码就是等价的:

var o = new Object();
var b = new Object;

构造函数用创建一个新的空对象,这个对象继承自构造函数的prototype属性。构造函数视图初始化这个新创建的对象,并将这个对象用做其调用上下文,因此构造函数可以使用this关键字来引用这个新创建的对象。注意,尽管构造函数看起来像一个方法调用,他依然会使用这个新对象作为调用上下文。也就是说,在表达式new o.m()中,调用上下文并不是o。
构造函数通常不使用return关键字,它们通常初始化新对象,当构造函数的函数体执完毕时,它会显示返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。然后如果构造函数显式地使用returen语句返回一个对象,那么调用表达式的值就是这个对象。如果构造函数使用return语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果。

8.2.4 间接调用
JavaScript中的函数也是对象,和其他JavaScript对象没什么区别,函数对象也可以包含方法。其中的两个方法call()和apply()可以用来间接地调用函数。两个方法都允许显式指定调用所需的this值,也就是说,任何函数可以作为任何对象的方法来调用,哪怕这个函数不是那个对象的方法。两个方法都可以指定调用的实参。call()方法使用它自有的实参列表作为函数的实参,apply()方法则要求以数组的形式传入参数。

8.3 函数的实参和形参
JavaScript中的函数定义并未指定函数形参的类型,函数调用也未对传入的实参值做任何类型检查。实际上,JavaScript函数调用甚至不检查传入形参的个数,

8.3.1 可选形参
当调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的形参都将设置为undefined值。因此在调用函数是形参是否可选以及是否可以省略应当保持较好的适应性。为了做到这一点,应当给省略的参数赋一个合理的默认值,

    //将对象o中可枚举的属性名追加至数组a中,并返回这个数组a
    //如果省略a,则创建一个新数组并返回这个新数组。
    getPropertyNames:function (o,/*optional*/a) {
   
        if (a===undefined)a = [];//如果为定义,则使用新数组
        for (var property in o) a.push(property);
        return a ; 
    },
        var o ={x:1,y:2,z:3,w:4};
        //这个函数调用可以传入1个或2个参数
        var a = this.getPropertyNames(o); //将o的属性存储到一个新数组中
        var p ={a:1,b:2,c:3,d:4,e:5}
        this.getPropertyNames(p,a)//将p的属性追加至数组a中

如果在第一行代码中不使用if语句,可以使用“||”运算符,这是一种习惯用法:a = a || [];
“||”运算符,如果第一个实参是真值的话就返回第一个实参;否则返回第二个实参。在这个场景下,如果作为第二个实参传入任意对象,那么函数就会使用这个对象。如果省略掉第二个实参(或者传递null以及其他任何假值),那么他就新创建一个空数组,并赋值给a。
需要注意的是,当用这种可选实参来实现函数时,需要将可选实参放在实参列表的最后。那些调用你的函数的程序员是没有办法省略第一个实参并传入第二个实参的,他必须将undefined作为第一个实参显示传入。同样注意在函数定义中使用注释/optional/;熬;来强调形参是可选的。

8.3.2 可变长的实参列表:实参对象
当调用函数的时候传入的实参个别超过函数定义时的形参个数时,没有办法直接获得未命名值的引用。参数对象解决了这个问题。在函数体内,标识符arguments是指向实参对象的引用,实参对象是一个类数组对象(7.11)。这样可以通过数字下标就能访问传入函数的实参值,而不用非要通过名字来得到实参。
假设定义了函数f,它的实参只有一个x。如果调用这个函数时传入两个实参,第一个实参可以通过参数名x来获得,也可以通过arguments[0]
来得到。第二个实参只能通过argument[1]来得到。此外,和真正的数组一样,argument也包含一个length属性,用于标识其所包含元素的个数。因此,如果调用函数f()时穿传入两个参数,arguments.length的值就是2.
实参对象在很多地方都非常有用,下面的例子展示了使用他来验证实参的个数。从而调用正确的逻辑,因为JavaScript本身不会这么做:

      f:
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值