03 函数

1/15/2021 javascript

# 3、函数

# 3.1、函数定义和调用

  • 1、定义函数方式一
function abs(x) {
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
}

一旦执行到 return 时,函数就执行完毕,并将结果返回。 如果没有 return 语句,函数执行完毕后也会返回结果,只是结果为 undefined

  • 2、第二种定义函数的方式如下
var abs = function (x) {
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
};

在这种方式下, function (x) { ... } 是一个匿名函数,它没有函数名。但是,这个匿名函数赋值 给了变量 abs ,所以,通过变量 abs 就可以调用该函数。 上述两种定义完全等价,注意第二种方式按照完整语法需要在函数体末尾加一个 ; ,表示赋值语句结束。

  • 3、调用函数

调用函数时,按顺序传入参数即可:

abs(10); // 返回10
abs(-9); // 返回9

由于JavaScript允许传入任意个参数而不影响调用,因此传入的参数比定义的参数多也没有问题,虽然函数内部并不需要这些参数:

abs(10, 'blablabla'); // 返回10
abs(-9, 'haha', 'hehe', null); // 返回9

传入的参数比定义的少也没有问题:

abs(); // 返回NaN

此时 abs(x) 函数的参数 x 将收到 undefined ,计算结果为 NaN 。 要避免收到 undefined ,可以对参数进行检查:

function abs(x) {
    if (typeof x !== 'number') {
        throw 'Not a number';
    }
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
}
  • arguments

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

function foo(x) {
    console.log('x = ' + x); // 10
    for (var i = 0; i < arguments.length; i++) {
        console.log('arg ' + i + ' = ' + arguments[i]); // 10, 20, 30
    }
}
foo(10, 20, 30);

利用 arguments ,你可以获得调用者传入的所有参数。也就是说,即使函数不定义任何参数,还是可以拿到参数的值:

function abs() {
    if (arguments.length === 0) {
        return 0;
    }
    var x = arguments[0];
    return x >= 0 ? x : -x;
}
abs(); // 0
abs(10); // 10
abs(-9); // 9

实际上 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变为默认值
    }
    // ...
}
  • rest参数

由于JavaScript函数允许接收任意个参数,于是我们就不得不用 arguments 来获取所有参数:

function foo(a, b) {
    var i, rest = [];
    if (arguments.length > 2) {
        for (i = 2; i < arguments.length; i++) {
            rest.push(arguments[i]);
        }
    }
    console.log('a = ' + a);
    console.log('b = ' + b);
    console.log(rest);
}

为了获取除了已定义参数 a 、 b 之外的参数,我们不得不用 arguments ,并且循环要从索引2 开始以便排除前两个参数,这种写法很别扭,只是为了获得额外的 rest 参数,有没有更好的方 法? 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 参数只能写在最后,前面用 ... 标识,从运行结果可知,传入的参数先绑定 a 、 b ,多余的参数以数组形式交给变量 rest ,所以,不再需要 arguments 我们就获取了全部参数。

# 3.2、变量作用域

  • 变量的作用域

在JavaScript中,用 var 申明的变量实际上是有作用域的。 如果一个变量在函数体内部申明,则该变量的作用域为整个函数体,在函数体外不可引用该变量:

'use strict';

function foo() {
    var x = 1;
    x = x + 1;
}
x = x + 2; // ReferenceError! 无法在函数体外引用变量x

如果两个不同的函数各自申明了同一个变量,那么该变量只在各自的函数体内起作用。换句话说,不同函数内部的同名变量互相独立,互不影响:

'use strict';

function foo() {
    var x = 1;
    x = x + 1;
}

function bar() {
    var x = 'A';
    x = x + 'B';
}

由于JavaScript的函数可以嵌套,此时,内部函数可以访问外部函数定义的变量,反过来则不行:

'use strict';

function foo() {
    var x = 1;

    function bar() {
        var y = x + 1; // bar可以访问foo的变量x!
    }
    var z = y + 1; // ReferenceError! foo不可以访问bar的变量y!
}

如果内部函数和外部函数的变量名重名怎么办?来测试一下:

function foo() {
    var x = 1;

    function bar() {
        var x = 'A';
        console.log('x in bar() = ' + x); // 'A'
    }
    console.log('x in foo() = ' + x); // 1

    bar();
}
foo();

这说明JavaScript的函数在查找变量时从自身函数定义开始,从“内”向“外”查找。如果内部函数定义了与外部函数重名的变量,则内部函数的变量将“屏蔽”外部函数的变量。

  • 变量提升

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

'use strict';

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

结果:Hello, undefined , 说明 y 的值为 undefined; 这正是因为JavaScript引擎自动提升了变量 y 的声明,但不会提升变量 y 的赋值。 对于上述 foo() 函数,JavaScript引擎看到的代码相当于:

