💜《JavaScript 语言精粹》之函数篇

147 阅读11分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

所有的过失在未犯以前,都已定下应处的惩罚。 —— 威廉 · 莎士比亚,《一报还一报》(Measure for Measure)

4.1 函数对象

  • JavaScript 中的函数就是对象。对象是“名/值”对的集合并拥有一个连到原型对象的隐藏连接。对象字面量产生的对象连接到Object.prototype
  • 每个函数在创建时会附加两个隐藏属性:函数的上下文实现函数行为的代码。
  • 每个函数对象在创建时也随配一个 prototype 属性。它的值是一个拥有 constructor 属性且值为该函数的对象。
  • 函数可以保存在变量、对象和数组中。函数可以被当作参数传递给其他函数,函数也可以再返回函数。
  • 函数可以拥有方法
  • 函数的与众不同之处在于它们可以被调用

4.2 函数字面量

函数对象通过函数字面量来创建:

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

函数字面量包括 4 个部分:

  • 第 1 个部分是保留字 function
  • 第 2 个部分是函数名,它可以被省略。函数可以用它的名字来递归调用自己。如果没有给函数命名,比如上面这个例子,它被称为匿名函数
  • 第 3 个部分是包围在圆括号中的一组参数。多个参数用逗号分隔。这些参数的名称将被定义为函数中的变量。
  • 第 4 个部分是包围在花括号中的一组语句。这些语句是函数的主体,它们在函数被调用时执行。

4.3 调用

  • 调用一个函数会暂停当前函数的执行,传递控制权和参数给新函数。除了声明时定义的形式参数,每个函数还接收两个附加参数:thisarguments。参数 this 在面向对象编程中非常重要,它的值取决于调用的模式。

  • 在 JavaScript 中一共有 4 种调用模式:方法调用模式函数调用模式构造器调用模式apply 调用模式

  • 每个表达式产生一个参数值。每个参数值被赋予函数声明时定义的形式参数名。当实际参数的个数与形式参数的个数不匹配时,不会导致运行时错误。如果实际参数值过多了,超出的参数值会被忽略。如果实际参数值过少,缺失的值会被替换为 undefined。对参数值不会进行类型检查:任何类型值都可以被传递给任何参数。

4.3.1 方法调用模式

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

  • 方法可以调用 this 访问自己所属的对象,所以它能从对象中取值或对对象进行修改。this 到对象的绑定发生在调用的时候。通过 this 可取得它们所属对象的上下文的方法称为公共方法。

4.3.2 函数调用模式

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

var sum = add(3, 4);

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

myObject.double = function() {
	var that = this;
	var helper = function() {
		that.value = add(that.value, that.value)
	}
	helper();
}
myObject.double();
document.writeln(myObject.value)

4.3.3 构造器调用模式

  • JavaScript 是一门基于原型继承的语言。这意味着对象可以直接从其他对象继承属性。该语言是无类型的。
  • 如果在一个函数前面带上 new 来调用,那么背地里将会创建一个连接到该函数的prototype 成员到新对象,同时 this 会绑定到那个新对象上。
  • new 前缀也会改变 return 语句的行为。
// 创建一个名为 Quo 的构造器函数。它构造一个带有status 属性的对象。
var Quo = function(string) {
	this.status = string;
}
// 给 Que 的所有实例提供一个名为 get_status 的公共方法。
Que.prototype.get_status = function() {
	return this.status
}
// 构造一个 Que 实例
var myQue = new Que('confused');
document.writeln(myQue.get_status());  // 打印显示 “confused”

一个函数,如果创建的目的就是希望结合 new 前缀来调用,那么它就被称为构造器函数

4.3.4 Apply 调用模式

  • apply方法让我们构建一个参数数组传递给调用函数。它也允许我们选择this的值。apply方法接收两个参数,第 1 个是要绑定给this的值,第2个就是一个参数数组。
// 构造一个包含两个数字的数组,并将它们相加
var array = [3, 4];
var sum = add.apply(null, array);  // sum = 7
// 构造一个包含 status 成员的对象。
var statusObject = {
	status: 'A-OK'
};
// statusObject 并没有继承自 Quo.prototype,但我们可以在 statusObject 上调
// 用 get_status 方法,尽管 statusObject 并没有一个名为 get_status 的方法。
var status = Que.prototype.get_status.apply(statusObject);  // status = 'A-OK'

4.4 参数

  • 当函数被调用时,会得到一个“免费”配送的参数,那就是arguments数组。函数可以通过此参数访问所有它被调用时传递给它的参数列表,包括那些没有被分配给函数声明时定义的形式参数的多余参数。
  • 因为语言的一个设计错误,arguments并不是一个真正的数组。
  • 它只是一个“类似数组(array-like)”的对象。
  • arguments 拥有一个length属性,但它没有任何数组的方法。

