您的位置:首页 > Web前端 > JavaScript

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

2015-10-18 20:41 483 查看

函数声明和函数表达式

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) //undefined
var 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();//1
obj.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


我们把函数从头到尾摸了一遍,有收获到东西吗?白了个白~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: