JavaScript Core -- 函数详解(作用域&&参数值传递&&this关键字&&函数声明)

来源:互联网 发布:上实剑桥 知乎 编辑:程序博客网 时间:2024/05/01 06:02

    函数声明和函数表达式

    javasceipt中函数就是对象,函数的名字也只是一个指向函数对象的指针,不会与某个函数绑定,所以一个函数可以有多个名字。 

   我们声明函数时可以有两种选择:函数声明法,表达式定义法

    函数声明法:

  function sum (num1 ,num2){       return num1+num2  }
  表达式定义法:
  var sum = function(num1,num2){       return num1+num2  };/注意,一定要在结尾添加分号,因为想一想,你见过的所有表达式后面都会有一个 ; 对吧?
  他们是有区别的:解析器会率先读取函数声明,并使其在执行任何代码之前可以访问(函数声明提升放到代码树的顶部)。至于函数表达式,则必须等到解析器执行到他所在的代码行,才会真正执行。下面的两个函数使用不同的方式定义,调用的时候就会产生不同的的效果。

 

    

   PS: JavaScript是解释型语言,但它并不是直接逐步执行的,JavaScript解析过程分为先后两个阶段,一个是预处理阶段,另外一个就是执行阶段。在预处理阶段JavaScript解释器将把JavaScript脚本代码转换到字节码,然后第二阶段JavaScript解释器借助执行环境把字节码生成机械码,并顺序执行。

console.log(a); //Error:a is not defined ,直接报错,下面语句没法执行,以下结果为注释该句后结果console.log(b) //undefinedvar b="Test";console.log(b);//Test
    也就说JavaScript值执行第一句语句之前就已经将函数/变量声明预处理了,var b=”Test” 相当于两个语句,var b;(undefined结果的来源,在执行第一句语句之前已经解析),b=”Test”(这句是顺序执行的,在第二句之后执行)。这也是为什么我们可以在方法声明语句之前就调用方法的原因

    再次确认:函数名只是个变量,所以函数名可以作为值来使用,也可作为参数传递,然后该函数中的内容相当于直接放到了当前接受参数的函数中,通过下面这个案例进行诠释。

    案例1:改造sort函数:js中默认的sort()函数时按照每个位置上的值的编码来进行排序的,会出现 1<10<5的现象,下面按照我么的意愿来改造这个函数

  var values = [0,1,5,10,15];  values.sort(compare);  alert(values);//0 ,1 ,5 ,10 ,15  function compare(value1,value2){    return value1 - value2;//返回负数,升序;  }  var data = [{name:"feng",age:25},{name:"laing",age:28}];  data.sort(compareObj("age"));//按照每个对象的age进行排序  alert(JSON.stringify(data));//从对象中取出字符串 {name:"feng",age:25},{name:"laing",age:28}  function compareObj(age){    return function(obj1,obj2){        var value1 = obj1.age;        var value2 = obj2.age;        return value2 - value1;//返回正数,降序{name:"laing",age:28},{name:"feng",age:25}        }  }

