深入理解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()

 

函数的属性和方法
由于函数就是对象,对象是有属性和方法的,因此函数也有属性和方法。
每个函数都包含两个属性:lengthprototype。length是函数形参的个数。要注意区别 arguments.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
 
对于引用类型而言,ptototype 其实就是他们保存实例方法的地方,诸如toString()和valueOf()等方法实际上都是保存在prototype上面。
 
每个函数都包含两个非继承而来的方法:apply()  call()。这两个方法都是为了在特定的作用域中调用函数,实际上等于设置函数体内this的指向。
apply()方法接收两个参数,第一个是在其中运行函数的作用域,第二个是参数数组。
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
使用call()或者apply()来扩充作用域最大的好处就是对象不需要与方法有任何耦合关系。
 
ES5中还定义了一个方法: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"


 
posted @ 2020-09-08 21:21  非洲铜  阅读(457)  评论(0编辑  收藏  举报