zoukankan      html  css  js  c++  java
  • JavaScript之深入函数(一)

      在任何编程语言中,函数的功能都是十分强大的,JavaScript也不例外。之前已经讲解了函数的一些基本知识,诸如函数定义,函数执行和函数返回值等,今天就带大家深入了解JavaScript中函数的原理及执行过程。

    一   函数参数

      1,声明函数时可以添加参数,相当于在函数内部隐式的声明了变量。它的学名叫形式参数,简称形参,在执行函数使实际传递的值叫实际参数,简称实参。

    1 function add(a,b){
    2     return a+b;
    3 }
    4 add(1,2);//3
    5 //a和b就是形参,1和2就是实参

      

      2,JS中函数的参数不限制个数和数据类型,这意味着函数的形参和实参个数可以不相等,都可以是无限多个。

    1 function add(a,b){
    2     return a + b;
    3 }
    4 add(1);//NaN    1+undefined
    5 add(1,2,3);//3   忽略3
    6 add(1,"3");//"13"

      

      3,函数甚至可以没有形参,函数的arguments属性,以类数组的形式存储了函数执行时的实参。

    1 function add(){
    2     console.log(arguments);
    3 }
    4 add(1,2);//Arguments[1,2,...]

      

      4,函数有一个length属性,表示函数的形参个数。

    1 function test(){
    2     console.log(arguments);
    3     console.log(test.length);
    4 }
    5 test(1,2,3);
    6 //[0:1,1:2,2:3,length:3,...]
    7 //0

      

      5,arguments中的实参和形参是相互绑定的,修改其中一个,另一个也会改变,但形参和实参实际是两个变量。实参列表的个数是不可更改的,函数执行时传递几个就是几个。

     1  function test(a,b){
     2      console.log(arguments);
     3      console.log(b);
     4      b = 2;
     5      console.log(arguments);
     6      console.log(b);
     7      a = 10;
     8      console.log(arguments);
     9  }
    10  test(1);
    11 /*
    12 [0:1,length:1,...]
    13 undefined
    14 [0:1,length:1,...]
    15 2
    16 [0:10,length:1,...]
    17 */

    二   预编译

             前面介绍JavaScript时,提到它是单线程,解释性语言,即读到一行就执行一行。其实这只是它的表象,实际上JavaScript执行代码分为了3个大的步骤:

      

      1,  语法分析

      语法分析的工作大体就是检测是否有语法错误,是否符合版本规则等等。如果没有问题则进入预编译阶段,如果遇到问题则会抛出错误。

      

      2,  预编译

      在理解预编译之前,我们应该先明白两个概念:

        a:如果变量未申明即访问将报错,但是变量未声明即赋值,那么该变量将自动升级成全局对象(window)的属性。

        b:在全局申明的变量,也会自动升级成window的属性。

    1 console.log(a);//Reference error:a is not defined
    2 *************************************************
    3 a = 10;//10
    4 a === window.a;//true
    5 var b;
    6 b === window.b;//true

      程序预编译发生在即将执行之前,分为三步:

        1)  创建一个GO(Global Object)对象(也称为全局作用域),实际上就是window对象。

        2)  查找变量声明,并把他们作为GO对象的属性,值为undefined。

        3)  查找是否有函数声明,若有,则把函数名作为GO对象的属性,并把函数体赋值给该属性,若函数名和变量名相同,则会覆盖他们。

      函数也有预编译过程,函数的预编译发生在函数即将执行之前,分为四步:

        1)  创建一个AO(Active Object)对象(即执行期上下文,也叫函数作用域或本地作用域)。

        2)  查找形参和变量声明,并把他们作为AO对象的属性,值为undefined。

        3)  将实参赋值给形参。

        4)  查找函数内部是否有函数声明,若有,则把函数名作为AO对象的属性,并把函数体赋值给该属性,若函数名和形参或变量名相同,则会覆盖他们。

      总结下来,可以简单概括为:变量声明时,声明提升。函数声明时,整体提升。函数声明优先级大于变量和形参。

     1 function fn(a) {
     2       console.log(a); //ƒ a() {}
     3       console.log(b); //undefined
     4       console.log(c); //ƒ c() {}
     5       var a = 123;
     6       console.log(a); //123
     7       function a() {}
     8       console.log(a); //123
     9       var b = function b() {};
    10       console.log(b); //ƒ b() {}
    11       function c() {}
    12 }
    13 fn(1);
    14 /*
    15 GO  -->  {fn:fn}
    16 
    17 AO
    18 第一步:AO  -->  {}
    19 第二步:AO  -->  {a:undefined,b:undefined}
    20 第三步:AO  -->  {a:1,b:undefined}
    21 第四步:AO  -->  {a:function a() {},b:undefined,c:function c() {}}
    22 
    23 注意这里只是预编译过程,函数真正执行时,AO中的属性值会动态改变。所以:
    24 第一行代码直接打印fn a
    25 第二行打印undefined
    26 第三行打印fn c
    27 第四行声明变量a已经被提前执行了,这里直接赋值123
    28 第五行打印123
    29 第六行函数声明被整体提前了,这一行代码将被直接跳过
    30 第七行依然打印123
    31 第八行只执行赋值操作,b == fn b
    32 第九行则打印fn b
    33 第十行声明函数已经被整体提升了
    34 */

      

      3,  解释执行

      根据预编译后的代码顺序,一条一条的执行。

    三        作用域和作用域链

      

           每一个函数都有一个隐式的属性[[scope]],它存储的是函数在执行时创建的执行期上下文集合,即一堆AO和GO对象,当然他们是有顺序的,类似一个数组,它只能被系统调用,而不能被我们访问和使用。

           当一个函数在全局被创建时(没有被执行,这时还没有产生它自身的AO对象),[[scope]]将被插入GO对象。

      当函数执行时(这时已经创建了自己的AO对象),[[scope]]的头部将被插入一个自己的AO对象,类似数组的unshift()方法,这一过程在函数执行内部代码之前。

      现在[[scope]]中已经有两个执行期上下文的对象了:第0位的AO,第1位的GO。

    1 function fn(){}
    2 //fn.[[scope]]  -->  {0:GO}
    3 fn();
    4 //fn.[[scope]]  -->  {0:AO,1:GO}

             如果全局函数的内部定义了一个子函数(父函数正在执行),那么该子函数的[[scope]]属性类似的会存储:第0位父函数的AO,第1位GO(因为只有父函数执行才会产生子函数的定义,所以子函数被定义时就已经有两个执行期上下文对象了),这时子函数还没被执行,所以它只会存储这两个对象。

      当它被执行时,子函数[[scope]]属性的头部将被插入它自己的AO对象。如果子函数内部还定义的有其他函数,那么它的[[scope]]属性生成方式和上面相同。

    1 function fn(){
    2     function son(){};
    3 }
    4 //fn.[[scope]]  -->  {0:GO}
    5 //son.[[scope]] --> {} fn还没执行,son都还没声明
    6 fn();
    7 //fn.[[scope]]  -->  {0:AO(fn),1:GO}
    8 //son.[[scope]]  -->  {0:AO(fn),1:GO} 这里只是声明了函数son,所以并没有AO(son),如果function son(){}后面还有一行代码:son();那么当执行到这一行时,son.[[scope]]  -->  {0:AO(son),1:AO(fn),2:GO}

      这样就形成了函数的作用域链。当我们在函数内部访问变量时,实际上是在函数的[[scope]]属性里依次查找(从第0位开始),直到全局GO(window)对象。

      函数作用域链的最终表现就是:函数访问变量的权限由声明环境决定,而非执行环境。子函数可以访问父函数的变量,父函数不能访问子函数的变量。

      每次函数执行产生的执行期上下文都是独一无二的,当函数执行完毕,它自己的AO将被永远销毁,并更新自己的[[scope]]属性。下一次执行将产生一个新的AO对象,并添加到[[scope]]属性中。

  • 相关阅读:
    sfzwapp2
    linux-umount时提示device is busy时,如何查找被何进程占用?
    MySQL管理_数据库启动与关闭
    cache 比free 多
    NFS
    mysql备份多个库
    liunx修改时区,UTC 修改到CST
    mongodb备份恢复
    嵌入式新闻早班车-第24期
    【STM32H7的DSP教程】第48章 STM32H7的中值滤波器实现,适合噪声和脉冲过滤(支持逐个数据的实时滤波)
  • 原文地址:https://www.cnblogs.com/ruhaoren/p/11459963.html
Copyright © 2011-2022 走看看