对JS函数的深入理解

810 阅读8分钟

一、函数的定义

在js中,函数是一种特殊的对象,所有的函数都是Function类型的实例。函数和其他的数据类型一致,可以进行赋值操作,可以作为函数参数也可以作为函数的返回值。函数是用来解决问题的一系列代码的集合。

二、函数声明

函数声明有以下几种方式:

  1. 普通的函数声明:直接用function关键字声明一个函数,会进行变量提升,也就是说可以在函数声明的前面调用函数,函数的提升是将整个函数提升,可以直接使用。
//函数声明
function sayName(){
    console..log("函数声明")
}

如果同时用var声明一个和函数同名的变量,有以下几个情况。

  • var声明了和函数的同名变量,但是没有赋值
function sayName(){
    console.log("var声明了和函数的同名变量,但是没有赋值")
}
var sayName;
console.log(sayName);//会打印函数
  • var声明了和函数的同名变量,并且赋值。
function sayName(){
    console.log("var声明了和函数的同名变量,并且赋值")
}
var sayName = "赋值"
console.log(sayName);//赋值

总结:函数提升优先级高于变量提升,且不会被同名变量声明覆盖,但是会被变量赋值后覆盖。而且存在同名函数与同名变量时,优先执行函数

  1. 函数表达式:使用函数表达式声明的函数不能再赋值之前被使用,原因是只有进行了赋值操作了之后这个变量的值才为一个函数,才能正常调用。
//函数表达式
const sayName = function(){
    console.log("函数表达式")
}
  1. 箭头函数是es6中函数的简写方式,箭头函数没有自己的this,他的this是在定义的时候确定的,值为上一层的this。
//箭头函数
const sayName = ()=>{
    console.log("箭头函数")
}

三、函数参数

函数可以接收任意多个参数,可以使用函数中的arguments对象来获取所有的函数参数,arguments是一个类数组对象,要想使用数组方法需要先把他转换位数组结构。函数定义时写的参数为形参,可以取任意符合要求的变量名。只要在函数中使用对应的函数名即可。函数使用时传入的参数为实参,对应定义的形参的值。

  • 基本使用
    //写一个求和函数
    function sum(a,b){
        return a+b;
    }
    sum(1,3);//输出4
    //上面的a,b是函数的两个形参,下面调用sum函数时传入了1,3对应上面的a,b顺序,即调用函数时a = 1,b = 3
  • 参入的参数不确定的情况可以使用arguments对象,也可以使用es6的rest参数,rest参数只能放在参数最后
    //使用arguments对象来求任意参数的和
    function sum(){
        //先转换arguments对象为数组对象,然后使用数组的reduce方法求和
        let total = [...arguments].reduce((sum,current,currentIndex,array)=>{
            return sum+current
        },0)
        return total
    }
    sum(1,3);//4
    sum(1,4,5);//10
    //使用rest参数来计算
    function getSumByRest(...args){
        //这里的args已经是所有参数的数组了,可以直接使用reduce方法
        let total = args.reduce((sum,current,currentIndex,array)=>{
            return sum+current
        },0)
        return total
    }
    getSumByRest(1,3);//4
    getSumByRest(1,4,5);//10
  • es6新增形参默认值,可以在没有传参或者是参数为undefined的情况下给他一个默认值
 function sayHi(name,word = 'hi'
 ){
     console.log(word+":"+name)
 }
 sayHi("dog","hello");//hello:dog
 sayHi("dog");//hi:dog
 sayHi("dag",undefined);//hi:dog
 sayHi("dag",null);//null:dog

四、纯函数

传入相同的参数获得一样的结果的参数称为纯函数。可以利用纯函数的特点来做缓存,减少重复计算。

//非纯函数,这种函数的运行结果会被外部变量改变的函数是非纯函数。
let minAge = 18;
function canUse(age){
    return age>minAge
}
console.log(canUser(20));//true
minAge = 22;
console.log(canUser(20));//false
//纯函数,传入相同的参数得到的结果是一致的
function getMax(a,b){
    return a>b
}
console.log(1>2);//false
console.log(1>2);//false

