在 JS 中,函数就是一个方法,一般都是为了实现某个功能。
1. 函数的作用和创建
var total = 10;
total += 10;
total = total/2;
total = total.toFixed(2); //=> 保留小数点后面两位,toFixed 时候数字包装对象的方法,用来保留小数点后面的位数
在后续的代码中,依然想实现相同的操作,就需要重新编写代码。这样的方式会导致页面中存在大量冗余的代码,也降低了开发效率。
函数的诞生的目的就是为了实现封装:把实现一个功能的代码封装到一个函数中,以便后期重复利用。起到了**低耦合高内聚(减少页面中的冗余代码,提高代码的重复利用率)**的作用
创建函数
// ES5 中:
function 函数名([参数]) {
// 函数体
}
// 表达式创建
var 函数名 = function ([参数]) {
// 函数体
}
// ES6 箭头函数
let 函数名或者说变量名 = ([参数]) => {
// 函数体
}
2. 函数的创建执行机制
函数作为引用类型,也是按照引用地址来操作的。
【创建函数】
- 首先开辟一个新的堆内存,把函数体中的代码当作字符串存储在内存中(对象存储的是键值对)
- 在当前上下文中声明函数(变量),函数声明会提升到最前面
- 把开辟的堆内存地址赋值给函数名(变量名)
此时输出函数名 fn
(不是 fn()
),代表当前函数本身,如果我们要执行函数,就要加上小括号即 fn()
。这是两种不同本质的操作。
【函数执行】 目的:把之前存储到堆内存中的代码字符串变为真正的 JS 代码自上而下执行,从而实现应有的功能。
- 函数执行,首先会形成一个私有的作用域(一个供代码执行的环境,也是一个栈内存)
- 把之前在堆内存中存储的字符串复制一份到新开辟的栈内存中变为真正的 JS 代码
- 然后再进行变量提升 (var function 提前声明, 先形参赋值, 再变量提升)
- 最后在这个新开辟的作用域中自上而下执行
函数执行的时候,都会形成一个全新的私有作用域(私有栈内存),目的是:
- 把原有堆内存中存储的字符串变成真正的 JS 代码执行
- 保护里面的私有变量不受外界的干扰(和外界隔离)
我们把函数执行的这种保护机制,称之为“闭包”
函数内声明的变量都是私有变量。
3. 函数中的参数
参数是函数的入口:当我们在函数中封装一个功能,有一些不确定的因素,需要执行函数的时候由用户传递进来。此时我们就基于参数的机制,提供入口即可。
函数中的参数是按值传递的
//=> 此时的参数叫做形参(命名参数):入口,形参是变量
function sum(n, m) {
return n + m;
}
//=> 函数执行时传递的值叫做实参:实参是具体的数据值,即使写的是变量或者表达式,也是把变量或者表达式计算的结果作为值传递给形参变量
sum(1, 2); //=> n:1, m:2
sum(1); //=> n:1, m:undefined
sum(); //=> n:undefined, m:undefined
sum(1, 2, 3) //=> n:1, m:2, 3 没有形参变量接收
3.1 理解参数
JavaScript 中函数不介意传递多少个参数,也不在乎传进去的参数是什么数据类型。即使定义时,函数只有两个形参,实际执行的时候,也可以传递任意个参数。
原因是 JavaScript 中的参数在内部是用一个数组来表示的,函数接受到的始终是一个数组。
实际上,在函数内部可以通过 arguments
对象来访问这个参数数组,从而获取传递给函数的每一个参数。
3.2 arguments 对象
arguments
对象是一个类数组对象,可以通过索引访问元素,也有 length
属性访问长度。是函数内置的实参集合(内置:函数天生就存在的机制,不管你是否设置形参,是否传递实参,arguments
始终存在),只能在函数内访问。
命名参数是有局限性的:我们需要具体知道用户执行的时候传递实参数量、顺序等,才可以使用形参变量定义对应入口。
通过 arguments
对象,函数不显式的使用命名参数,也能够实现一样的功能。
function sum() {
return arguments[0] + arguments[1];
}
因此在 JavaScript 中函数的一个重要特点是:命名参数只提供便利,但不是必须的。
length 属性
可以通过其 length
属性获取传入参数的个数,利用这一点让函数能够接受任意个参数并分别实现适当的功能。
function add() {
if (arguments.length == 1) {
return arguments[0] + 10;
} else if (arguments.length == 2) {
return arguments[0] + arguments[1];
}
...
}
arguments
同样可以和命名参数一起使用。
arguments
的映射机制
arguments
中的值会与对应命名参数(形参)的值保持同步
function add(n, m) {
arguments[1] = 10;
return n + m;
}
add(1,2) //=> 11
// 反过来改变命名参数的值也是一样,同步改变
arguments
对象和形参之间的映射是在函数执行后形参赋值的一瞬间,浏览器通过 arguments
中的索引来完成和对应形参变量中的映射机制搭建。一开始没有建立起来的,即使在后面改变 arguments
对象,也无法形成映射。
如果形参比 arguments
中个数多,那么多出来的形参是无法和 arguments
中对应的索引建立关联的。
function fn(x, y) {
/* 形参赋值:x = 10, y = undefined
*
* argumenrs
* 0: 10
* length: 1
*/
var arg = arguments;
arg[0] = 100;
console.log(x); //=> 100,存在索引,形成映射
y = 200;
console.log(arg[1]); //=> undefined
//=> 后面再改变 arguments,也无法形成映射
arg[1] = 150;
console.log(y); //=> 200
}
fn(10);
注意:
- 这并不说明两个值访问相同的内存空间,它们的内存空间是相互独立的,只是值会保持同步
- arguments 对象的长度由传入的参数个数决定,不是由定义函数时的命名参数个数决定
- 没有传递值的命名参数将被自动赋值为 undefined
- 严格模式下,不允许改变
arguments
对象,同时与命名参数不同步
callee 属性 存储的是当前函数本身。严格模式下,访问会报错。
callee.caller 属性
存储的是调用函数的环境。全局调用的话,值为 null
。严格模式下,访问会报错。
应用 任意数求和:不管函数执行的时候,传递多少实参值进来,我们都可以求和
function sum() {
var total = 0;
for (var i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
优化:在累加时,把字符串转换为数字,对于非有效数字,不再相加。
function sum() {
var total = 0;
for (var i = 0; i < arguments.length; i++) {
var item = Number(arguments[i]);
isNaN(item) ? null : total += item;
}
return total;
}
//=> ES6
var sum = (...arg) => eval(arg.filter(item => !isNaN(item)).join('+'));
4. 返回值
返回值是函数的出口,把函数运行的结果或者函数体中的部分信息拿到函数外面去使用。
函数在任何时候都可以通过 return
返回一个值,返回之后,函数停止并立即退出,后面代码不再执行。
function fn(n, m) {
var total = 0;
total = n + m;
return total;
//=> 并不是把 total 变量返回,返回的是变量存储的值,return 返回的永远是一个值
}
fn(1,2) //=> 3
要么让函数始终返回一个值,要么始终不会返回值。
5. 匿名函数
匿名函数:没有函数名的函数
- 函数表达式:把函数当作值赋值给变量或者元素的事件
- 自执行函数:创建和执行一起完成(立即执行匿名函数)
- 回调函数:将匿名函数当作参数传入函数中
//=> 函数表达式
var sum = function() {
};
//=> 自执行函数
(function(){
})();
~function() {
}();
+function() {
}();
!function() {
}();