4.5 返回

  • 当一个函数被调用时,它从第一个语句开始执行,并在遇到关闭函数体的 } 时结束。然后函数把控制权交还给调用该函数的程序。
  • return 语句可用来使函数提前返回。当return被执行时,函数立即返回而不再执行余下的语句。
  • 一个函数总是会返回一个值。如果没有指定返回值,则返回undefined
  • 如果函数调用时在前面加了new前缀,且返回值不是一个对象,则返回this(该新对象)。

4.6 异常

  • 异常是干扰程序的正常流程的不寻常的事故。
  • throw语句中断函数的执行。它应该抛出一个exception对象,该对象包含一个用来识别异常类型的name属性和一个描述性的message属性。
  • 如果在try代码块内抛出了一个异常,控制权就会挑战到它的catch从句。
  • 一个try语句只会又一个捕获所有异常的catch代码块。如果你的处理手段取决于异常的类型,那么异常处理器必须检查异常对象的 name 属性来确定异常的类型。

4.7 扩充类型的功能

  • JavaScript 允许给语言的基本类型扩充功能。在第 3 章中,我们已经看到,通过 Object.prototype添加方法,可以让该方法对所有对象都可用。这样的方式对函数、数组、字符串、数字、正则表达式和布尔值同样适用。
  • 通过给基本类型增加方法,我们可以极大地提高语言的表现力
  • 基本类型的原型是公用结构,所以在类库混用时务必小心。一个保险的做法就是只在确定没有该方法时才添加它。

4.8 递归

  • 递归函数就是会直接或间接地调用自身的一种函数。它把一个问题分解为一组相似的子问题,每一个都用一个寻常解去解决。
  • 一般来说,一个递归函数调用自身去解决它的子问题。
  • 递归函数可以非常高效地操作属性结构,比如浏览器端的文档对象模型(DOM)。每次递归调用时处理制定的树的一小段。
  • 一些语言提供了尾递归优化。这意味着如果一个函数返回自身递归调用的结果,那么调用的过程会被替换为一个循环,他可以显著提高速度。遗憾的是,JavaScript 当前并没有提供尾递归优化。深度递归的函数可能会因为堆栈溢出而运行失败。
  • 尾递归 是一种在函数的最后执行递归调用语句的特殊形式的递归。

4.9 作用域

  • 在编程语言中,作用域控制着变量与参数的可见性及声明周期。它减少了名称冲突,并且提供了自动内存管理。
  • 大多数类 C 语言语法的语言都拥有块级作用域。在一个代码块中定义的所有变量在代码块的外部是不可见的。定义在代码块中的变量在代码块执行结束后会被释放掉。
  • 糟糕的是,尽管 JavaScript 的代码块语法貌似支持会计作用域,但实际上 JavaScript 并不支持。

4.10 闭包

作用域的好处是内部函数可以访问定义它们的外部函数的参数和变量(除了 this 和 arguments)

内部函数拥有比它外部函数更长的生命周期。

闭包内容很重要,我总结不好。面试必考,你必看!

4.11 回调

函数使得对不连续事件的处理变得更容易。例如,假定有这么一个序列,由用户交互行为触发,向服务器发送请求,最终现实服务器的响应。

request = prepare_this_requrest();
response = send_request_synchronously(request);
display(response)
  • 这种方式的问题在于,网络上的同步请求会导致客户端进入假死状态。如果网络传输或服务器很慢,响应会慢到让人不可接受。

  • 更好的方式是发起异步请求,提供一个当服务器的响应到达时随机触发的回调函数。异步函数立即返回,这样客户端就不会被阻塞。

request = prepare_this_requrest();
send_request_synchronously(request, function (response) {
	display(response);
});

我们传递一个函数作为参数给send_request_synchronously函数,一旦接受到响应,它就会被调用。

4.12 模块

  • 我们可以使用函数和闭包来构造模块。模块是一个提供借口却隐藏状态与实现的函数或对象。通过使用函数产生模块,我们几乎可以完全摒弃全集变量的使用。

  • 模块模式利用了函数作用域和闭包来创建被绑定对象与私有成员的内容。

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

  • 使用模块模式就可以摒弃全局变量的使用,它促进了信息隐藏和其他u欧iu设计实践。模块模式也可以用来产生安全的对象。

4.13 级联

有一些方法没有返回值。如果我们让这些方法返回this而不是undefined,就可以启用级联。在一个级联中,我们可以在单独一条语句中依次调用同一个对象的很多方法。

getElement('myBoxDiv')
	.move(350, 150)
	.width(100)
	.height(100)
	.color('red')

级联技术可以产生出极富表现力的接口。它也能给那波构造“全能”接口的热潮降降温,一个接口没必要一次做太多事情。

4.14 柯里化

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

4.15 记忆

函数可以将先前操作的结果记录在某个对象里,从而避免无谓的重复运算。这种优化被称为记忆。JavaScript 的对象和数组要实现这种优化是非常方便的。