第四章《函数》

引言

JavaScript中最好的特性就是它对函数的实现。它几乎无所不能,但是,函数再JavaScript里也并非万能药。

函数包含一组语句,它们是JavaScript的基础模块单元,用于代码复用、信息隐藏和组合调用。函数用于指定对象的行为。一般来说,所谓编程就是将一组需求分解成一组函数与数据结构的技能。

1.函数对象

在JavaScript中函数就是对象。对象是“名/值”对的集合并拥有一个连到原型对象的隐藏链接。对象字面量产生的对象连接到Object.prototype。函数对象连接到Function.prototype(该原型对象本身连接到Object.prototype)。每个函数在创建时附有两个隐藏属性:函数的上下文和实现函数行为的代码

每个函数对象在创建时也随带有一个prototype属性,它的值是一个拥有constructor属性且值即为该函数的对象。这和隐藏连接到Function.prototype完全不同。

因为函数也是对象,所以它们可以像任何其他值一样被使用。函数可以存放在变量、对象和数组中,函数可以被当做参数传递给其他函数,函数也可以再返回函数。而且因为函数是对象,所以函数可以拥有方法。

函数的与众不同之处在于它可以被调用

2.函数字面量

函数对象可以通过函数字面量来创建。

函数字面量包括四个部分,第一个部分是保留字function。第二部分是函数名,它可以被省略(匿名函数)。第三部分是包围在圆括号里的一组参数。第四部分是花括号里的一组语句。

3.调用

调用一个函数将暂停当前函数的执行,传递控制权和参数给了新函数。除了声明时定义的形式参数,每个函数接收两个附加的参数:this和arguments。参数this在面向对象编程中非常重要,它的值取决于调用的模式。在JavaScript中一共有四种调用模式:方法调用模式、函数调用模式、构造器调用模式apply调用模式。

调用运算符是跟在任何产生一个函数值的表达式之后的一对圆括号。圆括号内可包含0个或多个用逗号隔开的表达式。每个表达式产生一个参数值,每个参数值被赋予函数声明时定义的形式参数名。当实际参数(arguments)与形式参数(parameters)的个数不匹配时不会导致运行时错误。如果实际参数值过多了,超出的参数值将被忽略。如果实际参数值过少,缺失的值会被替换成undefined。对参数值不会进行类型检查,任何类型的值都可以传递给参数。

方法调用模式

当一个函数被保存为对象的一个属性时,我们称它为一个方法。当一个方法被调用时,this被绑定到该对象。如果一个调用表达式包含一个属性存取表达式(即 . 或者 [ ] 表达式),那么它被当作一个方法来调用。

方法可以使用this去访问对象,所以它能从对象中取值或修改该对象。this到对象的绑定发生在调用的时候。这个“超级”迟绑定(very late binding)使得函数可以对this高度复用。通过this可取得它们所属对象的上下文的方法称为公共方法。

函数调用模式

当一个函数并非一个对象的属性时,那么它被当作一个函数来调用。

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

构造器调用模式

JavaScript是一门基于原型继承的语言。这意味着对象可以直接从其他对象继承属性。该语言是无类别的。

这偏离了当今编程语言的主流。当今大多数语言都是基于类的语言。尽管原型继承有着强大的表现力,但它并不被广泛理解。JavaScript本身对其原型的本质也缺乏信心,所以它提供了一套和基于类的语言类似的对象构建语法。有类型化语言编程经验的程序员少有愿意接受原型继承的,并且认为借鉴类型化语言的语法模糊了这门语言真实的原型本质。

如果在一个函数前面带上new来调用,那么将创建一个隐藏连接到该函数的prototype成员的新对象,同时this会被绑定到那个新对象上。

new前缀也会改变return语句的行为。

// 创建一个名为Qua的构造器函数,它构造一个带有status属性的对象var Qua = function(string){ this.status = string}// 给Qua的所有实例提供一个名为get_status的公共方法Qua.prototype.get_status = function(){ return this.status}// 构造一个Qua实例var myQua = new Qua('confused')console.log(myQua.get_status()) // 'confused'

结合new前缀调用的函数被称为构造器函数。按照约定,它们保存在以大写格式命名的变量里。如果调用构造器函数时没有在前面加上new,可能会发生很糟糕的事情,既没有编译时警告,也没有运行时警告,所以大写约定十分重要。但不推荐这种形式的构造器函数

Apply调用模式

因为JavaScript是一门函数式的面向对象编程语言,所以函数可以拥有方法。

apply方法让我们构建一个参数数组并用其去调用函数,它也允许我们选择this的值。

apply方法接收两个参数,一个就是将被绑定给this的值,第二个就是一个参数数组。

4. 参数

当函数被调用时,会得到一个“免费”奉送的参数,那就是arguments数组。通过它函数可以访问到所有它被调用时传递给它的参数列表,包括哪些没有被分配给函数声明时定义的形式参数的多余参数,这使得编写一个无须指定参数个数的函数成为可能。

这不是一个特别有用的模式,我们可以给数组添加一个相似的方法达到同样的效果。

由于语言的一个设计错误,arguments并不是一个真正的数组,它是一个类似数组(array-like)的对象,arguments拥有一个length属性,但是缺少其他所有的数组方法,这个设计错误导致的后果后面将写到。

5. 返回

当一个函数被调用时,它从第一个语句开始执行,并在遇到关闭函数体的 } 时结束。那使得函数把控制权交还给调用该函数的程序部分。

return语句可用来使函数提前返回。当return语句被执行时,函数立即返回而不再执行余下的语句。

一个函数总是会返回一个值,若没有指定返回值,则返回undefined。

如果函数以在前面加上new前缀的方式来调用,且返回值不是一个对象,则返回this(该新对象)。

6. 异常

JavaScript提供了一套异常处理机制。异常是干扰程序的正常流程的非正常(但并非完全是出乎意料)的事故。当查出这样的事故时,你的程序应该抛出一个异常。

throw语句中断函数的执行,它应该抛出一个exception对象,该对象可识别异常类型的name属性和一个描述性的message属性。你也可以添加其他属性。

该exception对象会被传递给一个try语句的catch语句中。

如果在try代码块内抛出了一个异常,控制权就会跳转到它的catch从句。

一个try语句只会有一个将捕获所有异常的catch代码块。如果你的处理手段取决于异常的类型,那么异常处理器必须检查异常对象的name属性以确定异常的类型。

7. 给类型增加方法

JavaScript允许给语言的基本类型增加方法,通过给Object.prototype添加方法可以使得该方法对所有对象可用。这样的方式对函数、数组、字符串、数字、正则表达式和布尔值同样适用。举例来说,我们可以通过给 Function.prototype增加方法使得该方法对所有函数可用:

通过给 Function.prototype 增加一个method方法,我们就不必键入 prototype 这个属性名。这个缺点也就被掩盖了。

JavaScript并没有单独的整数类型,因此有时候只提取数字中的整数部分是必要的。JavaScript本身提供的取整方法有些丑陋,我们可以通过给Number.prototype添加一个integer方法来改善它:

通过给基本类型增加方法,我们可以大大提高语言的表现力,因为JavaScript原型继承的动态本质,新的方法立刻被赋予到所有的值(对象实例)上,哪怕值(对象实例)是在方法创建之前就创建好了。

基本类型的原型是公共的结构,所以在类库混用时务必小心,一个保险的做法就是只在确定没有该方法的时候才添加它:

8、递归

递归函数是会直接或间接地调用自身的一种函数。递归将一个问题分解为一组相似的子问题,每一个都用一个寻常解去解决。一般来说,一个递归函数调用自身去解决它的子问题。 

一些语言提供了尾递归优化,这意味着如果一个函数返回自身递归调用的结果,那么调用的过程会被替换为一个循环,它可以显著提高速度。遗憾的是,JavaScript当前并没有提供尾递归优化,深度递归的函数可能会因为返回堆栈溢出而运行失败。