利用纯函数的特点写一个具有缓存作用的函数。

/*
    思路如下:
    需要具有缓存作用,也就是需要一个变量来确定是否有缓存,这个变量需要一直存在。可以定义一个全局变量来接收所有需要缓存的值(全局变量可以在其他的地方被修改,如果自己修改了全局变量的值,那么可能得出的结果就是错误的,可以使用闭包来创建一个私有变量来解决这个问题。),不光是要缓存上一次的值,要把之前所有计算过得值都换成起来,并且和传入的参数一一对应,这里可以使用一个对象来实现。将每一个调用函数传入的参数集合作为键,结果作为值。每一次可以看参数键是否有对应的值,有则为有缓存,没有则进行计算。
*/    
    let cacheData = {}
    function cacheSum(){
        //获取健名
        const key = JSON.stringify(arguments);
        //有值则为有缓存,直接返回缓存数据
        if(cacheData[key]){
            return cacheData[key]
        }
        else{
         //否则进行计算并且设置为对应的缓存并返回
         console.log("计算")
         cacheData[key] = [...arguments].reduce((sum,current,currentIndex,array)=>sum+current,0)
         return cacheData[key]
        }
    }
    console.log(cacheSum(1,3));
    console.log(cacheSum(1,3));
    console.log(cacheSum(1,3));
    console.log(cacheSum(1,4,5));
    console.log(cacheSum(1,4,5));
    console.log(cacheSum(1,4,5));

1635152501(1).png 执行6次,只进行了两次计算

五、高阶函数

参数为函数的函数或者是返回值为函数的函数为高阶函数。

  • 参数为函数的函数:如我们经常使用的数组方法map,forEach等等。
    //直接使用map方法在一个数组的基础上,生成一个新的数组
    const arr = [1,2,3];
    const newArr = arr.map(item=>item*2)
    console.log(newArr);//[2,4,6]
    //简单实现以下map方法,map方法需要接受两个参数,需要操作的数组,
    //以及需要进行的操作,最后返回一个新的数组。
    function map(arr,func){
        //创建一个数组来接收新的数据
        const newArr = []
        //循环操作每一个数组元素,
        for(let i = 0;i < arr.length;i++){
        //将元素在的元素当做func参数传入,将func参数得我返回值作为结果返回交给新的数组
            newArr.push(func(arr[i]))
        }
        //返回这个新的数组
        return newArr
    }
    const myNewArr = map(arr,(item)=>item*2);
    console.log(myNewArr);//[2,4,6]

上面自定义的map方法第二个参数接收了一个函数作为参数,具体的逻辑由传入的参数执行,可以自己编写具体的操作,实现相应的逻辑,灵活度很高。

  • 返回一个函数:给函数添加额外功能,返回处理后的函数。这是函数柯里化的基础。
    //防抖函数
    function debounce(fn,delay){
     let timer = null;
     return function(){
         timer ? clearTimeout(timer):''
         timer = setTimeout(()=>{
             fn.call(this,...arguments)
         },delay)
     }
    }
    let num = 0;
    const add = debounce(()=>{
        num++
        console.log(num);
    },1000)
    add()
    add()
    add()
    add()
    setTimeout(()=>{
        add()
    },1000)
  //打印1,2

六、闭包

