深入理解javascript函数定义与函数作用域

看了人家的blog,发现自己还是没有理解好js函数这事,再来学一次

1.函数的定义

  1.1:函数声明

  1.2:函数表达式

  1.3:命名函数的函数表达式

  1.4:函数的重复声明

2.函数的部分属性和方法

  2.1:name属性

  2.2:length属性

  2.3:toString()方法

3.函数作用域

  3.1:全局作用域和局部作用域

  3.2:函数内部的变量提升

  3.3:函数自身的作用域
  
4.函数参数

4.1:参数是什么

  4.2:参数的省略

  4.3:参数默认值

  4.4:参数传递方式

  4.5:同名参数

  4.6:arguments对象
  
1.函数的定义

  1.1:函数声明

  函数就是一段可以反复调用的代码块。
  函数声明由三部分组成:函数名,函数参数,函数体。整体的构造是function命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。
  当函数体没有使用return关键字返回函数时,函数调用时返回默认的undefined;如果有使用return语句,则返回指定内容。函数最后不用加上冒号。

    function keith() {}
    console.log(keith())   // 'undefined'

    function rascal(){
        return 'rascal';
    }
    console.log(rascal())    // 'rascal'

函数声明是在预执行期执行的,也就是说函数声明是在浏览器准备解析并执行脚本代码的时候执行的。所以,当去调用一个函数声明时,可以在其前面调用并且不会报错。
 其实这段代码没有报错的原因还有一个,就是与变量声明提升一样,函数名也会发生提升。

 console.log(rascal())   // 'rascal'
2     function rascal(){
3         return 'rascal';
4     }

 1.2:匿名函数表达式
 函数表达式是把一个匿名函数赋给一个全局变量。
 这个函数又称为函数表达式,因为赋值语句的等号右侧只能放表达式。函数表达式末尾需要加上分号,表示语句结束。

 var keith = function() {
2         //函数体
3     };

函数表达式与函数声明不同的是,函数表达式是浏览器解析并执行到那一行才会有定义。也就是说,不能在函数定义之前调用函数。函数表达式并不像函数声明一样有函数名的提升。如果采用赋值语句定义函数并且在声明函数前调用函数,JavaScript就会报错。

 keith();
 var keith = function() {};
// TypeError: keith is not a function

上面的代码等同于下面的形式。

var keith;
console.log(keith());   // TypeError: keith is not a function
keith = function() {};

 上面代码第二行,调用keith的时候,keith只是被声明了,还没有被赋值,等于undefined,所以会报错。
 
1.3:命名函数的函数表达式

var keith = function boy(){
      console.log(typeof boy);
    };

    console.log(boy);
    // ReferenceError: boy is not defined

    keith();
    // function

在函数表达式中,加入了函数名boy。这个boy只在函数体内部可用,指代函数表达式本身,其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。

1.4:函数的重复声明

  如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。  

function keith() {
        console.log(1);
    }
    keith(); //2
    function keith() {
        console.log(2);
    }
    keith(); //2

 上面代码中,后一次的函数声明复盖了前面一次。而且,由于函数名的提升,前一次声明在任何时候都是无效的。JavaScript引擎将函数名视同变量名,所以采用函数声明的方式声明函数时,整个函数会像变量声明一样,被提升到代码头部。表面上,上面代码好像在声明之前就调用了函数keith。但是实际上,由于“变量提升”,函数keith被提升到了代码头部,也就是在调用之前已经声明了。

2.函数的部分属性和方法

2.1:name属性

  name属性返回紧跟在function关键字之后的那个函数名。
  

function k1() {};
    console.log(k1.name); //'k1'

    var k2 = function() {};
    console.log(k2.name); //''

    var k3 = function hello() {};
    console.log(k3.name); //'hello'

name属性返回function 后面紧跟着的函数名。
对于k2来说,返回一个空字符串,注意:匿名函数的name属性总是为空字符串。
对于k3来说,返回函数表达式的名字(真正的函数名为k3,hello这个函数名只能在函数内部使用。)

2.2:length属性

  length属性返回函数预期传入的参数个数,即函数定义之中的参数个数。返回的是个数,而不是具体参数。  

 function keith(a, b, c, d, e) {}
  console.log(keith.length)    // 5

上面代码定义了空函数keith,它的length属性就是定义时的参数个数。不管调用时输入了多少个参数,length属性始终等于5。也就是说,当调用时给实参传递了6个参数,length属性会忽略掉一个。

