最早,函数的概念由伽利略在十七世纪提出,主要用来描述一个变量对另一个变量的依赖关系。随着时间的推移,函数的概念越来越明确和严谨。
传统函数定义
一般的,在一个变化过程中,假设有两个变量 x、y,如果对于任意一个 x 都有唯一确定的一个 y 和它对应,那么就称 x 是自变量,y 是 x的函数。x 的取值范围叫做这个函数的定义域,相应 y 的取值范围叫做函数的值域。
近代定义
设 A,B 是非空的数集,如果按照某种确定的对应关系 f,使对于集合 A 中的任意一个数 x,在集合 B 中都有唯一确定的数和它对应,那么就称映射
为从集合A到集合B的一个函数,记作
或
其中 x 叫作 自变量,叫做 x 的函数,集合
叫做函数的 定义域,与 x 对应的 y 叫做 函数值,函数值的集合
叫做函数的 值域,
叫做 对应法则。其中,定义域、值域和对应法则被称为函数三要素。一般书写为
若省略定义域,一般是指使函数有意义的集合 。
JavaScript 函数
在编程语言中,函数是重要的基础组成部分,一般用来将一些强关联的程序代码组织在一起形成一个整体,然后在需要使用这段逻辑的地方进行传参调用即可。在 JavaScript 中,定义函数的方式有如下四种:
Function 构造函数
在 JavaScript 编译器初始化的时候,会创建两个构造函数:
- Object 构造函数,主要用于创建对象
- Function 构造函数,主要用于创建函数
抛开 Object 不说,着重介绍一下利用 Function 构造函数创建函数,形式如下:
const myFunc = new Function ([arg1[, arg2[, ...argN]],] functionBody)
// [arg1, arg2, ... argN] 可选,为一些列被函数使用的参数的名称
// functionBody 一个含有包括函数定义的 JavaScript 语句的字符串
用 Function 构造函数创建一个简单的 sum 函数如下:
const sum = new Function('a', 'b', 'return a + b')
sum(1, 2) // => 3
虽然 JavaScript 为我们提供了这样一种创建函数的方式,但是以字符串形式书写很显然不是常人所为。这里给出的例子虽然看上去很简单、明了,但是实际的项目开发中函数的传参和函数的逻辑体会很复杂,这种形式也就不适用了。
另外,这种形式还有一定的性能问题:使用 Function 造器生成的 Function 对象是在 函数创建时解析的。这比使用函数声明 或者 函数表达式 定义的函数调用更为低效,因为使用后者创建的函数是跟其他代码一起解析的。
注:Vue 使用这种方式构造了 Render Function。Vue 在将 template 编译后,是通过字符串拼接的形式生成最终可执行的 JavaScript 代码,所以比较适合利用这个方式构造 Render Function!
函数声明
函数声明是我们平时开发中使用比较多的一种函数声明方式,其声明形式如下:
function myFunc([arg1[...argN]]) {
// do something
}
其中 function 为关键字,后面空一格紧跟函数名称 myFunc,这个名称只要是符合 JavaScript 规范的都是有效的,紧接着就是一对小括号,里面可以包裹一个或多个形参,小括号后面跟着一对大括号,大括号里面就是实际的函数逻辑代码了。
通过上面的方式创建一个 sum 函数,如下:
function sum(a, b) {
return a + b
}
// 函数调用
sum(1, 2) // => 3
函数表达式
除了上面函数声明的方式创建函数外,由于在 JavaScript 中函数具有一等公民特性,所以还有另外一种常用的函数定义的方式 —— 函数表达式。
所谓表达式,一般是形如 a = b + c 这样的形式,所以函数表达式就是形如:
const myFunc1 = function ([arg1[...argN]]) {
// do something
}
// or
const myFunc2 = function myFunc3 ([arg1[...argN]]) {
// do something
}
上面给出的两段示例代码都是将一个函数以值的形式赋值给变量,然后通过这个变量加上一对小括号进行函数的调用。但是他们之间有一些细微的区别,上面的一段为匿名函数表达式,下面的为具名函数表达式,说到底就是有没有给函数取一个名字。
给函数取没取名字并不能影响函数的正常使用,但是没有取名的函数在断点调试的时候显示的函数名称就是 anonymous,不利于对函数的定位,特别是编译后的代码。
const sum2 = function(a, b) {
return a + b
}
sum2(1, 2) // => 3
const sum3 = function sum3(a, b, c) {
return a + b + c
}
sum3(1, 2, 3) // => 6
箭头函数
箭头函数是 ES6 推出的一种全新的、极简的声明函数的方式。形如:
const sum = (a, b) => a + b
箭头函数由于其极简的特性,导致它没有很多普通函数所拥有的特性:
- 没有属于自己的 this。在箭头函数出现之前,函数内部都有一个名为 this 的关键字,其指向是由函数在运行时如何被调用而决定的,导致很多人对此摸不清头脑。而箭头函数的出现很好的解决了 this 指向的问题,因为箭头函数本身不会创建自己的 this,而是通过词法作用域从自己作用域的上一层继承而来。
- 不绑定 arguments,访问的 arguments 是父级函数作用域的
- 不能通过 new 操作符实例化
- 默认没有 prototype 属性
- 不能使用 yield 关键字
函数参数处理
函数的参数是函数重要的组成部分,没有参数的函数可复用性就会显得不那么强。
对于函数的参数,我们都知道分实参和形参两种:
// 定义一个命名为 myFunc 的函数,函数接受 a, b, c 三个参数
// 这里函数接受的 a, b, c 三个参数就是我们通常说的形参了
function myFunc(a, b, c) { // do something }
// 通过函数名称加 (1, 2, 3) 的形式调用函数,并传参 1, 2, 3
// 这里调用函数传递的 1, 2, 3 就是实参了
myFunc(1, 2, 3);
- 实参:全称实际参数,指的是调用函数时传递的真实的值。对于基本类型,传递的是实际值,对于引用类型传递的是引用地址
- 形参:全称形式参数,指的是定义函数时接受被调用时传的实际值的变量,是一个虚拟的代号。对于基本类型,接受的是实际值,对于引用类型,接受的是引用的地址。然后在函数的逻辑中通过这些变量或代码进行函数逻辑的处理。
函数的形参和实参,大家估计都已经了解了,接下来说说函数形实参的几种方式的处理吧!
形参多于实参
// 定义命名为 myFunc 并接受两个参数的函数
function myFunc(a, b) {
console.log(a, b);
}
// 调用 myFunc 函数,并传递三个实参
myFunc(1, 2, 3);
// 结果只输出了由左到右匹配到的两个参数 1, 2,
// 没有匹配上或者多余的参数被忽略了,不会做任何处理
// 1, 2
形参少于实参
// 定义命名为 myFunc 并接受三个参数的函数
function myFunc(a, b, c) {
console.log(a, b, c);
}
// 调用 myFunc 函数,并传递两个实参
myFunc(1, 2);
// 仍旧,函数的参数会从左到右进行匹配,由于函数的接受参数有三个
// 而调用时,传递的参数只有两个
// 根据从左到右进行匹配的原则,a 接受 1,b 接受 2,c 接收不到值就是 undefined
// 所以最后输出 1, 2, undefined
这里提一下 ES6 参数默认值的特性,上面的函数可以改写成:
// 定义命名为 myFunc 并接受三个参数的函数
function myFunc(a = undefined, b = undefined, c = undefined) {
console.log(a, b, c);
}
// 调用 myFunc 函数,并传递两个实参
myFunc(1, 2);
// 仍旧,函数的参数会从左到右进行匹配,由于函数的接受参数有三个
// 而调用时,传递的参数只有两个
// 根据从左到右进行匹配的原则,a 接受 1,b 接受 2,c 接收不到值就是默认值 undefined 了
// 所以最后输出 1, 2, undefined
这样是不是就能更好的理解这个输入结果了呢!
形参等于实参
// 定义命名为 myFunc 并接受三个参数的函数
function myFunc(a, b, c) {
console.log(a, b, c);
}
// 调用 myFunc 函数,并传递三个实参
myFunc(1, 2, 3);
// 根据从左到右进行匹配的原则,a 接受 1,b 接受 2,c 接收 3
// 所以最后输出 1, 2, 3
获取所有实参
在很多场景下,我们无法确定调用函数的传参个数究竟是多少个或者我们知道函数需要接受的参数就很多,总不能一个一个全部列出来吧!那这种情况怎么解决呢?幸好,JavaScript 函数给我提供了 arguments 这个函数变量。
arguments 是一个类数组,也就是包含 length 属性的对象。它的属性值里面包含调用函数是所有传递的实参,基本使用如下:
// 定义命名为 myFunc 并接受未知个参数的函数
function myFunc() {
console.log(arguments);
console.log(arguments.length);
}
// 调用 myFunc 函数,并传递三个实参
myFunc(1, 2, 3);
// 最终输出的结果会是一个包含 length 属性值为 3 的 arguments 类数组对象
上面的示例代码,输出如下:
最终的输出结果和我们预想的一样。
说到这里,总结一句话:当发生函数调用的时候,实参被保存在一个叫做 arguments 的类数组对象中,而 arguments 中对应属性的值始终与被调用函数的参数保持一致。