前端系统化学习【JS篇】:(六-2)引用数据类型之Function篇

654 阅读11分钟

前言

  • 细阅此文章大概需要 20分钟\color{red}{20分钟}左右
  • 本篇中详细讲述\color{red}{详细讲述}了:
    1. 函数function的简述
    2. 创建函数
    3. 执行函数
    4. 形参变量
    5. 返回值
    6. arguments 函数内置实参集合
    7. 匿名函数
    8. eval
    9. 【ES6】arrow function箭头函数
    10. 函数的三种角色
    11. 【THIS相关】函数的四种执行方式
  • 如果有任何问题都可以留言给我,我看到了就会回复,如果我解决不了也可以一起探讨、学习。如果认为有任何错误都还请您不吝赐教,帮我指正,在下万分感谢。希望今后能和大家共同学习、进步。
  • 下一篇会尽快更新,已经写好的文章也会在今后随着理解加深或者加入一些图解而断断续续的进行修改。
  • 如果觉得这篇文章对您有帮助,还请点个赞支持一下,谢谢大家!

函数 function

  • 函数就是一个方法或者一个功能体,函数就是把实现某个功能的代码放到一起进行封装,以后想要操作实现这个功能,只需要把函数执行即可=> 【"封装":减少页面中的冗余代码,提高代码重复使用率(低耦合高内聚)
    //洗衣机就是一个函数,生产洗衣机就是封装一个函数【把实现某些功能的代码封装进来】
    //生产的时候,不知道用户洗衣服的时候放什么水、衣服、洗衣液、
    //所以我们设计时需要提供入口【提供的入口在函数中叫做形参,执行的时候具体放进去的东西叫做实参】
    //洗完衣服需要能拿出来,洗衣机提供一个出口【函数中叫做返回值:把函数处理后的结果能够返回给调用处来使用】
  • 创建函数
    • 形参:
    • 返回值:
  • 执行函数
    • 实参
  • arguments函数内置的实参集合
  • 函数底层的运行机制
  • ...

创建函数

  • 创建函数,会开辟一个新的堆内存,用来存储函数体中的代码, 【但是是按照字符串格式来存储的】
    • 在函数为执行之前,这就是一堆存储在堆中的字符串。
  • 创建函数的两种方式
    1. 函数声明
      //ES3/5老规范
          function 函数名(形参1,形参2...){
              //函数体:基于js完成需要实现的功能
              return[处理后的结果(返回值)]
          }
          函数名(实参1,实参2...);//函数的执行
      
    2. 函数表达式【又称函数字面量】
         //ES3/5老规范
         var 函数名 = function (形参1,形参2...){
             //函数体:基于js完成需要实现的功能
             return[处理后的结果(返回值)]
         }
         函数名(实参1,实参2...);//函数的执行
      
    3. 两者的区别(体现在变量提升)
      • 函数声明:
        • 在词法解析阶段【的变量提升中】,会先读取函数声明(不管之后是否用得到都声明+定义(创建堆内存并与变量关联)了), 并使其在执行任何代码之前可以访问;
      • 函数表达式
        • 而函数表达式则必须等到代码执行时,执行到它所在的代码行才会真正被解释执行 (创建堆内存并与变量关联)。

执行函数

  • 每一次函数执行的目的,都是把堆中存储的函数体中的代码(先从字符串变为代码)执行=> 【在执行阶段才!!!】当函数被执行时会形成一个全新的私有执行上下文【私有栈内存】【vo->Ao】
  • 而且每一次执行,都是把函数体的代码从头执行一遍,而且会创建一个新的私有执行上下文【私有栈内存】
        let aa = 函数名(实参1,实参2...);//函数的执行
    
  • 先把函数执行,再把执行后的返回结果和aa关联在一起
    • 【函数的返回值只看return,后面是啥返回值就是啥,没有的就是undefined】
        //如:
        function sum(n,m){
            //【形参默认值处理:如果没有传递形参,给予一个默认值 】
            if(n===undefined){
                n = 0;
            }
            if(typeof m==='undefined'){
                m = 0;
            }
            let result = n+m;
            result *= 10;
            result /=2;
            console.log(result);
        }
        console.log(sum);//=>sum(n,m){...}
        //sum是函数名,代表函数本身
        sum(10,20);//=>150
        sum(20,30);//=>250 这都是传递给形参变量的值(实参)
    

形参变量

  • 形参:是变量,用来存储函数运行时传递的实参
  • 创建函数的时候,若设置了形参变量,但执行的时候并没有传对应的实参值,那么形参变量的值默认为:undefined
    • 若没有传值,在数据运算时,会造成结果为NaN,所以应在函数体内对【形参默认值处理:如果没有传递形参,给予一个默认值 】

