(第三天)函数

定义函数

关键字function用来定义函数。定义函数有两种方法

(1)函数定义表达式

1 var f = function(x) { return x+1; }

(2)函数声明语句

1 function funcname([arg1 [, arg2 [...,argn]]]) {
2 
3 }

 函数声明语句通常出现在JavaScript代码的最顶层,也可以嵌套在其他函数体内。但在嵌套时,函数声明只能出现在所嵌套函数的顶部。也就是说函数定义不能出现在if语句、while语句后者其他语句中。

二者异同

(1)都创建了相同的新函数对象

(2)函数声明语句中的函数名是一个变量名,变量指向函数对象。函数定义表达式并未声明一个变量,如果一个函数定义表达式包含名称,函数的局部作用域将会包含一个绑定到函数对象的名称。所以名称存在函数体中,并指代该函数本身,也就是说函数的名称将成为函数内部的一个局部变量。【注】函数定义表达式特别适合用来定义那些只会用到一次的函数

(3)函数声明语句中的函数被显式地“提前”到了脚本或函数顶部。因此它们在整个脚本和函数内都是可见的。但以函数表达式定义函数则不同,想要调用此定义的函数,必须要引用它,而要使用一个表达式方式定义的函数之前,必须要把他赋值给一个变量,前面说过:变量的声明提前了,但给变量赋值是不会提前的,所以,以表达式方式定义的函数在定义之前无法调用。如下代码:

1 person();   /*函数声明语句显式提前至顶部,所以能在定义之前调用*/
2 function person() {}
3 person();              /*在函数定义之后调用毋庸置疑正确*/
1 p();
2 
3 var p = function(){
4 
5 }
6 
7 /*
8 打印出:undefined is not a function 变量p还未初始化,因此函数定义表达式无法在函数定义之前调用
9 */

函数调用 

(1)作为函数

1 function person(age,name){
2 
3 }
4 
5 person(12,"嘿嘿");   /*函数调用*/

(2)作为方法

 1 var calculator = {
 2       operator1 : 1,
 3       operator2 : 2,
 4       add: function(){
 5              this.result = this.operator1 + this.operator2;
 6       }
 7 
 8 };
 9 
10 calculator.add();      //方法调用计算1+1的结果 
11 calculator.result      =>2
12  
13 /*
14 通过对象直接量来创建属性和方法,此时calculator作为调用上下文,所以this关键字则引用该对象
15 */

(3)作为构造函数

1 var o = new Object();
2 
3 var o = new Obejct;
4 
5 /*
6 构造函数调用创建一个新的空对象,这个对象继承自构造函数的prototype属性。构造函数试图初始化这个新创建的对象,并将这个对象用做其调用上下文,因此构造函数可以使用this关键字来引用这个新创建的对象
7 */

【注】如果构造函数没有形参,JavaScript构造函数调用的语法是允许省略实参列表和圆括号的。所以凡是没有形参的构造函数调用都可以省略圆括号。

(4)通过它们的call()和apply方法间接调用 【后面讲】

this关键字

