js中的函数

2019-03-29  本文已影响0人  不会潜水的猫小喵

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

//函数基本格式
function functionName(arg0, arg1, ..., argN) {
    statements
}

//函数示例
function sayHi(name, message) {
    console.log("Hello" + name + "," + message);
}

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

sayHi("World", "The wind is very strong today."); //HelloWorld,The wind is very strong today.

js中的函数在定义时不必指定是否返回值。如果需要返回值,通过return语句跟要返回的值来实现返回值。例如:

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

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

function sum(num1, num2) {
    return num1 + num2;
    console.log("HelloWorld!"); //永远不会执行
}

return语句也可以不带任何返回值,在这种情况下,函数在停止执行后将返回undefined值。这种用法一般用在需要提前停止函数执行又不需要返回值的情况下。例如:

function sayHi(name, message) {
    return;
    console.log("Hello" + name + "," + message); //永远不会调用
}

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

函数实际上是对象。每个函数都是Function类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。

  1. 使用函数声明语法定义
function sum(num1, num2) {
    return num1 + num2;
}
  1. 使用函数表达式定义
var sum = function(num1, num2) {
    return num1 + num2;
};

3.使用Function构造函数

var sum = new Function("num1", "num2", "return num1 + num2"); //不推荐

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

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

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

console.log(sum(1, 2)); //3

var anotherSum = sum;
console.log(anotherSum(1, 2)); //3

sum = null;
console.log(anotherSum(1, 2)); //3

解析器向执行环境中加载数据时,会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。

console.log(sum(1, 2)); //3
function sum(num1, num2) {
    return num1 + num2;
}

以上代码完全可以正常运行。因为在代码开始执行之前,解析器就已经通过一个名为函数声明变量提升的过程,读取并将函数声明添加到执行环境中。对代码求值时,js引擎在第一遍会声明函数并将它们放到源代码树的顶部。如果像下面的例子所示的,把上面的函数声明改为等价的函数表达式,就会在执行期间导致错误。

console.log(sum(1, 2)); //Uncaught TypeError: sum is not a function
var sum = function(num1, num2) {
    return num1 + num2;
}

除了什么时候可以通过变量访问函数这一点区别之外,函数声明和函数表达式的语法其实是等价的。

也可以同时使用函数声明和函数表达式,例如var sum = function sum() {}。不过,这种语法在Safari中会导致错误。

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

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

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

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

在这个重写后的factorial()函数的函数体内,没有再引用函数名factorial。这样,无论引用函数时使用的是什么名字,都可以保证正常完成递归调用。例如:

var anotherFactorial = factorial;
factorial = function() {
    return 0;
};

console.log(anotherFactorial(5)); //120
console.log(factorial(5)); //0

函数内部的另一个特殊对象是thisthis引用的是函数据以执行的环境对象——或者也可以说是this值(当在网页的全局作用域中调用函数时,this对象引用的就是window)。

window.color = "red";
var  o = {
    color: "blue"
};

function sayColor() {
    console.log(this.color);
}

sayColor(); //red
o.sayColor = sayColor;
o.sayColor(); //blue

因为js中的函数是对象,因此函数也有属性和方法。每个函数都包含两个属性:lengthprototype。其中length属性表示函数希望接收的命名参数的个数。对于js的引用类型而言,prototype是保存它们所有实例方法的真正所在。换句话说,诸如toString()valueOf()等方法实际上都保存在prototype名下,只不过是通过各自对象的实例访问罢了。在js中,prototype属性是不可枚举的,因此用for-in无法发现。

每个函数都包含两个非继承而来的方法: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]); //传入数组
}

console.log(callSum1(1, 2)); //3
console.log(callSum2(1, 2)); //3

call()方法与apply()方法的作用相同,区别仅在于接收参数的方式不同。对于call()而言,第一个参数是this不变,变化的是其余参数必须逐个列举出来传递给函数。

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

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

console.log(callSum(1, 2)); //3

事实上,传递参数并非apply()call()真正的用武之地;它们真正强大的地方是能够扩充函数赖以运行的作用域,对象不需要与方法有任何耦合关系。

window.color = "red";
var  o = {
    color: blue
};

function sayColor() {
    console.log(this.color);
}

sayColor.call(this); //red
sayColor.call(window); //red
sayColor.call(o); //blue

es5中还定义了一个方法:bind()。这个方法会创建一个函数的实例,其this值会被绑定到传递给bind()函数的值。

window.color = "red";
var  o = {
    color: blue
};

function sayColor() {
    console.log(this.color);
}

var objSayColor = sayColor.bind(o);
objSayColor(); //blue

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

参考书籍:《JavaScript高级程序设计(第3版)》。

上一篇下一篇

猜你喜欢

热点阅读