this

    this是javaScript的一个普通的关键字,代表函数运行时,自动生成的一个内部对象,只能在函数内部使用,比如

    function test(){      this.x = 1;    }
    同时this又是一个很特殊的关键字,他很灵活,在不同的场合下使用,this的值会发生变化,但是无外乎一个原则:this指向的永远是当前调用函数的那个对象   下面在不同的函数调用场合,为大家介绍this的使用方法

    1)纯粹的函数调用:函数最通用的方式就是全局调用,因此this就指向全局对象Global

    function  f1(){         return  this;     }     alert(f1()  ===  window);  //  true, this指向调用f1()的全局变量window(浏览器中为window对象,以下我们默认在浏览器环境下执行js代码,全局对象为window
     为了证明this就是全局对象,我们再来一个小例子
    var x = 1;  function test(){    this.x = 0;  }  test();  alert(x); //0

    这个函数第一行定义了一个全局变量x并复制为1 此时window.x = 1第五行在全局环境下调用test() ,在test()函数内部this指向window对象,且window.x = 0,所以在最后一行alert()的时候调用全局变量x的值为0

    再看下面一个例子

    var myObj = {        value :3    };    var add = function(a,b){        return a+b;    };    myObj.double = function(){        var helper = function(){            this.value = add(this.value,this.value);            alert(this.value)        };        helper();//以函数的形式调用    };    //以方法的形式调用    myObj.double();//

    以上代码达不到目的,因为以此模式调用时,this被绑定到了全局变量。这是语言设计上的一个错误,倘若语言设计正确,那么当内部函数被调用时,this应该绑定到外部函数的this变量。这个设计错误的后果就是方法不能利用内部函数来帮助它工作,因为内部函数的this被绑定了错误的值,所以不能共享该方法对对象的访问权。幸运的是,有一个很容易的解决方案:如果该方法定义了一个变量并给他赋值this,那么内部函数就可以通过那个变量访问到this. 按照约定,我们可以把那个变量命名that:

    var myObj = {        value :3    };    var add = function(a,b){        return a+b;    };    myObj.double = function(){        var that = this;//解决办法        var helper = function(){            that.value = add(that.value,that.value);            alert(that.value)        };        helper();//以函数的形式调用    };    //以方法的形式调用    myObj.double();//6

    2)作为对象方法的调用:此时this指向这个上级对象

    function test(){    console.log(this.x);//此时this指向的就是调用test()的对象o  }  var o = {};  o.x = 1;  o.m = test;  o.m(); // 1

    3)作为构造函数调用: 如果一个函数前面带上一个new来调用,那么背地里将会创建一个连接到该函数的prototype成员的新对象,同时this会绑定到那个新对象上

   function test(){    this.x = 1;  }  var o = new test();  alert(o.x); // 1

    为了加深印象,我们再上一个比较复杂的例子

    function MyClass(){        this.a = 37;    }    var o = new MyClass();    alert(o.a)//37    function C2(){       this.a= 37;       return{a:38};    }     o = new C2();     alert(o.a)// 38,这里比较特殊,因为C2中return 了一个新的对象所以this对象绑定到了返回的对象上
        一个函数总会有一个返回值,如果没有指定则返回undefined。如果函数调用时在前面加上了new前缀,且返回值不是一个对象,则返回this(该新对象)

    4)apply调用:apply()是函数对象的一个方法,它的作用是改变函数的调用对象,它的第一个参数就表示改变后的调用这个函数的对象。因此,this指的就是这第一个参数。apply()的参数为空时,默认调用全局对象

    var x = 0;  function test(){    alert(this.x);  }  var o={};  o.x = 1;  o.m = test;  o.m.apply(); //0

apply()的参数为空时,默认调用全局对象。因此,这时的运行结果为0,证明this指的是全局对象。如果把最后一行代码修改为

    o.m.apply(o); //1
    运行结果就变成了1,证明了这时this代表的是对象o。


