第八章、函数
函数是这样的一段JavaScript代码,只定义一次,但可能被执行或调用任意次。
JavaScript函数是参数化的
如果函数挂载在一个对象上,作为对象的一个属性,就称它为对象的方法。
用于初始化一个新创建的对象的函数称为构造函数(constructor)
函数即对象,程序可以随意操控它们。
JavaScript函数构成了一个闭包(closure),它给JavaScript带来了强劲的编程能力
函数定义
1.函数使用function关键字来定义。
函数名称标识符
一对圆括号
一对花括号
以表达式定义的函数,函数的名称是可选的
函数定义表达式特别适合用来定义那些只会用到一次的函数
eg:
//输出o的每个属性的名称和值,返回undefined
function printprops(o){
for(var p in o){
console.log(p + ":" + o[p] + "\n");
}
}
//计算两个笛卡尔坐标(x1,y1)和(x2,y2)之间的距离
function distance(x1,y1,x2,y2){
var dx = x2 - x1;
var dy = y2 - y1;
return Math.sqrt(dx*dx + dy*dy);
}
//计算阶乘的递归函数(调用自身的函数)
//x的值是从x到x递减(歩长为1)的值的累乘
function factorial(x){
if(x < 1){
return 1;
}
return x * factorial(x-1);
}
//这个函数表达式定义了一个函数用来求传人参数的平方
//注意我们把它赋值给一个变量
var square = function(x){
return x * x;
}
//函数表达式可以包含名称,这在递归时很有用
var f = functon fact(x){
if(x <= 1){
return 1
}else{
return x*fact(x-1)
}
}
//函数表达式也可以作为参数传给其他函数
data.sort(function(a,b){
return a-b;
})
//函数表达式有时定义后立即调用
var tensquared = (function(x){return x *x;}(10))复制代码
2.函数声明语句"被提前"到外部脚本或外部函数作用域的顶部,所以以这种方式声明的函数,可以被在它定义之前出现的代码所调用。
3.嵌套函数
function hypotenuse(a,b){
function square(x){
return x * x;
}
return Math.sqrt(square(a) + aquare(b))
}复制代码
函数调用
1.构成函数主体的JavaScript代码在定义之时并不会执行,只有调用该函数时,它们才会执行
- 作为函数
- 作为方法
- 作为构造函数
- 通过它们的call()和apply()方法间接调用
2.方法调用
(1).一个方法无非是个保存在一个对象的属性里的JavaScript函数。
var calculator = {
operand1:1,
operand2:1,
add: function(){
this.result = this.operand1 + this.operand2;
}
}
calculator.add(); //这个方法调用1+1的结果
calculator.result; //=>2复制代码
(2).方法调用和函数调用有一个重要的区别,即:调用上下文。属性访问表达式由两部分组成:一个对象和属性名称。函数体可以使用关键字this引用该对象。
(3).大多数方法调用使用点符号来访问属性,使用方括号(的属性访问表达式)也可以进行属性访问操作。
o["m"](x,y); //o.m(x,y)
a[o](z);
customer.surname.toUpperCase(); //调用customer.surname的方法
f().m(); //在f()调用结束后继续调用返回值中的方法m()复制代码
(4).任何函数只要作为方法调用实际上都会传人一个隐式的实参——这个实参是一个对象,方法调用的母体就是这个对象。
rect.setSize(width,height);
setReactSize(rect,width,height);复制代码
(5).方法链
//找到所有的header,取得它们id的映射,转换为数组并对它们进行排序
$(":header").map(function(){
return this.id
}).get().sort()复制代码
(6).当方法不需要返回值时,最好直接返回this
(7).this是一个关键字,不是变量,也不是属性名。JavaScript的语法不允许给this赋值
(8).和变量不同,关键字this没有作用域的限制,嵌套的函数不会从调用它的函数中继承this.如果嵌套函数作为方法调用,其this的值指向调用它的对象。如果嵌套函数作为函数调用,其this值不是全局对象(
非严格模式下)就是undefined(严格模式下)。
很多人误以为调用嵌套函数时this会指向调用外层函数的上下文。如果你想访问这个外部函数的this值,需要将this的值保存在一个变量里,这个变量和内部函数都同在一个作用域内。通常使用self来保存this
var o = { //对象o
m:function(){ //对象中的方法m()
var self = this; //将this的值保存至一个变量中
console.log(this === o); //输出true,this就是这个对象o
f(); //调用辅助函数f()
function f(){ //定义一个嵌套函数f()
console.log(this === o); //"false":this的值是全局对象或undefined
console.log(this === self); //"true":selft指外部函数的this值
}
}
}
o.m() //调用对象o的方法m()复制代码
3.构造函数调用
(1).如果函数或者方法调用之前带有关键字new,它就构成构造函数调用
(2).构造函数调用创建一个新的空对象,这个对象继承自构造函数的prototype属性
4.间接调用
(1).call()和apply()可以用来间接地调用函数,两个方法都允许显式指定调用所需的this值
(2).call()方法使用它自有的实参列表作为函数的实参,apply()方法则要求以数组的形式传人参数
函数的实参和形参
1.可选形参
(1).当调用函数的时候传人的实参比函数声明时指定的形参个数要少,剩下的形参都将设置为undefined值
function getPropertyNames(o,/*optional*/a){
if(a === undefined){
a=[]; //如果未定义,则使用新数组 ----》a = a || [];
}
for(var property in o){
a.push(property)
}
return a;
}
//这个函数调用可以传人1个或2个实参
var a = getPropertyNames(o); //将o的属性存储到一个新数组中
getPropertyNames(p,a); //将p的属性追加至数组a中复制代码
2.可变长的实参列表:实参对象
(1).在函数体内,标识符arguments是指向实参对象的引用,实参对象是一个类数组对象,这样可以通过数字下标就能访问传人函数的实参值,而不用非要通过名字来得到实参。
function(x,y,z){
//首先,验证传人实参的个数是否正确
if(arguments.length!=3){
throw new Error("function f called with" + arguments.length + "arguments,but it expects 3 arguments")
}
//再执行函数的其他逻辑...
}复制代码
(2).实参对象有一个重要的用处,就是让函数可以操作任意数量的实参。
内置函数Max.max()
function max(/*...*/){
var max = Number.NEGATIVE_INFINTY;
//遍历实参,查找并记住最大值
for(var i = 0;i < arguments.length;i++){
if(arguments[i] > max){
max = arguments[i];
}
}
return max;
}
var largest = max(1,10,100,98,10000) //=>10000复制代码
这种函数可以接收任意个数的实参,这种函数也称为"不定实参函数"(varargs function)
arguments并不是真正的数组,它是一个实参对象
function f(x){
console.log(x); //输出实参的初始值
arguments[0] = null; //修改实参数组的元素同样会修改x的值
console.log(x); //输出"null"
}复制代码
(3).callee和caller属性
callee属性指代当前正在执行的函数
caller指代调用当前正在执行的函数的函数。通过caller属性可以访问调用栈。
var factorial = function(x){
if(x <= 1){
return 1;
}
return x * arguments.callee(x-1)
}复制代码
3.将对象属性用做实参
通过名/值对的形式来传人参数
//将原始数组的length元素复制至目标数组
//开始复制原始数组的from_start元素
//并且将其复制至目标数组的to_start中
//要记住实参的顺序并不容易
function arraycopy(/*array*/from,/*index*/from_start,
/*array*/to,/*index*/to_start,
/*inter*/length)
{
//逻辑代码
}
//这个版本是实现效率稍微有些低,但你不必再记住实参的顺序
//并且from_start和to_start都默认为0
function easycopy(args){
arraycopy(args.from,
args.from_start || 0, //注意这里设置了默认值
args.to,
args.to_start || 0,args.length);
}
//来看如何调用easycopy()
var a = [1,2,3,4],b = [];
easycopy({from:a,to:b,length:4})复制代码
4.实参类型
function max(/*number...*/){
/*代码区*/
}
function sum(a){
if(isArrayLike(a)){
var total = 0;
for(var i=0;iif(element == null){
continue;
}
if(isFinite(element)){
total += element;
}else{
throw new Error("sum():elements must be finite numbers");
}
}
return total;
}else{
throw new Error("sum():argument must be array-like");
}
}复制代码
flexisum()可以接收任意数量的实参
function flexisum(a){
var total = 0;
for(var i = 0;i < arguments.length;i++){
var element = arguments[i],n;
if(element == null){ //忽略null和undefined实参
continue;
}
if(isArray(element)){ //如果实参是数组
n = flexisum.apply(this,element); //递归地计算累加和
}else if(typeof element === "function"){ //否则,如果是函数。。。
n = Number(element()) //调用它并做类型转换
}else{
n = Number(element) //否则直接做类型转换
}
if(isNaN(n)){ //如果无法转换为数字,则抛出异常
throw Error("flexisum():can't covert" + element + "to number")
}
total += n; //否则,将n累加至total
}
return total;
}复制代码
作为值的函数
函数可以定义,也可以调用,这是函数最重要的特性。
square仅仅是变量的名字,这个变量指代函数对象。函数还可以赋值给其他的变量
function square(x){
return x*x;
}
var s = square; //现在s和square指代同一个函数
square(4); //=>16
s(4); //=>16复制代码
当函数作为对象的属性调用时,函数就称为方法:
var o = { //对象直接量
square:function(x){
return x*x;
}
}
var y = o.square(16); //=>256复制代码
赋值给数组元素时:
var a = [function(x){return x*x},20]; //数组直接量
a[0](a[1]); //=>400复制代码
将函数用做值
//在这里定义一些简单的函数
function add(x,y){return x+y;}
function subtract(x,y){return x-y;}
function multiply(x,y){return x*y;}
function divide(x,y){return x/y;}
//这里的函数以上面的某个函数作为参数
//并给它传人两个操作数然后调用它
function operate(operator,operand1,operand2){
return operator(operand1,operand2);
}
//这行代码所示的函数调用实际上计算了(2+3) + (4*5)的值
var i = operate(add,operate(add,2,3),operate(multiply,4,5));
//我们为这个例子重复实现一个简单的函数
//这次实现使用函数直接量,这些函数直接量定义在一个对象直接量中
var operators={
add:function(x,y){return x+y;},
subtract:function(x,y){return x-y;},
multiply:function(x,y){return x*y;},
divide:function(x,y){return x/y;},
pow:Math.pow //使用预定义的函数
}
//这个函数接收一个名字作为运算符,在对象中查找这个运算符
//然后将它作用于所提供的操作数
//注意这里调用运算符函数的语法
function operate2(operation,operand1,operand2){
if(typeof operators[operation] === "function"){
return operators[operation](operand1,operand2);
}else{
throw "Unknown operator"
}
}
//这样来计算("hello" + " " + "world")的值
var j = operate("add","hello",operate2("add"," ","world"));
//使用预定义的函数Math.pow()
var k = operate2("pow",10,2)复制代码
自定义函数属性
JavaScript中的函数并不是原始值,而是一种特殊的对象。
//初始化函数对象的计数器属性
//由于函数声明被提前了,因此这里是可以在函数声明之前给它的成员赋值的
uniqueInteger.counter = 0;
//每次调用这个函数都会返回一个不同的整数
//它使用一个属性来记住下一次将要返回的值
function uniqueInteger(){
return uniqueInteger.counter++; //先返回计数器的值,然后计数器自增1
}复制代码
factorial()使用自身的属性(将自身当做数组来对待)来缓存上一次的计算结果:
function factorial(n){
if(isFinite(n) && n>0 && n==Math.round(n)){ //有限的正整数
if(!(n in factorial)){ //如果没有缓存结果
factorial[n] = n * factorial(n-1); //计算结果并缓存之
}
return factorial[n]; //返回缓存结果
}else{
return NaN; //如果输入有误
}
}
factorial[1]=1; //初始化缓存以保存这种基本情况复制代码
作为命名空间的函数
在函数中声明的变量在整个函数体内都是可见的(包括在嵌套的函数中),在函数的外部是不可见的。不在任何函数内声明的变量是全局变量,在整个JavaScript程序中都是可见的。
function mymodule(){
//模块代码
//这个模块所使用的所有变量都是局部变量
//而不是污染全局命名空间
}
mymodule(); //不要忘了还要调用这个函数
(function(){ //mymodule()函数重写为匿名的函数表达式
//模块代码
}()); //结束函数定义并立即调用它复制代码
特定场景下返回带补丁的extend()版本
//定义一个扩展函数,用来将第二个以及以后续参数复制至第一个参数
//这里我们处理了IE bug:在多数IE版本中
//如果o的属性拥有一个不可枚举的同名属性,则for/in循环
//不会枚举对象o的可枚举属性,也就是说,将不会正确地处理诸如toString的属性
//除非我们显示检测它
var extend = (function(){ //将这个函数的返回值赋值给extend
//在修复它之前,首先检查是否存在bug
for(var p in {toString:null}){
//如果代码执行到这里,那么for/in循环会正确工作并返回一个简单版本的extend()函数
return function extend(o){
for(var i=1; isource = arguments[i];
//复制所有的可枚举属性
for(var prop in source){
o[prop] = source[prop];
}
}
return o;
};
}
return function patched_extend(o){
for(var i=1;isource = arguments[i];
//复制所有的可枚举属性
for(var prop in source){
o[prop] = source[prop]
}
//现在检查特殊属性
for(var j=0;jif(source.hasOwnProperty(prop)){
o[prop] = source(prop);
}
}
}
return o;
};
//这个列表列出了需要检查的特殊属性
var protoprops = ["toString","valueOf","constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString"]
}())复制代码
闭包
1.JavaScript也采用词法作用域(lexical scoping)
2.函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为"闭包"
3.所有的JavaScript函数都是闭包。
var scope = "global scope"; //全局变量
function checkscope(){
var scope = "local scope"; //局部变量
function f(){return scope;} //在作用域中返回这个值
return f();
}
checkscope(); //=>"local scope"
var scope = "global scope"; //全局变量
function checkscope(){
var scope = "local scope"; //局部变量
function f(){return scope;} //在作用域中返回这个值
return f;
}
checkscope()(); //=>"local scope"复制代码
4.它们可以捕捉到局部变量(和参数),并一直保存下来
var uniqueInter = (function(){ //定义函数并立即调用
var counter =0; //函数的私有状态
return function(){return counter++;}
}())复制代码
5.嵌套的函数是可以访问作用域内的变量的,而且可以访问外部函数中定义的counter变量
function counter(){
var n = 0;
return {
count:function(){return n++;},
reset:function(){return n = 0;}
}
}
var c = counter(), d = counter(); //创建两个计数器
c.count() //=>0
d.count(); //=>0:它们互不干扰
c.reset(); //reset()和count()方法共享状态
c.count(); //=>0:因为我们重置了c
d.count(); //=>1:而没有重置d复制代码
6.counter()函数返回了一个"计数器"对象,这个对象包含两个方法:count()返回下一个整数,reset()将计数器重置为内部状态
这两个方法都可以访问私有变量n
每次调用counter()都会创建一个新的作用域链和一个新的私有变量
如果调用counter()两次,则会得到两个计数器对象,而且彼此包含不同的私有变量,调用其中一个计数器对象的count()或reset()不会影响另外一个对象。
function counter(n){ //函数参数n是一个私有变量
return{
//属性get()方法返回并给私有计算器var递增1
get count(){return n++;}
//属性set不允许n递减
set count(m){
if(m > n){
n = m;
}else{
throw Error("count can only be set to a larger value")
}
}
}
}
var c = counter(1000);
c.count; //=>1000
c.count; //=>1001
c.count=2000; //=>2000
c.count; //=>2000
c.count = 2000; //=>Error复制代码
7.利用闭包实现的私有属性存取器方法
//这个函数给对象o增加了属性存取器方法
//方法名称为get和set.如果提供了一个判断函数setter方法就会用它来检测参数的合法性,然后在存储它
//如果判断函数返回false,setter方法抛出一个异常
//这个函数有一个非同寻常之处,就是getter和setter函数
//所操作的属性值并没有存储在对象o中,相反,这个值仅仅是保存在函数中的局部变量中
//getter和setter方法同样是局部函数,因此可以访问这个局部变量
//也就是说,对于两个存取器方法来说这个变量是私有的没有办法绕过存取器方法来设置或修改这个值
function addPrivateProperty(o,name,predicate){
var value; //这是一个属性值
//get方法简单地将其返回
o["get" + name] = function(){return value;};
//set方法首先检查值是否合法,若不合法就抛出异常
o["set" + name] = function(v){
if(predicate && !predicate(v)){
throw Error("set" + name + ":invalid value" +v);
}else{
value = v;
}
}
}
var o = {};
addPrivateProperty(o,"Name",function(x){return typeof x == "string"});
o.setName("Frank"); //设置属性值
console.log(o.getName()); //得到属性值
o.setName(o); //视图设置一个错误类型的值复制代码
8.在同一个作用域链中定义两个闭包,这两个闭包共享同样的私有变量或变量
//这个函数返回一个总是返回v的函数
function constfunc(v){return function(){return v;}}
//创建一个数组用来存储常数函数
var funcs = [];
for(var i = 0; i < 10;i++){
funcs[i] = constfunc(i);
}
funcs[5](); //=>5复制代码
9.this是JavaScript的关键字,而不是变量。每个函数调用都包含一个this值,如果闭包在外部函数里是无法访问this的,除非外部函数将this转存为一个变量
var self = this; //将this保存至一个变量中,以便嵌套的函数能够访问它复制代码
10.闭包具有自己所绑定的arguments,因此闭包内无法直接访问外部函数的参数数组,除非外部函数将参数数组保存到另外一个变量中
var outerArguments = arguments; //保存起来以便嵌套的函数能使用它复制代码
函数属性、方法和构造函数
对函数执行typeof运算会返回字符串"function",但是函数是JavaScript中特殊的对象。
1.length属性
arguments.length表示传人函数的实参的个数
函数的length属性是只读属性,它代表函数实参的数量
//这个函数使用arguments.callee,因此它不能在严格模式下工作
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; //再执行函数的后续逻辑
}复制代码
2.prototype属性
每个函数都包含一个prototype属性,这个属性是指向一个对象的引用,这个对象称做"原型对象"(prototype
object).每一个函数都包含不同的原型对象。当将函数用做构造函数的时候,新创建的对象会从原型对象上继承属性。
3.call()方法和apply()方法
call()和apply()看做是某个对象的方法,通过调用方法的形式来间接调用函数
call()方法来调用一个对象的Object.prototype.toString方法,用以输出对象的类。
call()和apply()的第一个实参是调用函数的对象,它是调用上下文,在函数体内通过this来获得对它的引用。
以对象o的方法来调用函数f()
f.call(o);
f.apply(o);
o.m=f; //将f存储为o的临时方法
o.m(); //调用它,不传人参数
delete o.m; //将临时方法删除复制代码
call()和apply()的第一个实参都会变为this的值
f.call(0,1,2); //以对象o的方法的形式调用函数f(),并传入两个参数
f.apply(0,[1,2]); //实参都放入一个数组当中复制代码
如果一个函数的实参可以是任意数量,给apply()传人的参数数组可以是任意长度的。
var biggest = Math.max.apply(Math,array_of_numbers);
//将对象o中名为m()的方法替换为另一个方法
//可以在调用原始的方法之前和之后记录日志消息
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()函数接收两个参数,一个对象和有一个方法名,它将指定的方法替换为一个新方法,这个新方法是"包裹"原始方法的另一个泛函数。
4.bind()方法
这个方法的主要作用就是将函数绑定至某个对象。
function f(y){ //这个是待绑定的函数
return this.x +y;
}
var o={x:1}; //将要绑定的对象
var g=f.bind(o); //通过调用g(x)来调用o.f(x)
g(2);
//返回一个函数,通过调用它来调用o中的方法f(),传递它所有的实参
function bind(f,o){
if(f.bind){
return f.bind(o); //如果bind()方法存在的话,使用bind()方法
}else{
return function(){ //否则,这样绑定
return f.apply(o,arguments);
}
}
}复制代码
除了第一个实参之外,传人bind()的实参也会绑定至this
var sum = function(x,y){return x+y}; //返回两个实参的和值
//创建一个类似sum的新函数,但this的值绑定到null
//并且第一个参数绑定到1,这个新的函数期望只传人一个实参
var succ = sum.bind(null,1);
succ(2); //=>3:x绑定到1,并传入2作为实参y
function f(y,z){return this.x+y+z}; //另外一个做累加计算的函数
var g = f.bind({x:1},2); //绑定this和y
g(3) //=》6:this.x绑定到1,y绑定到2,z绑定到3复制代码
我们将这个方法另存为Function.prototype.bind,以便所有的函数对象都继承它
if(!Function.prototype.bind){
Function.prototype.bind = function(o/*,args*/){
//将this和arguments的值保存至变量中以便在后面嵌套的函数中可以使用它
var self = this,boundArgs = arguments;
//bind()方法的返回值是一个函数
return function(){
//创建一个实参列表,将传人bind()的第二个及后续的实参都传人这个函数
var args = [],i;
for(i=1;ifor(i=0;ireturn self.apply(o,args);
}
}
}复制代码
bind()方法返回的函数是一个闭包
5.toString()方法
函数也有toString()方法,返回一个字符串
6.Function()构造函数
不管是通过函数定义语句还是函数直接量表达式,函数的定义都要使用function关键字。但函数还可以通过Function()构造函数来定义
var f = new Function("x","y","return x*y;");
var f = function(x,y){return x*y;}复制代码
Function()构造函数可以传人任意数量的字符串实参。
var scope = "global";
function constructFunction(){
var scope = "local";
return new Function("return scope") //无法捕获局部作用域
}
//这一行代码返回global,因为通过Function()构造函数所返回的函数使用的不是局部作用域
constructFunction()(); //=>"global"复制代码
7.可调用对象
"可调用对象"(callable object)是一个对象,可以在函数调用表达式中调用这个对象。所有的函数都是可调用的,但并非所有的可调用对象都是函数
如果想检测一个对象是否是真正的函数对象
function isFunction(x){
return Object.prototype.toStirng.call(x) === "[object Function]"
}复制代码
8.函数式编程
JavaScript并非函数式编程语言
1.使用函数处理数组
非函数式编程风格
var data = [1, 1, 3, 5, 5];
var total = 0;
for (var i = 0; i < data.length; i++) {
total += data[i];
}
var mean = total / data.length; //平均值
//计算标准差,首先计算每个数据减去平均数之后偏差的平方然后求和
total = 0;
for (var i = 0; i < data.length; i++) {
var deviation = data[i] - mean;
total += deviation * deviation;
}
var stddev = Math.sqrt(total / (data.length - 1));复制代码
使用数组方法map()和reduce()来实现同样的计算
var sum = function(x, y) { return x + y };
var square = function(x) { return x * x };
var data = [1, 2, 3, 4, 1];
var mean = data.reduce(sum) / data.length;
var deviations = data.map(function(x) { return x - mean });
var stddev = Math.sqrt(deviations.map(square).reduce(sum) / (data.length - 1));复制代码
|版权声明:本文为summer博主原创文章,未经博主允许不得转载。