“>

这一章我们来学习一个非常锻炼我们思维的概念:函数,这个概念能够帮助我们掌握抽象思维,在单语句编程的基础上上升到另一个维度。

函数的基本概念

一个函数是一个一旦被调用,一堆代码就会开始运行的名字。函数可以返回特定的值,也可以通过接受参数的传递影响运行结果。

我们为什么需要函数呢?好处就是我们可以把相同的逻辑放入一个函数中,这样我们每次想运行这堆代码的时候,不需要重新编码,只需要调用相同函数即可。简单来说,函数可以让我们的代码只需被编辑一次,就能被多次调用,省去我们复写相同逻辑的麻烦。

比如我们定义一个以下这个叫做printMultiplication的方法:

function printMultiplication(num1, num2) {
  console.log(num1 * num2);
}

那么每次我们想要打印出两个数字相乘的结果,只需要调用此函数即可:

printMultiplication(5, 6); // 30
printMultiplication(2, 3); // 6

函数的定义和调用

函数的定义必须以 function 关键字为开头,然后跟上函数名,紧接着加上圆括号( )。函数名的定义和变量一样,只需要使用字母、数字和下划线即可,然后不同单词之间要么以首字母大写为区分,或者用下划线进行分割。圆括号中间的内容是参数(被传入函数的数据),不同参数之间可以用逗号隔开。以下就是函数的定义模板:

function funcName(parameter1, parameter2, parameter3) {
    // code to be executed
}

其中funcName是函数名,parameter1, 2, 3都是参数,参数会被传到函数的内部,然后函数开始执行特定的代码。如果不需要参数,括号中什么都不加就可以了。

来看一个简单的函数,以下这个函数能将两个数字相加,并打印出相加之和结果:

function addTwoNum(num1, num2) {
    var sum = num1 + num2;
    console.log(sum);
}

如果要调用这个函数,我们只需要输入函数名 addTwoNum,然后圆括号中加上相加的参数,并用逗号隔开即可:

addTwoNum(9, 5); // 14

返回值 Function Return

函数也可以返回特定的值,只需要在函数中使用 return 关键字跟上要返回的值即可。比如以下的函数会将任何的参数加上5之后,再返回新的数值:

function addFive(num) {
    return num + 5;
}

如果我们将数值11作为参数传入,那么就会返回16:

console.log(addFive(11)); // 16

当函数有返回值后,我们就可以将函数的值赋给变量了,如下:

num1 = addFive(6);
console.log(num1); // 11

函数除了能返回数值之外,也能返回空值、布尔、字符串、和数组:

function isEven(num) {
    return num % 2 == 0;
}

以上的函数能根据参数返回一个布尔值,用于判断参数是否为偶数,如果参数num为偶数,返回值为true,否则false。我们也可以在一个函数中调用其他的函数,如下:

function evenNumberOperator(num) {
    if (isEven(num)) { 
        return addFive(num);
    } else {
        return num;
    }
}

这个函数首先检测参数是否为偶数,如果是,就将原来的数值加上5并返回,否则直接返回原数:

console.log(evenNumberOperator(5)); // 5
console.log(evenNumberOperator(4)); // 9

要注意的是,一旦函数碰到return,等于在for loop中碰到break一样,直接中断后面的内容,跳出函数了。比如调用下面这个函数的话,return后面的内容是不会被执行的:

function testReturn() {
    console.log("Before return");
    return;
    console.log("After return");
}
testReturn() // only print "Before return"

内置函数

JavaScript语言中有很多写好的函数,我们直接调用就行了。来看一看JavaScript数组自带的函数,如果我们要查看一个数组中是否包含特定的元素,我们可以直接调用 includes 函数:

var cars = ["BMW", "AUDI", "VOLVO"];
console.log(cars.includes("BMW")); // true

如果我们要查看数组中一个特定元素的下标,我们可以使用indexOf函数:

console.log(cars.indexOf("AUDI")); // 1

结合数组自带的函数,我们可以创建出一个自己的函数。以下这个函数的参数为数组类型,函数的作用是自动找出一个元素在数组的位置,如果不存在,就打印出提示字符:

function checkArrayItem(arrayInput, target) {
    if (arrayInput.includes(target)) { 
        console.log("Index is " + arrayInput.indexOf(target));
    } else {
        console.log("Array doesn't includes " + target);
    }
}

这样我们就能使用这个函数简化数组元素的查询了:

cars = ["BWM", "AUDO", "VOLVO"];
checkArrayItem(cars, "AUDO"); // Index is 1
checkArrayItem(cars, "HONDA"); // Array doesn't includes HONDA

箭头函数

