目录
函数是一段可以反复调用的代码块。函数还能接受输入的参数,不同的参数会返回不同的值。
函数的声明
- function命令
function print(s) {
console.log(s);
}
- 函数表达式
var print = function(s) {
console.log(s);
};
- Function 构造函数
var add = new Function(
'x',
'y',
'return x + y'
);
// 等同于
function add(x, y) {
return x + y;
}
你可以传递任意数量的参数给Function
构造函数,只有最后一个参数会被当做函数体,如果只有一个参数,该参数就是函数体。
函数的属性与特性
- name属性
函数的name
属性返回函数的名字。
function f1() {}
f1.name // "f1"
// 如果是通过变量赋值定义的函数,那么name属性返回变量名。
var f2 = function () {};
f2.name // "f2"
- leng属性
函数的length
属性返回函数预期传入的参数个数,即函数定义之中的参数个数。
function f(a, b) {}
f.length // 2
不管调用时传入多少个参数,leng属性值始终等于2,length
属性就是定义时的参数个数,与调用无关。
- 函数名提升
JavaScript 引擎将函数名视同变量名,所以采用function
命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。所以,下面的代码不会报错。
f();
function f() {}
函数作用域
作用域分全局作用域 和 函数作用域,全局作用域,变量在整个程序中一直存在,所有地方都可以读取;函数作用域,变量只在函数内部存在。
- 在函数内部定义的变量,外部无法读取,称为“局部变量”。
function f(){
var v = 1;
}
v // ReferenceError: v is not defined
- 函数内部定义的变量,会在该作用域内覆盖同名全局变量。
var v = 1;
function f(){
var v = 2;
console.log(v);
}
f() // 2
v // 1
- 函数作用域内部也会产生“变量提升”现象。
var
命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部
- 函数本身也是一个值,也有自己的作用域。就是其声明时所在的作用域,与其运行时所在的作用域无关(与调用无关)。
var a = 1;
var x = function () {
console.log(a);
};
function f() {
var a = 2;
x();
}
f() // 1
// 函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。
var x = function () {
console.log(a);
};
function y(f) {
var a = 2;
f();
}
y(x)
// ReferenceError: a is not defined
// 同样的,函数体内部声明的函数,作用域绑定函数体内部。
function foo() {
var x = 1;
function bar() {
console.log(x);
}
return bar;
}
var x = 2;
var f = foo();
f() // 1
函数参数的传递方式
- 函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递
这意味着,在函数体内修改参数值,不会影响到函数外部。
var p = 2;
function f(p) {
p = 3;
}
f(p);
p // 2
- 函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递。
也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。
var obj = { p: 1 };
function f(o) {
o.p = 2;
}
f(obj);
obj.p // 2
如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。
var obj = [1, 2, 3];
function f(o) {
o = [2, 3, 4];
}
f(obj);
obj // [1, 2, 3]
这是因为,形式参数(o
)的值实际是参数obj
的地址,重新对o
赋值导致o
指向另一个地址,保存在原地址上的值当然不受影响。
就是说 o 其实作为参数代表数组 [1,2,3] 的地址,又被赋予了 [2,3,4] 的地址,所以不会影响到 obj。
arguments 对象
- 由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是
arguments
对象的由来。
arguments
对象包含了函数运行时的所有参数,arguments[0]
就是第一个参数,arguments[1]
就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。
- 虽然
arguments
很像数组,但它是一个对象。数组专有的方法(比如slice
和forEach
),不能在arguments
对象上直接使用。
arguments
对象带有一个callee
属性,返回它所对应的原函数。
var f = function () {
console.log(arguments.callee === f);
}
f() // true
闭包
理解闭包,首先必须理解变量作用域。前面提到,JavaScript 有两种作用域:全局作用域和函数作用域。函数内部可以直接读取全局变量。但是函数外部无法读取函数内部声明的变量。出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
上面代码中,函数f1
的返回值就是函数f2
,由于f2
可以读取f1
的内部变量,所以就可以在外部获得f1
的内部变量了。
闭包就是函数f2,
由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。
闭包的最大用处用两个:
- 可以读取外层函数内部的变量
- 让外层函数的变量始终保持在内存中
为什么闭包能够返回外层函数的内部变量:
闭包用到了外层变量,导致外层函数不能从内存释放。只要闭包没有被垃圾回收机制清除,外层函数提供的运行环境也不会被清除,它的内部变量就始终保存着当前值,供闭包读取。