JavaScript函数笔记

JavaScript的函数不但是“头等公民”,而且可以像变量一样使用,具有非常强大的抽象能力。 函数就是最基本的一种代码抽象的方式。

函数定义

第一种定义方式:

// 如果没有return语句,函数执行完毕后也会返回结果,只是结果为undefined。
function abs(x) {
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
}

第二种定义方式:

// 由于JavaScript的函数也是一个对象,
// 上述定义的abs()函数实际上是一个函数对象,而函数名abs可以视为指向该函数的变量。
// 因此,第二种定义函数的方式如下:
var abs = function (x) {
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
}; // 注意这种方式按照完整语法需要在函数体末尾加一个;,表示赋值语句结束。

// 在这种方式下,function (x) { ... }是一个匿名函数,它没有函数名。
// 但是,这个匿名函数赋值给了变量abs,所以,通过变量abs就可以调用该函数。

上述两种定义完全等价.

调用函数

// 由于JavaScript允许传入任意个参数而不影响调用,
// 因此传入的参数比定义的参数多也没有问题,虽然函数内部并不需要这些参数:
abs(10, 'blablabla'); // 返回10
abs(-9, 'haha', 'hehe', null); // 返回9

// 传入的参数比定义的少也没有问题;
// 此时abs(x)函数的参数x将收到undefined,计算结果为NaN;
// 要避免收到undefined,可以对参数进行检查.
abs(); // 返回NaN

arguments关键字

JavaScript还有一个免费赠送的关键字arguments,它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。

// 实际上arguments最常用于判断传入参数的个数。你可能会看到这样的写法:
// foo(a[, b], c)
// 接收2~3个参数,b是可选参数,如果只传2个参数,b默认为null:
function foo(a, b, c) {
    if (arguments.length === 2) {
        // 实际拿到的参数是a和b,c为undefined
        c = b; // 把b赋给c
        b = null; // b变为默认值
    }
    // ...
}

// 要把中间的参数b变为“可选”参数,就只能通过arguments判断,然后重新调整参数并赋值。

rest参数

rest参数替代arguments关键字的作用.

// ES6标准引入了rest参数:
function foo(a, b, ...rest) {
    console.log('a = ' + a);
    console.log('b = ' + b);
    console.log(rest);
}

foo(1, 2, 3, 4, 5);
// 结果:
// a = 1
// b = 2
// Array [ 3, 4, 5 ]

foo(1);
// 结果:
// a = 1
// b = undefined
// Array []
// 如果传入的参数连正常定义的参数都没填满,也不要紧,rest参数会接收一个空数组(注意不是undefined).

变量作用域

不同函数内部的同名变量互相独立,互不影响.

// 由于JavaScript的函数可以嵌套,
// 此时,内部函数可以访问外部函数定义的变量,反过来则不行:
function foo() {
    var x = 1;
    function bar() {
        var y = x + 1; // bar可以访问foo的变量x!
    }
    var z = y + 1; // ReferenceError! foo不可以访问bar的变量y!
}

变量提升

JavaScript的函数定义有个特点,它会先扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部.

function foo() {
    var x = 'Hello, ' + y;
    console.log(x);
    var y = 'Bob';
}

// 因为JavaScript引擎自动提升了变量y的声明,但不会提升变量y的赋值.
foo(); // Hello, undefined

// 我们在函数内部定义变量时,请严格遵守“在函数内部首先申明所有变量”这一规则。
// 最常见的做法是用一个var申明函数内部用到的所有变量:
function foo() {
    var
        x = 1, // x初始化为1
        y = x + 1, // y初始化为2
        z, i; // z和i为undefined
    // 其他语句:
    for (i=0; i<100; i++) {
        ...
    }
}

全局作用域

JavaScript实际上只有一个全局作用域。任何变量(函数也视为变量),如果没有在当前函数作用域中找到,就会继续往上查找,最后如果在全局作用域中也没有找到,则报ReferenceError错误。

// 不在任何函数内定义的变量就具有全局作用域,
// 实际上,JavaScript默认有一个全局对象window,
// 全局作用域的变量实际上被绑定到window的一个属性,
// 因此,直接访问全局变量course和访问window.course是完全一样的。
var course = 'Learn JavaScript';
alert(course); // 'Learn JavaScript'
alert(window.course); // 'Learn JavaScript'

// 顶层函数的定义也被视为一个全局变量,并绑定到window对象:
function foo() {
    alert('foo');
}

foo(); // 直接调用foo()
window.foo(); // 通过window.foo()调用

名字空间

全局变量会绑定到window上,不同的JavaScript文件如果使用了相同的全局变量, 或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。

// 减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。例如:
// 唯一的全局变量MYAPP:
var MYAPP = {};

// 其他变量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;

// 其他函数:
MYAPP.foo = function () {
    return 'foo';
};

把自己的代码全部放入唯一的名字空间MYAPP中,会大大减少全局变量冲突的可能。

许多著名的JavaScript库都是这么干的:jQuery,YUI,underscore等等。

块级作用域(let)

由于JavaScript的变量作用域实际上是函数内部,我们在for循环等语句块中是无法定义具有局部作用域的变量的:

function foo() {
    for (var i=0; i<100; i++) {
        //
    }
    i += 100; // 仍然可以引用变量i
}

// 为了解决块级作用域,
// ES6引入了新的关键字let,用let替代var可以申明一个块级作用域的变量:
function foo() {
    var sum = 0;
    for (let i=0; i<100; i++) {
        sum += i;
    }
    // SyntaxError:
    i += 1;
}

常量

// ES6标准引入了新的关键字const来定义常量,const与let都具有块级作用域:
const PI = 3.14;
PI = 3; // 某些浏览器不报错,但是无效果!
PI; // 3.14

解构赋值

从ES6开始,JavaScript引入了解构赋值,可以同时对一组变量进行赋值。

// 什么是解构赋值?我们先看看传统的做法,如何把一个数组的元素分别赋值给几个变量:
var array = ['hello', 'JavaScript', 'ES6'];
var x = array[0];
var y = array[1];
var z = array[2];

// 现在,在ES6中,可以使用解构赋值,直接对多个变量同时赋值: