zoukankan      html  css  js  c++  java
  • (Frontend Newbie)JavaScript基础之函数

    函数可以说是任何一门编程语言的核心概念。要能熟练掌握JavaScript,对于函数及其相关概念的学习是非常重要的一步。本篇从函数的基本知识、执行环境与作用域、闭包、this关键字等方面简单介绍JavaScript中的函数的使用。

    基础

    我们通常通过如下两种方式定义函数:

    function myFunc() {
        console.log("this is myFunc");    
        return;
    }
    
    var myFunc = function () {
    }
    

    与其他面相对象语言不同的是,JavaScript的函数没有规定返回值,实际上,我们可以在函数中返回任何值,甚至没有返回(没有显式return语句的函数返回undefined)。

    arguments

    在函数中,我们经常接触arguments对象,故名思议,它表示函数的参数。实际上,arguments对象是一个类数组对象,JavaScript通过它保存函数的所有参数。这也是JavaScript函数不在乎传进来多少个参数,也不在乎传进来的参数是什么类型的原因。
    看如下一个例子:

    function testArgs(arg1, arg2) {
        console.log(arguments.length);
    }
    
    testArgs(1, 2);  // 2
    testArgs(1);  //1
    testArgs(1, 2, 3); //3
    

    有人会问,使用arguments对象和直接使用函数声明的参数有什么区别。其实,本质上没有什么区别,函数声明的参数在函数的内部作用域中只是一个局部变量而已,它保存调用函数时传递的参数的值。

    注意
    JavaScript中函数的传参都是按值传递,引用类型的变量也是按值传递。

    JavaScript中的函数没有重载,但是通过arguments对象,我们可以简单实现JavaScript函数的重载功能。

    function doAdd() { 
        if(arguments.length == 1) {
            alert(arguments[0] + 10); 
        } else if (arguments.length == 2) { 
            alert(arguments[0] + arguments[1]); 
        } 
    }
    doAdd(10);         //20 
    doAdd(30, 20);     //50 
    

    在调用doAdd函数时,如果只传递一个参数,则将该数加10后返回结果,如果传递了两个参数,则将这两个参数相加返回结果。

    函数是对象

    在上一篇中,我们介绍了常用的JavaScript的数据类型,还有一种类型没有说,就是Function类型。之所以说Function类型是一种数据类型,是因为在JavaScript中,函数也是对象,是一等公民。由于函数类型在堆内存中进行实例化,函数名只是指向这个函数对象位置的指针而已,不会与某个具体的函数绑定。
    以下是一种显式的调用Function构造函数的方式定义函数的例子:

    var sum = new Function("num1", "num2", "return num1 + num2");
    

    在这个例子中,sum就是新定义的函数的名字,它与一般的变量没有实质的区别,它只保存新定义的函数的地址而已。从这个角度来理解为什么JavaScript函数没有重载就好理解多了。

    既然函数是对象,函数名只是一个普通变量而已,那么我们就可以像使用普通变量一样使用函数。我们可以将函数作为参数传递给另一个函数,也可以将函数作为另一个函数的返回值返回。甚至我们可以给函数添加属性,当然不推荐这样做。

    doAdd.add = function (a, b) {
        return a + b;
    }
    

    关于函数,还有一点需要特别注意的是,函数声明与函数表达式的区别。
    什么是函数声明呢?开篇的两种定义函数的第一种就是函数声明的方式。第二种就是函数表达式的方式。
    这两种方式都定义了一个函数,具体有什么区别呢?JavaScript解析器存在一个叫做函数声明提升(function declaration hoisting)的过程,在代码开始执行之前,解析器通过函数声明提升读取并将函数声明添加到执行环境中,对代码求值时,JavaScript引擎在第一遍会声明函数并将放到源代码树的顶部。所以即使声明函数的代码在调用它的代码后面,JavaScript引擎也能把函数声明提升到顶部。

    执行环境与作用域

    执行环境(execution context)是JavaScript中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用到它。
    每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。

    JavaScript中的函数时通过词法来划分作用域的,而不是动态的划分作用域的。这就意味着它们在定义它们的作用域里运行,而不是在执行它们的作用域里运行。当定义了一个函数,当前的作用域链就保存起来,并且成为函数的内部状态的一部分。
    当调用一个函数时,JavaScript解析器首先将作用域设置为定义函数的时候起作用的那个作用域链,接下来,它在作用域链的前端添加一个新的对象,叫做激活对象(activation object)。激活对象用一个名为arguments的属性来初始化,这个属性引用了函数的Arguments对象。函数的命名参数添加到激活对象的后面,用var语句声明的任何变量也都定义在这个对象中。因此,局部变量,函数的命名参数和Arguments对象都在函数内的作用域中。作用域链的用途是保证执行环境有权访问的变量和函数的有序访问。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直持续到全局执行环境,全局执行环境的变量对象始终是作用域链的最后一个变量对象。
    标识符解析是沿着作用域链一级一级的搜索标识符的过程,搜索过程始终从作用域的前端开始,然后逐级的向后回溯。
    注意,尽管当一个函数定义了的时候,作用域链就固定了,但作用域中定义的属性还没有固定。某种程度上说作用域链是活的,函数在调用的时候,可以访问任何当前绑定的作用域,并修改其中的属性。
    下面的例子形象的展示的作用域链的工作机制:

    function compare(value1, value2){ 
       if (value1 < value2){ 
          return -1;
       } else if (value1 > value2){ 
          return 1; 
       } else { 
          return 0; 
       } 
    } 
    
    var result = compare(5, 10); 
    

    scope-chain
    从图中可以清晰看出,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
    无论什么时候在函数中访问一个变量是,就会从作用域链中搜索具有相应名字的变量。一般来说,在函数执行完毕后,局部激活对象就会被销毁,内存中仅保留全局作用域。但是,在有闭包存在的情况下,情况又有所不同。

    闭包

    闭包是指有权访问另一个函数作用域中的变量的函数。广义上说,任何函数都是闭包,是将要执行的代码代码和执行这些代码的作用域构成的一个综合体。
    上面的作用域的例子中的compare函数实际上就是一个闭包,在compare函数内部,可以访问到全局对象(window)的属性。
    再看一个闭包的例子:

    function createComparisonFunction(propertyName) { 
    
        return function(object1, object2){ 
           var value1 = object1[propertyName]; 
           var value2 = object2[propertyName]; 
    
           if (value1 < value2){ 
               return -1; 
           } else if (value1 > value2){ 
               return 1; 
           } else { 
               return 0; 
           } 
        }; 
    } 
    
    //创建函数 
    var compareNames = createComparisonFunction("name"); 
    
    //调用函数 
    var result = compareNames({ name: "Nicholas" }, { name: "Greg" }); 
    
    //解除对匿名函数的引用(以便释放内存) 
    compareNames = null; 
    

    closure

    使用闭包的注意事项

    闭包虽然可以通过作用域链的方式访问其他函数作用域中的变量,但是它只能取得包含函数中任何一个变量的最后一个值。

    function createFunctions(){ 
        var result = new Array(); 
        
        for (var i=0; i < 10; i++){ 
            result[i] = function(){ 
                return i; 
            };
        } 
    
        return result; 
    } 
    

    上面函数执行的结果result保存了十个函数,但每个函数的返回值都是10(i的最后一个值)。要解决这个问题,我们可以通过如下的方法:

    function createFunctions(){ 
        var result = new Array();
    
        for (var i=0; i < 10; i++){ 
            result[i] = function(num){ 
                return function(){ 
                   return num;
                }; 
            }(i); 
        } 
    
        return result; 
    } 
    

    原理相信大家也都明白了。

    在使用闭包的时候,还有一点需要注意,就是当涉及到一些dom操作时,要小心使用闭包,操作不当将导致内存泄露。

    function assignHandler(){ 
        var element = document.getElementById("someElement"); 
    
        element.onclick = function(){ 
           console.log(element.id); 
        }; 
    }
    

    以上代码创建了一个作为 element 元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对 assignHandler()的活动对象的引用,因此就会导致无法减少 element 的引用数。只要匿名函数存在,element 的引用数至少也是 1 ,因此它所占用的内存就永远不会被回收。不过,这个问题可以通过稍微改写一下代码来解决,如下所示。

    function assignHandler(){ 
        var element = document.getElementById("someElement"); 
        var id = element.id; 
    
        element.onclick = function(){ 
           alert(id); 
        }; 
    
        element = null; 
    } 
    

    在上面的代码中,通过把 element.id 的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。但仅仅做到这一步,还是不能解决内存泄漏的问题。必须要记住:闭包会引用包含函数的整个活动对象,而其中包含着 element。即使闭包不直接引用 element,包含函数的活动对象中也仍然会保存一个引用。因此,有必要把 element 变量设置为 null。这样就能够解除对 DOM 对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。

    this关键字

    在使用函数的过程中,我们经常碰到this这个对象,在没有搞明白this原理之前,我们经常对this究竟代表什么对象感到疑惑。

    function testThis() {
        console.log(this.name);
    }
    var name = "window";
    
    var obj = {
        name: "object",
        func: testThis
    };
    
    testThis();  // => window
    obj.func(); // => object
    

    在上面这个例子中,由于函数名testThis只是一个指针,所以testThis和obj.func实际上指向同一个函数对象。为什么执行结果不一样呢?
    其实要理解this关键字,主要记住一句话就可以了,this永远指向函数的调用者。如果函数在全局执行环境中被调用,那么this指向全局对象(window)。因此,this的取值是在运行时决定的,这点与作用域链不同。
    在理解this关键字时,不要与作用域链混淆到一起,this是一个关键字,它指向函数的调用者,不在函数的激活对象中。这一点可以与arguments对比来看。
    arguments对象有一个属性,arguments.callee,指向被调用的函数本身。但是,arguments是函数的活动对象的一部分。

    apply() 和 call()

    说到this关键字,就不得不说说apply()和call()了。
    这两个函数都是函数的内部属性,都用于改变函数的调用者,即改变this的指向。

    function sum(num1, num2){ 
        return num1 + num2; 
    } 
    
    function callSum1(num1, num2){ 
        return sum.apply(this, arguments);        // 传入arguments 对象 
    } 
    
    function callSum2(num1, num2){ 
        return sum.apply(this, [num1, num2]);    // 传入数组 
    } 
    
    alert(callSum1(10,10));   //20 
    alert(callSum2(10,10));   //20 
    
    
    function callSum(num1, num2){ 
        return sum.call(this, num1, num2); 
    } 
    
    alert(callSum(10,10)); //20 
    

    apply()和call()函数的功能相同,唯一的区别是传递参数的方式不同。apply()第一个参数是this的值,第二个参数是参数数组。call()函数的第一个参数也是this的值,但是传递给函数的参数都要直接放在call()的参数列表中。

    小结

    本篇主要介绍了JavaScript函数的各方面的基础知识,其中核心就是函数的执行环境与作用域链。在此基础上,介绍了闭包的概念、使用方法,以及常见的问题。最后简单说明了函数中this的使用,以及如何改变函数的this值。
    其实了解原理只是第一步,关键是在开发过程中不断的运用,时刻有这样的意识,用的多了,就理解吸收了。

  • 相关阅读:
    vue实现图片路径传送
    title中添加小图标
    张钊的第一份作业
    张钊的第二份作业
    在Windows Server 2008 R2环境下安装活动目录失败的一个解决方法
    如何把SubVersion的服务程序变为Window后台服务形式
    一个关于静态方法调用的问题。
    WCF配置中遇到的问题:如何把Hostname修改成IP
    删除Visual Studio最近的项目(转载)
    有时候用ifstream或ofstream打开带有中文路径的文件会失败
  • 原文地址:https://www.cnblogs.com/bingooo/p/5096674.html
Copyright © 2011-2022 走看看