引用类型--Function类型

ECMAScript中最有意思的莫过于函数了,根源在于函数实际上是对象每个函数都是Function类型的实例,而且都与其它引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定

函数定义方式

1.函数声明

function sum(num1, num2) {
	return num1 + num2;
}

2.函数表达式

var sum = function(num1, num2) {
	return num1 + num2;
};

3.使用Function构造函数。Function构造函数可以接收任意数量的参数,但最后一个参数始终都被看成是函数体,而前面的参数则枚举出了新函数的参数。

var sum = new Function("num1", "num2", "return num1 + num2");          //不推荐

由于函数名仅仅是指向函数的指针,因此函数名与包含对象指针的其他变量没有什么不同。换句话说,一个函数可能会有多个名字。

function sum(num1, num2) {
	return num1 + num2;
}
alert(sum(10, 10));                      //20

var anotherSum = sum;
alert(anotherSum(10, 10));    //20

sum = null;
alert(anotherSum(10, 10));      //20

函数声明与函数表达式区别

实际上,解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁。解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。

alert(sum(10, 10));
function sum(num1, num2) {
	return num1 + num2;
}

以上代码完全可以正常运行。函数声明的一个重要特征是函数声明提升(function declaration hoisting),意思是在执行代码之前会先读取函数声明,意味着可以把函数声明放在调用它的语句后面。对代码求值时,JavaScript引擎在第一遍会声明函数并将它们放到源代码树的顶部。所以,即使函数声明的代码在调用它的代码后面,JavaScript引擎也能把函数声明提升到顶部。

alert(sum(10, 10));                 //运行出错
var sum = function(num1, num2) {
	return num1 + num2;
};

以上代码之所以会在运行期间出错,原因在于函数位于一个初始化语句中,而不是一个函数声明。换句话说,在执行到函数所在的语句之前,变量sum中不会保存有对函数的引用;而且,由于第一行代码就会导致"unexpected identifier"(意外标识符)错误,实际上也不会执行到下一行。
除了什么时候可以通过变量访问函数这一点区别之外,函数声明与函数表达式的语法其实是等价的。

没有重载(深入理解)

function addSomeNumber(num) {
	return num + 100;
}

function addSomeNumber(num) {
	return num + 200;
}

var result = addSomeNumber(100);       //300

虽然,上面声明了两个同名函数,而结果是后面的函数覆盖了前面的函数。以上代码实际上与下面的代码没有什么区别。

var addSomeNumber = function(num) {
	return num + 100;
};
adddSomeNumber = function(num) {
	return num + 200;
};

var result = addSomeNumber(100);           //200

通过观察重写之后的代码,能清楚看到–在创建第二个函数时,实际上覆盖了引用第一个函数的变量addSomeNumber。

作为值的函数

因为ECMAScript中的函数名本身就是变量,所以函数也可以作为值来使用。也就是说,不仅可以像传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。

function callSomeFunction(someFunction, someArgument) {
	return someFunction(someArgument);
}

这个函数接受两个参数。第一个参数应该是一个函数,第二个参数应该是要传递给该函数的一个值。然后,这可以像下面的例子一样传递函数了。

function add10(num) {
	return num + 10;
}
var result = callSomeFunction(add10, 10);
alert(result);              //20

function getGreeting(name) {
	return "Hello, " + name;
}
var result2 = callSomeFunction(getGreeting, "Nicholas");
alert(result2);                              //"Hello, Nicholas"

还记得吧,要访问函数的指针而不执行函数的话,必须去掉函数名后面的那对圆括号。因此上面例子中传递给callSomeFunction()的是add10 和 getGreeting,而不是执行它们之后的结果。
当然,可以从一个函数中返回另一个函数,而且这也是极为有用的一种技术。

function createComparisonFunction(propertyName) {
	return function(object1, object2) {
		var value1 = object1[propertyName];
		var value2 = object2[propertyName];
		if(value1 < value2) {
			return -1;
	 	} else if(value1 > value2) {
	 		return 1;
	 	} else {
	 		return 0;
	 	}
	 };
}

var data = [{name: "Zachary", age: 28}, {name: "Nicholas", age: 29}];
data.sort(createComparisonFunction("name"));  
alert(data[0].name);                                 //Nicholas

data.sort(createComparisonFunction("age"));
alert(data[0].name);                                 //Zachary

理解参数