JavaScript中还有一种特殊的函数叫箭头函数,这种函数的语法比正常函数更简洁。以下两个函数执行结果一样,第二个是箭头函数,语法会更加简洁:

hello = function() {
    return "Hello World!";
}
hello = () => {
    return "Hello World!";
}

我们甚至可以把尖括号去掉,让这个函数变得更短:

hello = () => "Hello World";

如果我们要在箭头函数中加入参数,可以直接在圆括号中加入参数,如下:

concatenateTwoWords = (w1, w2) => w1 + " " + w2;
console.log(concatenateTwoWords("Hello", "World")); // Hello World

为什么我们没使用 return 也能有返回内容呢?因为箭头函数会自动返回执行内容,以下两个箭头函数都能自动将执行的结果赋予 func 变量:

var func = x => x * x;                  
// concise body syntax, implied "return"

var func = (x, y) => { return x + y; }; 
// with block body, explicit "return" needed

可以看到箭头函数非常简洁,对于逻辑简单的代码,我们可以直接使用箭头函数实现。

变量作用域 var vs. let

我们除了可以使用 var 关键字定义变量外,还能使用另一个关键字let来定义变量,并限制变量的生存范围。let 变量的生存周期不是全局的,而是被限制在一个区块内,而 var 变量都是全局变量:

function varTest() {
  var x = 1;
  {
    var x = 2;  // same variable!
    console.log(x);  // 2
  }
  console.log(x);  // 2
}

上面代码中的两个 var x 都是指向相同的 x,所以第二个 x 被改变之后,输出的 x 也是2。

如果我们使用 let 来定义 x,那么他们所属的生存周期则不是全局的:

function letTest() {
  let x = 1;
  {
    let x = 2;  // different variable
    console.log(x);  // 2
  }
  console.log(x);  // 1
}

可以看到第一个 x 的生存周期在第二个 x 生存周期外,所以第二个 x 的尖括号结束后,打印出的 x 是1,代表他们的生存范围是不同的。以下是 let 和 var 的结合:

function varAndLet() {
    var x = 1;
    { 
        let x = 2;
        console.log(x); // 2
    }
    console.log(x); // 1
}

这个例子中第一个 x 的生存范围是全局的,而第二个是局部的,所以跳出括号后,x的值则是全局变量的值1。再来看最后一个例子:

function varAndLet() {
    let x = 1;
    { 
        var x = 2;
        console.log(x); // Error
    }
    console.log(x); // 1
}

这个函数运行后,在定义第二个x处会弹出错误信息,提示x已经被定义过了。这是因为我们在一开始就定义了局部变量x,那么在第二处x再想定义一个相同名字的全局变量就会有冲突。

Callback 回调

在JavaScript中,函数的完成时间各不相同。有时候某个函数因为需要调用网络资源,可能执行的时间比较久,而如果我们在这个函数之后执行一个非常简单的函数,很有可能会出现第一个函数还没执行完,第二个函数就开始执行的情况,这可能会导致不必要的麻烦。

以下代码中,first函数虽然出现在second之前,可是second中的内容还是会先被执行。

function first(){
  // Simulate a code delay
  setTimeout( function(){
    console.log(1);
  }, 500 );
}
function second(){
  console.log(2);
}
first();
second();

为了控制函数的执行顺序,我们可以使用JavaScript中的回调机制。简单来说,回调能够确保一个函数在另一个函数执行完毕之后再执行。比如我们可以将以上的代码改为:

function first(callback) {
    setTimeout(function() {
        console.log(1);
        callback();
    }, 500);
}
first(function() {
    console.log(2);
});

这样我们调用函数first的时候,就能确保输出2的语句会在1输出完毕之后才被执行。以下这个例子也能保证结束做作业的提示,会在开始做作业的提示出现后才执行。

function doHomework(subject, callback) {
  alert(`Starting my ${subject} homework.`);
  callback();
}

doHomework('math', function() {
  alert('Finished my homework');
});

在真正的网站开发中回调是非常重要的,比如在一个function从别的网站下载数据的时候,我们希望特定函数必须在这个数据下载完毕后才能执行,那么我们就需要使用回调,不让函数的执行顺序就因为需求回应的时间不同而乱套了。

实践练习

请自定义一个函数sumArray(array1, array2),参数必须是两个长度相同的数组,然后在函数中将两个数组中的元素依次相加,返回一个含有相加元素的新数组。以下是调用此方法的效果:

var array1 = [1, 5, 8, 9];
var array2 = [2, 4, 8, 1];
var newArray = sumArray(array1, array2); // [3, 9, 16, 10]

答案请参看:JavaScript实践练习答案