JS函数

3,743 阅读10分钟

前言

该文章记录一些函数基本知识,慢慢将会添加到一些函数高级知识,所有内容均从网上整理而来,加上自己得理解做一个整合,方便工作中使用。

1.创建函数

  1. 函数表达式创建函数(匿名函数)
    let fn = function([形参1,形参2,...形参N]){
        console.log("我是匿名函数")//代码语句
    }
    fn()//执行函数
    
  2. 使用函数声明创建函数
    function 函数名([形参1,形参2,...形参N]){
         console.log("我是一个函数")//代码语句
    } 
    函数名() //执行函数
    
  3. 箭头函数(属于匿名函数)
    let fn3 = ([形参1,形参2,...形参N])=>{
        //代码语句
    }
    //箭头函数的执行语句只有一句时,可以省略{},省略return;只有一个形参省略小括号
    let fn4 = () => console.log(123)
    let fn5 = name => name  //相当于rerutn name
    
  4. 立即执行函数(匿名函数,只会调用一次,很少使用)
    //写法一
    (function(){
        let a =10 
        console.log(a)
    })()
    //写法二
    (function(){
        let a =10 
        console.log(a)
    }())
    

2.函数的参数

  • 在函数创建时,可以设置形参,数量不受限制,形参之间逗号隔开(相当于在函数内部声明了对应的变量,且并未赋值)
    function sum(a,b){
        //a,b就是函数的形参,相当于在函数内声明变量a,b,且未赋值
    }
    
  • 在调用函数时,可以传递实参
    function fn(num1,num2){
        return num1+num2
    }
    
    // 实参将会赋值给函数中对应的形参,调用函数时解析器不会检查实参的类型,实参可以是任意数据类型
    fn('abc',123)
    
    // 不会检查实参的数量,多余参数不会被赋值;实参数量少于形参,则没有对应实参的形参则是undefined
    fn(12)
    fn(123,345,567,892)
    
    
  • 定义形参时,可以为形参设置默认值
    function fn( num1=10,num2=2 ){
        return num1+num2
    }
    fn() //不传递实参,函数的形参则使用默认值,函数返回值为12
    fn(1) //函数形参num2未接收到实参,则使用默认值,函数返回值为3
    
    
  • 函数每次调用 - 形参默认值会重新被赋值
    // 函数每次调用,都会重新创建默认值,重新被赋值
    function fn2(a={name:"李白"}){
        console.log(a.name)
        a.name="杜甫"
        console.log(a.name)
    }
    fn2() // 第一次调用函数,输出:李白、杜甫
    fn2() // 第二次调用函数,输出:李白、杜甫
    // 解释:function fn2(a={name:"李白"}){} == function fn2(){let a={name:"李白"} }
    //如果默认值这个对象是在函数中直接生成的,那么函数执行完也会销毁这个对象,下次函数执行则会重新生成
    
    let obj={name:'李白'}
    function fn3(a=obj){
        console.log(a.name)
        a.name="杜甫"
        console.log(a.name)
    }
    fn2() // 第一次调用函数,输出:李白、杜甫
    fn2() // 第二次调用函数,输出:杜甫、杜甫
    //obj对象是在函数外声明定义的,第一次函数执行obj对象的name属性值已经被改变,
    //第二次函数再执行是,形参a被赋值时,obj中的name已经被改为"杜甫"
    
  • arguments参数(不建议使用)
    • arguments是函数中一个隐藏参数,箭头函数没有
    • arguments是伪数组,不可以使用数组的方法,可以for循环遍历
    • arguments用来存储函数的实参,无论函数是否定义形参,实参都会存储在其中
    //不建议使用arguments
    function fn() {
        console.log(arguments)
    }
    fn(1, 2, 3, 4, 5) //输出Arguments(5) [1, 2, 3, 4, 5, callee: ƒ, Symbol(Symbol.iterator): ƒ]
    
  • 可变参数
    • 可变参数可以接收任意数量的实参,并将它们统一存储在一个数组中返回
    • 可变参数的作用和arguments基本一致,但有不同点:
      • 可变参数的名字可以自定义
      • 可变参数就是一个数组
      • 可变参数可以配合其他参数使用
    //可变参数
    function fn(...nums){
        console.log(nums)
    }
    fn(1,2,3) //输出 [1,2,3]
    
    //和其他参数
    function fn(num1, num2, ...nums){
        console.log(nums)
    }
    fn(1,2,3,4,5) //输出[3,4,5]
    

3.函数的返回值

  • 函数通过return返回值,可以声明一个变量接收函数返回值
    function fn(){
     returun 123
    }
    let result = fn()
    
    - 函数return后的语句都不会再执行,函数可以return返回任意类型的值
    - 函数若不写return,则返回undefined;或者return后没有值,默认返回undefined
    
    • 箭头函数简写返回对象
    let fn = ()=>{name:"胡歌"} //错误
    let fn = ()=>({name:"胡歌"}) //正确.需要小括号将对象的花括号包裹
    

