九 高阶函数
如果一个函数的参数或返回值是函数,则这个函数就称为高阶函数
将函数作为参数,意味着可以对另一个函数动态的传递代码
9.1 实例
9.1.1 实例1
定义一个函数,该函数可以实现根据需求动态打印数组中的数据
let arr = [1,2,3,4,5,6,7]
// 定义一个judge函数
function judge(arr,Condition){ // 接收一个数组对象和一个函数对象
for (let i = 0; i < arr.length; i++) {
if(Condition(arr[i])){ // if的判断条件是一个函数的返回值
console.log(arr[i]);
}
}
}
judge(arr,(x)=>x>3) // 传入一个数组和一个函数,该函数的返回值是false或者true,作为for循环的判断条件
9.1.2 实例2
实现函数的日志功能
也就那个意思,看不懂不看,我也是自己瞎写的。
function judge(arr,Condition){
for (let i = 0; i < arr.length; i++) {
if(Condition(arr[i])){
console.log(arr[i]);
}
}
}
function outer(Func,...condition){
return ()=>{
console.log("记录日志");
const res = Func(...condition)
return res
}
}
let fn = outer(judge,[1,2,3,4,5,6,7],(x)=>x<3)
fn()
9.2 闭包
9.2.1 引子
题目:请设计一个函数,第一次调用时打印1,第二次调用打印2,以此类推
let num = 0
function fn(){
num++
console.log(num);
}
fn() // 1
fn() // 2
num = 100
fn() // 101
当我们调用函数fn()
时能够正确打印出num的值,且num的值会自增。
但该num是处于全局作用域中,这在实际生产环境中是十分危险的。所以就产生了一种成为闭包的实践。
闭包:可以利用函数来隐藏不希望被外部访问到的变量,闭包就是能访问到外部函数作用域中变量的函数。
所以产生闭包需要两个条件:
- 函数嵌套(外层函数使函数内部的变量不会被外部访问,内层函数实现变量的修改)
- 内部函数要引用外部函数中的变量
- 内部函数要作为返回值返回
let num = 0
function outer(){
let num = 0
return ()=>{
num++
console.log(num);
}
}
const fn = outer()
fn() // 1
fn() // 2
outer.num = 100
num = 100
fn() // 3
上诉写法实现了我们的要求。
9.3 词法作用域
观察以下代码,你能猜到输出是什么吗?
var a = 1;
function bar(){
console.log(a);
}
function foo(){
var a = 2;
bar();
}
foo();
输出1还是输出2呢?答案是1,说明bar函数不是从foo函数的作用域查找的变量a,而是从全局作用域中获取的a的值。
所以bar函数的上层作用域在定义的时候就已经确定了 ,不会随着调用地点的不同而改变。这就成为词法作用域,又叫做静态作用域。与之相对的叫做动态作用域。
参考链接:
闭包利用的就是词法作用域,通常使用静态作用域的编程语言都能实现闭包…吧。
9.4 闭包的注意事项
闭包的生命周期:
-
闭包在外部函数调用时产生,外部函数每次调用都会产生一个全新的闭包
-
在内部函数丢失时销毁(内部函数被垃圾回收了,闭包才会消失)
注意事项:
-
闭包主要用来隐藏一些不希望被外部访问的内容,这就意味着闭包需要占用一定的内存空间。相较于类来说,闭包比较浪费内存空间(类可以使用原型而闭包不能)
-
需要执行次数较少时,使用闭包
-
需要大量创建实例时,使用类
9.5 递归
递归是一种函数自己调用自己的方式。
自己调用自己绝对不可以一直调用下去直到把内存撑满,所以递归一定有一个结束的判断,称为递归的终止条件。
9.5.1 练习1
创建一个函数,可以用来求任意数的阶乘
例如:
3的阶乘 = 3*2*1
4的阶乘 = 4*3*2*1
比较容易的想法是使用一个循环:
function foo(n){
let res = 1
for (let i = 2; i <= n; i++) {
res = res * i
}
return res
}
console.log(foo(4));
如果用递归来解决阶乘的问题?
5! = 4! x 5
4! = 3! x 4
3! = 2! x 3
2! = 1! x 2
1! = 1
递归的核心思想就是将一个大的问题拆分为一个一个小的问题,小的问题解决了,大的问题也就解决了
递归的作用和循环是一致的,不同点在于,递归思路的比较清晰简洁,循环的执行性能比较好
在开发中,一般的问题都可以通过循环解决,也是尽量去使用循环,少用递归
只在一些使用循环解决比较麻烦的场景下,才使用递归
感觉这里使用递归也不是很好… …
function jieCheng2(num){
// 基线条件
if(num === 1){
return 1
}
// 递归条件
// num! = (num-1)! * num
return jieCheng2(num-1) * num
}
result = jieCheng2(5)
/*
jieCheng2(5)
- return jieCheng2(4) * 5
- return jieCheng2(3) * 4
- return jieCheng2(2) * 3
- return jieCheng2(1) * 2
- return 1
*/
console.log(result)
9.5.2 练习2
由不死神兔引出对递归问题的思考 - 知乎 (zhihu.com)
9.6 可变参数
arguments
是一个对应于传递给函数的参数的类数组对象。
类数组对象我记得之前已经写了,再写一遍:
和数组相似,可以通过索引来读取元素,也可以通过for循环变量,但是它不是一个数组对象,不能调用数组的方法
// 类似如下的对象:
{
length:3
0:"你好,世界!"
1:"Hello"
2:"World"
}
实践:
function test(a,b,c){
console.log("a:",a);
console.log("b:",b);
console.log("c:",c);
console.log("arguments[0]:",arguments[0]);
console.log("arguments[1]:",arguments[1]);
console.log("arguments[2]:",arguments[2]);
}
test(1,2,3)
/*
a: 1
b: 2
c: 3
arguments[0]: 1
arguments[1]: 2
arguments[2]: 3
*/
当我们把形参删除,同时注释掉形参的打印语句:
function test(){
// console.log("a:",a);
// console.log("b:",b);
// console.log("c:",c);
console.log("arguments[0]:",arguments[0]);
console.log("arguments[1]:",arguments[1]);
console.log("arguments[2]:",arguments[2]);
//
}
test(1,2,3)
发现实参能够正常打印,相信你已经懂了argumens的用法了,但是挺无聊的。
他还有一个length属性可以打印
function test(){
console.log(arguments.length);
}
test(1,2,3) // 3
s如果编写兼容ES6的代码,推荐使用剩余参数,写法如下所示:
function test(...argus){
console.log(argus);
}
test(1,2,3) // [1,2,3]
9.7 函数的this
9.7.1 调用方式
根据函数调用方式的不同,this的值也不同:
- 以函数形式调用,this是window
- 以方法形式调用,this是调用方法的对象
- 构造函数中,this是新建的对象
- 箭头函数没有自己的this,由外层作用域决定
- 通过call和apply调用的函数,它们的第一个参数就是函数的this
function test(){
console.log(this);
}
let testObj = {
user:"大眼神龙",
testFn:test
}
test() // global {global: global, clearInterval: ƒ, clearTimeout: ƒ, setInterval: ƒ, setTimeout: ƒ, …}
testObj.testFn() // {user: '大眼神龙', testFn: ƒ}
9.7.2 call和apply
调用函数除了通过 函数() 这种形式外,还可以通过其他的方式来调用函数,比如,我们可以通过调用函数的call()和apply()来个方法来调用函数。
函数.call()
函数.apply()
call 和 apply除了可以调用函数,还可以用来指定函数中的this
call和apply的第一个参数,将会成为函数的this
通过call方法调用函数,函数的实参直接在第一个参数后一个一个的列出来
通过apply方法调用函数,函数的实参需要通过一个数组传递
function test(){
console.log(this);
console.log(arguments.length);
}
test.call({user:"大眼神龙"},1,2,3,4,5) // {user: '大眼神龙'} 5
test.apply({user:"大眼神龙"},[1,2,3,4,5]) // {user: '大眼神龙'} 5
9.7.3 bind
bind()
是函数的方法,可以用来创建一个新的函数
-
bind可以为新函数绑定this
-
bind可以为新函数绑定参数
箭头函数没有自身的this,它的this由外层作用域决定,也无法通过call apply 和 bind修改它的this。箭头函数中没有arguments。
function test(){
console.log(this);
}
const newFunc = test.bind({user:"大眼神龙"})
newFunc() // {user: '大眼神龙'}
而且不能再通过call和apply修改
function test(){
console.log(this);
}
const newFunc = test.bind({user:"大眼神龙"})
newFunc() // {user: '大眼神龙'}
newFunc.call({test:"测试"}) // {user: '大眼神龙'}
newFunc.apply({test:"测试"}) // {user: '大眼神龙'}