JavaScript 函数

 

一,函数概念

函数对任何语言来说都是一个核心的概念,通过函数可以封装任意多条语句,而且可以在任何地方、任何时候调用执行。ECMAScript中的函数使用function关键字来声明,后跟一组参数以及函数体。函数的基本语法如下所示:

function functionName(arg0, arg1, ..., argN) {
    statements;
}

 以下是一个函数示例:

function sayHi(name, message) {
    alert('hello ' + name + ',' + message);
}

 这个函数可以通过其函数名来调用,后面还要加上一对圆括号和一对参数(圆括号中的参数如果有多个,可以用逗号隔开)。调用sayHi()函数的代码如下所示:

sayHi('Nicholas', 'how are you today?'); //"hello Nicholas,how are you today?"

 函数中定义的命名参数name和message被用作了字符串拼接的两个操作数,而结果通过警告框显示了出来。

ECMAScript中的函数在定义时不必指定是否返回值。实际上任何函数在任何时候都可以通过return语句后跟要返回的值来实现返回值,请看下面的例子:

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

 这个sum函数的作用是把两个值加起来返回一个结果,我们注意到,除了return语句之外,没有任何声明表示该函数会返回一个值。调用这个函数的示例代码如下:

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

var result = sum(5, 10);
alert(result);//15

 这个函数会在执行完return语句之后停止并立即退出。因此,位于return语句之后的任何代码都永远不会执行。例如:

function sum(num1, num2) {
    return num1 + num2;
    alert('hello world');//永远不会执行
}

 在这个例子中,由于调用alert()函数的语句位于return语句之后,因此永远不会显示警告框。当然,一个函数中也可以包含多个return语句,如下面的例子所示:

function diff(num1, num2) {
    if (num1 < num2) {
        return num2 - num1;
    } else {
        return num1 - num2;
    }
}
var result = diff(7, 10);
alert(result);//3

 这个例子中定义的diff()函数用于计算两个数值的差。如果第一个数比第二个数小,则用第二个数减第一个数,否则,用第一个数减第二个数。代码中的两个分支都具有自己的return语句,分别用于执行正确的计算。

另外,return语句也可以不带有任何返回值。在这种情况下,函数停止执行后将返回undefined值。这种用法一般用在需要提前停止函数执行而又不需要返回值的情况下。比如在下面这个例子中,就不会显示警告框:

function sayHi(name, message) {
    return;
    alert('hello ' + name + ',' + message);//永远不会调用
}

 推荐的做法是要么让函数始终都返回一个值,要么永远都不要返回值。否则,如果函数有时候返回值,有时候不返回值,会给调试代码带来不便。

1,理解参数

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

其实,arguments对象只是与数组类似(它并不是Array的实例),因为可以使用方括号语法访问它的每一个元素(即第一个元素是arguments[0],第二个元素是arguments[1],以此类推),使用length属性来确定传递进来多少个参数。在前面的例子中,sayHi()函数的第一个参数的名字叫name,而该参数的值也可以通过访问arguments[0]来获取。因此,那个函数也可以像下面这样重写,即不显示地使用命名参数:

function sayHi() {
    alert('hello ' + arguments[0] + ',' + arguments[1]);
}
sayHi('Nicholas', 'how are you today?'); //"hello Nicholas,how are you today?"

 这个重写后的函数中不包含命名的参数,虽然没有使用name和message标识符,但函数的功能依旧。这个事实说明了ECMAScript函数的一个重要特点:命名的参数只提供便利,但不是必须的。另外,在命名参数方面,其他语言可以需要事先创建一个函数签名,而将来的调用必须与该签名一致。但在ECMAScript中,没有这些条条框框,解析器不会验证命名参数。

通过访问arguments对象的length属性可以获知有多少个参数传递给了函数。下面这个函数会在每次被调用时,输出传入的参数个数:

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

 执行以上代码会依次出现3个警告框,分别是2、0和1,由此可见,开发人员可以利用这一点让函数能够接收任意个参数并分别实现适当的功能。请看下面的例子:

function doAdd() {
    if (arguments.length == 1) {
        alert(arguments[0] + 10);
    } else if (arguments.length == 2) {
        alert(arguments[0] + arguments[1]);
    }
}
doAdd(10); //20
doAdd(30, 20); //50

 函数doAdd()会在只有一个参数的情况下给该参数加上10,如果是两个参数,则将参数简单相加并返回结果。因此,doAdd(10)会返回20,而doAdd(30,20)则返回50,虽然这个特性算不上完美的重载,但也足够弥补ECMAScript的这一缺憾了。