4.函数的作用域

  • 全局作用域
    • 全局作用域在网页运行时创建,在网页关闭时销毁
    • 全局作用域中的变量是全局变量,可以被任何作用域中使用
  • 局部作用域
    • 块作用域( 花括号包裹{代码块} ) --使用得少
      • 块作用域是一种局部作用域,在代码块执行时创建,执行完毕就销毁
      • 块作用域中的变量只能在块的内部使用,块的外部无法使用
    • 函数作用域
      • 函数作用域在函数调用时产生,调用结束后销毁,所以每次调用函数都会产生不同作业域
      • 在函数中定义的变量,只能在函数中调用,不能在函数外调用

  • 作用域链
    • 当要调用一个变量时,JS解析器优先在当前作用域寻找变量,从内到外,直到全局作用域中未找到,则报错

5.函数中this指向

  • 当以函数的形式调用时,this指向window

    function fn(){
        console.log(this)
    }
    fn() //this指向的是windowfn() === window.fn(),实际就是fn就是window这个对象的方法
    
  • 以对象的方法的形式调用时,this指向调用这个方法的对象

    let obj={
      name:"胡歌",
      say(){
        console.log(this)
        return this
      }
    }
    obj.say() //this指向的是obj这个对象,因为是obj调用的say方法
    obj.say() === obj //true 全等
    
  • 箭头函数中的this(箭头函数没有自己的this,它的this由外层作用域决定,和调用方式无关)

    //声明函数
    const fn1 = function(){console.log(this)}
    //箭头函数
    const fn2 = ()=> console.log(this)
    fn1() //函数式调用---输出window
    fn2() //函数式调用---输出window
    //赋值给对象,作为对象方法
    let student={
        name:"胡歌",
        fn1,
        fn2  
    }
    student.fn1() //方法式调用---输出student这个对象
    student.fn2() //箭头函数---方法式调用---输出window,箭头函数和调用无关
    
    let person = {
        name:'胡歌',
        say(){
            function fn1(){ console.log(this) }
            fn1() //函数式调用---输出window
            ---
            let fn2 = ()=>console.log(this)
            fn2() //箭头函数---函数式调用---输出person这个对象,因为此箭头函数外层作用域中的this指向person对象
        }
    }
    

    注意:可以使用 call()、apply()、bind() 改变this指向;放在补充部分

6. 高阶函数

  • 函数的参数或者返回值是函数,则该函数被称为高级函数
    • 函数作为参数的作用(回调函数的作用):可以动态的传递代码,不改变原函数添加功能
    function sum(num1, num2) {
      return num1 + num2
     }
    console.log(sum(1, 2)) //输出3
    
    //新增提示功能 num1 + num2 = xxx
    function tips(callback) {
      return (num1, num2) => {
        let res = callback(num1, num2)
        return `${num1}+${num2} = ${res}`
      }
    }
    let res = tips(sum)
    console.log(res(3, 4)) //输出3+4 = 7
    
    //解析一下
    // tips() --> (num1, num2)=>{ xxxx }  其实返回的是一个函数
    // 所以res = tips(sum) 等同于
    //res=(num1, num2) => {
    //    let res = sum(num1, num2)
    //    return `${num1}+${num2} = ${res}`
    // }
    //这样既用上了原函数,又不改变原函数,又新增了功能
    

7. 闭包

  • 为什么要使用闭包?

    • 有些变量不想在全局作用域被访问调用,就可以用闭包
  • 闭包构成条件

    • 函数嵌套
    • 内部函数引用外部函数中声明的变量
    • 内部函数要作为外部函数的返回值
    let star = '胡歌'
    function say() {
      console.log(star)
    }
    say() //输出:胡歌
    
    function sayHi() {
      let star = '李白'
      return () => {
        console.log(star)
      }
    }
    let result = sayHi() 
    console.log(result)  //输出的是sayHi函数返回的一个函数
    result() //输出:李白
    
    总结:
    1.定义在全局的star如果在某处被重新赋值,则say函数打印的就改变了
    2.定义在函数中的star变量则不会被修改
    
  • 函数的作用域

    • 函数的作用域在函数创建时,(词法作用域)就已经确定了,与调用函数时的位置无关
    • 闭包就是利用词法作用域
    let star = '胡歌'
    function say() {
      console.log(star)
    }
    say() //输出:胡歌
    
    function sayHi() {
      let star = '李白'
      say() 
    }
    sayHi() //输出:胡歌
    

    总结:为什么sahHi里say函数输出的不是'李白'?

    1.根据作用域链,say函数中的star先在函数自身寻找,没有找到就会根据作用域链往外查找 2.在sayHi函数中,say函数外面一层作用域不是sayHi函数的作用域,而是全局,因为say在全局创建的,和调用位置无关

  • 闭包的生命周期

    • 闭包在外部函数调用时产生,外部函数每次调用都会产生一个全新的闭包
    • 在内部函数丢失时销毁
    function saveNum() {
      let num = 0
      return () => {
        console.log(++num)
      }
    }
    let result = saveNum()
    result()  //输出1
    result()  //输出2
    result()  //输出3
    //销毁内部函数
    result = null
    //重新生成内部函数,值又重新计算
    result = saveNum()
    result()  //输出1
    result()  //输出2
    