ECMAScript函数的参数与大多数语言中函数的参数有所不同。ECMAScript函数不介意传递进来多少个参数,也不在乎传进来的参数是什么数据类型。也就是说,即便你定义的函数只接收2个参数,在调用这个函数时也未必一定要传递2个参数。可以传递一个、三个甚至不传参数,而解析器永远不会有什么怨言。原因是****ECMAScript中的参数在内部是用一个数组来表示的。函数接收到的始终都是这个数组,而不关心数组中包含哪些参数(如果有参数的话)。如果这个数组中不包含任何元素,无所谓;如果包含多个元素,也没有问题。实际上,在函数体内可以通过arguments对象访问这个参数数组,从而获取传递给函数的每一个参数。
arguments是一个类数组对象,包含着传入函数中的所有参数。可以使用方括号语法访问它的每一个元素,使用length属性来确定传递进来多少个参数。
通过访问arguments对象的length属性可以获知有多少个参数传递给了函数。

function howManyArgs() {
	alert(arguments.length);
}
howManyArgs("string", 45);                   //2
howManyArgs();                                   //0
howManyArgs(12);                              //1

function doAdd() {
	if(argumens.length == 1) {
		alert(arguments[0] + 10);
	} else if(arguments.length == 2) {
		alert(arguments[0] + arguments[1]);
	}
}

doAdd(10);      //20
doAdd(30, 20);     //50

ECMAScript函数的一个重要特点:命名的参数只提供便利,但不是必需的。另外,在命名参数方面,其他语言可能需要事先创建一个函数签名,而将来的调用必须与该签名一致。但在ECMAScript中,没有这些条条框框,解析器不会验证命名参数。
另一个与参数相关的重要方面,就是arguments对象可以与命名参数一起使用

function doAdd(num1, num2) {
	if(arguments.length == 1) {
		alert(num1 + 10);
	} else if(arguments.length == 2) {
		alert(arguments[0] + num2);
	}
}

在重写后的这个doAdd()函数中,两个命名参数都与arguments对象一起使用。由于num1的值与arguments[0]的值相同,因此它们可以互换使用(当然,num2与arguments[1]也是如此)。
关于arguments的行为,还有一点比较有意思。那就是它的值永远与对应命名参数的值保持同步

function doAdd(num1, num2) {
	arguments[1] = 10;
	alert(arguments[0] + num2);
}

每次执行这个doAdd()函数都会重写第二个参数,将第二个参数的值修改为10。因为arguments对象中的值会自动反映到对应的命名参数,所以修改arguments[1],也就修改了num2,结果它们的值都会变成10。不过,这并不是说读取这两个值会访问相同的内存空间;它们的内存空间是独立的,但它们的值会同步。另外要记住如果只传入一个参数,那么为arguments[1]设置的值不会反应到命名参数中。这是因为arguments对象的长度是由传入的参数个数决定的,不是由定义函数是的命名参数的个数决定的。
注:没有传递值的命名参数将自动被赋予undefined值。这就跟定义了变量但又没有初始化一样。
注:ECMAScript中的所有参数传递的都是值,不可能通过引用传递参数。

函数内部属性

在函数内部,有2个特殊的对象:arguments和this。虽然arguments对象的主要用途是保存函数参数,但这个对象还有一个名叫callee 的属性,该属性是一个指针,指向拥有这个arguments对象的函数。
请看下面这个非常经典的阶乘函数。

function factorial(num) {
	if(num <=1) {
		return 1;
	} else {
		return num * factorial(num-1);
	}
}

定义阶乘函数一般都要用到递归算法;如上面代码所示,在函数有名字,而且名字以后也不会变的情况下,这样定义没有问题。但问题是这个函数的执行与函数factorial 紧紧耦合在了一起。为了消除这种紧密耦合的现象,可以像下面这样使用arguments.callee。

function factorial(num) {
	if(num <=1) {
		return 1;
	} else {
		return num * arguments.callee(num-1);
	}
}

在这个重写后的factiorial()函数的函数体内,没有再引用函数名factorial。这样,无论引用函数时使用的是什么名字,都可以保证正常完成递归调用。

var trueFactorial = factorial;
factorial = function() {
	return 0;
};

alert(trueFactorial(5));                      //120
alert(factorial(5))                            //0

在此,变量trueFactorial获得了factorial的值,实际上是在另一个位置上保存了一个函数的指针。然后,将一个简单地返回0的函数赋值给factorial变量。如果像原来的factorial()那样不使用arguments.callee,调用trueFactorial()就会返回0。在解除了函数体内的代码与函数名的耦合状态之后,trueFactorial()仍然能够整场地计算阶乘;至于factorial(),它现在只是一个返回0的函数。
函数体内的另一个特殊对象是thisthis引用的是函数据以执行的环境对象–或者也可以说是this值(当在网页的全局作用域中调用函数时,this对象引用的就是window)。

window.color = 'red';
var o = {color: 'blue'};

