在《JS 数据的运算》一文中,讨论了数据的运算是依托于运算符和函数的,那篇文章主要介绍了运算符,这篇文章主要介绍函数。
想要彻底理解函数的概念,先来看看历史上不同时期对函数的理解!

上图可以清晰的看到,函数的概念发展顺序是:运算——解析式——图像——变量的依赖关系——变量的对应关系——映射——集合的对应关系——序偶集。
函数最开始的概念是运算,虽然经过多年的发展,其内涵得到了很大的扩展,但从广义上来说,我们可以认为函数的本质还是一种运算,因为解析式、图像、关系、映射、集合、序偶集也能称为"运算"嘛!
所以,笔者认为函数是数据的运算的一种特殊方式,特殊之处在于它的不使用运算符,且能自定义运算规则。
以上是从纯数学的角度看函数,但是如果从编程语言的角度看函数,是否有所不同呢?
答案是,差别并不大。
在编程语言中,函数是一段可重复使用的代码块,它接受输入参数并执行一系列计算,然后返回一个输出结果,这不还是"运算"吗?而且编程语言中的的函数概念本来是借鉴的数学函数概念。
在清楚了编程语言中函数的概念和位置后,再来看JS中的函数,它就更容易理解,因为它不是孤立的某个知识点了。
接下来,先看看JS中函数的创建及分类,请看:《JS 数据的表达》
然后,再来看看JS中函数的性质,请接着往下看!
JS函数是一种特殊的对象,总体上,可以把函数分为两个部分:构造函数和非构造函数!
非构造函数基本只能用来被调用,执行一些复杂的重复逻辑,充当"运算"的角色,没有更多的功能性。
而构造函数就具备更多功能了,它是面向对象编程中的重要角色,JS中所有对象都是通过构造函数产生的。
有关JS的函数,笔者觉得有五个核心方面知识点:函数参数、this、构造函数、箭头函数、闭包。
关于函数参数
- arguments:arguments是一个类数组对象, 保存着函数的实参,ES6不再建议使用它来获取参数列表,更推荐使用剩余参数来收集位置数量的参数
- 默认参数:函数参数的默认值是 undefined,这些默认值可以在函数定义时设置
- 剩余参数:此语法允许将不确定数量的参数表示为数组
关于this
this指向一个对象,这取决于它出现的上下文:函数或全局,在全局作用域中使用this,指代全局对象;在函数中使用this,它指向完全取决于函数如何被调用的。
- 独立调用(method()):非严格模式下,this指向全局对象;严格模式下,this会被置为undefined
- 通过对象调用(obj.method()):this指向前面的对象
- call(method.call(ctx)):this指向call的第一个参数
- apply(method.apply(ctx)):this指向apply的第一个参数
- bind(method.bind(ctx);method()):this指向bind的第一个参数,注意bind返回绑定上下文的新函数(延迟执行),而call和apply是立即执行函数
- 通过new调用(new method()):this指向新对象
关于构造函数
造函数是一种特殊的方法,用于创建和初始化对象,使得数据表达更加简洁有力,有关构造函数笔者总结了以下几点:
- JS中只有三种方式创建构造函数:一是通过function关键字函数声明(不是函数表达式),二是通过class声明,三是通过Function创建
- 判断是否是构造函数对象,就是看这个对象有没有prototype属性,此属性指向其原型对象
- 构造函数与普通定义的函数本质上一样,只是习惯上首字母大写
- 非构造函数可以通过new关键字、函数表达式或箭头函数表达式来创建,成员函数一般都是通过这几种方式创建的,所以一般成员函数都是非构造函数
- 通过函数表达式创建的函数虽然是非构造函数,但是,当使用new关键字调用时,JS会自动创建一个新的对象,并将该对象的原型设置为该函数的[[Prototype]]属性。然后,该函数内部的this关键字将指向这个新创建的对象,并在函数执行完毕后返回该对象。因此,虽然这样的函数不是构造函数,但是它们仍然可以使用new关键字来创建新的对象
- JS中存在一些不完整的构造函数,比如Symbol和BigInt,它们虽然是构造函数,但不能使用new关键字
关于箭头函数
- 所有使用函数表达式的位置,均可以替换为箭头函数。
- 不能使用new调用
- 没有原型,即prototype属性
- 没有arguments
- 没有this,其内部的this是在定义时确定的,而不是在调用时确定的,它会捕获其所在上下文的this值,作为自己的this值
关于闭包
它是一种特殊的对象,由两部分组成,一是执行上下文A,以及在该执行上下文中创建的函数B,当B执行时,如果访问了A中变量对象的值,那么就产生了闭包。一般当一个函数运行完毕之后,其执行上下文的变量对象里的数据都会失去引用而被垃圾回收机制回收,但是闭包会阻止垃圾回收机制进行回收,因为它的本质就是在函数外部保持了内部变量的引用。因此在使用闭包时要保持警惕,如果滥用闭包,很可能会因为内存的原因导致程序性能过差。闭包保存的是整个变量对象,而不是某个特殊的变量,但在浏览器断点调试中,做了优化,浏览器查看闭包时仅仅只保留了会被访问到的变量。
闭包的应用
(1)循环、setTimeout与闭包
在循环中直接使用setTimeout,会得到最后一次循环的值,如果想每次循环的值都能保留下来,可以通过闭包的方式。
(2)单例模式与闭包
对象中如果想要有私有变量,可以通过闭包的方式把想给外界调用的接口函数返回出去。
(3)模块化与闭包
函数自执行与闭包可以实现js的模块化,早期的js确实是这样来实现简单的模块化,但现代的模块加载系统,如CommonJS和ES6模块化提供了更强大、更灵活且更易于管理的模块化解决方案,而不需要直接依赖于函数自执行和闭包。
(4)在函数式编程中,运用闭包,会有更多封装函数的优秀方式,比如函数回调、高阶函数等等。
总结
本文结合函数的历史分析,把函数这块知识划分到"数据的运算"里面,然后介绍了函数的五个核心知识点:函数参数、this、构造函数、箭头函数、闭包。
至此,我们对JS函数有了比较深入的理解,除此之外,函数作为一种对象的存在,构造函数在面向对象编程中也有着重要的使用地位,笔者会写一篇《JS 面向对象编程》的文章,里面还会谈到如何使用构造函数创建对象,以及如何使用构造函数实现继承?有兴趣的话,可以去看一下。