返回值

  • 函数执行时,会形成私有上下文,函数体内部创建的变量我们在函数外部是无法获取和操作的,如果想获取内部的信息,我们需要基于return返回值机制,把信息返回才可以。
    • return的一定是值,不是变量也不是别的 (就算是变量也是返回的其值)
    • 函数运行不一定非要有返回值,若不写return,函数默认的返回值是undefined
    • 函数体中遇到return,后面的代码就不再执行了
       function sum(n,m){
            let result = n+m;
            return result;
        }
        console.log(sum(10,20));//=>30
    

arguments 函数内置实参集合

  • 类数组(arguments):函数内置的实参集合
    1. 类数组集合,集合中 存储着所有函数执行时,传递的实参的值
    2. 不管是否设置形参,arguments都存在
    3. 不论是否传递实参,arguments也都存在
  • arguments.callee:存储的时当前函数本身(一般不用的,JS严格模式下禁止使用这些属性)
  • 【在非严格模式下】,形参赋值完成后,会和ARGUMENTS中的每一项建立映射机制(一个改另一个跟着改【而且是双向的】)
    • 而且该映射机制是在函数执行之前完成的,如果在那个阶段没有建立映射,那后续也就不会新增映射了。
        var a = 4;
        function b(x,y,a){
            console.log(a);//3
            arguments[2] = 10;
            console.log(a);//10 形参赋值完成后,会和ARGUMENTS中的每一项建立映射机制
        }
        a = b(1,2,3);
        console.log(a);//undefined【函数没有return】
    

        var a = 4;
        function b(x,y,a){
            //EC(B)私有
            //<EC(B),EC(G)>
            //arguments[0:1,1:2,length:2]【类数组】
            //形参赋值:x = 1 y = 2 a = undefined此时只建立了前两项的映射,因为arguments中并没有第三项,所以a和arguments没关系了
            //变量提升:--
            a = 3;
            console.log(arguments[2]);//undefined//没有2这项
        }
        a = b(1,2);
    

    		/* 不论是否传递实参,arguments也都存在 */
            function sum(){
                console.log(arguments);
                let total = null;
                for(let i = 0; i<arguments.length;i++){
                    //获取的每一项都要先转换为数字(数学运算)
                    //且非有效数字不进行运算
                    let item = Number(arguments[i]);
                    if(isNaN(item)){
                        continue;
                    }
                    total+=item;
                }
                return total;
            }
            let total = sum(10,20,'aa',true);//true在isNaN时转换为数字1
            console.log(total); //=>31
    

匿名函数

  • 匿名函数之函数表达式:
    • 把匿名函数本身作为值赋值给其他东西 ,这种函数一般不是手动触发执行【没有函数名】,而是靠其他程序驱动触发执行
  • 匿名函数之自执行函数:
    • 创建完一个匿名函数,紧接着就把当前函数加小括号执行
    //如
       document.body.onclick = function(){}//点击触发
       setTimeout(function(){},1000)//设置定时器,一秒后执行【匿名函数】
       function(n){}(100)//【自执行函数】
    
  • 匿名函数具名化:
    • 在使用函数表达式创建函数时,把原本作为值的【函数表达式匿名函数】“具名化”。
    • (虽说是起了名字,但是这个名字在 创建函数的上下文以及更外部的上下文中访问不到 =>也就是说 不会在创建该函数的上下文中声明这个名字
    • 当函数执行后,在函数形成的 私有上下文中,会把这个“具名化”的名字作为 私有上下文中的所声明的一个变量其值就是这个函数本身来处理
    • 而在函数内部若修改这个名字对应的属性值,默认是不能修改的,结果依然是指函数本身【除非在函数内部重新声明了这个"名字"】,当重新声明之后,该"名字"的所有都按照重新声明的为准
          var b = 10;
          (function b(){
              b = 20;//修改,是无效的
              //var b = 20//若重新声明,则变为b = 20
              console.log(b);//undefined
              //因为在函数内部若修改这个名字对应的属性值,默认是不能修改的
          })();
              console.log(b);//10
      
    • 在递归调用时,可以使用具名化的名字执行函数, 不用再使用严格模式下不支持的arguments.callee
          //如
          var func = function AAA(){
              console.log(AAA);//=>ƒ AAA(){}【当前函数】
          }
          }
          console.log(AAA);//AAA is not defined
      

