初探一级函数


在 JavaScript 中,函数是对象,是Function的实例。因为是Function实例,你可以将之传给另一个变量参考。例如:

function max(num1, num2) {
    return num1 > num2 ? num1 : num2;
}

var maximum = max;

console.log(max(10, 20));      // 20
console.log(maximum(10, 20));  // 20

使用function定义函数,函数名称就相当于使用var定义了变量名称,严格模式下不可使用delete来删除,在上例中,如果试图delete max,会引发SyntaxError

注意,在将max指定给maximum时,max后并没有加上()运算符,这表示要将max参考的对象指定给maximum参考(加上括号表示要执行函数)。将一个函数指定给变量,就像将一个数字指定给一个变量一样,这看来如果觉得奇怪的话,或许下这个看来比较不奇怪:

var max = function(num1, num2) {
    return num1 > num2 ? num1 : num2;
};

var maximum = max;

console.log(max(10, 20));      // 20
console.log(maximum(10, 20));  // 20

函数就如同数值,可以指定给变量,函数与数值的地位相同,并不会像有些语言中,无法像数值一样地被指定,不会沦为二等公民,因此,对于支持函数可如数值一样指定给变量的语言,我们称函数在这个语言中是一等(First-class)函数或一级函数。

上面你所看到的函数编写方式,称之为函数字面量(Function literal),这就像你写下一个数值字面量、对象字面量或数组字面量,会产生数值或对象等:

var number = 10;        // Number literal
var obj = {x : 10};     // Object literal
var array = [1, 2, 3];  // Array literal
var func = function() { // Function literal
    // do something...
};

函数字面量会产生Function实例,在 JavaScript 中,无论是函数定义或函数字面量,都会产生Function实例。事实上,你也可以直接指定创建Function实例:

var max = new Function('num1', 'num2', 
    'return num1 > num2 ? num1 : num2'
);

var maximum = max;

console.log(max(10, 20));      // 20
console.log(maximum(10, 20));  // 20

基本上,实务上很少会直接创建Function实例,以上只是表示,函数确实是Function实例。

既然函数是对象,它就可以任意指定给其他变量,也就可以指定作为另一个函数的实参,那它就不仅能被调用,还可以主动要求另一个函数执行所指定的函数内容。例如:

var printIt = function(elem) {
    console.log(elem);
};

[1, 2, 3].forEach(printIt); // 1 2 3

var naturalOrder = function(num1, num2) {
    return num1 - num2;
};

[5, 1, 7, 3, 2].sort(naturalOrder)  // 1 2 3 5 7
               .forEach(printIt);

上例以Array为例,forEach可以对数组的每个元素作「某些事」,「某些事」是由你使用函数来指定,数组会逐一将元素传入给你指定的函数作为实参。

sort则可以进行排序,但两个元素的大小关系要由你告知,返回正值表示传入的num1顺序上大于num2,要排在num2的后面,返回 0 表示两个顺序相同,返回负值表示num1顺序上小于num2,要排在num2的前面。

像这种将函数主动丢入函数作为实参,在 JavaScript 等具备一级函数的语言中,是很常见到的应用。事实上,若不需要名称,你也可以如下:

var numbers = [5, 1, 7, 3, 2];
numbers.sort(function(num1, num2) { // 1 2 3 5 7
           return num1 - num2;
       })
       .forEach(function(elem) {
            console.log(elem);
       });

直接传入函数很方便,不过函数名称有时是必要的,像上面的可读性并不会比较好。

你也可以从函数中返回函数,这通常会形成闭包(Closure)绑定某些运算过后的资源,再返回函数,这之后还会再谈到应用。

以函数字面量所创建的Function实例,在指定给别的变量前,称为所谓的匿名函数(Anonymous function)。你可以完全不使用名称来执行函数:

(function() {
    console.log('匿名函数...');
})();

实际上,函数字面量也可以指定名称。例如:

var maximum = function max(num1, num2) {
    return num1 > num2 ? num1 : num2;
};

console.log(maximum(10, 20));  // 20
console.log(max(10, 20));      // ReferenceError: max is not defined

上例中,函数字面量所创建的max名称,似乎不能使用,事实上,这种语法适用于使用函数字面量创建Function实例,但又需递归的场合。例如:

var gcd = function g(num1, num2) {
    return num2 != 0 ? g(num2, num1 % num2) : num1;
};

console.log(gcd(10, 5));  // 5

函数既然是对象,本身亦可拥有特性。例如函数有个length特性,代表其参数个数:

var gcd = function g(num1, num2) {
    return num2 != 0 ? g(num2, num1 % num2) : num1;
};

console.log(gcd.length); // 2

函数也可以拥有方法,这个之后再谈,你也可以在其上新增特性或方法,就如同一个普通的对象。

函数定义与函数字面量在运用上,几乎是相同的,但还是有细微的差别。例如,以下可以正常执行:

func();
function func() {
    console.log('func');
}

不过以下会发生TypeError

func(); // TypeError: undefined is not a function
var func = function() {
    console.log('func');
};

错误消息告诉你,func值是undefined。原因在于,解释器在加载 .js 时,会先处理所有的定义,包括变量与函数定义,接着再执行程序。所以在第一个范例中,是以函数定义方式,解释器已处理完成,因此接下来再执行时,就可以找到func所参考的函数。

而在第二个程序中,仅定义了func变量,解释器处理完这个定义后,接下来要执行程序时,范围中可以看到func变量,但此时还没有指定值给func,所以是undefined,因此不能完成函数的执行。

虽然不重要,但还是提一下的是,以上两种方式,在遇到要创建函数实例时,都不会再重新直译,但如果你以直接构造Function实例的方式,则每次都会针对你实参的字符串再作直译动作。

var max = new Function('num1', 'num2', 
    'return num1 > num2 ? num1 : num2'
);




展开阅读全文