第八章:函数
2017.02.28-2017.03.07
函数
定义一次,多次调用执行。
JavaScript函数是参数化的:函数的定义包括一个称为形参的标识符列表,这些参数在函数体中像局部变量一样工作。除了实参之外,每次调用还会有一个this关键字——本次调用的上下文。
如果一个函数挂载在一个对象上,作为对象的一个属性,就称它为对象的方法。当通过对象来调用函数时,该对象就是此次调用的上下文(context),也就是该函数的this的值。用处初始化一个新创建的对象的函数称为构造函数(constructor)。
JavaScript的函数可以嵌套在其他函数中定义,这样他们就可以访问被定义时所处的作用域中的任何变量。这意味着JavaScript函数构成了一个闭包(closure),其给JavaScript带来了非常强劲的编程能力。
函数定义
function关键字,可以用在函数定义表达式或者函数声明语句里。
var f=function(){return 1;}//函数定义表达式,只有名称提前声明,在定义之前无法调用实际函数
function f(){return 1;}//函数声明语句,可以再定义前出现的代码中调用
- 函数名称标识符。函数声明语句必须的部分,它的用途就像变量的名字,新定义的函数对象会赋值给这个变量。对函数表达式来说,这个名字是可选的;如果存在,改名字只存在于函数体中,并指代函数对象本身。
- 一对圆括号,其中包含0个或多个逗号隔开的标识符组成的列表。标识符是函数的参数名称,就像函数体中的局部变量一样。
- 一对花括号,其中包含0条或多条JavaScript语句。这些语句构成了函数体:一旦调用函数,就会执行这些语句。
【函数定义表达式中的函数名称标识符】
var f=function fact(x){if(x<=1) return 1;else return x*fact(x-1);};
【函数表达式定以后立即调用】
var s=(function(x){return x*x}(10));
【函数作为参数传给其他函数】
data.sort(function(a,b){return a-b;});
内部函数或私有函数的函数名通常以一条下划线为前缀。
没有返回值的函数有时候称为过程。
嵌套函数,函数可以潜逃在其他函数里。嵌套函数可以访问嵌套它们的函数的参数和变量。
函数声明语句并非真正的语句,ECMAScript规范只允许它们作为顶级语句。它们可以出现在全局代码里或者嵌套在其他函数中,但不能呢个出现在循环、条件判断或者try/catch/finally以及with语句中。(有些浏览器实现未遵守规则。)
函数定义表达式语句可以出现在JavaScript代码任意地方。
函数调用
有四种方式调用JavaScript函数:
- 作为函数
- 作为方法
- 作为构造函数
- 通过他们的call()和apply()方法间接调用
函数调用
每个函数表达式都是由一个函数对象和左圆括号、参数列表和右圆括号,参数列表是由逗号分割的另个或多个参数表达式组成。
如果函数表达式是一个属性的访问表达式,即该函数是一个对象的属性或数组中的一个元素,那么他就是一个方法调用表达式。
this在非严格模式下指代全局对象,在ES5严格模式下则是undefined。使用this判断是否是严格模式:
var strict=(function(){return !this;}());
方法调用
o.m=function(){return 1;}//访问 o.m(); 或者 o["m"]();
o.m=function(x,y){return x+y;}//访问 o.m(1,2);或者 o["m"](1,2);
更复杂的
customer.surname.toUpperCase();
f().m();
方法和this关键字是面向对象编程范例的核心。任何函数只要作为方法调用实际上都会传入一个隐式的实参——方法调用的母体。
方法链:当对象的返回值是一个对象,这个对象还可以再调用它的方法。这种方法调用序列中(通常称为”链“)或者”级联“每次的调用结果都是另外一个表达式的组成部分。
//找到所有的header,取得id映射,转换为数组,进行排序
$(":header").map(function(){return this.id}).get().sort();
当方法不需要返回值时,最好直接返回this。
shape.setX(100).setY(100).setSize(50).setOutline("red").setFill("blue").draw();
this是一个关键字,不是变量也不是属性名。JavaScript的语法不允许给this赋值。
this没有作用域的限制,嵌套的函数不会从调用它的函数中继承this。如果嵌套函数作为函数调用,其rhis值是全局对象(非严格模式)或undefined(严格模式)。如果想访问外部函数的this值。需要将this的值保存在一个变量里,这个变量和内部函数都同在一个作用域内。
var o={
m:function(){
var self=this;
console.log(this===o);
f();
function f(){
console.log(this===o);
console.log(self===o);
}
}
}
o.m() //true false true
构造函数调用
如果函数或者方法调用之前带有关键字new,它就构成构造函数调用。
凡是没有形参的构造函数都可以省略圆括号。
var o=new Object();
var o=new Object;//二者等价
构造函数创建一个新的空对象,这个对象继承自构造函数的prototype属性。构造函数试图初始化这个新创建的对象,并将这个对象用做其调用上下文,因此构造函数可以使用this关键字来引用这个新创建的对象。
构造函数通常不使用return关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,他会显示返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。如果构造函数显示地使用return语句返回一个对象,那么调用表达式的值就是这个对象。如果构造函数使用return语句但没有返回指定值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果。
间接调用
call(),apply()可以用来间接地调用函数。两个方法都允许显示指定调用所需的this值。也就是说,任何函数可以作为任何对象的方法来调用,哪怕这个函数不是那个对象的方法。两个方法都可以指定调用的实参。call()方法使用它自有的实参列表作为函数的参数,apply()方法则要求以数组的形式传入参数。
函数的实参和形参
JavaScript中的函数定义并未指定函数形参的类型,也未对传入的实参值做任何类型检查。实际上,JavaScript甚至不检查传入形参的个数。
可选形参
当调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的形参都将设置为undefined。
当用可选实参来实现函数值时,需要将可选实参放在实参列表的最后。当函数的实参可选时往往传入一个无意义的占位符(null或undefined)。同样之一在函数定义中使用注释/*optional*/
来强调形参可选。
可变长的实参列表:实参对象
当调用函数的时候传入的参数个数超过函数定义时的参数个数时,没有办法直接获得未命名值的引用。参数对象解决了这个问题。在函数体内,标识符arguments是指向实参对象的引用,实参对象是一个类数组对象,这样可以通过数字下表就能访问传入函数的参数值。
arguments[0];//第一个参数
arguments[1];//第二个参数
arguments包含一个length属性,用以标识其所包含元素的个数。
实参对象可以让函数操作任意数量的实参。这种函数也被称为”不定实参函数“。
function max(){
var max=Number.NEGATIVE_INFINITY;
for(var i=0;imax) max=arguments[i];
return max;
}
var largest=max(1,10,100,1000,2,3,4,5,100000);//10000
不定实参函数的实参个数不能为零,arguments[]对象适用于下面函数:函数包含固定个数的命名和必需参数,以及随后个数不定的可选实参。
arguments并不是真正的数组,它是一个实参对象。每个实参对象都包含以数字索引的一组元素以及length属性。可以理解为:是一个对象,只是碰巧具有以数字为索引的属性。
数组对象在非严格模式下,当一个函数包含若干形参,实参对象的数组元素是函数形参所对应实参的别名,实参对象中以数字索引,并且形参名称可以认为是相同变量的不同命名。通过实参名字来修改实参值的话,通过arguments[]数组也可以获取到更改后的值。
function f(x){
console.log(x);
arguments[0]=null;
console.log(x);//输出 null
}
在非严格模式下:函数里的arguments仅仅是一个标识符,在严格模式中,它变成了一个保留字。严格模式中的函数无法使用arguments作为形参名或局部变量名,也不能给arguments赋值。
callee和caller
除了数组元素,实参对象定义了callee和caller属性。严格模式下,对写这两个属性都会产生类型错误。非严格模式下,ES标准规范规定callee属性指代当前正在执行的函数。callee是非标准的,但大多数浏览器都实现了这个属性,它指代调用当前正在执行的函数的函数。通过caller属性可以访问调用栈。
var factorial=function(x){
if(x<=1) return 1;
return x*arguments.callee(x-1);
}
将对象属性用做实参
定义参数的时候,传入的实参都写入一个单独的对象之中,在调用的时候传入一个对象,对象中的名/值对是真正需要的实参数据。
function easycopy(args){
arraycopy(args.from,
args.from_start||0,//设置默认值0
args.to,
args.to_start||0,
args.length);
}
var a=[1,2,3,4],b=[];
easycopy({from:a,to:b,length:4});
实参类型
JavaScript方法的形参并未声明类型,在形参传入函数体之前也未做类型检查。可以采用语义化的单词来给函数实参命名。
作为值的函数
函数可以定义也可以调用。
function square(x){return x*x;}
var s=square;
square(4);//16
s(4);//16
var o={square:function(x){return x*x;}}//对象直接量,方法
o.square(16);//256
var a=[function(x){return x*x;},20];//数组直接量
a[0](a[1]);//400
作为命名空间的函数
JavaScript中无法声明只在一个代码块内可见的变量。基于此,我们常常见到地定义一个函数用作临时的命名空间,在这个命名空间内定义的变量都不会污染到全局命名空间。
function mymodule(){
//模块代码
//变量是局部变量,不会污染全局命名空间
}
mymodule();//调用函数
(function(){
//模块代码
}(););//定义函数并立即调用
定义匿名函数并立即调用它的写法非常常见,已经成为惯用法了。
闭包
JavaScript采用词法作用域(lexical scoping),函数的执行依赖于变量作用于,这个作用域是在函数定义时决定的,而不是函数调用时决定的。
为了实现这种词法作用域,JavaScript函数对象的内部状态不仅包含函数的代码逻辑,还不许引用当前的作用域链。
函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内。这种特性在计算机科学文献中称为”闭包“。
从技术的角度讲,所有的JavaScript函数都是闭包:它们都是对象,它们都关联到作用域链。定义大多数函数时的作用域链在调用函数时依然有效,单着并不影响闭包。当调用函数时闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域链时,事情就变得非常微妙。当一个函数潜逃了另外一个函数,外部函数将嵌套的函数对象作为返回值返回的时候往往会发生这种事情。
var scope="global scope";
function checkscope(){
var scope="local scope";
function f(){return scope;}
return f();
}
checkscope();//localscope
var scope="global scope";
function checkscope(){
var scope="local scope";
function f(){return scope};
return f;
}
checkscope()();//localscope
JavaScript函数的执行用到了作用域链,这个作用域链是函数定义的时候创建的。嵌套的函数定义在这个作用域链里。
闭包可以捕捉到局部变量(和参数),并一直保存下来,看起来像这些变量绑定到了在其中定义它们的外部函数。
实现闭包
函数定义时的作用域连接到函数执行时仍然有效。每次调用JavaScript函数的时候,都会为之创建一个新的对象来保存局部变量,把每个对象添加至作用域链中。当函数返回的时候,就从作用域链中将这个绑定变量的对象删除。如果不存在嵌套的函数,也没有其他引用指向这个绑定对象,它就会被当做垃圾回收掉。如果定义了嵌套函数,每个嵌套的函数都各自对应一个作用域链,并且这个作用域链指向一个变量绑定对象。但如果这些嵌套的函数对象在外部函数中保存下来,那么它们也会和指向的变量绑定对向一样当做垃圾回收。但是如果这个函数定义了嵌套的函数,并将它作为返回值返回或者存储在某处的属性里,这时就会有一个外部引用指向这个嵌套的函数。他就不会被当做垃圾回收,并且他所指向的变量绑定对象也不会被当做垃圾回收。
function counter(){
var n=0;
return {
count:function (){return n++;},
reset:function (){n=0;}
};
}
var c=counter(),d=counter();
c.count();//0
d.count();//0
c.reset();
c.count();//0
d.count();//1
函数属性、方法和构造函数
length属性
arguments.length表示传入函数的实参的个数。
函数本身的length属性是只读的,代表函数参数的数量。这里是指在函数定义时给出的实参个数,通常也是在函数调用时期望传入函数的实参的个数。
function check(args){
var actual=args.length;
var expected=args.callee.length;
if (actual!=expected){
throw Error("Expected"+expected+"args;got"+actual);
}
}
function f(x,y,z){
check(arguments);
return x+y+z;
}
f(1,2);//Error: Expected3args;got2
f(1,2,3);//6
prototype 属性
指向一个原型对象的引用,每一个函数都包含不同的原型对象。
call()/apply()
可以将call(),apply()看作是某个对象的方法,通过调用方法的形式来间接调用函数。
call()/apply()的第一个实参室要调用函数的母对象,它是调用上下文,在函数体内通过this来获得对它的引用。
要想以对象o的方法来调用函数f(),可以使用 call()/apply()
f.call(o);
f.apply(o);
类似于下面代码的功能(假设对象o中不存在名为m的属性)
o.m=f;
o.m();
delete o.m;
严格模式下,call()/apply()第一个实参都会变成this值,哪怕传入的实参是原始值甚至是null或undefined。
非严格模式下,传入的null,undefined都会被全局对象代替,而其他原始值也会被相应的包装对象所替代。
对于call()来说,第一个调用上下文实参之后的所有是按就是要传入待调用函数的值。
f.call(o,1,2);
apply()传入实参的形式和call()不同,它的实参都放入一个数组当中。
f.apply(o,[1,2]);
如果一个韩式的实参可以是任意数量,给apply()传入的实参数组可以是任意长度的。
例如:使用Math.max()找到数组中最大的元素。
var biggest=Math.max.apply(Math,array_of_numbers);
注意,传入apply()的实参数组可以是类数组对象也可以是真实数组。可以将当前函数的arguments数组直接传入apply()。
【为函数调用添加日志】
var o = {f:function(a,b){
return a+b;
}};
function trace(o,m){
var original=o[m];
o[m]=function(){
console.log(new Date()+"Entering:"+m);
var result=original.apply(this,arguments);//用原来的参数调用原来的函数
console.log(new Date()+"Exiting:"+m);
return result;
}
}
trace(o,"f");//修改原来的函数,修改之后,再调用f之前和之后都会输出日志
o.f(1,2);
//输出下面三行
Tue Mar 07 2017 16:33:51 GMT+0800 (CST)Entering:f
Tue Mar 07 2017 16:33:51 GMT+0800 (CST)Exiting:f
3
bind()
函数的主要作用就是将函数绑定至被某个对象。当在函数f()上调用bind()方法并传入一个对象o作为参数,这个方法将返回一个新的函数。调用新的函数将会把原始的函数f()当做o的方法来调用。传入新函数的任何实参都将传入原始函数。
function f(y){return this.x+y;};//待绑定的函数
var o={x:1};//要绑定的对象
var g=f.bind(o);//通过调用g(x)来调用o.f(x)
g(2);//3
o;//{x:1}
g;//[Function: bound f](nodejs输出) function f(y){return this.x+y;}(chrome输出)
g.toString();// 'function () { [native code] }'(nodejs输出) "function () { [native code] }" (chrome 输出)
bind()方法不仅仅是将函数绑定至一个对象,还附带一些其他应用:除了第一个实参之外,传入bind()的实参也会绑定至this,这个附带的应用是一种常见的函数式编程技术,有时也被称为“柯里化”(currying)。
var sum=function(x,y){return x+y;};
var succ=sum.bind(null,1);//将succ绑定到null,并将第一个参数x绑定到1
succ(2);//3
function f(y,z){return this.x+y+z};
var g=f.bind({x:1},2);//将f绑定到{x:1}对象,并且将第一个参数y绑定到2.
g(3);//6 this.x绑定到1,y绑定到2,z绑定到3
toString()
返回一个字符串。大多数的toString()返回函数的完整源码。内置函数旺旺返回一个类似'[native code]'的字符串作为函数体。
Function()构造函数
不管是通过函数定义语句还是函数直接表达式,函数的定义都要使用function关键字。此外,还可以通过Function()构造函数定义。
var f=new Function("x","y","return x*y");
几乎等价于:
var f=function(x,y){return x*y;}
Function()构造函数可以传入任意数量的字符串实参,最后一个实参所表示的文本就是函数体,可以包含任意的JavaScript语句,每两条语句之间用分号分隔。传入构造函数的其他所有的实参字符串都是制定函数的形参名字的字符串。如果定义的函数不包含任何参数,只须给构造函数简单的传入一个字符串函数体即可。
Function()创建一个匿名函数。
- Function()构造函数允许JavaScript在运行时动态的创建并编译函数。
- 每次调用Function()构造函数都会解析函数体,并创建新的函数对象。如果是在一个循环或者多次调用的函数中执行这个构造函数,执行效率会受影响。
- Function()所创建的函数并不是使用词法作用域,相反,函数体代码的编译总是会在全局作用域执行。
【实例】
var scope="global";
function constructFunction(){
var scope="local";
return new Function("return scope")();
}
constructFunction();//global
var scope="global";
function constructFunction(){
var scope="local";
return new Function("return scope");
}
constructFunction()();//global
可调用的对象
所有的函数都是可调用的,但是并非所有的可调用对象都是函数。
【检测对象是否是真正的函数对象】
function isFunction(x){
return Object.prototype.toString.call(x)==="[object Function]";
}
isFunction(RegExp);//true,chrome58下,nodejs6.9.5下
isFunction(Window.alert);//false,chrome58下
isFunction(document.getElementById);//true,chrome58下
函数式编程
JavaScript不是函数式编程语言,但是JavaScript中可以像操作对象一样操作函数,即可以在JavaScript中应用函数式编程技术。
使用函数处理数组
高阶函数
高阶函数就是操作函数的函数,接收一个或多个函数作为参数,并返回一个新函数。
不完全函数
比如bind
记忆
将上次的计算结果缓存起来。