另一个与参数相关的重要方面,就是arguments对象可以和命名参数一起使用,请看下面的例子所示:

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

 在重写后的doAdd()函数中,两个命名参数都与arguments对象一起使用。由于num1的值与arguments[0]的值相同,因此它们可以互换使用(当然,num2和arguments[1]也是如此)。

关于参数还要记住最后一点:没有传递值的命名参数将自动被赋予undefined值。这就跟定义了变量但又没有初始化一样。例如,如果只给doAdd()函数传递了一个参数,则num2中就会保存undefined值。(ECMAScript中的所有参数传递的都是值,不可能通过引用传递参数)。

2,没有重载

ECMAScript函数不能像传统意义上那样实现重载。而在其他语言(如Java)中,可以为一个函数编写两个定义,只要这两个定义的签名(接收的参数的类型和数量)不同即可。如前所述,ECMAScript函数没有签名,因为其参数是由包含零或多个值的数组来表示的。而没有函数签名,真正的重载是不可能做到的。

如果在ECMAScript中定义了两个名字相同的函数,则改名字只属于后定义的函数。请看下面的例子:

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

function addSomeNumber(num) {
    return num + 200;
}
var result = addSomeNumber(100); //300
alert(result);

 在此,函数addSomeNumber()被定义了两次,第一个版本给参数加100,而第二个版本给参数加200.由于后定义的函数覆盖了先定义的函数,因此当在最后一行代码中调用这个函数时,返回的结果就是300.

如前所述,通过检查传入函数中的参数的类型和数量并做出不同的反应,可以模仿方法的重载。

3,小结

JavaScript的核心语言特性在ECMA-262中是以名为ECMAScript的伪语言的形式来定义的。ECMAScript中包含了所有基本的语法、操作符、数据类型以及完成基本的计算任务所必需的对象,但没有对取得输入和产生输出的机制作出规定。理解ECMAScript及其缤纷复杂的各种细节,是理解其在Web浏览器中的实现——JavaScript的关键。目前大多数实现所遵循的都是ECMA-262第3版中定义的ECMAScript。以下简要总结了ECMAScript中基本的要素:

a,ECMAScript中的基本数据类型包括Undefined、Null、Boolean、Number和String;

b,与其他语言不同,ECMAScript没有为整数和浮点数值分别定义不同的数据类型,Number类型可用于表示所有数值;

c,ECMAScript中也有一种复杂的数据类型,即Object类型,该类型是这门语言中所有对象的基础类型

d,ECMAScript提供了很多与C及其他类C语言中相同的基本操作符,包括算术操作符、布尔操作符、关系操作符、相等操作符及赋值操作符;

e,ECMAScript从其他语言中借鉴了很多流控制语句,例如if语句、for语句和switch语句等,ECMAScript中的函数与其他语言中的函数有诸多不同之处;

f,无需指定函数的返回值,因为任何ECMAScript函数都可以在任何时候返回任何值;

g,实际上,未指定返回值的函数返回的是一个特殊的undefined值。ECMAScript中也没有函数签名的概念,因为其函数参数是以一个包含零或多个值的数组的形式传递的;

h,可以向ECMAScript函数传递任意数量的参数,并且可以通过arguments对象来访问这些参数;

i,由于不存在函数签名的特性,ECMAScript函数不能重载。

 二,Function类型

说起来 ECMAScript中什么最有意思,我想那莫过于函数了——而有意思的根源,则在于,函数实际上是对象。每个函数都是Funciton类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。函数通常是使用函数声明语法定义的,如下面的例子所示:

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

 这与下面使用函数表达式定义函数的方式几乎相差无几:

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

 以上代码定义了变量sum并将其初始化为一个函数。有读者可能会注意到,funciton关键字后面没有函数名。这是因为在使用函数表达式定义函数时,没有必要使用函数名——通过变量sum即可以引用函数。另外, 还要注意函数末尾有一个分号,就像声明其他变量时一样。

最后一种定义函数的方式是使用Function构造函数。Funciton构造函数可以接收任意数量的参数,但最后一个参数始终都被看成是函数体,而前面的参数则枚举出了新函数的参数,来看下面的例子:

var sum = new Funciton('num1', 'num2', 'returne num1 + num2'); //不推荐

 从技术角度讲,这是一个函数表达式。但是,我们不推荐读者使用这种方法定义函数,因为这种语法会导致解析两次代码(第一次是解析常规ECMAScript代码,第二次是解析传入构造函数中的字符串),从而影响性能。不过,这种语法对于理解“函数是对象,函数名是指针”的概念倒是非常直观的。

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

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

 以上代码首先定义了一个名为sum()的函数,用于求两个值的和。然后,又声明了变量anotherSum,并将其设置为与sum相等(将sum的值赋给anotherSum)。注意,使用不带圆括号的函数名是访问函数指针,而非调用函数。 此时,anotherSum和sum就都指向了同一个函数,因此,anotherSum()也可以被调用并返回结果。即使将sum设置为null,让它与函数“断绝关系”,但仍然可以正常调用anotherSum().

1,没有重载

将函数名想象成指针,也有助于理解为什么ECMAScript中没有函数重载的概念。以下是曾在前面使用过的例子:

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

function addSomeNumber(num) {
    return num + 200;
}
var result = addSomeNumber(100); //300

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

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

var addSomeNumber = function(num) {
    return num + 200;
};
var result = addSomeNumber(100); //300

 通过观察重写之后的代码,很容易看清楚到底是怎么回事儿——在创建第二个函数时,实际上覆盖了引用第一个函数的变量addSomeNumber。

2,函数声明和函数表达式
到目前为止,我们一直没有对函数声明和函数表达式加以区别。而实际上,解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁。解析器会率先读取函数声明,并使其在执行任何代码之前可用(可用访问);至于函数表达式,则必须要等到解析器执行到它所在的代码行,才会真正被解释执行。请看下面的例子:
alert(sum(10, 10)); //20
function sum(num1, num2) {
    return num1 + num2;
}

 以上代码完全可以正常运行。因为代码开始执行之前,解析器就已经读取函数声明并将其添加到执行环境中了。如果像下面例子所示的,把上面的函数声明改为变量初始化方式(即使用函数表达式),就会在执行期间导致错误:

alert(anotherSum(10, 10)); //VM167:1 Uncaught ReferenceError: anotherSum is not defined(…)(anonymous function) @ VM167:1
var sum = function(num1, num2) {
    return num1 + num2;
};

以上代码之所以会在运行期间产生错误,原因在于函数位于一个初始化语句中,而不是一个函数声明。换句话说,在执行到函数所在的语句之前,变量sum中不会保存有对函数的引用。而且,由于第一行代码 就会导致错误,实际上也不会执行到下一行。

除了什么时候可以通过变量访问函数这一点区别之外,函数声明和函数表达式的语法实际上是等价的。(也可以同时使用函数声明和函数表达式,例如 var sum = function sum() {};)

3,作为值的函数

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

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

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

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

function add10(num) {
    return num + 10;
}

var result1 = callSomeFunciton(add10, 10);
alert(result1); //20

function getGreeting(name) {
    return 'Hello, ' + name;
}

var result2 = callSomeFunciton(getGreeting,'Nicholas');
alert(result2); //"Hello, Nicholas"

这里的callSomeFunciton()函数是通用的,即无论第一个参数中传递进来的是什么函数,它都会返回执行第一个参数后的结果。还记得吧,要访问函数的指针而不执行函数的话,必须去掉函数名后面的那对圆括号。因此上面例子中传递给callSomeFunciton()的是add10和getGreeting,而不是执行它们之后的结果。

当然,如果从一个函数中返回另一个函数,而且这也是极为有用的一种技术。例如,加入有一个对象数组,我们想要根据某个对象属性对数据进行排序。而传递给数组sort()方法的比较函数要接收两个参数,即要比较的值。可是,我们需要一种方式来指明按照哪个属性来排序。要解决这个问题,可以定义一个函数,它接收一个属性名,然后根据这个属性名来创建一个比较函数,下面就是这个函数的定义:

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;
        }
    };
}

 这个函数定义看起来有点复杂,但实际上无非就是在一个函数中嵌套了另一个函数,而且内部函数前面加了一个return操作符。在内部函数接收到propertyName参数后,它会使用方括号表示法来取得给定属性的值。取得了想要的属性值之后,定义比较函数就比较简单了。上面这个函数可以像下面例子中这样使用:

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"

 这里,我们创建了一个包含两个对象的数组data。其中,每个对象都包含一个name属性和一个age属性。在默认情况下,sort()方法会调用每个对象的toString()方法以确定它们的次序,但得到的结果往往不符合人类的思维习惯。因此,我们调用createComparisonFunction('name')方法创建了一个比较函数,以便按照每个对象的name属性值进行排序。而结果排在前面的第一项是name为'Nicholas',age为29的对象,然后,我们又使用了createComparisonFunction('age')返回的比较函数,这次是按照对象的age属性排序,得到的结果是name值为'Zachary',age值为28的对象排在了第一位。

