深入理解JS中的函数
JS中的函数是对象这一特性,是导致JS中函数难以理解的根源!
JS中的函数是对象
JS中每个函数都是Function类型的实例,函数名就是一个指向函数对象的指针,所以函数名跟普通对象引用并没有什么区别。JS中的函数没有函数签名,这导致JS函数不能重载。
函数定义通常有两种方式,第一种是函数声明,如下
function sum (num1, num2) { return num1 + num2; } console.log(typeof sum) // function
第二种是函数表达式,如下
var sum = function(num1, num2){ return num1 + num2; }; console.log(typeof sum) // function
不管是函数声明还是函数表达式,函数名sum都是函数对象的引用。函数声明和函数表达式唯一的区别是,函数声明可以在声明之前的位置调用函数;函数表达式不行,它只能在函数表达式之后调用。
我们可以通过如下小案例验证
console.log(typeof sum) // function function sum(num1, num2) { return num1 + num2; } console.log(typeof sub) // undefined var sub = function (num1, num2) { return num1 - num2; };
最后,为了证明函数确实是Function类型的实例,我们还可以用创建对象的方式来创建函数。
var sum = new Function("num1", "num2", "return num1 + num2") console.log(typeof sum) // function console.log(sum(10, 20)); // 30
使用这种方式来创建函数,就可以很直观的看到函数确实是Function类型的实例,它是个对象。但是我们不推荐使用这种方式创建函数,太难看了。
JS中的函数没有重载
由于函数名是一个对象指针,所以使用不带圆括号的函数名是访问函数指针,而不是调用函数。因为JS函数没有签名,所以它也无法实现函数重载。跟普通对象引用一样,如果存在两个函数名相同的函数,后面的函数将覆盖前面的函数。
JS中函数可以作为入参或者返回值
因为JS中的函数是对象,而函数名是对象引用。因此与普通对象一样,JS中的函数既可以作为另一个函数的入参,也可以作为返回值。
我们先看一个作为入参的例子
function callSomeFunction(someFunction, someArgument){ return someFunction(someArgument); } function add10(num){ return num + 10; } var result1 = callSomeFunction(add10, 10); //传入一个加10的函数 alert(result1) function getGreeting(name){ return "Hello, " + name; } var result2 = callSomeFunction(getGreeting, "Nicholas"); //传入一个字符串拼接函数 alert(result2)
我们在调用 callSomeFunction() 函数时,就可以动态传入另外一个函数。
我们也可以把函数当成返回值,例如
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; } }; }
createComparisonFunction 函数中就返回了一个另外一个函数,返回的函数可以用来比较两个对象中指定的属性值。下面我们调用这个函数。
var data = [{name: "Zachary", age: 28}, {name: "Nicholas", age: 29}]; data.sort(createComparisonFunction("name")); // 比较name属性 alert(data[0].name); // Nicholas data.sort(createComparisonFunction("age")); // 比较age属性 alert(data[0].name); //Zachary
函数的内部属性
函数的内部,有两个特殊对象:arguments 和 this。
arguments是一个数组对象,它包含了传入该函数的所有实参。arguments中又包含一个名叫callee的属性,该属性是一个指针,指向当前函数。
arguments配合其内部属性callee,可以实现代码的松耦合。我们以非常有名的斐波拉契函数为例:
function factorial(num){ if (num <=1) { return 1; } else { return num * factorial(num-1) } }
factorial函数内部又调用了自己,如果factorial函数名改变,那么内部调用的地方也需要一起改变。由于arguments.callee指向当前函数本身,所以可以使用 arguments.callee 来代替自身调用。如下
function factorial(num){ if (num <=1) { return 1; } else { return num * arguments.callee(num-1) } }
这样可以实现函数内部与函数名的解耦。
函数内部另外一个特殊对象是 this。this的指向在函数被调用前是不确定的,只有在调用时才会动态绑定this的指向。它可能指向window,也可能指向某个对象。
window.color = "red"; // 在window中定义了颜色为red var o = { color: "blue" }; // 在对象o中定义了颜色为blue function sayColor(){ alert(this.color); // 该函数在调用前,this的指向是不确定的。 } sayColor(); //在全局中调用函数,其实是通过window对象调用函数,这时候this绑定的是window对象,这时候打印的red o.sayColor = sayColor; // 把sayColor赋给对象o o.sayColor(); // 然后通过对象o去调用sayColor,这时候打印的是blue
由于函数名仅仅是一个包含指针的变量而已。因此,即使是在不同的环境中执行,全局的sayColor()函数和o.sayColor()函数指向的仍然是同一个函数。
ES5中规范了另外一个函数对象属性:caller,这个属性保存了调用当前函数的函数引用。如果是在全局作用域中调用当前函数,它的值为null。例如:
function outer() { inner(); } function inner() { alert(inner.caller); } outer()
inner() 函数是由 outer( )函数来调用的,所以 inner.caller 保存的是 outer() 函数的引用。这个属性也可以用来实现代码的松耦合,上面的代码就可以改造成如下方式
function outer() { inner(); } function inner() { alert(arguments.callee.caller); } outer()
函数的属性和方法
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
function sum(num1, num2) { return num1 + num2; } // 我们可以直接使用call来调用sum函数。这时候this指向window。 sum.apply(this, [10, 20]); // 也可以在函数中去调用sum方法 function callSum1(num1, num2){ return sum.apply(this, arguments); // 传入 arguments 对象 } alert(callSum1(10, 10)) function callSum2(num1, num2){ return sum.apply(this, [num1, num2]); // 传入数组 } alert(callSum2(10,10)); //20
call() 方法与 apply() 方法的作用相同,它们的区别在于接收参数的方式有点不一样。对于call()方法而言,第一个参数this的值没有变化,变化的是其他参数比如逐个传入。
function sum(num1, num2){ return num1 + num2; } alert(sum.call(this, 10, 10)); // 20 function callSum(num1, num2){ return sum.call(this, num1, num2); } alert(callSum(10,10)); // 20
事实上,传递参数并非 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
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"。