JavaScript 笔记(六):函数

函数是一种引用数据类型(对象类型),可以存储在一个变量中,基本格式如下:

function funcName(parameterList) {
    // statement
    // ...
    // return
}

函数的形参与返回值可以有,也可以没有,如果函数没有返回值,那么默认返回 undefined;return 可以立即结束函数的执行;调用函数时,实参的个数与形参的个数可以不同,实参默认与形参以从左至右的顺序一一对应,如果实参个数小于形参个数,那么没有相应实参的形参的值在函数作用域中为 undefined,如果实参个数大于形参个数,那么多余的实参将被丢弃;下面是一些使用函数的示例:

let sum = function(a, b) {
    return a + b;
}

let num = 5;
console.log(sum(num, 10));  // 15

function sayHello(name) {
    console.log("Hello, " + name);
}

sayHello("Reyn");   // Hello, Reyn

在 JavaScript 中,由于函数为一种引用类型,可以存储在变量中,因此也可以作为函数的参数与返回值,示例如下:

function getFunc() {
    let sayHello = function () {
        console.log("Hello World");
    }
    return sayHello;
}
function callFunc(fnc) {
    fnc();
}
callFunc(getFunc());    // Hello World

在 JavaScript 中,可以在函数内定义函数,称为函数的嵌套定义

arguments

每一个函数内部默认都有一个名为 arguments 的伪数组,可以保存所有传递给函数的实参,示例如下:

function getSum() {
    let sum = 0;
    for (let i = 0; i < arguments.length; i++) {
        sum += arguments[i];
    }
    return sum;
}
console.log(getSum(1, 2, 3, 4, 5)); // 15

使用 ES6 新增的扩展运算符同样可以实现此功能,扩展运算符将所有传递给函数的实现打包到一个数组中,示例如下:

function getSum(...args) {
    let sum = 0;
    for (let i = 0; i < args.length; i++) {
        sum += args[i];
    }
    return sum;
}
console.log(getSum(1, 2, 3, 4, 5)); // 15

值得注意的是,函数中的扩展运算符与赋值运算符左侧的扩展运算符相同,扩展运算符所在的参数必须为最后一个

默认值

在 ES6 之前使用逻辑运算符实现为参数指定默认值,示例如下:

function getFullName(firstName, lastName) {
    firstName = firstName || "Steven";
    lastName = lastName || "Jobs";
    return firstName + ' ' + lastName;
}
console.log(getFullName("Reyn", "Morales"));    // Reyn Morales
console.log(getFullName()); // Steven Jobs

在 ES6 之后可以使用如下形式实现为参数指定默认值:

function getFullName(firstName = "Steven", lastName = "Jobs") {
    return firstName + ' ' + lastName;
}
console.log(getFullName("Reyn", "Morales"));    // Reyn Morales
console.log(getFullName()); // Steven Jobs

值得注意的是,在 ES6 之后,默认值也可以从其它函数中获取,示例如下:

let celebrityLastName = function () {
    return "Jobs";
}
function getFullName(firstName = "Steven", lastName = celebrityLastName()) {
    return firstName + ' ' + lastName;
}
console.log(getFullName("Reyn", "Morales"));    // Reyn Morales
console.log(getFullName()); // Steven Jobs

匿名函数

匿名函数即为没有名称的函数,不可以只定义不使用,示例如下:

(function (function () {    // 将匿名函数作为参数
    return function () {    // 将匿名函数作为返回值
        console.log("Hello World"); // Hello World
    };
}){
    arguments[0]();
})();  // 调用一个匿名函数

以上写法是错误的,旨在一次性展示匿名函数的所有用法,值得注意的是,如果在定义匿名函数时立即调用,那么必须将定义置于小括号中,并在使用小括号置于其后,表明调用函数,格式如下:

(function () {
    console.log("Hello World");
})();

箭头函数

在 ES6 中新增的一种定义函数的方式,目的在于简化代码,格式如下:

let functionName = (parametersList) => {
    // statement
}

示例如下:

let say = function(name) {
    console.log("Hello, " + name);
};
say("Reyn Morales");    // Hello, Reyn Morales

let hello = (name) => {
    console.log("Hello, " + name);
};
hello("Reyn Morales");  // Hello, Reyn Morales

let sayHello = name => console.log("Hello, " + name);
sayHello("Reyn Morales");   // Hello, Reyn Morales

使用箭头函数时,如果只有一个形参,那么可以省略 (),如果函数体中只有一条语句,那么 {} 也可以省略

递归函数

递归函数即为函数调用自己本身,示例如下:

let fibonacci = num => {
    if (num == 1)
        return 1;
    return num * fibonacci(num - 1);
}
console.log(fibonacci(5));  // 120

作用域

