相关页面 ↓
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;
}
- 常量
由于 var
和 let
申明的是变量,如果要申明一个常量,在ES6之前是不行的,我们通常用全部大写的变量来表示“这是一个常量,不要修改它的值”:
var PI = 3.14;
ES6标准引入了新的关键字 const
来定义常量, const
与 let
都具有块级作用域:
'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
可以拿到 xiaoming
的 birth
属性。
让我们拆开写:
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, 参数为空