1. 简述
函数实际上就是对象,每一个函数都是 Function
类型的实例,与其他引用一样具有属性和方法。由于函数是对象,因此,函数名实际上就是一个指向函数对象的指针,不会与某个函数绑定。
2. 创建
// 函数声明语法
function sum(num1, num2) {
return num1 + num2;
}
// 函数表达式
var sum = function (num1, num2) {
return num1 + num2;
}
注意第一种后面不加分号,有函数名,而第二个相当于一条声明变量语句,要加分号,且不需要函数名,通过变量就可以引用函数。
函数声明和函数表达式,在解析上有着很大区别,所以建议使用函数声明
解析器会率先读取函数声明,并使其在执行任何代码之前可用,至于函数表达式,则必须等到解析器执行到所在代码,才会真正被解析执行。
在代码开始执行之前,解析器就已经通过函数声明提升的过程,读取并将函数声明添加到执行环境中,JavaScript 引擎会将函数声明放在源代码顶部,所以在函数声明之前使用函数不会出错
函数表达式,在函数表达式代码前就使用函数,就会导致错误
除了什么时候可以通过变量访问函数的区别外,函数声明和函数表达式是等价的
另外一种使用 Function 构造函数的方法,不使用,只是说明有这种方法
vat sum = new Function("num1", "num2", "return num1 + num2");
接受任意数量参数,前面的参数枚举传入函数的参数,最后一个始终被看做是函数体。
由于这种语法会导致两次解析代码,第一次是解析常规的 ECMAScript 代码,第二次是解析传入构造函数的字符串,从而影响性能。
注意:访问带圆括号的函数名是调用函数,而不带圆括号的函数名是访问函数的指针
3. 没有重载
ECMAScript 函数没有重载,就是因为函数是对象,函数名是函数对象的指针
var addSomeNumber = function (num) {
return num + 100;
}
addSomeNumber = function (num) {
return num + 200;
}
从上例中就很容易理解为什么没有重载。因为函数名是函数对象的指针,也就相当于变量,再次赋值,会覆盖前面的值,指向另一个函数对象地址。所以没有重载。
通过对传入参数的类型和数量进行检测,可以模拟重载
4. 作为值的函数
ECMAScript 中的函数名本身就是变量,所以函数也可以作为值来使用。也就是,不仅可以将函数当做参数传递给另一个函数,而且可以作为另一个函数的结果返回
对数组排序的 sort() 方法的比较函数,需要传递两个参数,就是要比较的两个值。但我们需要一种方式来指明按照哪个属性来排序。
function creatComparisonFunction(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(creatComparisonFunction("name"));
console.log(data[0].name); // Nicholas
data.sort(creatComparisonFunction("age"));
console.log(data[0].name); // Zachary
定义一个函数,接受一个属性名,然后根据属性名来创建比较函数。
5. 函数内部属性
在函数内部,有两个特殊对象:arguments
和 this
。
arguments
是一个类数组对象,包含传入函数的所有参数,这个对象还有一个 callee
属性,该属性是一个指针,指向拥有这个 arguments
对象的函数。
多使用于递归算法
function factorial(num) {
if (num <= 1) {
return 1;
}
else {
return num * factorial(num - 1);
}
}
由于函数自身调用自身,这个函数的执行就与函数名紧紧耦合在了一起,也就是说,一旦修改函数名,需要修改两处,不利于维护。为了消除这种紧密耦合现象,就可以:
function factorial(num) {
if (num <= 1) {
return 1;
}
else {
return num * arguments.callee(num - 1);
}
}
这样,无论引用函数时使用什么名字,都可以保证正常完成递归调用。
另一个特殊对象 this
引用的是函数据以执行的环境对象。
window.color = "red";
var o = { color: "blue" };
function sayColor() {
console.log(this.color);
}
sayColor(); // "red"
o.sayColor = sayColor;
o.sayColor(); // "blue"
另一个函数对象的属性 caller
,保存着调用当前函数的函数的引用。如果是全局作用域中调用当前函数,它的值为 null
function outer() {
inner();
}
function inner() {
alert(inner.caller);
//alert(arguments.callee.caller)
}
outer(); // 显示 outer() 函数的源代码
注意:在严格模式下,访问
arguments
、callee
、caller
等,都会导致错误。都是为了增强这门语言的安全性,这样第三方代码就不能在相同的环境窥视其他代码。
6. 函数的属性和方法
length
属性表示希望接受的命名参数个数
prototype
属性保存着引用类型的所有实例和方法的真正所在。
每个函数都包含两个非继承而来的方法:apply()
和 call()
。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内的 this
对象值。
apply()
方法接受两个参数,一个是在其中运行的函数的作用域,另一个是参数数组。第二个参数可以是 Array
的实例,也可以是 arguments
对象
function sum(num1, num2) {
return num1 + num2;
}
function callSum1(num1, num2) {
return sum.apply(this, arguments);
}
function callSum2(num1, num2) {
return sum.apply(this, [num1, num2]);
}
alert(callSum1(10, 10)); //20
alert(callSum2(10, 10)); //20
call()
方法与 apply()
方法的作用相同,区别仅是接收参数的方式不同。
第一个参数是 this
值,其余参数都是直接传递给函数。也就是说,使用 call()
方法,传递给函数的参数必须逐个列举
function sum(num1, num2) {
return num1 + num2;
}
function callSum(num1, num2) {
return sum.call(this, num1, num2);
}
alert(callSum(10, 10)); //20
这两个方法真正强大的地方是能够扩充函数赖以运行的作用域,最大好处就是对象不需要与方法有任何的耦合。
bind()
方法会创建一个函数的实例,其 this
值会绑定到传入参数的值
window.color = "red";
var o = { color: "blue" };
function sayColor() {
alert(this.color);
}
var objectSayColor = sayColor.bind(o);
objectSayColor(); // blue
toLocaleString()
、toString()
、valueOf()
方法会返回函数的源代码,但是源代码的格式因浏览器而异,所以无法根据返回结果来实现任何功能,只能用于调试。