关于 JS 函数的一切
本文基于: Bilibili - 自由的加百利
前置条件:
- 需掌握函数的编写、传参、返回、调用
- 理解作用域、掌握定时器的用法
- 知道引用类型和基本数据类型的区别
- 知道函数也是引用类型
- 听说过同步异步的概念
- 了解类和对象的关系
匿名函数
来看一下一个函数的基本属性:
匿名函数的自运行
我们可以将一个普通函数去掉它的名字,这样就成功的创建了一个匿名函数,并且编译器不会报错。
那么这个函数既然没有名字,我们又该怎么调用它呢?这时只需要使用一个小括号包裹住整个函数,再在函数体的末尾添加一个小括号就可以在创建函数之后立即执行这个函数。
这种写法,也叫作 匿名函数的自运行
其与直接在外部书写函数体内部的语句相比,优点就是不会造成变量污染,会在匿名函数内形成一个 封闭的作用域
小括号的作用
在匿名函数的外部加上一个小括号,实际的作用是 将该函数的声明变成了一个优先计算的表达式
( function(){...} )()
而表达式的运算结果就是这个 匿名函数 本身。拿到了函数本身之后,就可以在其后面加上一个小括号来调用它了。
把函数变成表达式?
既然小括号的作用是将函数的声明变成表达式,那么在函数周围加上运算符会不会有同样的效果呢?
+function(){...}()
!function(){...}()
~function(){...}()
void function(){...}()
delete function(){...}()
以上的几种写法都可以成功执行匿名函数,而且使用 +function(){...}()
这种方式执行函数自运行的效率是最高的。
递归函数
递归函数 是指一个函数直接或间接的调用自身,并在特定的情况下结束并放回运行结果
这里我们举一个 阶乘 的例子:
function F(N) {
return N * F(N - 1);
}
表面看上去,这个函数可以接收一个参数,并计算出这个数的阶乘。但是仔细想想就会发现不对劲,当 N = 1
时函数并没有停止自身的继续传递,也就是说这个函数没有停止条件,最终便会陷入一个死循环。结果就是 会在某一时刻,大量的函数将内存空间占满导致内存溢出。
也就是说我们上面写的这个函数,只有 递 没有 归
改造递归
我们尝试改变一下上面的 递归函数
首先要弄清楚,我们需要计算的是一个数 它的阶乘是多少。计算一个数字的阶乘便是让这个数每次乘以比他自身小 1 的数,直到乘到1。(说得不是很清楚,大家自行理解)
那么关键点就在于这个 直到
我们不能让它无止境的传递下去,在上面的例子中,参与递归的 N
为 1 时还在继续向内传递,0, -1, -2, -3...
我们所要做的就是当函数传递到 N = 1
时停止向内传递,直接返回 1 自身,将其自己交给外部的函数来调用,代码更改如下:
function F(N) {
if (N == 1) return 1;
return N * F(N - 1);
}
上面 if
语句的作用是:当 N 为 1 时,直接返回 1
这时运行一下就会发现,函数不报错了,而且也得到了我们想要的结果。
回调函数
回调函数,并不是指一种特殊的函数,而是指函数的使用方式
看一下下面的代码:
function f1(){
console.log(111);
}
function f2(){
console.log(222);
}
f1();
f2();
输出结果的顺序自然是先输出 111,再输出 222
但是如果我们给 f1()
添加一个定时器呢?
function f1(){
setTimeout(function(){
console.log(111);
}, 1000)
}
function f2(){
console.log(222);
}
f1();
f2();
这时便会先输出 222,一秒后输出 111。这种含有异步操作的函数就被称为 异步函数 ,异步函数最大的特点就是 后续的代码不需要排队,异步函数时可以和后续的代码并行的。f1()
就是一个典型的异步函数,你无法知道 f1()
和 f2()
哪一个会先结束。
回调函数引出
那么在有异步函数的情况下,如果我希望先输出 111,再输出222,要怎么做呢?
目前看来,唯一的办法是 把函数 f2()
放在 f1()
的内部调用
function f1(){
setTimeout(function(){
console.log(111);
f2();
}, 1000)
}
function f2(){
console.log(222);
}
f1();
假设有这样一个场景,项目组里有小白、小黄、小绿三个人,有一个工具函数 getToken()
function getToken(){
//异步函数......
}
它是一个异步函数,大家都在使用这个函数完成自己的业务,并且每个人都希望在 getToken()
结束后执行自己的代码,于是它们将函数写成了下面这样:
但是这种写法显然是错误的,因为异步函数保证不了函数的执行顺序。那么现在只能想办法将自己所写的函数放在异步函数内部,才有机会在其后面执行。
首先,我们给 getToken()
函数增加一个参数 callback
function getToken(callback){
//异步函数......
}
之后,三个人的代码就可以改成这样:
把自己的函数传进去,最后在 getToken()
的最后调用这个 callback
function getToken(callback){
//异步函数......
callback();
}
现在,所有人的代码都会在异步函数最后执行,这极大的提高了代码的可复用性,降低了开发维护的成本。
这种函数调用的方式就叫回调
字面意思就是:把自己的函数交给别人,回头再调。
构造函数
- 这一节需要理解 什么是面向对象
一个函数除了可以被当作函数,还可以被当作
class
function fn(){
}
let obj = new fn();
console.log( typeof obj );
我们可以直接使用 new
关键字来声明一个对象,这个时候,我们就说 fn()
是一个构造函数
那么 fn()
明明是一个空函数,这个对象是怎么来的呢?
构造函数的执行流程
问题的关键就在于这个 new
关键字。当你调用函数时在前面加上了 new
关键字,浏览器就会启动 构造函数 的执行流程:
function fn(){
this = {}
// 创建一个空对象,将其保存在this关键字中
...... //your code
return this;
}
let obj = new fn();
当然了,上面部分代码是不可见的。一个函数到底是普通函数还是构造函数,取决于你来怎么使用它。
但是通常,按照习惯,我们会将构造函数的首字母大写,普通函数的首字母小写。也就是说,如果你看到一个函数的首字母是大写的,在绝大多数的时候,它不应该被直接调用。
function User() {
......
}
let user = User(); ×
let user = new User(); √
在最新版的 JavaScript
已经支持了 class
关键字,你可以像 Java
一样定义一个类,并通过构造方法来生成对象。
闭包函数
function a(){
let x = 1;
function b(){
console.log(x);
}
}
函数 b()
是一个定义在函数 a()
内部的函数,所以其可以访问到变量 x
,变量 x
相对于函数 b()
来说就是一个全局变量。
如果我们把函数 b()
作为函数 a()
的返回值:
function a(){
let x = 1;
return function b(){
console.log(x);
}
}
let c = a();
c();
我们已知,函数 c()
就是函数 b()
,有由于函数 c()
是全局变量,因此,相当于在全局范围调用了函数 b()
,打破了函数 b()
只能在局部使用的限制,最终我们打印出了变量 x
在这里,函数 a()
所形成的作用域,叫做 闭包,函数 b()
被称作 闭包函数
函数的柯里化
这一节来源于知乎:https://zhuanlan.zhihu.com/p/163838720#:~:text=函数柯里化,就是,后,才执行原函数
function add(a, b) {
return a + b
}
function curry(fn) {
return function (a) {
return function (b) {
return fn(a, b)
}
}
}
let fn = curry(add)(1)(2)