前端修炼(第三天)函数
Posted 史红星
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端修炼(第三天)函数相关的知识,希望对你有一定的参考价值。
定义函数
关键字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)作为函数
(2)作为方法
(3)作为构造函数
【注】如果构造函数没有形参,JavaScript构造函数调用的语法是允许省略实参列表和圆括号的。所以凡是没有形参的构造函数调用都可以省略圆括号。
(4)通过它们的call()和apply方法间接调用 【后面讲】
this关键字
this是 一个关键字,不是变量,也不是属性名。JavaScript的语法不允许给this赋值。和变量不同,关键字this没有作用域的限制,嵌套的函数不会从调用它的函数中继承this。对此我们用实例说明
综上所知:
(1)如果嵌套函数作为方法调用,其this的值指向调用它的对象。
(2)如果嵌套函数做为函数调用,其this值不是全局对象(非严格模式下)就是undefined(严格模式下)。
(3)如果想访问外部函数的this的值,需要将this的值保存在一个变量里,这个变量和内部函数都同在一个作用域内。通常使用变量self来保存this。
作为命名空间的函数
引入
当有一段JavaScript模块代码,这段代码将要用在不同的JavaScript程序中(对于客户端JavaScript来讲通常是用在各种各样的网页中)。假如这段代码定义了一个用以存储中间计算结果的变量。如此就出现一个问题,当模块代码放在不同的程序中时,你无法得知这个变量是否已经建好,如果已经存在这个变量,那么将会和代码发生冲突。解决办法就是将代码放入一个函数内,然后调用这个函数。这样全局变量就变成了函数内的局部变量。用匿名函数来定义,用法如下
这里有个疑问尚未解决,如果有园友知道,希望能帮助我解决,谢谢!不知道大家注意到上述两个匿名函数的不同之处,对,在(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。
总结传参
无论是传递原始类型(值类型)还是对象(引用类型)只需要看栈上变量的值是存的值还是地址,若存的是值,则相当于复制一份即副本不会改变原来的值,若是变量的值存的是地址,若在函数体内改变值则原来对象中的值也会受影响。
书犹药也,善读之可以医愚
以上是关于前端修炼(第三天)函数的主要内容,如果未能解决你的问题,请参考以下文章