this是 一个关键字,不是变量,也不是属性名。JavaScript的语法不允许给this赋值。和变量不同,关键字this没有作用域的限制,嵌套的函数不会从调用它的函数中继承this。对此我们用实例说明

 1 var o ={
 2       m: function() {
 3              var self = this;
 4              console.log(this === o);  (1 5              f();
 6             
 7              function() {
 8                      console.log(this === o);  (2 9                      console.log(self == o);     (310              }
11       }
12 }
13 
14 /*
15 上述(1)中打印出true,因为this就指代o。(2)中输出false,此时this是全局对象或undined。(3)输出true,self指代外部函数的this值
16 */

综上所知:

(1)如果嵌套函数作为方法调用,其this的值指向调用它的对象。

(2)如果嵌套函数做为函数调用,其this值不是全局对象(非严格模式下)就是undefined(严格模式下)。

(3)如果想访问外部函数的this的值,需要将this的值保存在一个变量里,这个变量和内部函数都同在一个作用域内。通常使用变量self来保存this。

作为命名空间的函数

引入

当有一段JavaScript模块代码,这段代码将要用在不同的JavaScript程序中(对于客户端JavaScript来讲通常是用在各种各样的网页中)。假如这段代码定义了一个用以存储中间计算结果的变量。如此就出现一个问题,当模块代码放在不同的程序中时,你无法得知这个变量是否已经建好,如果已经存在这个变量,那么将会和代码发生冲突。解决办法就是将代码放入一个函数内,然后调用这个函数。这样全局变量就变成了函数内的局部变量。用匿名函数来定义,用法如下

1 (function(){
//模块代码中所使用的变量都是局部变量
2 console.log(12) (13 }()); //结束函数定义并立即调用它 4 5 或者 6 7 (function(){ 8 console.log(12); (29 })();

这里有个疑问尚未解决,如果有园友知道,希望能帮助我解决,谢谢!不知道大家注意到上述两个匿名函数的不同之处,对,在(1)中输出的最后没有加分号,若加分号则错误,无法输出,不加分号则正常输出,(2)中则是好使的!希望得到各位园友的帮助,在此表示感谢!

这种定义匿名函数并立即在单个表达式中调用它的写法很常见,如代码检测中是否出现了一个bug,如果出现这个bug,就返回一个带补丁的函数的版本! 

函数属性、方法

(1)length属性

 length到没什么可说的,最主要是要属函数的arguments的属性了,在函数体内,arguments.length表示传入函数的实参的 个数,以此来模拟函数重载。请看下面代码

 1                          function person(age, name, gender, addr) {
 2                              this.age = age;
 3                              this.name = name;
 4                              this.gender = gender;
 5                              this.addr = addr;
 6                                 /*获得传入实参的个数*/ 
 7                              switch(arguments.length)
 8                              {
 9                                   case  1:
//
10 break; 11 case 2:
//
12 break; 13 case 3:
//
14 break; 15 case 4:
//
16 break; 17 } 18 } 19 20 var p = new person(1,'嘿嘿'); 21 var p1 = new person(1,'嘿嘿',false); 22 var p2 = new person(1,'嘿嘿',false,'hunan'); 23 24

补充:arguments的callee属性

引入

1             var factorial = function(x) {
2                 if (x <= 1) return 1;
3                 return x * factorial(x - 1);
4             }
5             console.log(factorial(5));
6                 /*打印出120*/

上述代码为求一个数的阶乘,毫无疑问没有错误。现在进行一点小改动,如下

1             var factorial = function(x) {
2                 if (x <= 1) return 1;
3                 return x * factorial(x - 1);
4             }
5             var fact2 = factorial;
6             factorial = function() {
7                 return 0;
8             }
9             console.log(fact2(5));

上述很明显fact2变量指向两个函数,当要求调用fact2(5)时,先执行 第一个 factorial函数,当执行到 return x * factorial(x - 1); 时,这时执行的就是 第二个 factorial函数,所以此时打印出0。很显然这不是我们想要的结果,我们需要的是求阶乘即执行的函数接下来还是它本身也就是第一个,所以这个时候callee()方法就派上了用场:用于调用自身。所以上述代码这样修改即可 

1             var factorial = function(x) {
2                 if (x <= 1) return 1;
3                 return x * arguments.callee(x - 1);
4             }
5             var fact2 = factorial;
6             factorial = function() {
7                 return 0;
8             }
9             console.log(fact2(5));

 

【注】arguments还有一个长得相似callee的属性就是caller,而在非严格模式下,ECMAScript标准规范规定callee属性指代当前正在执行的函数。caller是非标准的但大多数浏览器都 实现了这个属性,它指代当前正在执行的函数的函数。通过caller属性可以访问调用栈。而通过callee来进行递归调用自身,因为它保存了当前执行方法的地址,而不会出差错。

(2) prototype属性

每个函数都包含一个prototype属性,这个属性指向一个对象的引用,这个对象叫做原型对象。每一个函数都包含不同的原型对象。当将函数用做构造函数的时候,新创建的对象会从原型对象上继承属性。有关原型对象前面已讲,请参考原型、继承这一讲

(3)call()和apply()方法

这两种方法可以用来间接地调用函数,两个方法都允许显式指定调用所需的this的值,任何函数可以作为任何对象的方法来调用,哪怕这个函数不是那个对象的方法。两个方法都可以指定调用的实参。call()方法使用它自有的实参列表作为函数的实参,apply()方法则要求以数组的形式传入参数。在ECMAScript5的严格模式中,call()和apply()的第一个实参都会变为this的值,哪怕传入的实参是原始值甚至是null或undifined。在ECMAScript3和非严格模式中,传入的null和undefined都会被全局对象代替,而其他原始值则会被相应的包装对象所替代。

 下面用代码来解释上述概念

(1)call()方法

 1                 function person(age,name){
 2                     this.age=age;
 3                     this.name=name;
 4                 }
 5    
 6                 var obj=new Object();
 7                 person.call(obj,12,'小黑');
 8                 console.log(obj.age);
 9                 console.log(obj.name);
10 
11                /*
12                创建空对象obj,此时调用person类的call()方法,并将obj传递进去,此时obj成为其调用上下文,此时this即obj,最终能打印出12和小黑
13                */

(2)apply()方法 

 更多用法请参考:apply()详情

(3)bind()方法

bind()是在ECMAScript5中新增的方法,但在ECMASript3中可以轻易模拟bind(),从名字可以看出,这个方法的主要作用就是将函数绑定至某个对象上。

 1 function f(y){
 2        return this.x + y;
 3 }
 4 var o = { x : 1 };
 5 var g =  f.bind(o);
 6 console.log(g(2));
 7 
 8 function bind(f,o){
 9        if(f.bind) return f.bind(o);
10        else return function(){
11               return f.apply(o, arguments);
12        };
13 }

上述将函数f绑定至对象o上,当在函数f()上调用bind()方法并传入一个对象o作为参数,这个方法将返回一个新的函数。调用新的函数将会把原始的函数f()当做o的方法来调用。传入新函数的任何实参都将传入原始函数。上述中 f.bind(o); 则此时函数f()中的this即为o,此时this.x=1,绑定后返回新的函数g,再调用函数g(2),此时函数f()即为其参数,所以打印出3。

函数传参

(1)传递原始类型(所谓c#中的值类型)

1                          function person(age){
2                              age++;
3                          }
4                          var age = 12;
5                          person(age);
6                          console.log(age);

根据上述代码你觉得会打印出多少呢?我们一句一句分析:

 (1)var age = 12;栈上开辟一段空间地址假设为0x1,此时0x1存的是变量age并且其值为12。

 (2)person(age);调用函数person并将实参传进去,此时person中的形参相当于是局部变量,所以同样在栈上开辟一段空间地址为0x2,变量为age,将上述(1)中的值赋给(2)中的age所以同样为12,然后进入函数体内,将age++,此时相加的是新创建的局部变量的值,并未改变(1)中的age值。

(3)综上打印出12。

(2)传递对象(所谓c#中的引用类型)

1                          function person(p){
2                                 p.age++;
3                          }
4                          var p = { age : 12 };
5                          person(p);
6                          console.log(p.age);

同理进行分析

(1) var p = { age : 12 };首先在栈上开辟一段空间地址为0x1值为null,然后右边在堆上首先开辟一段空间地址为0x2的空对象,然后定义属性age并且其值为12,并且此对象指向变量p,所以此时变量的值为0x2。

(2)person(p);同样p此时相当于是person类的局部变量,首先在栈上开辟一段空间地址为ox3名字为p的变量,此时将(1)中p的存的值0x2赋给0x3中p的值,所以此时0x3中的值为0x2,也就是此时ox2指向了堆上那个地址为0x2,属性为age值为12的对象。然后进入函数体内部,对其年龄进行age++,此时堆上对象的属性age变为13。

(3)综上,此时打印出的P中的age为13。

 总结传参

无论是传递原始类型(值类型)还是对象(引用类型)只需要看栈上变量的值是存的值还是地址,若存的是值,则相当于复制一份即副本不会改变原来的值,若是变量的值存的是地址,若在函数体内改变值则原来对象中的值也会受影响。

 

上一篇:SQL Server-聚焦sp_executesql执行动态SQL查询性能真的比exec好?


下一篇:(第五天)数组