能访问自由变量的函数称为闭包,所谓的自由变量是指既不是函数的参数也不是在函数中定义的变量。能访问外部函数变量的函数称为闭包,将整个js当做一个大的函数的话,所有的函数都是一个闭包。 闭包的本质是:函数在运行时会创建一个执行上下文,执行完毕后就会被销毁,但是因为内部函数存在对外部函数的变量的引用,所以当外部函数运行完之后,执行上下文被销毁,但是被引用的成员变量不会被释放,内部的函数依然能够访问外部函数的变量,实际开发中有很多地方用到了闭包,比如防抖,节流,定时器等等。用闭包来改写上面说的缓存函数。

  
    function cacheSum(){
        const cacheData = {};
        return function(){
           //获取健名
        const key = JSON.stringify(arguments);
        //有值则为有缓存,直接返回缓存数据
        if(cacheData[key]){
            return cacheData[key]
        }
        else{
         //否则进行计算并且设置为对应的缓存并返回
         console.log("计算")
         cacheData[key] = [...arguments].reduce((sum,current,currentIndex,array)=>sum+current,0)
         return cacheData[key]
        } 
        }
        
    }
    const sum = cacheSum()
    console.log(sum(1,3));
    console.log(sum(1,3));
    console.log(sum(1,3));
    console.log(sum(1,4,5));
    console.log(sum(1,4,5));
    console.log(sum(1,4,5));

1635152501(1).png 执行6次,只进行了两次计算,并且外部也不能改变cacheData的值。

七、函数柯里化

函数柯里化是指将多个参数的一个函数转化为多个函数。实现对参数的自由控制。

//函数柯里化类似下面这种
function sum(a,b,c){
    return a+b+c
}
sum(1,2,3);//6
//柯里化之后:伪代码
sum(1)(2)(3)

下面来实现一个柯里化函数

/*思路分析
    1.定义一个一curry函数,接收一个需要柯里化的函数,返回一个新的函数curryFunc
    2.curryFunc函数执行时需要判断当前参数个数是否是fn所需要的参数个数
    3.如果是则直接执行fn函数并且将参数传给fn。
    4.如果不是则需要返回一个函数继续传入新的参数,返回的这个函数需要把之前的参数传递过去和接下来的参数合并,才是当前的所有参数。重复234步骤,一直到参数个数满足条件
    
*/
function curry(fn) {
   return function curryFunc(...args) {
     if (args.length < fn.length) {
        return function () {
           return curryFunc(...args, ...arguments)
        }
      }
    else {
        return fn(...arguments)
       }
    }
}
function sum(a, b, c) {
    return a + b + c;
}
let add = curry(sum);
console.log(add(1)(2)(3));//6
console.log(add(1, 2)(3));//6
console.log(add(1, 2, 3))//6

八、函数组合

函数组合:如果一个函数需要经过多个函数进行处理才能得到最终结果,那么我们可以把中间的多个函数组合成一个函数。函数组合默认从右到左执行。函数组合可以将任意已有函数结合使用,得到不同的结果。

    //写一个获得数组最后一个元素的组合函数。
    //接受任意个函数参数,这里进行两步,第一步反转g,第二步获取第一个元素f
    function compose(f,g){
        return function(value){
           return f(g(value))
        }
    }
    function reverse(array){
        return array.reverse()
    }
    function first(array){
        return array[0]
    }
    //默认从右到左执行,所以传入的函数顺序要对
    const getLast = compose(first,reverse)
    console.log(getLast([1,3,5]));//5
    //通用的组合函数,传入的函数个数不确定,可以使用剩余参数接收
    function normalCompose(...args){
        //返回一个可执行的函数,改函数接收一个参数,此参数为第一个要执行的函数的参数
        return (value)=>{
            //从右到左执行函数先反转数组用reverse,将value作为第一个执行的函数的参数,结果作为第二个执行的函数的参数,接下来的执行都是将上一个函数的执行结果作为当成执行函数的参数,最后返回最终结果;用reduce刚合适
            return args.reverse().reduce((previce,fn,fnIndex,arr)=> fn(previce),value);
            
        }
    }
    const getLastByNormal = normalCompose(first,reverse);
     console.log(getLastByNormal([1,3,5]));//5

满足结合律,也就是先组合前面的和先组合后面的的结果都是一致的。 函数组合要使用一个参数的函数,如果是多个函数的参数可以使用函数柯里化