2.3:toString()方法
  函数的toString方法返回函数的代码本身。

function keith(a, b, c, d, e) {
2         // 这是注释。
3     }
4     console.log(keith.toString());
5     //function keith(a, b, c, d, e) { // 这是注释。 }

可以看到,函数内部的注释段也被返回了。

3.函数作用域

 3.1:全局作用域和局部作用域

  作用域(scope)指的是变量存在的范围。
  Javascript只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取,在全局作用域中声明的变量称为全局变量;
  另一种是局部作用域,变量只在函数内部存在,此时的变量被称为局部变量。

var a=1;
2     function keith(){
3         return a;
4     }
5     console.log(keith())    //1

 上面代码中,全局作用域下的函数keith可以在内部读取全局变量a。

  在函数内部定义的变量,只能在内部访问,外部无法读取,称为局部变量。注意这里必须是在函数内部声明的变量

 function keith(){
       var a=1;
       return a;
   }
  console.log(a)    //Uncaught ReferenceError: a is not defined

在上面代码中,变量a在函数内部定义,所以是一个局部变量,外部无法访问。

  函数内部定义的变量,会在该作用域下覆盖同名变量。注意以下两个代码段的区别。

var a = 2;

    function keith() {
        var a = 1;
        console.log(a);
    }
    keith(); //1

    var c = 2;

    function rascal() {
        var c = 1;
        return c;
    }
    console.log(c); //2
   console.log(rascal());  //1

上面代码中,变量a和c同时在函数的外部和内部有定义。结果,在函数内部定义,局部变量a覆盖了全局变量a。

  注意,对于var命令来说,局部变量只能在函数内部声明。在其他区块声明,一律都是全局变量。比如说if语句。

if (true) {
2         var keith=1;
3     }
4     console.log(keith);    //1

从上面代码中可以看出,变量keith在条件判断区块之中声明,结果就是一个全局变量,可以在区块之外读取。但是这里如果采用ES6中let关键字,在全局作用域下是无法访问keith变量的。

3.2:函数内部的变量声明提升
与全局作用域下的变量声明提升相同,局部作用域下的局部变量在函数内部也会发生变量声明提升。var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。
以下两个函数是相同的

function keith(a) {
        if (a > 10) {
            var b = a - 10;
        }
    }

    function keith(a) {
        var b;
        if (a > 10) {
            b = a - 10;
        }
    }

3.3:函数本身的作用域

  函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。

var a = 1;
    var b = function() {
        console.log(a);
    };
    function c() {
        var a = 2;
        b();
    }
    c(); //1

    var a = 1;
    var b = function() {
        return a;
    };
    function c() {
        var a = 2;
        return b();
    }
    console.log(c()); //1

以上两个代码段相同。函数b是在函数c外部声明的。所以它的作用域绑定在函数外层,内部函数a不会到函数c体内取值,所以返回的是1,而不是2。

  很容易犯错的一点是,如果函数A调用函数B,却没考虑到函数B不会引用函数A的内部变量。
  

var b = function() {
        console.log(a);
    };
    function c(f) {
        var a = 1;
        f();
    }
    c(b); //Uncaught ReferenceError: a is not defined


    var b = function() {
        return a;
    };
    function c(f) {
        var a = 1;
        return f();
    }
    console.log(c(b)); //Uncaught ReferenceError: a is not defined

  上面代码将函数b作为参数,传入函数c。但是,函数b是在函数c体外声明的,作用域绑定外层,因此找不到函数c的内部变量a,导致报错。

  同样的,函数体内部声明的变量,作用域绑定在函数体内部。 

function keith() {
        var a = 1;

        function rascal() {
            console.log(a);
        }
        return rascal;
    }

    var a = 2;
    var f = keith();
    f(); //1

上面代码中,函数keith内部声明了rascal变量。rascal作用域绑定在keith上。当我们在keith外部取出rascal执行时,变量a指向的是keith内部的a,而不是keith外部的a。这里涉及到函数另外一个重要的知识点,即在一个函数内部定义另外一个函数,也就是闭包的概念。

1.函数参数

  1.1:参数是什么

  在定义一个函数时,有时候需要为函数传递额外的数据,不同的外部数据会得到不同的结果,这种外部数据就叫做参数。
  

function keith(a){
2         return a+a;
3     }
4     console.log(keith(3));  //6

上面代码中,给keith函数传递了参数a,并且返回了a+a表达式。

 1.2:参数的省略

  函数参数不是必须的,javascript规范允许省略调用时传递的实际参数。
  