9、作用域

在编程语言中,作用域控制着变量与参数的可见性以及生命周期。这对程序员来说是一个重要的帮助,因为它减少了命名冲突,并且提供了自动内存管理。

大多数使用C语言语法的语言都拥有块级作用域。在一个代码块中定义的所有变量在代码块的外部是不可见的,定义在代码块中的变量会在代码块执行结束之后被释放掉,这是件好事。

糟糕的是,尽管代码块的语法似乎表现出它支持块级作用域,但实际上JavaScript并不支持,这个混淆之处可能成为错误源(好在ES6语法中有了块级作用域的概念)。

JavaScript确实有函数作用域,那意味着定义在函数中的参数和变量在函数外部是不可见的,而且在一个函数中的任何位置定义的变量在该函数中的任何地方都可见。

很多现代语言都推荐尽量迟地声明变量,但用在JavaScript上确实糟糕的建议,因为它缺少块级作用域,所以最好的做法是在函数顶部声明函数可能需要用到的所有变量。

10、闭包

作用域的好处是内部函数可以访问定义它们外部函数的参数和变量(除了 this 和 arguments)。这是一件非常好的事情。

一个更有趣的情形是内部函数拥有比它的外部函数更长的生命周期。

下图中,当我们调用quo时,它返回包含get_status方法的一个新对象,该对象的一个引用保存在myQuo中,即使quo已经返回了,但get_status方法仍然享有访问quo对象的status属性的特权,get_status方法并不是访问该参数的一个拷贝,它访问的就是该参数本身,这是可能的,因为该函数可以访问它被创建时所处的上下文环境,这被称为闭包。

下面再来看一下对于闭包最容易出错的一个例子,即for循环里有闭包的情况,闭包里的变量向上取值时,for循环已经执行完毕了,所以会导致一些错误,解决方法就是把需要的变量提前给到闭包函数,比如在外再套一层自执行函数,或使用let关键字代替var声明变量。

11、回调

函数可以让不连续事件的处理变得容易。例如:假定有一个序列,由用户交互开始,向服务器发送请求,最终显示服务器的响应,最淳朴的写法可能如下:

这种方式的问题在于网络上的同步请求会导致客户端进入假死状态,如果网络传输或服务器很慢,响应性的降低是不可接受的。更好的方式是发起异步请求,提供一个当服务器响应到达时将被调用的回调函数,异步的函数立即返回,这样客户端不会被阻塞。

我们传递了一个函数作为参数给send_request_asynchronously函数,它将在收到响应时被调用。

12、模块

我们可以使用函数或闭包来构造模块,模块是一个提供接口却隐藏状态或实现的函数或对象。通过使用函数去产生模块,我们几乎可以完全摒弃对全局变量的使用,从而缓解这个JavaScript最糟糕的特性之一带来的影响。

模块模式的一般形式是:一个定义了私有变量和函数的函数;利用闭包创建可以访问私有变量和函数的特权函数;最后返回这个特权函数,或者把它们保存到一个可以访问的地方。

使用模块模式就可以摒弃全局变量的使用,它促进了信息隐藏和其他优秀的设计实践。对于应用程序的封装,或者构造其他单例对象,模块模式非常有效。

13、级联

有一些方法没有返回值,例如一些设置或修改某个对象的状态却不返回任何值就是典型的例子,如果我们让这些对象返回this而不是undefined,就可以启动级联。在一个级联中,我们可以在一条单独的语句中一次调用同一个对象的很多方法。

级联可以产生出具备很强表现力的接口。

14、套用

函数也是值,从而我们可以用有趣的方式去操作函数值,套用允许我们将函数与传递给他的参数相结合去产生一个新的函数。

15、记忆

函数可以用对象去记住先前操作的结果,从而能避免无所谓的运算。这种优化被称为记忆,JavaScript要实现这种优化是很方便的。

你可能感兴趣的:(第四章《函数》)