eval

  • 是一个内置函数,作用是把字符串变为JS表达式执行

arrow function箭头函数 【ES6】

  • 如果函数体中只有一行return,可以省略return和大括号,一行搞定
  • 如果函数体中只有一个形参,可以省略括号
  • 若形参未赋值,需要赋默认值时,箭头函数可直接写为【形参=xxx】,【函数执行形参未赋值时,则该参数就会被赋值为这个xxx,若有实参赋值时,则按照实参为准】
  • 【箭头函数当中没有arguments】,但是我们可以用【剩余运算符(...arg)作为形参】获取到传递的实参集合 【该集合是数组,不是类数组,可以使用数组内置的方法】
  • 箭头函数中的this某些场景也是方便我们操作的
    • 箭头函数没有自己的this, 它的this是继承所在上下文中的this
        function sum(){
            return n+m;
        }
    //改写箭头函数
        let sum = (n,m) => {
            return n+m;
        }
    //如果函数体中只有一行return,可以省略return和大括号,一行搞定
        let sum = (n,m) => n+m;
    //如果函数体中只有一个形参,可以省略括号,一行搞定
        function fn(n){
                return function(m){
                    return n+m;
                }
            }
    //改写箭头函数
        let fn = n => m => n+m ;
    

        //若形参未赋值,需要赋默认值时,箭头函数可直接写为形参=xxx,当函数执行形参未赋值时,则给该参数赋值为xxx
        function fn(n,m){
            if(typeof n === 'undefined'){
                n = 0;
            }
            if(typeof m === 'undefined'){
                m = 0;
            }
            return n+m;
        }
        //改写箭头函数
        let sum = (n = 0,m = 0) => n+m;
    

      //箭头函数实现任意数求和
      //eval是一个内置函数,作用是把字符串变为JS表达式执行
      let sum=(...arg) =>eval(arg.join('+'));
      sum(1,2,3,4);
    

函数的三种角色

  1. 普通函数(作用域和作用域链)
  2. 构造函数(类/实例/原型和原型链)
  3. 普通对象(键值对/属性值属性名)
  • 但是 主角色是函数:因为所有的函数都是Function类的实例
    • 所以函数既可以【作为函数调用Function.prototype上的方法】,也可以【作为Function的实例调用Object.prototype上的方法】【因为函数既是函数也是对象】
    • 但是对象一定不是函数

函数的四种执行方式【THIS相关】

  1. 【直接调用】

    • obj.func();
      • 直接用对象调用,或直接方法调用(这样执行时,函数的this是点前面的调用者)
  2. 【call改变this指向】【0~多个参数】

    • [function].call([context],params1,params2....);
    • 而在call方法执行时,对【当前函数】进行的操作就是【先把当前函数的this改变成为call方法中的第一个参数[conctxt]】,除了第一个参数,剩下的参数params1,params2....都是【在当前函数执行时要传入的参数】。然后把当前函数执行
    • call方法的第一个参数
      • 如果不传递或者传递null/undefined,在【非严格模式】下都是让this指向window 如果传递的是一个基本类型值,则为其所属内置类
      • 【严格模式】下传递的是谁this就是谁,不传就是undefined
          var person = {
          	fullName: function(city, country) {
              	return this.firstName + " " + this.lastName + "," + city + "," + country;
          	}
          }
          var person1 = {
              firstName:"Bill",
              lastName: "Gates"
          }
          var person2 = {
              firstName:"Steve",
              lastName: "Jobs"
          }
          var x = person.fullName.call(person1, "Seatle", "USA"); //Bill Gates,Seatle,USA
          //【第一个参数是call的this指向,后面的参数是调用call的方法的参数】
      

  3. 【apply改变this指向】【0~2个参数】

    • [function].apply([context],[params1,params2....]);
      • 参数是数组,apply同样可以改变函数运行的作用域,和call的作用一样,只不过 【给函数的参数是以数组的形式传递给apply】
  4. 【bind改变this指向】【0~多个参数】【不兼容IE678】

    • [function].bind([context],params1,params2....);
    • 语法上和call一样,但是作用和call与apply不同
      • call和apply都是把当前函数 立即执行,并且改变函数中的this指向
      • 而bind是一个预处理的思想 ,基于bind只是预先把函数中的this指向[context],把params这些参数预先存储起来,但是此时函数并没有被执行。
        • 等到何时条件成立,才会以指定的this和参数将函数执行
    • 原理:
      • bind实际上就是用此原理来实现的,bind执行后,会返回一个类似的匿名函数,而当每次点击事件执行, 实际上是执行了返回的匿名函数