function keith(a, b, c) {
        return a;
    }
    console.log(keith(1, 2, 3)); //1
    console.log(keith(1)); //1
    console.log(keith()); // 'undefined'

上面代码中,keith函数定义了三个参数,但是在调用时无论传递了多少个参数,javascript都不会报错。被省略的参数的默认值就变为undefined。
函数的length属性会返回参数个数。需要注意的是,length属性与实际参数的个数无关,只是返回形式参数的个数。

  (实际参数:调用时传递的参数。 形式参数:定义时传递的参数。)

  但是没有办法省略只靠前的元素,而保留靠后的元素。如果一定要省略靠前的元素,只有显式传入undefined。 

function keith(a, b) {
         return a;
     }
     console.log(keith(, 1)); //SyntaxError: expected expression, got ','
     console.log(keith(undefined, 2)); //'undefined'

上面代码中,如果省略了第一个参数,浏览器就会报错。如果给第一个参数传递undefined,则不会报错。

1.3:默认值

  在JavaScript中,函数参数的默认值是undefined。然而,在某些情况下设置不同的默认值是有用的。一般策略是在函数的主体测试参数值是否为undefined,如果是则赋予一个值,如果不是,则返回实际参数传递的值。

 function keith(a, b) {
        (typeof b !== 'undefined') ? b = b: b = 1;
        return a * b;
    }
    console.log(keith(15)); //15
    console.log(keith(15, 2)) //30

上面代码中,做了个判断。当在调用时没有传入b参数,则默认为1。

  从ECMAScript 6开始,定义了默认参数(default parameters)。使用默认参数,在函数体的检查就不再需要了。

  function keith(a, b = 1) {
        return a * b;
    }
    console.log(keith(15)); //15
    console.log(keith(15, 2)) //30

 1.4:参数传递方式

  函数参数的传递方式有两种,一个是传值传递,一个是传址传递。

  当函数参数是原始数据类型时(字符串,数值,布尔值),参数的传递方式为传值传递。也就是说,在函数体内修改参数值,不会影响到函数外部。
  

    var a = 1;

    function keith(num) {
        num = 5;
    }
    keith(a);
    console.log(a); //1

上面代码中,全局变量a是一个原始类型的值,传入函数keith的方式是传值传递。因此,在函数内部,a的值是原始值的拷贝,无论怎么修改,都不会影响到原始值。

  但是,如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的是原始值的地址,因此在函数内部修改参数,将会影响到原始值。

var arr = [2, 5];

    function keith(Arr) {
        Arr[0] = 3;
    }
    keith(arr);
    console.log(arr[0]); //3

上面代码中,传入函数keith的是参数对象arr的地址。因此,在函数内部修改arr第一个值,会影响到原始值。

  注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。
  

var arr = [2, 3, 5];

    function keith(Arr) {
        Arr = [1, 2, 3];
    }
    keith(arr);
    console.log(arr); // [2,3,5]

上面代码中,在函数keith内部,参数对象arr被整个替换成另一个值。这时不会影响到原始值。这是因为,形式参数(Arr)与实际参数arr存在一个赋值关系。

  1.5:同名参数

  如果有同名参数,则取最后面出现的那个值,如果未提供最后一个参数的值,则取值变成undefined。

function keith(a, a) {
        return a;
    }

    console.log(keith(1, 3)); //3
    console.log(keith(1)); //undefined

 如果想访问同名参数中的第一个参数,则使用arguments对象。
 

function keith(a, a) {
        return arguments[0];
     }

     console.log(keith(2));  //2

1.6 arguments对象

  JavaScript 中每个函数内都能访问一个特别变量 arguments。这个变量维护着所有传递到这个函数中的参数列表。

  arguments 对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。

  可以访问arguments对象的length属性,判断函数调用时到底带几个参数。
  

function keith(a, b, c) {
        console.log(arguments[0]); //1
        console.log(arguments[2]); //3
        console.log(arguments.length); //4
    }

    keith(1, 2, 3, 4);

arguments对象与数组的关系

  arguments 对象不是一个数组(Array)。 尽管在语法上它有数组相关的属性 length,但它不从 Array.prototype 继承,实际上它是一个类数组对象。因此,无法对 arguments 变量使用标准的数组方法,比如 push, pop 或者 slice。但是可以使用数组中的length属性。

  通常使用如下方法把arguments对象转换为数组。

 var arr = Array.prototype.slice.call(arguments);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章