function foo() {
    var y; // 提升变量y的申明,此时y为undefined
    var x = 'Hello, ' + y;
    console.log(x);
    y = 'Bob';
}

由于JavaScript的这一怪异的“特性”,我们在函数内部定义变量时,请严格遵守“在函数内部首先申明所有变量”这一规则。最常见的做法是用一个 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默认有一个全局对象 window ,全局作用域的变量实际上被绑定到 window 的一个属性:

'use strict';
var course = 'Learn JavaScript';
alert(course); // 'Learn JavaScript'
alert(window.course); // 'Learn JavaScript

因此,直接访问全局变量 course 和访问 window.course 是完全一样的。 因此,顶层函数的定义也被视为一个全局变量,并绑定到 window 对象:

'use strict';

function foo() {
    alert('foo');
}
foo(); // 直接调用foo()
window.foo(); // 通过window.foo()调用

进一步大胆地猜测,我们每次直接调用的 alert() 函数其实也是 window 的一个变量:

'use strict';
window.alert('调用window.alert()');
// 把alert保存到另一个变量:
var old_alert = window.alert;
// 给alert赋一个新函数:
window.alert = function () {}
alert('无法用alert()显示了!');
// 恢复alert:
window.alert = old_alert;
alert('又可以用alert()了!');

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

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

减少冲突的一个方法是把自己的所有变量和函数 全部绑定到一个全局变量中。例如:

// 唯一的全局变量MYAPP:
var MYAPP = {};
// 其他变量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;
// 其他函数:
MYAPP.foo = function () {
    return 'foo';
};

把自己的代码全部放入唯一的名字空间 MYAPP 中,会大大减少全局变量冲突的可能。 许多著名的 JavaScript 库都是这么干的:jQuery,YUI,underscore等等。

  • 局部作用域

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

'use strict';

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

为了解决块级作用域,ES6引入了新的关键字 let ,用 let 替代 var 可以申明一个块级作用域的变量:

'use strict';

function foo() {
    var sum = 0;
    for (let i = 0; i < 100; i++) {
        sum += i;
    }
    
    // SyntaxError:
    i += 1;
}
  • 常量

由于 varlet 申明的是变量,如果要申明一个常量,在ES6之前是不行的,我们通常用全部大写的变量来表示“这是一个常量,不要修改它的值”:

var PI = 3.14;

ES6标准引入了新的关键字 const 来定义常量, constlet 都具有块级作用域:

'use strict';
const PI = 3.14;
PI = 3; // 某些浏览器不报错,但是无效果!
PI; // 3.14

# 3.3、方法

  • 定义方法

在一个对象中绑定函数,称为这个对象的方法。 也是一个对象的属性,只不过这个属性是函数。

在JavaScript中,对象的定义是这样的:

var xiaoming = {
    name: '小明',
    birth: 1990
};

但是,如果我们给 xiaoming 绑定一个函数,就可以做更多的事情。比如,写个 age() 方法,返 回 xiaoming 的年龄:

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: function () {
        var y = new Date().getFullYear();
        return y - this.birth;
    }
};
xiaoming.age; // function xiaoming.age()
xiaoming.age(); // 今年调用是25,明年调用就变成26了

绑定到对象上的函数称为方法,和普通函数也没啥区别,但是它在内部使用了一个 this 关键字,这个东东是什么?

在一个方法内部, this 是一个特殊变量,它始终指向当前对象,也就是 xiaoming 这个变量。所 以, this.birth 可以拿到 xiaomingbirth 属性。

让我们拆开写:

function getAge() {
    var y = new Date().getFullYear();
    return y - this.birth;
}

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: getAge
};

xiaoming.age(); // 25, 正常结果
getAge(); // NaN

单独调用函数 getAge() 怎么返回了 NaN ?请注意,我们已经进入到了JavaScript的一个大坑里。 JavaScript的函数内部如果调用了 this ,那么这个 this 到底指向谁? 答案是,视情况而定!

如果以对象的方法形式调用,比如 xiaoming.age() ,该函数的 this 指向被调用的对象,也就是 xiaoming ,这是符合我们预期的。

如果单独调用函数,比如 getAge() ,此时,该函数的 this 指向全局对象,也就是 window

  • apply

不过,我们还是可以控制 this 的指向的!

要指定函数的 this 指向哪个对象,可以用函数本身的 apply 方法,它接收两个参数,第一个参数就是需要绑定的 this 变量,第二个参数是 Array ,表示函数本身的参数。

function getAge() {
    var y = new Date().getFullYear();
    return y - this.birth;
}
var xiaoming = {
    name: '小明',
    birth: 1990,
    age: getAge
};
xiaoming.age(); // 25
getAge.apply(xiaoming, []); // 25, this指向xiaoming, 参数为空
更新时间: Sunday, December 5, 2021 12:03