函数属性&&arguments

     1.理解参数&&没有重载:ECMAScript函数不介意传递进来多少个参数,也不在乎传进来参数是什么数据类型。因为ECMAScript中的参数在内部是用一个数组来表示的。函数接收到的始终是这个数组,而不关心数组中包含哪些参数(如果有参数的话)。实际上,在函数体内可以通过arguments对象来访问这个参数数组(arguments[0]是第一个元素,以此类推),可以使用Length属性来确定到底传进来了多少个参数。

    function sayHi(firN,SecN){        alert('hello'+firN+SecN);    }    //替换成    function sayHi(){alert('hello'+arguments[0]+arguments[1])    }
    所以说:命名的参数只提供便利,不是必须的。arguments对象的长度是由传入参数个数决定的,不是由定义函数时的命名参数的个数决定的所以,就没有函数签名,ECMAScript也就没有重载。所谓的参数是由0~多值组成的数组表示的,后定义的函数会覆盖先定义的同名函数。

    下面一个例子用来说明可以通过arguments动态的修改参数

    function  foo(x,  y,  z)  {         arguments.length;  //  2         arguments[0];  //  1         arguments[0]  =  10;         x;  //  change  to  10;         arguments[2]  =  100;         z;  //  still  undefined  !!!         arguments.callee  ===  foo;  //  true     }     foo(1,  2);     foo.length;  //  3 

    2.参数的值传递 :ECMAScript中所有函数的参数都是按值传递的。参数把函数外部的值复制给函数内部的,就和把值从一个变量复制到另一个变量以一样。

    在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数,或者用ECMAScript的概念来说,就是arguments对象的一个元素),仅仅是具有相同的值。在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部。再额外举一个例子,证明对象时按值传递的

 function setName(obj){obj.name = "feng";obj = new Object();//1obj.name = "haha";//2  }  var person = new Object();  setName(person);  alert(person.name);//feng

    无论加不加1 2两行都输出feng,说明,即使在函数内部修改了参数的值,但原始的引用(person->obj)仍然未变。好比把a1钥匙复制成a1,a2,现在两把钥匙都能开开A门,但是把a2改成开B门的样子后,原来a1仍然可以开A门,而此时a2与a1无关了。


执行环境&&作用域

    每一个执行环境(以下简称,环境)都有一个与之相关联的变量对象(Variable  Object,  缩写为VO),环境中定义的所有变量和函数都保存在这个对象中,它是一个抽象概念中的“对象”,它用于存储执行上下文中的:变量,函数声明,函数参数。

    某个环境中所有代码执行完毕后该环境被销毁(windows对象在关闭浏览器网页后)。每个函数都有自己的环境,当执行流进入一个函数时,函数的环境被推入一个环境栈中,但函数执行完毕后,栈将其环境弹出,将控制权返回给之前的环境。作用域链从当前函数的活动对象开始,以最外层的全局执行环境的变量对象为尽头,过程中直到找到需要的标识符为止。即内部环境可以通过一条作用域链访问所有的外部环境。

    举个例子:

  var  a  =  10;   function  test(x)  {     var  b  =  20;   }   test(30);
    这个函数对应的VO结构如下

  VO(globalContext)  =  { //全局变量对象    a  :  10,     test  :  <ref  to  function>   };   VO(test  functionContext)  =  { //函数变量对象    x  :  30,     b:  20   };
    下面详细的对变量的声明和赋值过程进行拆分

     一、变量初始化阶段:VO按照如下顺序填充:(也可以用来解释函数式声明的优先定义)
    1.  函数参数  (若未传入,初始化该参数值为undefined) 
    2.  函数声明  (若发生命名冲突,会覆盖) 
    3.  变量声明  (初始化变量值为undefined,若发生命名冲突,会忽略。)

  function  test(a,  b)  {     var  c  =  10;     function  d()  {}     var  e  =  function  _e()  {};     (function  x()  {});     b  =  20;    }     test(10); 
   对应的初始化情况为
  AO(test)  =  {     a:  10,     b:  undefined,     c:  undefined,     d:  <ref  to  func  "d">     e:  undefined   };
   具体的过程如下:首先进行函数参数的初始化:a被初始化为10,而b未传入复制为undefined(本质上为数组中arguments[1] == undefined);然后进行函数声明,d被声明为函数引用;然后进行变量声明c被声明为undefined。注意一点,变量如果已声明却未赋值,则显示undefined;但调用一个未声明的变量则会报错,这两者是不同的。你能区分一下的情况吗
    //未声明    alert(x);//error
    //声明未赋值    var x;    alert(x);//undefine 
    //赋值前调用    alert(x);//undefined ,因为alert()处于赋值语句之前,在函数初始化阶段x首先被初始化为undefined,所以此处x值为undefined,但是并不会报错    var x = 10;

      这里的第二点可以用来验证一下函数声明提升(就是函数式声明的方式可以再其在执行任何代码之前可以访问它),我们在举两个例子来说明一下函数冲突的解决方式

    function foo(x,y,z){        function x(){};        alert(x)    }    foo(100);// alert funxtion x(){},发生冲突时,变量x被声明为一个函数引用覆盖掉了传入的参数声明

      再举个例子说明一下第三点

    function foo(x,y,z){        function func(){};        var func;        alert(func)    }    foo(100);// alert funxtion x(){},当变量声明冲突时,会自动忽略掉,保持原有的声明状态


      二、代码执行阶段
   AO(test)  =  {     a:  10,     b:  20,     c:  10,     d:  <reference  to  FunctionDeclaration  "d">     e:  function  _e()  {};    };
     举个例子说明一下现在的状况
   function foo(x,y,z){        function func(){};        var func = 1;        alert(func)   }   foo(100);//1,因为的func已经被赋值了x,已经不是初始化的默认值了
     案例三,也是一道经典的JavaScript面试题,这道题搞对了,你就通关了

    alert(x);    //  function                    var  x  =  10;     alert(x);   //  10                     x  =  20;     function  x()  {}     alert(x);    //  20     if  (true)  {       var  a  =  1;     }  else  {       var  b  =  true;     }     alert(a);    //  1      alert(b);    //  undefined




我们把函数从头到尾摸了一遍,有收获到东西吗?白了个白~

0 0
原创粉丝点击