zoukankan      html  css  js  c++  java
  • js之函数

    一、定义:

      1.函数声明   function func () {}   

      2.函数表达式   var func = function () {} 

      注意点:

      

    var func = function test () {}
    func(); // ok
    test(); // 报错, test isn't defined

    匿名函数表达式 和 命名函数表达式 区别

      (1)命名函数表达式

    function test () {}
    console.log(test.name); // test

      (2)匿名函数表达式

    var test  = function func () {}
    console.log(func.name); // func is not defined
    console.log(test.name); // func

     二、return作用

      1.返回经过函数一系列处理的结果值

      2.终止函数的运行

    三、实参传递的数目和设定的形参数目相比,可多,可少,都不算错

      为什么? 因为函数的形式上下文(一个对象)中有个名为arguments属性,其值为一个类数组,储存着所有传递过来的实参,所以调用函数传实参时直接将所有实参按形参名作为属性名存入argumengs这个类数组中,而不会去在意实参的数目和形参设定的数目是否一样

       可通过funcName.length 查看形参数目, 通过arguments.length 查看实参数目

    四、作用域

      1、定义:变量(变量作用域又称为上下文)和函数生效(能被访问)的区域

      2.作用于相关定义:

        (1)执行期上下文:当函数执行时,会创建一个称为执行期上下文的内部对象。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,自己的执行上下文被销毁(注意仅仅销毁自己本身的执行期上下文)。

        (2)查找变量:从作用域链(scope chain的顶端依次向下查找。

      3、通过函数来简单理解的作用域

     如下js代码:

      

    function a() {
                var aa = 'aa';
                function b() {
                    var bb = 'bb';
                    console.log(aa);
              console.log(b); }
    return b; } var demo = a(); demo(); // aa function b(){...}

      首先,在a函数定义时会生成一个将全局执行期上下文存入scope chain(作用域链),但不会产生自己本身的执行期上下文。

      然后,在a函数执行后,会再次形成一个新的属于自己的scope chain(作用域链),同时将全局执行期上下文(执行期上下文是一个对象,为引用值)存入刚形成的scope chain顶部,然后再将自己本身的执行期上下文存入刚形成的scope chain顶部,也可这样理解,scope chain是一个数组,现在0序号位置存的是a函数自己本身的执行期上下文引用,1序号位置存的是全局执行器上下文引用(从数组头部插入)

    (若在a函数在内部访问一个变量,则去scope chain中寻找该变量,先从0位置开始寻找,若0位置存储的执行期上下文没有该变量,则再看1序号位置存储的,直到找到该变量为止,最坏的情况,scope chain 中没有改变量的信息,则肯定是报错了呗,嘿嘿),

    我们需要注意一个情况,在a函数执行的过程中在a函数内定义了一个函数b,所以b函数会和a函数一样形成scope chain,再将全局执行期上下文的引用、a的执行器上下文的引用按顺序存入scope chain中

      (只有a函数执行,才会产生a函数自己本身的执行器上下文,才会b函数被定义,  也正因为如此,b函数定义时才能将a函数的执行期上下文存入自己的scope chain中,  所以最终在b函数内部才能打印出b函数,这是串行操作,中间一个环节断了都不行,这是我的个人理解,仅限参考,嘿嘿)

      (最后最重要的,a函数执行的最终结果是将定义好的b函数返回给demo,此过程是将b函数的引用赋值给demo(所以demo函数执行打印的是b函数),需要注意的是,此赋值过程同时是变量demo将b函数定义时产生的scope chain也同样完整的继承了过去,即demo的scope chain中1和0序号位置分别存储了全局执行期上下文的引用的函数a的执行期上下文的引用,所以demo函数才能打印出a函数内定义的变量aa和a函数内定义的函数b)。

      ( 注意这里的存入,值得是scope chain该索引位置存的是执行期上下文对象的引用,可以形象的想象为scope chain该索引位置用箭头指向了执行期上下文)

      最后一点,其实其中所说的执行期上下文就是预编译中所创建的AO对象(Activation Object),不懂预编译的可以去了解一下,将作用域和预编译一起理解感觉会好一些

    五、闭包

      1、定义:当内部函数(在函数内定义的函数)被保存到外部时,将会生成闭包。这是最常见的一种 情况而已,广义来说,就是一个作用域引用或保存了另一个作用域的值,导致另一个作用域 本该销毁被无法销毁,这就是闭包。

      2、闭包是这样的:

      仍然通过上边的例子解释:

    function a() {
                var aa = 'aa';
                function b() {
                    var bb = 'bb';
                    console.log(aa);
              console.log(b);
                }
                return b;
           }
           var demo = a();
           demo(); // aa  function b(){...}

       首先,问一个问题?你们是否发现在a函数中完毕后,按定义说a的执行器上下文应该被销毁啊,那为什么demo执行后仍然能打印出aa和b函数呢?

       原因就是形成了闭包,导致a函数执行结束后a的执行期上下文没有被销毁,所有demo执行仍能正常打印,也就是由于函数b被return出来给了demo,使demo和a的执行期上下文有联系,也就是demo占用了a的执行期上下文,所以a执行完毕后a的执行期上下文不会被销毁,最重要的是demo执行结束后,demo本身的执行期上下文被销毁,但其连接的a的执行期上下文中存储了b函数,即b函数的引用(相当于demo),所以a的执行期上下文永远不会被销毁(可以通过赋值demo为null,表示demo不占用a的执行期上下文,那么a的执行期上下文就可以被销毁了),这就形成了闭包,这就是闭包的基本原理。

      3.缺点:闭包会导致原有作用域链不释放,造成内存泄露。

        解释: 上面本来a函数执行完毕后其执行期上下文应该被销毁的,但因为闭包原因没有被销毁,所以导致可使用的总内存量减少,即内存泄漏。

      4.简单应用:

        (1)实现公有变量:

      eg: 函数累加器

      

      // 闭包形成的累加器,变量num就类似java中说的共有变量
          function bibao() {
              var num = 0;
              return function () {
                  console.log(num++);
              }
          }
          var add1 = bibao();
          add1(); //0
          add1(); //1
          add1(); //2
          add1(); //3
          add1(); //4  

      有于闭包原因,导致bibao函数的执行期上下文不会被销毁,所以num一直可以在之前基础上累加    

      (2)可以做缓存:

      

    //    两个函数公用eater函数的作用域链
            function eater() {
                var food;
                var obj = {
                    eat: function () {
                        if (food) {
                            console.log('I eat ' + food);
                            food = null;
                        }else {
                            console.log('There is nothing');
                        }
                    },
                    push: function (myFood) {
                        food = myFood;
                    }
                }
                return obj;
            }   
            var oEater = eater();
            oEater.eat(); //There is nothing
            oEater.push('apple');
            oEater.eat(); //I eat apple

      (3)、实现封装,属性私有化

       又是我们需要这样一些需求,我们希望函数内部的某些属性无法被外部写,仅仅只能被外部读而已等等。

      最初,大家是共同制定了一个约定,那就是在函数内部不希望被外部写的属性钱加一个下划线,标示着该属性不能被其他人更改,但是该约定约束性不强。

      后来,大家想到了一个方法,那就是用闭包来实现属性的私有化,此方法具有很强的约束性。如下demo举例:

    function demo () {
         var num = 1,
             name = 'lyl';
    
          // 和返回一个函数形成的闭包是一样的,只不过此处是返回多个函数,且多个函数用一个对象包装了。
          // 该对象中的多个函数都占用了demo执行时产生的执行期上下文,导致demo执行时产生的执行期上下文不会被销毁,从而产生闭包
          return  {
             sayName: function () {
               console.log(name);
             },
             sayNum: function () {
               console.log(num);
             }
          }
       }
    
       var obj = demo();
       obj.sayName(); // lyl
       console.log(obj.num); //undefined

      5.闭包的防范:闭包会导致多个执行函数共用一个公有变量,如果不是特殊需要,应尽量防止这种情况发生。

     如下例子:arr数组中多个值公用一个test函数执行期上下文的变量 i,导致打印结果超出意料

      

     function test() {
                var arr = [];
                for(var i = 0; i < 10; i++ ) {
                //arr中所有值对应一个执行期上下文,其中的i是随循环不断累加变化的,最终i固定为10不变,所以打印出的都为10
                    arr[i] = function () {
                        console.log(i);
                    }
                }
                return arr;
            }
            var retArr = test();
            for(var i = 0, len = retArr.length; i < len; i++ ) {
                retArr[i]();
            } 
            // 打印结果是10个10

      解决方法: 利用立即执行函数(下面会讲),让arr数组中每个值都对应一个独立的执行期上下文,避免多个值公用一个执行期上下文的变量

    function test() {
                var arr = [];
                for(var i = 0; i < 10; i++) {
                   (function(j) {
                    //    每次都将对应的i转换为j,所以每次arr[j]对应一个自己独有的执行期上下文,其中的j是固定不变的,所以能从0打印到9
                    // 即arr数组中有10个数,则分别对应10个独立的执行期上下文
                       arr[j] = function () {
                           console.log(j);
                       }
                   } (i) );
                }
                return arr;
            }
            var retArr = test();
            for(var i = 0, len = retArr.length; i < len; i++) {
                retArr[i]();
            }
            // 打印结果为0 1 2 3 4 5 6 7 8 9 

      

      

     六、立即执行函数

      1.定义:此类函数没有声明,在一次执行过后即释放。适合做初始化工作。(即此函数不需调用,直接执行)

      2、使用形式如下:

     //    立即执行函数基本形式
            // 第一种,推荐
           (function (形参) {
               //handle code
           } (实参) );
            // 第二种
            (function (形参) {
                // handle code
            } )(实参);   
        // demo
            var ret = (function (a, b) {
                return a + b;
            } (1, 2) );
            console.log(ret); //3

       3、立即执行函数的原理:

       为什么这样写,就可以执行了?大概原理如下:

      其实重点在于小括号上,小括号()代表了执行,而()前放什么才可以执行呢?答案是表达式。 那表达式是什么呢?权威指南上关于表达式有这样一句话‘表达式是JavaScript的一个短语,JavaScript解释器会将其计算出一个结果’,简单来说只要是某一个类型的值,就可以看作表达式,当然此处关于表达式的理解是不全面的,但对于立即执行函数的立即来说是足够的了。所以函数执行就可以解释了,函数名是该函数的引用,为一个函数表达式,所以加上()后也就可以执行了。下面根据此理论来解释立即执行函数:

      凡是()圈起来的都是表达式,所以(function (){})为一个表达式,然后再加上一个()则可以执行,所以实参放入后面那个括号中,形参放于function后的括号中也就合理了;而(function() {} ())此种形式可以算作立即执行函数的一个标准写法,省去了function (){}转换为表达式的时间,执行能更快一些。

      除了常用的一些正规写法外,利用其原理,还有一些其他写法,同样能实现立即执行函数的效果。只要()前是表达式即可。如下:

    // 成功的demo
            var demo1 = function func() {console.log('demo1')}(); //demo1,  demo1被赋值为实名函数表达式
    
            var demo2 = function () {console.log('demo2')}; // demo2, demo2被复制为匿名函数表达式
    
            +function (){console.log('demo3')}; // demo3, 通过+运算符的隐士类型转换将+后面的转换为number原始表达式,所以也就可以执行了
            console.log(typeof +function (){}); //number
    
            -function (){console.log('demo4')}; // demo4, 通过-运算符的隐士类型转换将-后面的转换为number原始表达式,所以也就可以执行了
             console.log(typeof -function (){}); //number
    
            !function (){console.log('demo5')}; // demo5, 通过+运算符的隐士类型转换将+后面的转换为boolean原始表达式,所以也就可以执行了
            console.log(typeof !function (){}); //boolean
    
            1 && function (){console.log('demo6')}(); // demo6, 通过&&运算符的隐士类型转换将&&后面的转换为boolean原始表达式,所以也就可以执行了,但此处需要注意一点,
                                    &&运算符仅仅判断是将值隐士转换为boolean类型的,但返回的是原值,因为都判定为true所以返回&&后面的原始值,否则返回&&前面的原始值
    0 || function () {console.log('demo7')}(); // demo7, ||隐士类型转换为boolean原始表达式,所以可以执行,0 判定失败, 看||后面的,||后面的判定为true,返回该函数所以执行 // 失败的需要注意的demo *function (){console.log('demo1')}(); // 报错,以为*无法隐士类型转换 1 || function (){console.log('demo2')}(); // 报错,因为1判定为true,所以不看||后面的,直接返回1,所以无法执行 0 && function (){console.log('demo3')}(); // 报错,因为0判定为false,所以不看&&后面的,直接返回0,所以无法执行

     七、预编译:

       1、js运行三部曲:

        (1)、语法分析(通篇分析查找是否存在低级语法错误,低级语法错误是直接可以看出的,不用执行就可以找出的)

      *低级语法错误:

      

      console.log('aa');
             console.log('a';  //低级语法错误,不用执行就可以直接看出的 |* 影响上面代码的执行,也影响下面代码的执行
            //  执行结果: missing ) after argument list

      *逻辑语法错误:

        

     console.log('aa'); 
             console.log(a);  //逻辑语法错误,无法直接看出,需要执行浏览器执行才能找出 |*不影响上面代码的执行,但影响下面代码的执行
             //执行结果: 
             //aa
             //a is not defined(…) 

        (2)、预编译

      *预编译前奏:

        i、imply global 暗示全局变量:即任何变量,如果变量未经声明就赋值,此变量就为全局对象所有

      

            function test() {
                var a = b = 10;
            }
            test();
            console.log(b); // 10
            console.log(a); //  a is not defined(…)
            // 此demo存在一个坑, 就是连续赋值 var a = b = 10;
            // 该赋值可以分解为两条语句,分别为b = 10; var a = b;
            // 所以b归全局对象所有,而a归test执行时的执行期上下文所有,
            //外部全局对象window访问不了test内部的a变量,并且test执行结束后a会随着test的执行期上下文销毁而消失

         ii、一切声明的全局变量,全是window的属性。

      

            var a = 10;
            console.log(window.a); // 10

     (题外话:关于此处有关于对象的一个知识点,关联一下, delete操作符可以删除对象的属性,所以未经声明就赋值的变量为window对象的属性,可以用delete操作符删除,但在全局环境中用var声明的变量虽然可以用window.name访问,但无法用delete操作符删除)(可以这样理解,不用var声明直接为变量赋值的行为是为全局对象的属性赋值的操作,而用var声明的变量是真正的我们所说的变量,而不是某个对象的属性,当然该变量也是当前环境下this的属性)

      *预编译四部曲:

      i、创建AO(Activation Object)对象

      ii、找形参和变量声明,将变量和形参名作为AO属性名,值为undefined

      iii、将实参值和形参统一

      iiii、在函数体里面找函数声明,值赋予函数体

      var a = 20;
           function test () {
               console.log(a);
            //    此处一个小坑
               if(false) {
                   var a = 10;
               }
           }
           test();  // 20 or undefined? answer is undefined
        //    预编译时与if判断的正误没有关系,只有在解释执行时才会在乎if判断的正误

        (3)、解释执行

    ------------------------------end

      

  • 相关阅读:
    Life -1b The secrets of long life? Teacher: GABRIELE
    Life -1a How well do you sleep? Teacher: GABRIELE
    Kubernetes(K8s) 学习笔记 updating...
    English trip EM3-PE1B Teacher:Taylor
    V3 -A Hard Bargain Teacher: GABRIELE
    802.11k/v/r WiFi无缝漫游
    V3 -Who Am I? Teacher: GABRIELE
    Dell OMSA(updating...)
    (转)红蓝对抗之Windows内网渗透
    WiFi6(整理网上资料进行汇总查看,相关内容摘取与互联网)
  • 原文地址:https://www.cnblogs.com/Walker-lyl/p/5860153.html
Copyright © 2011-2022 走看看