8. 递归

  • 递归函数

    • 调用自身的函数成为递归函数
    • 递归的作用和循环基本一致;循环执行性能好,递归思路清晰,多用循环
  • 递归函数条件

    • 基线条件 -- 递归终止条件
    • 递归条件 -- 对问题进行拆分
  • 案例

    //阶乘问题
    //1!= 1
    //2!= 1! * 2
    //3!= 2! * 3
    //4!= 3! * 4
    //···
    
    //1.循环解决阶乘问题
    function jieCheng(num) {
        let result = 1
        for (let i = 2; i <= num; i++) {
          result *= i
        }
        return result
    }
    let result = jieCheng(5)
    console.log(result) // 输出120
    
    //2.用递归解决阶乘问题
    function jieCheng(num) {
       // 基线条件--跳出递归
       if (num === 1) return 1
       // 递归条件
       return jieCheng(num - 1) * num
    }
    let result = jieCheng(5)
    console.log(result) //输出120
    
    //解析:
    //jieCheng(5)开始执行
    // - 不满足基线条件,执行return jieCheng(4) * 5,执行一个新函数jieCheng(4)
    //  - 不满足基线条件,执行return jieCheng(3) * 4,执行一个新函数jieCheng(3)
    //   - 不满足基线条件,执行return jieCheng(2) * 3,执行一个新函数jieCheng(2)
    //    - 不满足基线条件,执行return jieCheng(1) * 2,执行一个新函数jieCheng(1)
    //     - 满足基线条件 返回1,然后依次返回上面函数的执行结果
    // 递归就是当不满足基线条件时,一直调用自身,不断执行新函数,先执行的函数没结果就等着,直到满足基线条件
    //然后逐渐返回每个函数的结果
    

9. 补充关于函数this指向

  • 根据函数调用方式不同,this的指向也不同
    • 以函数形式调用,this是window
    • 以方法形式调用,this是调用方法的对象
    • 构造函数例外:this指向新生成的实例对象
    • 箭头函数例外:没有this,只由创建时外层作用域决定,和调用方式无关
    • 通过call和apply调用的函数,它们的第一个参数就是函数的this
    • 通过bind返回的函数,this也是由bind方法第一个参数确定,且this不可被再次修改
  • call和apply
    • 函数调用方法除了" 函数名( ) "这种形式外,还可以使用"函数名.call( )"、"函数名.apply( )"
      function fn() {
       console.log('函数调用了', this)
      }
      fn() //输出window
      fn.call() //输出window
      fn.apply() //输出window
      
    • 使用 call( thisArg , arg1, arg2, ··· ) 方法
      • 第一个参数thisArg是函数的this的值,后面接着写函数的实参
        function fn(num) {
         console.log(num, this)
        }
        let obj = {
          name: '胡歌'
        }
        fn(18) //输出: 18 Window
        fn.call(obj, 18) //输出:18 {name: '胡歌'}
        
    • 使用 apply( thisArg, argsArray ) 方法
      • 第一个参数thisArg是函数的this的值,所有实参必须写在一个数组中
        function fn(num) {
         console.log(num, this)
        }
        let obj = {
          name: '胡歌'
        }
        fn(18) //输出: 18 Window
        fn.apply(obj, [18]) //输出:18 {name: '胡歌'}
        //apply方法传参需要用数组包裹所有实参,函数根据数组顺序对应分配给形参
        
  • bind方法
    • bind( )是函数的方法,返回一个新的函数
    • bind的第一个参数,为新函数绑定this,锁死this
    function fn(num) {
      console.log(num, this)
    }
    let obj = {
      name: '胡歌'
    }
    fn(18) //输出: 18 Window
    
    let newfn = fn.bind(obj) //bind返回一个新函数
    newfn(20) //输出: 20 {name: '胡歌'}
    
    //注意:如果fn.bind(obj,10) 如果传了实参,则返回的新函数中形参num的值固定永远为10
    let newfn2 = fn.bind(obj,10) //bind返回一个新函数
    newfn2(20) //输出: 10 {name: '胡歌'} ,即使在传递新实参,也不能改变