第11章 使用函数
函数是这样的一段JavaScript代码,它只定义一次,但可能被执行或调用任意次。在JavaScript中,函数不仅可以表示一段代码,它还扮演着多重角色,占据重要位置:在网页设计中,函数是JavaScript框架的基础;函数也是表达式运算中的运算数或子表达式;同时函数也是对象,是面向对象编程的构造器。灵活使用函数,能够提升JavaScript程序性能,帮助用户设计各种灵活的代码。本章主要讲解函数的基本概念和基本用法,在第12章中将会讲解有关函数式编程的主题。
【学习重点】
▲ 理解JavaScript函数
▲ 定义函数
▲ 使用函数
▲ 灵活使用函数参数
▲ 掌握函数对象的使用
▲ 精通函数内部解析机制和this关键字
11.1 认识函数
函数(function)源于数学映射运算,如图11-1所示,它定义了一种关系,这种关系使一个集合里的每一个元素对应到另一个(可能相同的)集合里的唯一元素。
图11-1 函数的盒模型
在JavaScript中,函数是第一型数据,而对象是第二型数据,这说明函数在JavaScript程序设计中扮演着重要角色。
11.1.1 函数是代码块
从程序代码的角度来分析,函数实际上就是代码块,一段被封闭严实的代码块。
一个较大的程序一般会分为若干个程序模块,每一个模块都会实现特定的功能。所有的高级语言中都有子程序这个概念,用子程序来实现模块的功能。在JavaScript语言中,子程序的作用是由函数来实现的。一个JavaScript程序可以由若干个函数构成,函数之间可以相互引用,即使是同一个函数也可以被自己或其他多个函数重复调用,从而构成了一个复杂的函数调用逻辑网络。
在程序设计中,可以把一些常用的功能模块编写成函数,构成一个函数库。善于利用函数,可以减少代码编写的工作量。
11.1.2 函数是数据
在JavaScript中,函数并不只是一种语法,它还是一类数据,用户可以把函数作为值赋值给变量,或者把函数存储在对象属性或数组元素中。
从数据类型的角度来分析,函数就是一种数据类型,它就是具有复杂结构的引用型数据。函数不仅仅是功能模块用来避免重复开发,更重要的是它是一个数据容器。
【示例1】在下面代码中,定义了一个函数对象,并把这个函数对象赋值给变量f。实际上,函数名仅是一个变量,存储着指向函数结构的地址,因此可以把这个函数赋值给其他变量,函数依然是函数,而变量无非就是一个标识符。
【示例2】示例1中的函数仅是一个普通函数,如果把函数赋值给对象的属性,那么函数就被称为方法,当然不管是函数,还是方法,它的数据结构依然是相同的,本质上没有发生变化。
函数可以没有函数名,此时它是一个匿名函数,用户可把匿名函数赋值给一个变量、数组元素或者对象属性等。
使用小括号运算符调用函数,将返回函数运算的返回值。因此可以把函数看作是一种结构复杂的表达式,并可以把它作为运算单元放在其他表达式中,设计复杂表达式。
11.1.3 函数是对象
从面向对象的角度来分析,函数是一种对象,它是一类抽象类(构造函数),所有对象都通过类型构造而来,因此它构成了JavaScript对象的原型。
函数是进行模块化程序设计的基础,编写复杂的JavaScript应用程序,必须对函数有着更深入的了解。JavaScript函数不同于其他语言,每个函数都是作为一个对象被维护和运行的。
JavaScript预定义了很多类型对象,如日期对象(Date)、数组对象(Array)、字符串对象(String)等。这些内置对象作为构造函数是由JavaScript本身定义的,通过new运算符调用这些构造函数,可以返回一个实例对象。
【示例】在JavaScript中,函数对象对应的类型是Function,正如数组对象对应的类型是Array,日期对象对应的类型是Date一样,可以通过new Function()来创建一个函数对象。
11.2 定义函数
定义函数的方法主要包括3种:function语句、Function()构造函数和函数直接量。不管使用哪种方法定义函数,它们都是Function对象的实例,并将继承Function对象的方法和属性。
在前面章节中曾经介绍过使用function语句声明函数的方法,下面介绍构造函数和函数直接量的定义方法。
11.2.1 构造函数
在JavaScript中,构造函数就是一种类型结构,即一种数据类型。构造函数与普通函数没有什么区别,不管是函数结构,还是用法都基本相同,但是构造函数没有返回值,通过new运算符调用构造函数后,它会返回一个实例对象,这个调用过程被称为类型实例化。
在构造函数体内,可以使用this关键字指代实例化后的对象,虽然在定义构造函数时,无法确定实例对象具体是谁。
【示例1】用户可以自定义构造函数。下面代码定义一个Me()构造函数。
提示:根据习惯,一般构造函数的名称首字母要大写,以区别普通函数。
自定义构造函数后,可以使用如下方法实例化对象。
【示例2】在构造函数体内如果使用return语句返回一个对象,则返回的对象将覆盖new运算符生成的实例对象,此时this引用对象将失效。
上面示例演示了如何使用返回的对象覆盖构造函数的实例对象。
此时可以使用小括号直接调用构造函数,而不需要使用new运算符调用。针对上面示例可以直接调用构造函数来引用返回对象。
【示例3】当构造函数返回值是原始值时,使用new运算符实例化时将被忽略,但是如果使用小括号运算符以普通函数的方式调用构造函数,可以获得返回值。
JavaScript预定义了很多构造函数,如Function()、Array()、Date()、string()等,如果去掉小括号,它们实际上就是JavaScript内置对象。
使用Function()构造函数可以快速生成普通函数。具体用法如下:
var f=new Function(p1, p2, ..., pn, body);
构造函数Function()的参数类型都是字符串,p1~pn表示所创建函数的参数名称列表,body表示所创建函数的函数结构体语句,在body语句之间通过分号进行分隔。
【示例4】用户可以省略所有参数,仅为构造函数传递一个字符串,用来表示函数体。
var f=new Function("a","b","return a+b"); //通过构造函数来克隆函数结构
在上面代码中,f就是所创建函数的名称。同样是定义函数,使用function语句可以设计相同结构的函数:
【示例5】使用Function()构造函数可以不指定任何参数,创建一个空函数结构体。
var f=new Function(); //定义空函数
【示例6】在Function()构造函数参数中,p1~pn是参数名称的列表,即p1不仅能代表一个参数,它可以是一个逗号隔开的参数列表。下面的定义方法是等价的:
var f=new Function("a","b","c","return a+b+c")
var f=new Function("a, b, c","return a+b+c")
var f=new Function("a,b","c","return a+b+c")
使用Function()构造函数不是很常用,因为一个函数体通常会包含很多代码,如果将这些代码以一行字符串的形式进行传递,代码的可读性会很差。
当然,使用Function()构造函数可以动态创建函数,它不会把用户限制在function语句预声明的函数体中。使用Function()构造函数,能够把函数当作表达式来使用,而不是当作一个结构,因此使用起来会更灵活。其缺点就是,Function()构造函数在执行期被编译,执行效率非常低。
11.2.2 函数直接量
函数直接量就是结构固定的函数体。
【示例1】下面的代码定义了一个函数直接量。
在上面代码中,函数直接量与使用function语句定义函数结构基本相同,它们的结构都是固定的。但是函数直接量没有指定函数名,而是直接利用关键字funciton来表示函数的结构,这种函数也被称为匿名函数。
【示例2】匿名函数就是一个表达式,即函数表达式,而不是函数结构的语句。在本示例中把匿名函数作为一个值赋值给变量f。
当把函数结构作为一个值赋值给变量之后,变量就可以作为函数被调用,此时变量就指向那个匿名函数。
alert(f(1,2)); //返回数值3
【示例3】匿名函数作为值,可以参与更复杂的表达式运算。针对上面示例可以使用如下代码完成函数定义和调用一体化操作。
11.2.3 定义嵌套函数
函数可以相互嵌套,因此用户可以定义复杂的嵌套结构函数。
【示例】使用function语句声明两个相互嵌套的函数体结构。
嵌套的函数只能够在函数体内部可见,函数外不允许调用。
11.3 案例:优化函数定义
使用function语句、Function()构造函数和函数直接量都可以定义函数,但是3种方法存在很多差异,详细比较如表11-1所示。
表11-1 函数定义方法比较
11.3.1 函数作用域
使用Function()构造函数创建的函数具有顶级作用域,而function语句和函数直接量定义的函数都有局部作用域(函数作用域)。
【示例1】为了理解顶级作用域和局部作用域异同,下面看一个示例。
在上面示例中,分别在函数体外和函数体内声明并初始化变量n,然后在函数体内使用function语句定义一个函数e,定义该函数返回变量n的值。最后在函数体外调用函数的返回函数。结果发现返回值为局部变量n的值为2,也就是说function语句定义的函数拥有局部作用域。
【示例2】如果使用函数直接量定义函数e,当调用该返回函数时,返回的值是2,而不是1,也说明函数直接量定义的函数拥有局部作用域。
【示例3】如果使用Function构造函数定义函数e,则调用该返回函数时,返回的值是1,而不是2,因为Function构造函数定义的函数作用域需要动态确定,而不是在定义函数时确定的,所以具有全局作用域。
11.3.2 解析机制
使用function语句和函数直接量定义的函数一般是先解析后执行,而使用Function构造函数定义的函数不是提前解析,当在运行时动态解析和执行。因此,从时间角度分析,function语句和函数直接量定义的函数具有静态特性,而Function构造函数定义的函数具有动态特性。这种解析机制的不同,必然带来不同的执行效率。
【示例】在本示例中,分别把function语句定义的空函数和Function构造函数定义的空函数放在一个循环体内,让它们空转十万次,则比较发现使用function语句定义的空函数运行效率高。
在执行循环结构之前,JavaScript解释器首先把function语句定义的函数提取出来进行编译,这样每次循环执行该函数时,就不再重新编译该函数,而Function构造函数定义的函数每次循环时都需要动态编译一次,这样效率就非常低。
11.3.3 兼容性和灵活性
从兼容角度考虑,使用function语句定义函数不用考虑JavaScript版本问题,所有版本都支持这种方法。而Function构造函数只能够在JavaScript 1.1及其以上版本中使用,函数直接量仅能够在JavaScript 1.2及其以上版本中有效。当然,在目前大部分用户都已经使用了支持JavaScript 1.5版本的浏览器,版本问题已经不是大问题了。
Function构造函数和函数直接量定义函数方法有点相似,它们都是使用表达式来创建,而不是语句创建。这也给它们带来很大的灵活性。当函数仅需要调用一次时,非常适合使用函数直接量的方式创建匿名函数。
由于Function构造函数和函数直接量定义函数不需要额外的变量,它们直接参与表达式运算,从而节省了资源,避免了使用function语句定义函数占用系统资源的弊端。
对于Function构造函数来说,由于定义函数的主体必须以字符串的形式来表示,使用这种方法定义复杂的函数就比较笨拙,出现语法错误也不易发现。
11.4 使用函数
函数结构比较复杂,它提供了两个接口,实现与外界的交互,其中使用参数定义入口,接收外界信息,然后使用返回值,作为出口,与外界实现互动。本节讲解如何使用函数的参数、返回值,介绍函数作用域,以及函数调用方法等相关函数使用的技巧。
11.4.1 函数返回值
在函数体内,使用return语句可以设置函数的返回值,一旦执行return语句,它将停止函数的运行,并把return关键字后面的表达式的运算值返回。如果函数不包含return语句,则执行完函数体内每条语句后,最后返回undefined值。
JavaScript是一种弱类型语言,所以函数对于接收和输出数据都没有类型限制,JavaScript也不会自动检测输入和输出数据的类型。
【示例1】下面代码定义函数的返回值为函数。
【示例2】函数的参数没有限制,但是返回值只能是一个,如果要输出多个值,可以通过数组或对象进行设计。
在上面代码中,函数返回值为数组,该数组包含两个元素,从而实现一个return语句,返回两个值的目的。
【示例3】在函数体内可以包含多个return语句,但是仅能执行一个return语句,因此在函数体内可以使用分支结构或条件结构决定函数返回值。
11.4.2 调用函数
定义的函数在默认状态下是不会被执行的,一般使用小括号运算符(())来**函数运行,在小括号运算符中可以包含零个或多个参数,参数之间通过逗号进行分隔。
【示例1】在本示例中,通过在函数中调用函数的方法实现多重调用,也就是把函数调用作为一个表达式的值直接作为参数进行传递,这样节省了两个临时变量。
如果按一般过程化设计,则上面代码可以转换为:
【示例2】如果函数返回值为一个数,则在调用时可以使用多个小括号运算符反复调用。
【示例3】在下面代码中,定义函数的返回值为函数自身,设计一种递归返回函数自身的操作,这样就可以通过无数个小括号运算符反复调用,但是最终返回值都是函数结构体自身。
当然,上述设计方法在实际开发中没有任何应用价值,不建议采用。
【示例4】在嵌套函数中,JavaScript遵循从内到外的原则就近调用函数,但是不会从外到内调用函数。这样就避免了嵌套函数中调用同名函数可能引发的冲突。
在上面示例中,在全局作用域内调用函数f(),则将调用最顶级函数f(),同样在全局作用域内调用函数o(),将调用最顶级函数o()。当调用顶级函数o()时,**内部脚本并返回调用内部函数o(),继续**并调用最里层的函数f()。如果没有最里层的函数f(),则将向上搜索函数f(),并将调用嵌套函数f(),返回数值2。如果还没有检索到函数f(),则将调用顶层函数f(),最后返回数值为1。
11.4.3 函数生命周期
JavaScript解释器在解析程序时,会以块为单位逐行解析,这个块表示的就是以<script>标签包含的脚本。在程序段内,解释器会先预编译声明的变量和函数结构,然后再逐行执行代码。但是对于匿名函数,则在代码执行期进行编译。
【示例1】当函数编译之后,其生命周期开始**。
在上面示例中,可以很直观地看到:在代码块中,使用function语句声明的函数在预编译期提前被编译,而函数直接量和构造函数定义的函数都属于表达式函数,在代码执行时,才被编译并**其生命周期。
在生命周期内,函数内部成员(如参数、私有变量、私有函数等)是一直存在的,在函数内是可见的。但是当函数调用完毕,即执行return语句之后,函数生命周期结束,JavaScript会自动释放函数所占用的所有资源。
【示例2】在某种情况下,函数的生命周期会被延长,即使调用函数完毕,它的生命周期依然存在,其内部私有变量依然是可见的。
上面示例中,首先在函数f()内定义两个私有变量,分别存储参数和匿名函数,在匿名函数中引用外部函数的参数值。当调用函数f()之后,其生命周期依然存在,用户可以继续访问函数内部的私有变量。
11.4.4 函数作用域
JavaScript把函数视为一个封闭的结构体,与外界完全独立,在函数内声明的变量、参数、私有函数等对外是不可见的。
调用函数时,函数内代码将在一个临时的局部作用域中执行,这个作用域不同于全局作用域,它临时创建了一个调用对象,该调用对象被添加到作用域链的头部,函数内所有私有变量都将作为调用对象的属性而存在。调用对象是不可见的,仅为函数及其嵌套函数内访问私有变量提供依据。
【示例1】一般函数的结构比较特殊,没有对象结构(Object)的作用域链特性,即通过点号运算符访问对象内部成员。在函数结构体外,用户无法通过点号运算符访问其内部包含的成员。
在上面示例中,函数f()内部的结构是符合语法规范的,但是用户无法通过点号运算符来引用它的成员。如果在对象内部是完全可以引用的。
【示例2】函数作用域通过return语句向外界开放内部成员。例如,在本示例中可以调用成员函数g。
在上面示例中,外界是无法调用函数f()内成员e,当执行return语句后,通过返回值的形式向外界开发内部函数e(),允许外部调用。但是对于变量b和函数c来说,将永远被封闭在函数体内,且不会被执行。
11.5 使用参数
函数结构虽然比较封闭,但是它提供了两个接口:通过参数接收外部信息,通过返回值向外部反馈信息。下面介绍函数参数的相关知识和使用技巧。
11.5.1 认识形参和实参
函数参数包括两种类型:形参和实参。形参就是函数声明的参数变量,它是一个私有变量,仅在函数内部可见,而实参就是实际传递的参数值。
【示例1】下面代码定义一个简单的函数。
在上面示例中,函数结构中的变量a、b就是形参,而在调用函数时向函数传递的变量x、y就是实参。
JavaScript函数可以包含零个或多个形参。函数定义时的形参可以通过length属性获取。
【示例2】针对上面的函数,使用如下方法可以获取它的形参个数。
alert(f.length); //返回2。获取函数的形参个数
一般情况下,函数的形参和实参数量应该相同,但是JavaScript并没有要求形参和实参必须相同,在特殊情况下,函数的形参和实参数量可以不相同。
【示例3】如果函数实参数量少于形参数量,那么多出来的形参的值默认为undefined。
【示例4】如果函数实参数量多于形参数量,那么多出来的实参就不能够通过形参标识符访问,函数会忽略掉多余的实参。在本示例中,实参3和4就被忽略掉了。
在实际应用中,经常存在实参数量少于形参数量,这是因为函数在体内初始化形参,并设置了参数默认值。在调用函数时,如果用户不传递或少传递参数,则函数会采用默认值。而形参数量少于实参的情况比较少见,这种情况一般发生在参数数量不确定的函数中。
【示例5】形参与函数体内使用var语句声明的变量都属于局部变量,仅在函数体内可见。当私有变量与形参发生冲突时,则私有变量拥有较大的优先权。
在上面示例中,私有变量a将覆盖形参变量a,最后返回值为0,而不是参数值5。
11.5.2 使用Arguments对象
Arguments对象表示参数集合,它是一个伪类数组,拥有与数组相似的结构,可以通过数组下标的形式访问函数实参值。
【示例1】在本示例中,函数没有定义形参,但是在函数体内通过Arguments对象可以获取传递给该函数的每个实参值。
【示例2】Arguments对象仅能够在函数体内使用,作为函数的属性而存在。用户可以通过点运算符访问Arguments对象。由于Arguments对象在函数体内是可见的,也直接引用Arguments对象。
Arguments对象是一个伪类数组,可以使用数组下标的形式访问每个实参值,如arguments[i],其中arguments表示对Arguments对象的引用,变量i是arguments集合的下标值,从0开始,直到arguments.length。其中length是arguments对象的一个属性,表示arguments对象包含的实参个数。
【示例3】使用Arguments对象可以随时编辑实参值。在本示例中使用for循环遍历arguments对象,然后把循环变量的值传递给实参,以便动态改变实参值。
【示例4】通过修改Arguments对象的length属性值,也可以达到改变函数实参个数的目的。当length属性值增大时,则增加的实参值为undefined,如果length属性值减小,则会丢弃arguments数据集合后面对应个数的元素。
11.5.3 使用callee回调函数
Arguments对象包含一个callee属性,它引用当前Arguments对象所属的函数,使用该属性可以在函数体内调用函数自身。在匿名函数中,callee属性比较有用,利用它可以设计函数迭代操作。
【示例1】在本示例中,使用arguments.callee获取匿名函数,然后通过函数的length属性获取函数形参个数,最后比较实参与形参个数以检测用户传递的参数是否符合要求。
Function对象的length属性返回的是函数形参个数,而Arguments对象的length属性返回的是函数实参个数。
【示例2】如果不是匿名函数,则arguments.callee等价于函数名,可以把示例1改为如下形式。
11.5.4 案例实战
灵活使用Arguments对象,可以提升使用函数的灵活性,增强函数在抽象编程中的适应能力和纠错功能。下面结合两个典型示例展示Arguments对象在实践中的应用。
【示例1】使用Arguments对象能够增强函数应用的灵活性。例如,如果函数的参数个数不确定,或者函数的参数个数很多,而又不想为每个参数都定义一个形参变量,此时可以省略参数,直接在函数体内使用Arguments对象来访问调用函数的实参值。
下面示例定义一个求平均值的函数,它借助Arguments对象来计算函数接收参数的平均值。
【示例2】验证函数参数的合法性。在页面设计中经常需要验证表单输入值,本示例检测文本框中输入的值是否为合法的邮箱地址。
11.6 使用Function对象
在JavaScript中,使用Function构造函数可以创建函数,于是函数就继承了Function对象的所有属性和方法。本节将介绍Function对象包含的属性和方法,以及如何正确使用它们。
11.6.1 获取函数形参个数
使用Arguments对象的length属性可以获取函数的实参个数,而函数(Function)对象本身也定义了一个length属性,它可以返回定义函数时设置的形参个数,不过这个属性是一个只读属性。
【示例1】与Arguments对象的length属性不同,Function对象的length属性在函数体内外都可以使用。
【示例2】而Arguments对象的length属性仅能够在函数体内使用。
11.6.2 自定义属性
作为对象,用户可以为函数定义属性或方法,定义方法通过点号运算符实现,语法格式如下:
function.property
function.method
【示例1】函数属性可以在函数结构体内定义,也可以在函数体外定义。
【示例2】函数外定义的属性可以随时访问,但是在函数内定义的属性只有函数被调用后才可以访问。
【示例3】函数的方法与嵌套的函数不同。嵌套的函数仅能够在内部可见,而函数的方法可以在外部调用。
【示例4】通过静态属性设计递增变量。
设计在函数内定义临时变量,它的值能随着函数的反复调用而递增,类似下面写法:
但是,上面示例并非按设计每次返回递增值。因为局部变量的值在每次调用函数时,都被重新初始化。用户可以通过一个全局变量来设计:
但是这种设计方法缺乏封闭性,在复杂环境中存在安全隐患。因此,用户不妨为函数定义属性,然后利用函数属性实现函数每次返回递增值:
上面示例能很好地实现上述设计意图,同时也确保函数结构的封闭性。
11.6.3 案例:使用call()和apply()
call()和apply()是Function对象的原型方法,它们能够将特定函数当作一个方法绑定到指定对象上并进行调用。具体用法如下:
function.call(thisobj, args...)
function.apply(thisobj, args)
其中参数thisobj表示指定的对象,参数args表示要传递给被调用函数的参数。call()方法只能接收多个参数列表,而apply()只能接收一个数组或者伪类数组,数组元素将作为参数传递给被调用的函数。
【示例1】当函数被绑定到指定对象上之后,将利用传递的参数执行函数,并返回函数的返回值。
在上面示例中,f()是一个简单的函数,而o是一个构造函数对象。通过call()方法把函数f()绑定到对象o身上,变为它的一个方法,然后动态调用函数f(),同时把参数3和4传递给函数f(),则调用函数f()后返回值为7。
实际上,上面示例可以转换为下面代码:
【示例2】apply()与call()方法功能和用法都相同,唯一的区别是它们传递给参数的方式不同。其中apply()是以数组形式传递参数,而call()方法以多个值的形式传递参数。针对示例1,使用apply()方法来调用函数f(),则设计代码如下:
【示例3】设计把一个数组或伪类数组的所有元素作为参数进行传递时,使用apply()方法就非常便利。
在上面示例中,设计定义一个函数max(),用来计算所有参数中最大值参数。首先通过apply()方法,动态调用max()函数,然后把它绑定为Object对象的一个方法,并把包含多个值的数组传递给它,最后返回经过max()计算后的最大数组元素。
如果不使用call()方法,希望使用max()函数找出数组中最大值元素,就需要把数组中的所有元素全部读取出来,再逐一传递给call()方法,显然这种做法是比较笨拙的。
【示例4】也可以把数组元素通过apply()方法传递给Math的max()方法来计算数组的最大值元素。
【示例5】使用call()和apply()方法可以把一个函数转换为指定对象的方法,并在这个对象上调用该方法。这种行为只是临时的,函数实际上并没有作为对象的方法而存在,当函数被动态调用之后,这个对象的临时方法也会自动被注销。
【示例6】call()和apply()方法能够动态改变函数内this指代的对象,这在面向对象编程中是非常有用的。本示例使用call()方法不断改变函数内this指代对象,主要通过变换calll()方法的第一个参数值来实现。
【示例7】在函数体内,call()和apply()方法的第一个参数就是调用函数内this的值。为了更好理解,用户可以看下面示例。
上面示例显示,如果在函数体内,使用call()和apply()方法动态调用外部函数,并把call()和apply()方法的第一个参数值设置为关键字this,则当前函数e()将继承函数f()的所有属性。即使用call()和apply()方法能够复制调用函数的内部变量给当前函数体。
【示例8】在本示例中,使用apply()方法循环更改当前this指针,从而实现循环更改函数的结构。
执行上面示例,会看到提示信息框中的提示信息不断变化,如图11-2所示。该示例的核心就在于函数o()的设计。在这个函数中,首先使用一个临时变量存储函数r()。然后修改函数r()的结构,在修改的r()函数结构中,通过调用apply()方法修改原来函数r()的指针指向当前对象,同时执行原函数r(),并把执行函数f()的值传递给它,从而实现修改函数r()的return语句的后半部分信息,即为返回值增加一个前缀字符“=”。这样每次调用函数o()时,都会为其增加一个前缀字符“=”,从而形成一种动态的变化效果。
图11-2 apply()方法应用示例效果
11.7 函数解析机制
在JavaScript函数中有几个比较抽象的概念,如作用域、作用域链和调用对象等。本节将介绍这些概念,并进行深度剖析,以方便读者对JavaScript函数有一个全新的认识,为闭包学习和函数式编程奠定基础。
11.7.1 词法作用域与执行作用域
作用域(scope)是JavaScript语言的基石之一。JavaScript作用域可以细分为词法作用域和动态作用域。
☑ 词法作用域,是从静态角度来说的。在函数没有被调用之前,根据函数结构的嵌套关系来确定函数的作用域。因此它取决于源代码,所以通常编译器可以进行静态分析来确定每个标识符实际的引用。
☑ 动态作用域,是从动态角度来说的。当函数被调用之后,它的作用域会因为调用而发生变化,此时作用域链也会随之进行调整。
定义作用域也是词法作用域,即说明函数在定义时存在的嵌套关系。但是当函数被执行时,作用域可能会发生变化,于是执行作用域也是动态作用域。JavaScript函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。
【示例】函数的词法作用域通过静态的代码结构就能辨析出来,但是它们的动态作用域就必须根据调用关系来动态确定。
在上面示例中,函数f()包含函数e(),函数e()在函数f()中被执行,它们的词法作用域和动态作用域是重合的。作用域链包括:函数e()的调用对象(自身)、函数f()的调用对象(自身)和全局对象(window)。
匿名函数的词法作用域是固定的,它的作用域链是:匿名函数、函数f()和全局对象。但是,当在全局环境中调用函数f(),则问题就变得很复杂了,如下所示。
☑ 对于a=f(1)来说,函数f()的调用对象为a,它的作用域链就是匿名函数、a和全局对象。
☑ 对于b=f(2)来说,函数f()的调用对象为b,它的作用域链就是匿名函数、b和全局对象。
☑ 对于c=f(3)来说,函数f()的调用对象为c,它的作用域链就是匿名函数、c和全局对象。
虽然3个变量调用同一个函数,函数的结构也相同,但是由于作用域发生了变化,则返回值也各不相同,如果使用图示来表示,则如图11-3所示。
图11-3 动态作用域的变化示意图
11.7.2 执行上下文和作用域链
执行上下文(execution context)是ECMAScript规范中用来描述JavaScript代码执行的抽象概念。所有的JavaScript代码都是在某个执行上下文中运行的。在当前执行上下文中调用function会进入一个新的执行上下文。该function调用结束后会返回到原来的执行上下文中。如果function在调用过程中抛出异常,并且没有将其捕获,有可能从多个执行上下文中退出。在function调用过程,也可能调用其他的function,从而进入新的执行上下文,由此形成一个执行上下文栈。
每个执行上下文都与一个作用域链(scope chain)关联起来。该作用域链用来在function执行时求出标识符(Identifier)的值。该链中包含多个对象,在对标识符进行求值的过程中,会从链首的对象开始,然后依次查找后面的对象,直到在某个对象中找到与标识符名称相同的属性。在每个对象中进行属性查找时,会使用该对象的prototype链。在一个执行上下文中,与其关联的作用域链只会被with语句和catch子句影响。
在进入一个新的执行上下文时,会按顺序执行下面的操作:
(1)创建**(Activation)对象
**对象是在进入新的执行上下文时被创建出来的,并且与新的执行上下文关联起来。在初始化构造函数时,该对象包含一个名为arguments的属性。**对象在变量初始化时也会被用到。JavaScript代码不能直接访问该对象,但是可以访问该对象的成员(如arguments)。
(2)创建作用域链
接下来的操作是创建作用域链。每个function都有一个内部属性[[scope]],它的值是一个包含多个对象的链。该属性的具体值与function的创建方式和在代码中的位置有很大关系(见本节后面介绍的“function对象的创建方式”)。此时的主要操作是将第(1)步创建的**对象添加到function的[[scope]]属性对应的链的前面。
(3)变量初始化
这一步对function中需要使用的变量进行初始化。初始化时使用的对象是第(1)步中所创建的**对象,不过此时被称作变量对象。会被初始化的变量包括function调用时的实际参数、内部function和局部变量。在这一步中,对于局部变量,只是在变量对象中创建了同名的属性,其属性值为undefined,只有在function执行过程中才会被真正赋值。全局JavaScript代码是在全局执行上下文中运行的,该上下文的作用域链只包含一个全局对象。
函数总是在自己的上下文环境中运行,如读写局部变量、函数参数,运行内部逻辑结构等。在创建上下文环境的过程中,JavaScript会遵循一定的运行规则,并按照代码顺序完成一系列操作。这个操作过程如下。
第1步,根据调用时传递的参数,创建调用对象。
第2步,创建参数对象,存储参数变量。
第3步,创建对象属性,存储函数定义的局部变量。
第4步,把调用对象放在作用域链的头部,以便检索。
第5步,执行函数结构体内语句。
第6步,返回函数返回值。
针对上面的操作过程,下面进行详细描述。
首先,在函数上下文环境中创建一个调用对象。调用对象与上下文环境是两个不同的概念,也是另一种运行机制。对象可以定义和访问自己的属性或方法,不过这里的对象不是完整意思上的对象,它没有原型,并且不能够被引用,这与Arguments对象的arguments[]数组不是真正意思上的数组一样。
调用对象会根据传递的参数创建自己的Arguments对象,这是一个结构类似数组的对象,该对象内部存储着调用函数时所传递的参数。接着创建名为arguments的属性,该属性引用刚创建的Arguments对象。
然后,为上下文环境分配作用域。作用域由对象列表或对象链组成。每个函数对象都有一个内部属性(scope),这个属性值也是由对象列表或对象链组成的。scope属性值构成了函数调用上下文环境的作用域,同时,调用对象被添加到作用域链的头部,即该对象列表的顶部(作用域链的前端)。
实际上,这个头部是针对该函数的作用域链而言的,把调用对象添加到作用域的头部就是把调用对象排在函数作用域链的最上面。例如,在下面这个示例中,当调用函数e()时,将创建函数e()的调用对象和函数e()的作用域。但是在调用函数e()之前,会先调用函数g(),并且生成调用函数g()的对象。而调用函数e()的对象会在函数e()的作用域范围内处于头部位置,即排在最前面。
接着就是正式执行函数体内代码了,此时JavaScript会对函数体内创建的变量执行变量实例化操作(即转换为调用对象的属性)。下面进行具体说明。
将函数的形参也创建为调用对象的命名属性。如果调用函数时传递的参数与形参一致,则将相应参数的值赋给这些命名属性,否则会将命名属性赋值为undefined。
对于内部定义函数(注意其与嵌套函数的区分,两者语义不完全重合),会以其声明时所用名称为调用对象创建同名属性,对应的函数则被创建为函数对象,并将其赋值给该属性。
将在函数内部声明的所有局部变量创建为调用对象的命名属性。注意,在执行函数体内的代码并计算相应的赋值表达式之前不会对局部变量执行真正的实例化。
由于arguments属性与函数局部变量对应的命名属性都属于同一个调用对象,因此可以将arguments作为函数的局部变量来看待。
最后创建this对象并对其进行赋值。如果赋值为一个对象,则this将指向该对象引用。如果赋值为null,则this就指向全局对象。
创建全局上下文环境的过程与上面的描述稍微不同,因为全局上下文环境没有参数,所以不需要通过定义调用对象来引用这些参数。全局上下文环境会有一个作用域,即全局作用域,它的作用域链实际上只由一个对象组成,即全局对象(window)。全局上下文环境也会有变量实例化的过程,它的内部函数就是涉及大部分JavaScript代码的、常规的顶级函数声明。全局上下文环境也会使用this对象来引用全局对象。
JavaScript作用域可以细分为词法作用域和动态作用域。词法作用域又称为定义作用域,这是从静态角度来说的。在函数没有被调用之前,根据函数结构的嵌套关系来确定函数的作用域。因此词法作用域取决于源代码,通常编译器可以进行静态分析来确定每个标识符实际的引用。
动态作用域也称为执行作用域,这是从动态角度来说的。当函数被调用之后,其作用域会因为调用而发生变化,此时作用域链也会随之调整。
定义作用域就是用来说明函数在定义时存在的嵌套关系。当函数被执行时,作用域可能会发生变化。JavaScript函数运行在它们被定义的作用域中,而不是它们被执行的作用域中。
在JavaScript中,function对象的创建方式有3种:function声明、function表达式和使用Function构造器。
function a() {}
var a=function() {}
var a=new Function()
通过这3种方法创建出来的function对象的“scope”属性的值会有所不同,从而影响function执行过程中的作用域链,具体说明如下:
☑ 使用function语句声明的function对象是在进入执行上下文时的变量初始化过程中创建的。该对象的“scope”属性的值是它被创建时的执行上下文对应的作用域链。
☑ 使用function表达式的function对象是在该表达式被执行时创建的。该对象的“scope”属性的值与使用function声明创建的对象一样。
☑ 使用Function构造器声明一个function通常使用两种方式。常用格式是var funcName=new Function(p1, p2,..., pn, body),其中p1、p2到pn表示的是该function的形式参数,body是function的内容。使用该方式的function对象是在构造器被调用时创建的。该对象的“scope”属性的值总是一个只包含全局对象的作用域链。
function对象的length属性可以用来获取声明function时指定的形式参数的个数,而function对象被调用时的实际参数是通过arguments来获取的。
11.7.3 调用对象
JavaScript的函数是词法上的作用域,而不是动态作用域。这意味着它们运行在自己定义的作用域中,而不是运行在执行它们的作用域中。
【示例1】定义函数实际上就是定义作用域,即词法作用域,而不是定义动态作用域。当然动态作用域不是定义的,也是无法静态确定的,它是根据调用对象不同而变化的。函数都有自己的作用域,且都会在自己的作用域内运行,而不是在调用它的动态作用域中执行。
当把函数f()赋值给变量a时,调用对象是a,动态作用域也是a,此时变量a内存储的是返回的匿名函数结构体,而不是指向函数f()的引用地址。此时变量a的结构就变成了匿名函数结构体,因此说,var a=f(1)实际上是定义了一个函数作用域。
虽然在函数f()中已经声明了匿名函数结构,但它仅是一个被传递的数据,匿名函数结构处于冻结状态中,因此在f作用域内,并没有定义匿名函数作用域。所以说,不管动态作用域如何让人迷乱,函数始终在定义它的作用域中执行。
调用对象(Call Object)是执行上下文对象而不是函数外部的其他控制对象。
【示例2】为了能够理解调用对象的本质,下面举两个反例:
上面示例中的f是调用对象吗?不是,如果在函数体内,你可以使用this关键字进行引用,显示它仅是作用域链的上一级。
上面示例中,嵌套函数e()在函数f()中被调用,那么f不是函数e()的调用对象。
当函数被调用时,系统会自动为它创建一个对象,其中包含了函数参数(即Arguments对象)。然后根据函数结构体内定义的各个局部变量名,在调用对象中定义相应的变量作为调用对象的属性,用来存储函数局部变量的数据。最后,将这个调用对象放在作用域链的头部。
11.8 函数中的this
在JavaScript中,this是一个比较灵活的关键字,它表示当前调用对象。本节将详细讲解函数的this这个概念,以及如何使用this。
11.8.1 案例:认识this
this是函数体内自带的一个对象指针,它能够始终指向调用对象。当函数被调用时,使用this可以访问调用对象。
this关键字的使用范围必须局限于函数体内或者调用范围内。具体用法如下:
this[.属性]
如果this未包含属性,则传递的是当前对象。
【示例1】this代表当前操作对象。
下面这个按钮定义了一个单击事件属性,其中就包含了this关键字。
其中,onclick事件属性中包含的this就代表当前对象input。
【示例2】this代表构造函数的当前实例。
定义一个构造函数,在其中使用this关键字作为临时代表,然后使用new运算符实例化构造函数。
显然,这里的this就代表当前实例对象f。
【示例3】this代表当前对象直接量。
下面是一个对象直接量,它包含了两个属性,其中方法me()返回关键字this。
当调用对象直接量o的方法me()后,变量who的值就是this的值,它代表当前对象直接量o,然后读取对象o的属性name,则返回字符串“我是对象o”。
【示例4】this代表全局对象。
在函数f()中调用this关键字,并为this定义并初始化一个属性name,当调用函数之后,则可以直接读取属性name。
原来方法f()是全局对象window方法,this实际上就是代表window,而全局对象可以省略,所以就看到上面特殊的写法。实际上可以完整地写成如下这样:
如果进一步修改,它与对象直接量在用法上有几分相似之处:
【示例5】this代表当前作用域对象。
this关键字并不总是代表当前对象,下面这个示例能够很好地说明这个问题:
通过简单的比较:直接调用函数f(),则返回的字符串为"[object]",而使用new运算符调用函数f()之后,返回的字符串为"[object Object]"。使用new运算符实际上是把函数进行实例化,此时this指向当前实际对象。但是,直接调用函数f(),则this表示Window对象。同一个语境中的this关键字,但是由于使用方式的不同,所返回的引用是不同的。
实际上,这与this所在作用域有着密切关系。函数f()属于全局作用域,也就是说,它应该属于Window对象,如果直接调用函数f(),当然函数中的this关键字就代表Window对象。而使用new运算符实例化函数f()后,也就创建了一个新对象,当前作用域就不再是全局作用域了,而是对象作用域,所以this就代表这个新创建的实例对象。
【示例6】再看一个示例:
在上面示例中,该按钮的事件属性所包含的this就是指向当前对象。但是,第二个按钮的鼠标单击事件属性中是以调用的方式,调用全局作用域中的函数f(),所以其中的this就代表Window对象,而不是当前按钮(第二个按钮)。
如果改变函数的用法,首先,在脚本中获取第二个按钮对象的引用,然后把函数f()作为一个值传递给对象的onclick事件属性:
在上面示例中,this关键字就表示按钮对象本身,所以当单击之后,也就能够改变按钮的值。这也说明当函数赋值给按钮对象之后,虽然函数的定义作用域没有发生变化,但是它的执行作用域已经从全局作用域变为对象作用域,所以this关键字所代表的对象也会随之发生变化。
如果以同样的方式,把该函数作为事件处理函数赋值给不同的按钮对象,则this会分别代表不同的按钮对象。这也说明了,如果我们改变函数的执行作用域,则函数所包含的this关键字也会指向不同的对象。
【示例7】本示例能更好地说明作用域对于this关键字的影响:
首先,定义函数f(),设置其返回值为this关键字,然后把该函数作为值传递给不同作用域中的属性me,最后分别调用它们,会发现this所代表的对象是不同的。
【示例8】如果不是以值的方式传递函数f(),而是直接调用,则它们所包含的this都代表Window对象。也就是说,this的执行作用域并没有发生变化,仍然根据定义它的作用域来确定,即全局作用域。
【拓展】this是一个指针,一个具有动态特性的指针,this指针非常灵敏,它所指代的对象是由this所在的执行作用域决定的,而不是根据this所在的定义作用域决定,如图11-4所示,this是JavaScript执行作用域的一个属性,这个属性始终引用该上下文环境中的对象。
图11-4 this指针的变化示意图
但是JavaScript比较复杂,方法作为对象的属性,实际上是靠将函数实例赋值给对象属性或者对象原型来实现的,这种实现方式无疑是灵活的,无法在声明时确定。而且,JavaScript函数可以在多个地方被引用,从理论上说,既可以作为这个对象的方法,也可以作为那个对象的方法,而且这种引用是在执行时才确定。因此,JavaScript方法的灵活性注定了this指针只能在运行环境中动态地确定。
有一些特殊情况将破坏执行作用域的直观性,因此只有当this被最后执行时才能够准确确定。
【示例9】在JavaScript中,闭包扮演着非常重要的角色,它能够改变this的指向。本示例在一个对象中(如o1)引用或调用另一个对象(如o)的方法时,该方法中的this指针是变化的。当引用对象o的方法f()时,它的this就会根据执行对象而定,但是如果调用对象o的方法f()时,它的this还是指向原定义的对象o。
下面尝试把对象o的方法f()封装在闭包中,然后再进行引用:
这时方法f()中的this既不指向对象o,也不指向对象o1,而是指向对象Window。因为,在执行对象o1的方法f()时,仅是指定了闭包运行的上下文环境,即执行闭包的对象为o1,但是闭包内包裹的方法f()并没有被执行,而再次执行闭包内的方法时,此时执行环境已经发生了变化,执行对象从o1变为全局对象Window,所以this就指向了Window。
call()和apply()方法能够强制改变函数的执行作用域,所以它们也会破坏函数引用和调用的一般规律。另外,异步调用也会破坏this指针的一般应用规律,这是因为函数被传递给定时器或者事件处理函数时才被调用。
11.8.2 案例:使用this
this用法比较灵活,它可以存在于任何位置,它并不仅仅局限于对象的方法内,还可以被应用在全局域内、函数内,以及其他特殊上下文环境中。
【示例1】函数的引用和调用。
函数的引用和调用分别表示不同的概念。虽然它们都无法改变函数的定义作用域。但是引用函数,却能够改变函数的执行作用域,而调用函数是不会改变函数的执行作用域的。
在上面示例中,函数中的this所代表的是当前执行域对象o1:
var who=o.o1.me();
alert(who.name); //返回字符串"对象o1",说明当前this代表对象o1
如果把对象o1的me属性值改为函数调用:
则函数中的this所代表的是定义函数时所在的作用域对象o:
var who=o.o1.me;
alert(who.name); //返回字符串"对象o",说明当前this代表对象o
【示例2】使用call()和apply()。
call()和apply()方法可以直接改变被执行函数的作用域,使其作用域指向所传递的参数对象。因此,函数中包含的this关键字也指向参数对象。
在上面示例中,直接调用函数f()时,函数的执行作用域为全局域,所以this代表window。当使用new运算符调用函数时,将创建一个新的实例对象,函数的执行作用域为实例对象所在的上下文,所以this就指向这个新创建的实例对象。
而使用call()方法执行函数f()时,call会把函数f()的作用域强制修改为参数对象所在的上下文。由于call()方法的参数值为数字1,则JavaScript解释器会把数字1强制封装为数值对象,此时this就会指向这个数值对象。
在下面示例中,call()方法把函数f()强制转换为对象o的一个方法并执行,这样函数f()中的this就指代对象o,所以this.x的值就等于1,而this.y的值就等于2,结果就返回3。
【示例3】原型继承。
JavaScript通过原型模式实现类的延续和继承,那么在父类的成员中包含了this关键字时,当子类继承了父类的这些成员时,this的指向就变得很迷惑人。
在一般情况下,子类继承父类的方法后,this会指向子类的实例对象,但是也可能指向子类的原型对象,而不是子类的实例对象。
在上面示例中,基类Base包含4个成员,其中成员b和c以不同方式引用当前作用域内的方法m(),而成员a存储着当前作用域内方法m()的调用值。当这些成员继承给子类F后,其中m、b和c成为原型对象的方法,而a成为原型对象的属性。但是,c的值为一个闭包体,当在子类的实例中调用时,实际上它的返回值已经成为实例对象的成员,也就是说,闭包体在哪里被调用,则其中包含的this就会指向哪里。所以,会看到f.c()中的this指向实例对象,而不是F类的原型对象。
为了避免因继承关系而影响父类中this所代表的对象,除了通过上面介绍的方法,把方法的引用传递给父类的成员外,还可以为父类定义私有函数,然后再把它的引用传递给其他父类成员,这样就避免了因为函数闭包的原因,而改变this的值。
这样基类的私有函数_m()就具有完全隐私性,外界其他任何对象都无法直接访问基类的私有函数_m()。所以,在一般情况下,定义方法时,对于相互依赖的方法,可以把它定义为私有函数,并以引用方法的方式对外公开,这样就避免了外界对于依赖方法的影响。
【示例4】异步调用之事件处理函数。
异步调用就是通过事件机制或者计时器来延迟函数的调用时间和时机。通过调用函数的执行作用域不再是原来的定义作用域,所以函数中的this总是指向引发该事件的对象。
这里的方法f()所包含的this不再指向对象o,而是指向按钮button,因为它是被传递给按钮的事件处理函数之后,再被调用执行的。函数的执行作用域发生了变化,所以不再指向定义方法时所指定的对象。
如果使用DOM 2级标准为按钮注册事件处理函数:
则在IE浏览器中,this指向Window和button,而在符合DOM标准的浏览器中仅指向button。因为,在IE浏览器中,attachEvent()是Window对象的方法,调用该方法时,执行作用域为全局作用域,所以this会指向window。同时由于该方法被注册到按钮对象上,所以它的真正执行作用域应该为button对象所在的上下文。这一点可以通过在符合DOM标准的浏览器中看到。这种解释可能很勉强,但是在IE中this同时指向Window和button对象本身就让人迷惑不解。
为了解决这个问题,可以借助call()或apply()方法强制在对象o身上执行方法f(),也就是说,强制改变f()方法的执行作用域,避免因为环境的不同而影响函数作用域的变化。
这样当再次执行时,方法f()中包含的this关键字始终指向对象o,也就是说,它的执行作用域始终与它的定义作用域保持一致。
【示例5】异步调用之定时器。
异步调用的另一种形式,就是使用定时器来调用函数,定时器就是指调用Window对象的setTimeout()或setInterval()方法来延期调用函数。
例如,下面示例设计延期调用方法o.f()。
此时,经测试程序,会发现在IE中this指向Window和Button对象,具体原因与上面讲解的attachEvent()方法相同。但是,在符合DOM标准的浏览器中,this指向Window对象,而不是Button对象,因为方法setTimeout()是在全局作用域中被执行的,所以this自然指向Window对象。要解决这个问题,仍然可以使用call()或apply()方法来实现:
11.8.3 案例:this安全策略
this的复杂性很大程度上取决于用户的使用方式。由于this指代灵活,如果把它放在复杂的应用环境中,它也会变得很不确定。
提示:确保在同一域中操作包含this的方法或函数。应避免把包含有this的全局函数或方法动态用在局部作用域的对象中,也应避免在不同作用域的对象之间相互引用包含this的方法或属性。
【示例1】如果把this作为参数值来调用函数,就可以避免this多变的问题,因为this始终与当前对象保持一致。
下面的做法是错误的,因为this在这里始终指向Window对象,而不是所期望的当前按钮对象:
但是,如果换一种思维,把this作为参数值进行传递,那么它就会代表当前对象:
【示例2】设计静态的this指针。
如果要确保构造函数的方法在初始化之后方法所包含的this指针不再发生变化,一个很简单的方法就是:在构造函数中把this指针存储在私有变量中,然后在方法中使用私有变量来引用this指针,这样所引用的对象始终都是初始化的实例对象,而不会在类型继承中发生变化。
对于对象直接量来说,如果希望使用this代表当前对象直接量,则可以直接调用对象直接量的名称,而不用this关键字。
【示例3】设计静态的this扩展方法。
当然,作为一个动态指针,this也是可以被转换为静态指针的。实现的方法主要利用Function对象的call()或apply()方法。在前面的示例中也已经提及它们的用法,这两个方法都可以强制指定this的指代对象。例如,为Function对象扩展一个原型方法pointTo()。
这个方法将调用当前函数,并在指定的参数对象上执行,从而把this绑定到该对象上。
下面利用这个函数的扩展方法,以实现强制指定对象o的方法b()中的this始终指向定义对象o。具体如下:
还可以扩展new运算符的替代方法,从而间接使用自定义函数实例化类:
下面示例演示了如何使用这个自定义的实例化类方法把一个简单的构造函数转换为具体的实例对象:
通过这个示例也进一步说明,call()和apply()方法具有强大的功能,它不仅能够执行普通函数,也能够实例化构造函数,担当new运算符的运算功能。
11.8.4 案例:this应用
【示例1】下面看一个示例:
方法x()是在函数f体内定义,由于函数f是被window对象调用,所以this就指向window对象。如果使用如下方法可以正确调用方法x()。
alert(window.x(4)); //使用window对象调用方法x(),则返回4
【示例2】当使用new运算符调用函数,则this将指向实例对象,在下面代码中变量a就成为this指代对象,此时调用方法x()就不能够使用window对象。
如果使用window对象来调用方法x(),则就会显示编译错误。
alert(window.x(4)); //提示编译错误
【示例3】利用this可以为调用对象定义属性。
如果在显示变量x的值之前调用函数f():
比较会发现,当调用函数f()之后,函数内部的this指向当前对象window,则this.x就等于了window.x,该属性与全局变量x同名,于是覆盖全局变量的值,所以返回值就是2。
【示例4】在全局作用域内,所有变量和函数的调用对象都是window,对于上面示例可以这样设计。
在上面示例中,可以很直观地看到:全局变量x和函数f()的调用对象都是window。
【示例5】在本示例中先调用isNaN()方法,然后在函数f()体内使用tihs重写isNaN()方法,在全局作用域中调用函数f(),然后再调用isNaN()方法,则返回值永远是false。
【示例6】本示例演示了嵌套函数结构体内this的变化规律。
在多层嵌套的函数体内分别调用不同层级的函数,虽然它们的嵌套层级不同,但是当函数被调用后,每个函数体内的this都指window,说明变量a、b、c和d都是全局对象window的属性,说明它们都是全局变量,因此通过window可以获取它们的值。
【示例7】对于上面函数嵌套结构,如果不通过函数调用的方式来执行,而是通过对象实例化的方式来**,则发现this指向的对象完全不同。
在上面示例中,通过运算符new实例化构造函数,此时this在函数作用域内分别指向所在作用域的调用对象,因此返回值也就不同。如果在当前作用域内没有直接定义变量,则会返回undefined,而当undefined+undefined时就会返回NaN,所以就会出现异常提示信息。
11.9 案例实战
下面通过几个案例介绍函数应用,以提高使用函数的灵活性。
11.9.1 函数调用模式
在JavaScript中,共有4种函数调用模式:方法调用模式、函数调用模式、构造器调用模式和apply调用模式。这些模式在如何初始化this上存在差异。
提示:调用运算符是小括号,小括号内可以包含零个或多个用逗号隔开的表达式。每个表达式产生一个参数值。每个参数值被赋予函数声明时定义的形参。当实际参数(arguments)的个数与形式参数(parameters)的个数不匹配时不会导致运行时错误。如果实际参数值过多,超出的参数值将被忽略。如果实际参数值过少,缺失的值将会被替换为undefined。不会对参数值进行类型检查,任何类型的值都可以被传递给参数。
【示例1】方法调用模式。
当一个函数被保存为对象的一个属性值时,将称之为一个方法。当一个方法被调用时,this被绑定到当前调用对象。
在上面的代码中创建了obj对象,它有一个value属性和一个increment()方法。increment()方法接收一个可选的参数,如果该参数不是数字,那么默认使用数字1。
increment()方法可以使用this去访问对象,所以它能从对象中取值或修改该对象。this到对象的绑定发生在调用时。这个延迟绑定使函数可以对this高度复用。通过this可取得increment()方法所属对象的上下文的方法称为公共方法。
【示例2】函数调用模式。
当一个函数不是一个对象的属性时,它将被当作一个函数来调用:
var sum=add(3, 4); //7
当函数以此模式调用时,this被绑定到全局对象。这是语言设计上的一个缺陷,如果语言设计正确,当内部函数被调用时,this应该仍绑定到外部函数的this变量。这个设计错误的后果是方法不能利用内部函数来帮助它工作,因为内部函数的this被绑定了错误的值,所以不能共享该方法对对象的访问权。
解决方案:如果该方法定义一个变量并将它赋值为this,那么内部函数就可以通过这个变量访问this。按照约定,将这个变量命名为that。
【示例3】构造器调用模式。
JavaScript是基于原型继承的语言,对象可以直接从其他对象继承属性。当今大多数语言都是基于类的语言,虽然原型继承有着强大的表现力,但它偏离了主流用法,并不被广泛理解。JavaScript为了能够兼容基于类语言的编写风格,提供了一套基于类似类语言的对象构建语法。
如果在一个函数前面加上new运算符来进行调用,那么将创建一个隐藏连接到该函数的prototype原型对象的新实例对象,同时this将会被绑定到这个新实例对象上。注意,new运算符也会改变return语句的行为。
上面代码创建一个名为F的构造函数,此函数构建了一个带有status属性的对象。然后,为F所有实例提供一个名为get的公共方法。最后,创建一个实例对象,并调用get方法,以读取status属性的值。
结合new前缀调用的函数被称为构造函数。按照约定,构造函数应该保存在以大写格式命名的变量中。如果调用构造函数时没有在前面加上new,可能会发生非常糟糕的事情,既没有编译时警告,也没有运行时警告,所以大写约定非常重要。
【示例4】apply调用模式。
JavaScript是函数式的面向对象编程语言,函数可以拥有方法。apply就是函数的一个基本方法,使用这个方法可以调用函数,并修改函数体内的this值。apply()方法包括两个参数:第一个参数设置绑定给this的值;第二个参数是包含函数参数的数组。
上面代码构建一个包含两个数字的数组,然后使用apply()方法调用add()函数,将数组array中的元素值相加。
上面代码构建了一个构造函数F,为该函数定义了一个原型方法get,该方法能够读取当前对象的status属性的值。然后定义一个obj对象,该对象包含一个status属性,使用apply()方法在obj对象上调用构造函数F的get方法,将会返回obj对象的status属性值。
11.9.2 扩展函数方法
JavaScript允许为基本数据类型定义方法。通过为Object.prototype添加原型方法,该方法被所有的对象可用。这样的方式对函数、数组、字符串、数字、正则表达式和布尔值都适用。例如,通过给Function.prototype增加方法,使该方法对所有函数可用。
为Function.prototype增加一个method()方法后,就不必使用prototype这个属性了,然后调用method()方法直接为各种基本类型添加方法。
JavaScript并没有单独的整数类型,因此有时只提取数字中的整数部分是必要的。JavaScript本身提供的取整方法有些丑陋。下面通过为Number.prototype添加一个integer()方法来改善它。
Number.method()方法能够根据数字的正负来判断是使用Math.ceiling还是Math.floors,这样就避免了每次都编写上面的代码。
trim()方法使用了一个正则表达式,把字符串中的左右两侧的空格符清除掉。
通过为基本类型扩展方法,可以大大提高语言的表现力。由于JavaScript原型继承的本质,所有原型方法立刻被赋予到所有的实例,即使该实例在原型方法被创建之前就创建好了。
基本类型的原型是公共结构,所以在扩展基类时务必小心,避免覆盖掉基类的原生方法。一个保险的做法就是在确定没有该方法时才添加它。
另外,forlin语句用在原型上时表现很糟糕。可以使用hasOwnProperty方法筛选出继承而来的属性,或者查找特定的类型。
11.9.3 绑定函数
函数绑定就是为了纠正函数的执行上下文,特别是当函数中带有this关键字时,这点尤其重要,稍微不小心,就会使函数的执行上下文发生和预期不同的改变,导致代码执行上的错误。函数绑定具有3个特征:
☑ 函数绑定要创建一个函数,可以在特定环境中以指定参数调用另一个函数。
☑ 一个简单的bind()函数接收一个函数和一个环境,返回一个在给定环境中调用给定函数的函数,并且将所有参数原封不动传递过去。
☑ 被绑定函数与普通函数相比有更多的开销,它们需要更多内存,同时也因为多重函数调用而稍微慢一点,所以最好只在必要时使用。
函数绑定要创建一个函数,可以在特定的环境中以指定的参数调用另一个函数,该特征常常和回调函数及事件处理函数一起使用。
出现上述结果的原因在于没有保存handler.handleClick()环境(上下文环境),所以this对象最后指向了DOM按钮而非是handler。可以使用闭包修正问题:
这是特定于这段代码的解决方案。创建多个闭包可能会令代码变得难于理解和调试,因此,很多JavaScript库实现了一个可以将函数绑定到指定环境的函数bind()。
bind()函数的功能是提供一个可选的执行上下文传递给函数,并且在bind()函数内部返回一个函数,以纠正在函数调用上出现的执行上下文发生的变化。最容易出现的错误就是回调函数和事件处理程序一起使用。一个简单的bind()函数接收一个函数和一个环境,返回一个给定环境中调用给定函数的函数,并且将所有的参数原封不动传递过去。
在bind()中创建一个闭包,该闭包使用apply调用传入的参数,并为apply传递context对象和参数。
注意:这里使用的arguments对象是内部函数的,而非bind()的。在调用返回的函数时,会在给定的环境中执行被传入的函数并给出所有参数。
11.9.4 链式语法
jQuery最大亮点之一就是它的链式语法。在JavaScript中,很多方法没有返回值,一些设置或修改对象的某个状态却不返回任何值的方法就是典型的例子。如果让这些方法返回this,而不是undefined,那么就要启用级联功能,即所谓的链式语法。在一个级联中,单独一条语句可以连续调用同一个对象的很多方法。
在上面代码中,getElement()函数获取id='box'的DOM元素,然后通过链式语法分别调用DOM元素的扩展方法来移动元素、修改尺寸和样式,以及添加行为。每一个扩展方法都返回参数对象,所以调用返回的结果可以为下一次调用所用。链式语法可以产生出具备很强表现力的接口,以打造出试图一次做很多事情的接口的趋势。
【示例】在下面示例中,分别为String扩展了3个方法:trim()、writeln()和alert(),其中writeln()和alert()方法返回值都为this,而trim()方法返回值为修剪后的字符串。这样就可以利用链式语法在一行语句中快速调用这3个方法。
11.9.5 函数节流
函数节流的设计思想就是让某些代码可以在间断情况下连续重复执行。实现的方法是使用定时器对函数进行节流。
【示例1】在第一次调用函数时,创建一个定时器,在指定的时间间隔后运行代码。当每两次调用时,清除前一次的定时器并设置另一个,实际上就是前一个定时器演示执行,将其替换成一个新的定时器。
简化模式:
函数节流解决的问题是一些代码(特别是事件)的无间断执行,这个问题严重影响了浏览器的性能,可能会造成浏览器反应速度变慢或直接崩溃,如resize、mousemove、mouseover、mouseout等事件的无间断执行。这时加入定时器功能,将事件进行“节流”,即在事件触发时设定一个定时器来执行事件处理程序,可以在很大程度上缓解浏览器的负担。类似应用如支付宝中的“导购场景”导航(http://life.alipay.com/?src=life_alipay_index_big),以及当当网首页左边的导航栏(http://www. dangdang.com/)等,这些都是为了解决mouseover和mouseout移动过快时给浏览器处理带来的负担,特别是减轻涉及Ajax调用给服务器造成的极大负担。
【示例2】下面的示例是在事件处理函数中应用函数节流来解决执行效率问题的。
11.9.6 使用Arguments模拟重载
在JavaScript中,每个函数内部可以使用Arguments对象。该对象包含了函数被调用时的实际参数值。Arguments对象虽然在功能上与数组有些类似,但它不是数组。Arguments对象与数组的类似体现在它有一个length属性,同时实际参数的值可以通过[]操作符来获取,但是Arguments对象并没有数组可以使用的push()、pop()、splice()等方法。其原因是Arguments对象的prototype指向的是Object.prototype,而不是Array.prototype。
Java和C++语言都支持方法重载,即允许出现名称相同而形式参数不同的方法,但是JavaScript并不支持这种方式的重载。这是因为JavaScript中的function对象也是以属性的形式出现的,在一个对象中增加与已有function同名的新function时,旧的function对象会被覆盖。
【示例1】可以通过使用Arguments来模拟重载,其实现机制是通过判断Arguments中实际参数的个数和类型来执行不同的逻辑。
【示例2】callee是Arguments对象的一个属性,其值是当前正在执行的function对象。它的作用是使匿名function可以被递归调用。下面以一段计算斐波那契序列中第N个数的值的过程来演示arguments.callee的使用。