JS数据类型之函数
定义
函数可以理解为一个特定的代码块容器,它可以完成特定的需求,并且可以重复使用。
函数声明和调用
函数默认不会主动执行,必须通过函数名()
调用才会执行。
函数声明
JavaScript 有三种声明函数的方法。
function 命令
function
命令声明的代码区块,就是一个函数。function
命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。
function print(s) {
console.log(s);
}
上面的代码命名了一个 print
函数,以后使用 print
这种形式,就可以调用相应的代码。这叫做函数的声明。
函数表达式
可以采取变量赋值的方法命名函数
var print = function(s) {
console.log(s);
};
上面代码是将一个匿名函数赋值给变量。此时,这个匿名函数又称函数表达式,因为赋值语句的右侧只能放表达式。
需要注意的是:
function
命令后面不带有函数名;- 函数的表达式需要在末尾加上分号,表示语句结束。
Function 构造函数
使用 Function
构造函数,这种声明函数的方式非常不直观,几乎无人使用。
var add = new Function(
'x',
'y',
'return x + y'
);
// 等同于
function add(x, y) {
return x + y;
}
上面代码中,Function
构造函数接受了三个参数,除了最后一个函数是 add
函数的函数体,其他都是 add
函数的参数。
Function
构造函数可以不使用 new
命令,返回结果完全一样。
函数的重复声明
如果同一个函数被重复声明,后面的声明就会覆盖前面的声明。
function f() {
console.log(1)
}
f(); // 2
function f() {
console.log(2)
}
f(); // 2
上面代码中,后一次的函数声明覆盖了前面一次。而且,由于函数名的提升,前一次的声明在任何时候都是无效的。
函数名的提升
JavaScript 引擎将函数名视同变量名,所以采用function
命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。
f();
function f(){}
上面代码中看起来像在声明之前就调用了函数 f
。但是实际上,由于“变量提升”,函数 f
被提升到了代码头部,也就是在调用之前就已经声明了。
函数参数
函数运行的时候,有时需要提供外部数据,不同的外部数据会得到不同的结构,这种外部数据就叫做参数。
参数的省略
function f(a, b) {
return a;
}
f(1, 2, 3); // 1
f(1); // 1
f(); // undefined
console.log(f.length); // 2
上面代码的函数 f
定义了两个参数,但是运行时无论提供多少个参数(或者不提供参数),JavaScript都不会报错。省略的参数的值就变为 undefined
。需要注意的是,函数的 length
属性与实际传入的参数个数无关,只反映函数预期传入的参数个数。
但是,没有办法只省略靠前的参数,而保留靠后的参数,如果一定要省略靠前的参数,只有显示传入 undefined
。
传递方式
函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递。这意味着,在函数体内修改函数值,不会影响到函数外部。
var p = 2;
function f(p) {
p = 3;
}
f(p);
console.log(p) // 2
上面代码中,变量 p
是一个原始类型的值,传入函数 f
是传值传递。因此,在函数内部,p
的值是原始值的拷贝,无论怎么修改,都不会影响到原始值。
但是,如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。
var obj = { p: 1 };
function f(o) {
o.p = 2;
}
f(obj);
console.log(obj.p); // 2
上面代码中,传入函数 f
的是参数对象 obj
的地址。因此,在函数内部修改 obj
的属性 p
,会影响到原始值。
注意,如果函数内部修改的不是参数的某个属性,而是替换掉整个参数,此时不会影响到原始值。(这是因为,形参的值是实参的地址,重新对行参赋值导致行参指向另一个地址,保存在原地址上的值当然不受影响)
arguments 对象
由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内读取所有参数。这就是 arguments
对象的由来。
arguments
对象包含了函数运行时的所有参数,arguments[0]
就是第一个参数,arguments[1]
就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。
需要注意的是,虽然 arguments
很像数组,但它是一个对象。数组专有的方法(比如 slice
和 forEach
),不能在 arguments
对象上使用。
函数的返回值
将值返回给调用函数的地方,使用return
表示。
function doSomething() {
// doing
return result;
}
let res = doSomething(); // 可得到函数doSomething的结果
函数体内部的return
语句,表示返回。JavaScript 引擎遇到return
语句,就直接返回return
后面的那个表达式的值,后面即使还有语句,也不会得到执行。return
语句不是必需的,如果没有的话,该函数就不返回任何值,或者说返回undefined
。
闭包
函数内部可以直接读取全局变量。
var n = 999;
function f1() {
console.log(n)
}
f1(); // 999
但是,正常情况下, 函数外部无法读取函数内部的变量。
function f1() {
var n = 999;
}
console.log(n);
// Uncaught ReferenceError: n is not defined(
如果处于种种原因,需要得到函数内部的变量。正常情况下,这是办不到的。只有通过在函数的内部,再定义一个函数。
function f1() {
var n = 999;
function f2() {
console.log(n)
}
}
上面代码中,函数 f2
就在函数 f1
内部,此时 f1
内部的所有局部变量,对 f2
都是可见的。但是反过来则不行。这就是 JavaScript 语言特有的“链式作用域”结构,子对象会一级一级地向上寻找所有父对象的变量。
既然 f2
可以读取 f1
的局部变量,那么只要把 f2
作为返回值,我们便可在 f1
的外部读取它的内部变量了。
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
console.log(result()); // 999
上面代码中,函数 f1
的返回值就是函数 f2
,由于 f2
可以读取 f1
的内部变量,所有就可以在外部获得 f1
的内部变量了。
闭包就是函数 f2
,即能够读取其他函数内部变量的函数。由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解为“定义在一个函数内部的函数”。闭包最大的特点,就是它可以记住诞生的环境,比如 f2
就记住了它诞生的环境 f1
,所以从 f2
可以得到 f1
的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的最大用处有两个,一个是可以读取外层函数内部的变量,另一个就是让这些变量始终保存在内存中,即闭包可以使得它诞生的环境一直存在。
function createIncrementor(start) {
return function() {
return start++;
}
}
var inc = createIncrementor(5);
inc(); // 5
inc(); // 6
inc(); // 7
上面代码中,start
是函数createIncrementor
的内部变量。通过闭包,start
的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc
使得函数createIncrementor
的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。
注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。
共有 0 条评论