function sayColor() {
	alert(this.color);
}
sayColor();                            //'red'
o.sayColor = sayColor;
o.sayColor();                       //'blue'

上面这个函数sayColor()是在全局作用域中定义的,它引用了this对象。由于在调用函数之前,this的值并不确定,因此this可能会在代码执行过程中引用不同的对象。当在全局作用域中调用sayColor()时,this引用的是全局对象window;而当把这个函数赋给对象o并调用o.sayColor()时,this引用的是对象o。
注:函数的名字仅仅是一个包含指针的变量而已。因此,即使是在不同的环境中执行,全局的sayColor()函数与o.sayColor()指向的仍然是同一个函数。
ECMAScript5规范化了另一个函数对象的属性:caller。这个属性中保存着调用当前函数的函数的引用,如果是在全局作用域中调用当前函数,它的值为null。

function outer() {
	inner();
}

function inner() {
	alert(inner.caller);
}

outer();

以上代码会导致警告框中显示outer()函数的源代码。outer()调用了inner(),所以inner.caller就指向outer().为了实现更松散的耦合,也可以通过arguments.callee.caller来访问相同的信息。

function outer() {
	inner();
}

function inner() {
	alert(arguments.callee.caller);
}

outer();

函数的属性和方法

ECMAScript中的函数是对象,因此函数也有属性和方法。每个函数都包含2个属性:length和prototype。其中,length属性表示函数希望接收的命名参数的个数。

function sayName(name) {
	alert(name);
}

function sum(num1, num2) {
	return num1 + num2;
}

function sayHi() {
	alert('hi');
}

alert(sayName.length);                          //1
alert(sum.length);                                  //2
alert(sayHi.length);                              //0

在ECMAScript核心所定义的全部属性中,最耐人寻味的就要数prototype 属性了。对于ECMAScript中的引用类型而言,prototype 是保存它们所有实例方法的真正所在。换句话说,诸如toString()和valueOf()等方法实际上都保存在prototype名下,只不过是通过各自对象的实例访问罢了。在创建自定义引用类型以及实现继承时,prototype属性的作用是极为重要的。在ECMAScript5中,prototype属性是不可枚举的,因此使用for-in无法发现。
每个函数都包含两个非继承而来的方法:apply()和call()这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值。首先,apply()方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中第二个参数可以是Array的实例,也可以是arguments对象。

function sum(num1, num2) {
	return num1 + num2;
}
function callSum1(num1, num2) {
	return sum.apply(this, arguments);              //传入arguments对象
}
function callSum2(num1, num2) {
	return sum.apply(this, [num1, num2]);     //传入数组
}

alert(callSum1(10, 10));                //20
alert(callSum2(10, 20));              //20

callSum1()在执行sum()函数时传入了this作为this值(因为是在全局作用域中调用的,所以传入的就是window对象)和arguments对象。而callSum2同样也调用了sum()函数,但它传入的则是this和一个参数数组。这两个函数都会正常执行并返回正确的结果。
call()方法与apply()方法的作用相同,他们的区别仅在于接收参数的方式不同。对于call()而言,第一个参数是this值没有变化,变化的是其余参数都直接传递给函数。换句话说,在使用call()方法时,传递给函数的参数必须逐个列举出来。

function sum(num1, num2) {
	return num1 + num2;
}
function callSum(num1, num2) {
	return sum.call(this, num1, num2);
}

alert(callSum(10, 10));              //20

在使用call()方法的情况下,callSum()必须明确地传入每一个参数。结果与使用apply()没有什么不同。至于是使用apply()还是call(),完全取决于你采取那种函数传递参数的方式最方便。
实际上,传递参数并非apply()和call()真正的用武之地;它们真正强大的地方是能够扩充函数赖以运行的作用域。

window.color = 'red';
var o = {color: 'blue'};

function sayColor() {
	alert(this.color);
}

sayColor();                                            //red
sayColor.call(this);                              //red
sayColor.call(window);                       //red
sayColor.call(o);                               //blue

使用call()方法(或apply())来扩充作用域的最大好处,就是对象不需要与方法有任何耦合关系
ECMAScript5还定义了一个方法:bind()。这个方法会创建一个函数的实例,其this值会被绑定到传给bind()函数的值。

window.color = 'red';
var o = {color: 'blue'};

function sayColor() {
	alert(this.color);
}
var objectSayColor = sayColor.bind(o);
objectSayColor();                                  //blue

在这里,sayColor()调用bind()并传入对象o,创建了objectSayColor()函数。objectSayColor()函数的this值等于o,因此即使是在全局作用域中调用这个函数,也会看到’blue’。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值