在 JavaScript 中,位于 {} 之外区域称为全局作用域,位于函数之后的 {} 内部的区域称为局部作用域,位于其它语句(循环、分支等)之后的 {} 称为块级作用域

使用 var 定义变量时,如果在局部作用域内部,那么此变量为局部变量,如果在块级作用域内部,那么此变量为全局变量;此外,不论是在局部作用域还是块级作用域,如果省略了定义函数时的 var 或 let,那么此变量是一个全局变量

关键字

全局作用域

局部作用域

块级作用域

var

全局变量

局部变量

全局变量

let

全局变量

局部变量

局部变量

使用 let 定义变量时,在不同的作用域中定义的变量不是同一个变量,即使它们的标识符相同,示例如下:

{
    let name = "Reyn";
    {
        let name = "Jobs";
        console.log(name);  // Jobs
    }
    console.log(name);  // Reyn
}

在同一个作用域中,如果出现了使用 let 定义的一个变量,那么不能再使用相同的标识符定义一个变量,即使使用 var,不论顺序如何,均会报错,示例如下:

let name = "Reyn";
var name = "Jobs"; // 报错

var num = 520;
let num = 1024; // 报错

作用域链

ES6 之前

在研究 ES6 之前的作用域之前,明确如下:

  1. ES6 之前使用关键字 var 定义变量
  2. ES6 之前只有全局作用域和局部作用域,没有块级作用域
  3. ES6 之前函数大括号之外的均为全局作用域
  4. ES6 之前函数大括号之内的均为局部作用域

在 ES6 之前,全局作用域又称为 0 级作用域,如果定义了函数,那么就会开启 1、2、3、… 级作用域,JavaScript 将作用域链接在一起,形成一个作用域链:

0 -> 1 -> 2 -> 3 -> ...

除了 0 级作用域之外,其它作用域级别等于上一级作用域级别加一,示例如下:

// 0 级作用域(全局作用域)
function fnc() {
    // 1 级作用域
    function subFnc() {
        // 2 级作用域
        function subSubFnc() {
            // 3 级作用域
            function subSubSubFnc() {
                // ...
            }
        }
    }
}

当在某一个作用域中使用某个变量时,将先在当前作用域中查找此变量,如果没有找到,那么将在上一级作用域中查找,如果依然没有找到,那么将在上上一级作用域中查找,依次类推,直到 0 级作用域,如果在 0 级作用域任然没有找到,那么将会报错

ES6 之后

在研究 ES6 之后的作用域之前,明确如下:

  1. 在 ES6 之后使用 let 定义变量
  2. 在 ES6 之后除了全局作用域和局部作用域之外,还新增了块级作用域
  3. 在 ES6 之后虽然新增了块级作用域,但对于 let 定义的变量来说,在局部作用域和块级作用域中并没有区别,均为局部变量

在 ES6 之后的作用域链与 ES6 之后的作用域类似,不同的是在使用代码块时也会开启作用域,示例如下:

// 0 级作用域(全局作用域)
{
    // 1 级作用域
    function subFnc() {
        // 2 级作用域
        if (true) {
            // 3 级作用域
            while (false) {
                // ...
            }
        }
    }
}

当在某一个作用域中使用某个变量时,将先在当前作用域中查找此变量,如果没有找到,那么将在上一级作用域中查找,如果依然没有找到,那么将在上上一级作用域中查找,依次类推,直到 0 级作用域,如果在 0 级作用域任然没有找到,那么将会报错

预解析

浏览器在执行 JavaScript 代码之前,存在预解析的步骤,即先将代码加工处理之后再解释执行,预解析时,先将变量声明和函数声明提升到当前作用域的最前面,然后将剩余的代码以原来的顺序依次后置,函数的预解析示例如下:

使用 let 定义的变量不会被提升(预解析)

/* 格式一 */
say();
function say() {
    console.log("Hello World");
}

/* 格式二 */
say();  // 报错
let say = function() {
    console.log("Hello World");
}

/* 格式三 */
say();  // 报错
let say = () => console.log("Hello World");

在 JavaScript 中,除了函数头之外,函数体也为函数声明的一部分,因此格式一正确,在使用函数时,函数确实已经被定义了,而通过变量使用函数,将变量声明提升之后,在使用变量时,此变量还没有被赋予函数内容,因此格式二和格式三错误

在高级浏览器中,在 {} 之中的函数不会被预解析,只有在低级浏览器中才会以正常方式解析,此外,如果在同一级作用域下,如果变量名称与函数名称相同,那么函数的优先级高于变量,示例如下:

console.log(value);
var value = 520;
function value() {
    console.log("fnc value");
}
console.log(value);

/* 预解析 */
function value() {
    console.log("fnc value");
}
console.log(value); // function value() { console.log("fn value"); }
var value;
value = 520;
console.log(value); // 520