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

  

原文地址:https://www.cnblogs.com/Walker-lyl/p/5860153.html