4,函数内部属性

在函数内部,有两个特殊的对象:arguments和this。其中,arguments在前面有介绍过,它是一个类数组对象,包含着传入函数中的所有参数。虽然arguments的主要用途是保存函数参数,但这个对象还有一个名叫callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数,请看下面这个非常经典的阶乘函数:

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

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

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

 在这个重写后的factorial()函数的函数体内,没有再引用函数名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的函数。

函数内部的另一个特殊对象是this,其行为与Java和C#中的this大致类似。换句话说,this引用的是函数据以执行操作的对象——或者也可以说,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;换句换说,this.color求值会转换成对window.color的求值,于是结果就返回了"red"。当把这个函数赋给对象o并调用o.sayColor()时,this引用的是对象o,因此对this.color求值会转换成对o.color求值,结果就返回了"blue"。(请读者一定要牢记,函数的名字仅仅是一个包含指针的变量而已。因此,即使是在不同的环境执行,全局的sayColor)()函数与o.sayColor()指向的仍然是同一个函数)

5,函数属性和方法

前面曾经提到过,ECMAScript中的函数是对象,因此函数也有属性和方法。每个函数都包含两个属性: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

 以上代码定义了3个函数,但每个函数接收的命名参数个数不同。首先,sayName()函数定义了一个参数,因此其length属性的值为1。类似地,sum()函数定义了2个参数,结果其length属性中保存的值为2.而sayHi()没有命名参数,所以其length值为0.

在ECMAScript核心所定义的全部属性中,最耐人寻味的就要数prototype属性了。对于ECMAScript中的引用类型而言,prototype是保存它们所有实例方法的真正所在。换句话说,诸如toString()和valueOf()等方法实际上都保存在prototype名下,只不过是通过各自对象的实例访问罢了。在创建自定义引用类型以及实际继承时,prototype属性的作用是极为重要的(后面会详细介绍)

每个函数都包含两个非继承而来的方法: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, 10)); //20

在上面这个例子中,callSum1()在执行sum()函数时传入了this作为作用域(因为是在全局作用域中调用的,所以传入的就是window对象)和arguments对象。而 callSum2同样也调用了sum()函数,但它传入的则是this和一个参数数组。这两个函数都会正常执行并返回正确的结果。

call()方法和apply()方法的作用相同,它们的区别仅在于接收参数的方式不同。对于call()方法而言,第一个参数是作用域没有变化,变化的只是其余的参数都是直接传递给函数的。换句话说,在使用call()方法时,传递给函数的参数必须逐个列举出来,如下面的例子所示:

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

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

alert(callSum(10, 10));

 在使用call()方法的情况下,callSum()必须明确地传入每一个参数。结果与使用apply()没有什么不同。至于是使用apply()还是call(),完全取决于你采取哪种给函数传递参数的方式最方便。如果你打算直接传入arguments对象,或者包含函数中先接收到的也是一个数组,那么使用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

 这个例子是在前面说明this对象的示例基础上修改而成的。这一次,sayColor()也是作为全局函数定义的,而且当在全局作用域中调用它时,它确实会显示"red"——因为对this.color的求值会转换成对window.color的求值。而sayColor.call(this)和sayColor.call(window),则是两种显式地在全局作用域中调用函数的方式,结果当然都会显示"red"。但是,当运行sayColor.call(o)时,函数的执行换几个呢就不一样了,因为此时函数体内的this对象指向了o,于是结果显示的是"blue".

使用call()(或apply())来扩充作用域的最大好处,就是对象不需要与方法有任何耦合关系,在前面例子的第一个版本中,我们是先将sayColor()函数放到了对象o中,然后再通过o来调用它的。在这里重写的例子中,就不需要先前那个多余的步骤了。

每个函数继承的toLocalString()和toString()方法始终都返回函数的代码。返回代码的格式则因浏览器而异——有的返回的代码与源代码中的函数代码一样,而有的则返回函数代码的内部表示,即由解析器删除了注释并对某些代码作了改动后的代码。由于存在这些差异,我们无法根据这两个方法返回的结果来实现任何重要功能,不过,这些信息在调试代码时倒是很有用。另外一个继承的valueOf()方法同样也只返回函数代码。如下图:

(每个函数都有一个非标准的caller属性,该属性指向调用当前函数的函数。一般是在一个函数的内部,通过arguments.callee.caller来实现对调用栈的追溯。目前,IE、firefox、Safari和chrome都支持caller属性,但我们只建议将该属性用于调试目的。)

原文地址:https://www.cnblogs.com/iceflorence/p/6052800.html