04 函数
东东 Lv4

本文主要介绍 JavaScript 函数

JavaScript 设计得最出色的就是它的函数的实现。它几乎接近于完美。但是,想必你也能预料到,JavaScript 的函数也存在瑕症。

函数包含一组语句,它们是 JavaScript 的基础模块单元,用于代码复用、信息隐藏和组合调用。

函数用于指定对象的行为。一般来说,所谓编程,就是将一组需求分解成一组函数与数据结构的技能。

函数对象

JavaScript 中的函数就是对象。

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

每个函数对象在创建时也随配有一个 prototype 属性。它的值是一个拥有 construetor 属性且值为该函数的对象。这和隐藏连接到 Function.prototype 完全不同。这个令人费解的构造过程的意义将会在下个章节中揭示。

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

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

注:JavaScript 创建一个函数对象时,会给该对象设置一个 “call” 属性。当 JavaScript 调用一个函数时,可理解为调用此函数的 “调用” 属性。

函数字面量

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

1
2
3
4
// 创建一个名为 add 的变量,并用来把两个数字相加的函数赋值给它
var add = function (a, b) {
return a + b;
};

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

  • 第 1 个部分是保留字 function。

  • 第 2 个部分是函数名,它可以被省略。函数可以用它的名字来递归地调用自己。此名字也能被调试器和开发工具用来识别函数。如果没有给函数命名,比如上面这个例子,它被称为匿名函数(anonymous)。

  • 第 3 个部分是包围在圆括号中的一组参数。多个参数用逗号分隔。这些参数的名称将被定义为函数中的变量。它们不像普通的变量那样将被初始化为 undefined,而是在该函数被调用时初始化为实际提供的参数的值。

  • 第 4 个部分是包围在花括号中的一组语句。这些语句是函数的主体,它们在函数被调用时执行。

函数字面量可以出现在任何允许表达式出现的地方。函数也可以被定义在其他函数中。一个内部函数除了可以访问自己的参数和变量,同时它也能自由访问把它嵌套在其中的父函数的参数与变量。

通过函数字面量创建的函数对象包含一个连到外部上下文的连接。这被称为闭包(closure)。它是 JavaScript 强大表现力的来源。

调用

调用一个函数会暂停当前函数的执行,传递控制权和参数给新函数。

调用运算符是跟在任何产生一个函数值的表达式之后的一对圆括号。圆括号内可包含零个或多个用逗号隔开的表达式。每个表达式产生一个参数值。每个参数值被赋予函数声明时定义的形式参数名。当实际参数(arguments)的个数与形式参数(parameters)的个数不匹配时,不会导致运行时错误。

如果实际参数值过多了,超出的参数值会被忽略。如果实际参数值过少,缺失的值会被替换为 undefined。对参数值不会进行类型检查:任何类型的值都可以被传递给任何参数。

除了声明时定义的形式参数,每个函数还接收两个附加的参数:this 和 arguments (注:函数对象的 apply 方法就是接收这两个参数)。参数 this 在面向对象编程中非常重要,它的值取决于调用的模式。

在 JavaScript 中一共有 4 种调用模式:方法调用模式函数调用模式构造器调用模式apply 调用模式。这些模式在如何初始化关键参数 this 上存在差异。

注:即调用时 this 参数的值应该怎么取:三个自动(隐式赋值),一个手动(主动调用 call 方法提供 this 参数)。

1) 方法调用模式

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建 myObject 对象,他有一个 value 属性和一个 increment 方法
// increment 方法接受一个可选的参数。如果参数不是数字,那么默认使用数字 1

var myObject = {
value: 0,
increment: function (inc) {
this.value += typeof inc === 'number' ? inc : 1;
}
}

myObject.increment();
document.writeln(myObject.value) // 1

myObject.increment(2);
document.writeln(myObject.value) // 3

方法可以使用 this 访问自己所属的对象,所以它能从对象中取值或对对象进行修改。

this 到对象的绑定发生在调用的时候。这个 “超级” 延迟的绑定(very late binding)使得函数可以对 this 高度复用。通过 this 可取得它们所属对象的上下文的方法称为公共方法(public method)。

2) 函数调用模式

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

1
var sum = add(3,4);                 //sum 的值为 7

以此模式调用函数时,this 被绑定到全局对象。

这是语言设计上的一个错误。倘若语言设计正确,那么当内部函数被调用时,this 应该仍然绑定到外部函数的 this 变量。这个设计错误的后果就是方法不能利用内部函数来帮助它工作,因为内部函数的 this 被绑定了错误的值,所以不能共享该方法对对象的访问权。

幸运的是,有一个很容易的解决方案:如果该方法定义一个变量并给它赋值为 this,那么内部函数就可以通过该变量访问到 this。按照约定,我把该变量命名为 that:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 给 myObject 增加一个 double 方法
myObject.double = function() {
var that = this; // 解决方法

var helper = function () {
that.value = add(that.value, that.value);
};

helper(); // 以函数的形式调用 helper
}

myObject.double();
document.writeln(myObject.value); // 6

3) 构造器调用模式

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

这偏离了当今编程语言的主流风格。当今大多数语言都是基于类的语言。尽管原型继承极富表现力,但它并未被广泛理解。

JavaScript 本身对它原型的本质也缺乏信心,所以它提供了一套和基于类的语言类似的对象构建语法。有类型化语言编程经验的程序员们很少有愿意接受原型继承的,并且认为借鉴类型化语言的语法模糊了这门语言真实的原型本质。真是两边都不讨好。

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

new 前缀也会改变 return 语句的行为。我们将会在后面看到更多的相关内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建一个名为 Quo 的构造器函数。它构造一个带有 status 属性的对象
var Quo = function (string) {
this.status = string;
}

// 给 Quo 的所有实例提供一个名为 get_status 的公共方法
Quo.prototype.get_status = function() {
return this.status;
};

// 构造一个 Quo 实例
// 注:这相当于
// var this = {}; // 创建新对象,this 绑定到该新对象 {}
// this.prototype = Quo.prototype; // 连接到 Quo 的 prototype 成员
// this.status = "confused // 然后执行函数内容,而这时 this 已经指向新建的那个对象了
var myQuo = new Quo("confused");
document.writeln(myQuo.get_status()); // 打印显示 "confused"

一个函数,如果创建的目的就是希望结合 new 前级来调用,那它就被称为构造器函数。按照约定,它们保存在以大写格式命名的变量里。

如果调用构造器函数时没有在前面加上 new,可能会发生非常精糕的事情,既没有编译时警告,也没有运行时警告,所以大写约定非常重要。

我不推荐使用这种形式的构造器函数。在下一章中我们会看到更好的替代方式。

4) apply 调用模式

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

apply 方法让我们构建一个参数数组传递给调用函数。它也允许我们选择 this 的值。apply 方法接收两个参数,第 1 个是要绑定给 this 的值,第 2 个就是一个参数数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 示例 1. 构造一个包含两个数字的数组,并将它们相加
var array = [3, 4];
var sum = add.apply(null, array); // sum 值为 7

// 示例 2. 构造一个包含 status 成员的对象
var statusObject = {
status: "A-OK"
};

// statusObject 并没有继承自 Quo.prototype,但我们可以在 statusObject 上调用
// get_status 方法,尽管 statusObject 并没有一个名为 get_status 的方法

// status 值为 'A-OK'
var = status = Quo.prototype.get_status.apply(statusObject);

参数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
// 构造一个将大量的值相加的函数
// 注意该函数内部定义的变量 sum 不会与函数外部定义的 sum 产生冲突
// 该函数只会看到内部的那个变量

var sum = function() {
var sum = 0;
for (var i = 0; i < arguments.length; i += 1) {
sum += arguments[i];
}
return sum;
}

document.writeln(sum(4, 8, 15, 16, 23, 42)) // 108

这不是一个特别有用的模式。在第 6 章中,我们将会看到如何给数组添加一个相似的方法来达到同样的效果。

因为语言的一个设计错误,arguments 并不是一个真正的数组。它只是一个 “类似数组(array-like)” 的对象。

arguments 拥有一个 length 属性,但它没有任何数组的方法。我们将在本章结尾看到这个设计错误导致的后果。

返回

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

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

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

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

注:实例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function func() {
return 12;
}

func.val = "val"
func.prototype.val_in_proto = "val in proto";

var a = func();
var b = new func();

console.log(typeof a); // number
console.log(a.val) // undefined
console.log(a.val_in_proto); // undefined

console.log(typeof b); // object
console.log(a.val) // undefined
console.log(a.val_in_proto); // val in proto

new 前缀修饰函数调用,导致隐式创建了一个新对象,this 参数被绑定到该新对象上,同时该新对象会连接到该函数的 prototype 成员。

并且因为返回值不是 object, 因此返回的是该新建的对象,这改变了 return 的行为。若返回值是对象,则 return 行为不会被改变,它会正常返回给定的那个对象。

异常

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

1
2
3
4
5
6
7
8
9
var add = function (a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw {
name: 'TypeError';
message: 'add needs number'
};
return a + b;
}
}

throw 语句中断函数的执行。它应该抛出一个 exception 对象,该对象包含一个用来识别异常类型的 name 属性和一个描述性的 message 属性。你也可以添加其他的属性。 该 exception 对象将被传递到一个 try 语句的 catch 从句:

1
2
3
4
5
6
7
8
9
10
11
// 构造一个 try_it 函数,以不正确的方式调用之前的 add 函数。
var try_it = function () {
try {
add("seven");
}
catch (e) {
document.writeln(e.name + ': ' + e.message);
}
}

try_it();

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

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

扩充类型的功能

JavaScript 允许给语言的基本类型扩充功能。通过给 Object.prototype 添加方法,可以让该方法对所有对象都可用。这样的方式对函数、数组、字符串、数字、正则表达式和布尔值同样适用。

举例来说,我们可以通过给 Function.prototype 增加方法来使得该方法对所有函数可用:

1
2
3
4
Function.prototype.method = function (name, func) {
this.prototype[name] = func; // 注:这里的 this 指向的是特定的函数对象
return this;
};

通过给 Function.prototype 增加一个 method 方法,我们下次给对象增加方法的时候就不必键入 prototype 这几个字符,省掉了一点麻烦。

JavaSeript 没有专门的整数类型,但有时候确实只需要提取数字中的整数部分。JavaSeript 本身提供的取整方法有些丑陋。我们可以通过给 Number·prototype 增加一个 integer 方法来改善它。它会根据数字的正负来判断是使用 Math.ceiling 还是 Math.floor。

1
2
3
4
5
6
7
8
Number.method('integer', function() {
// 注:根据前面介绍的,函数被调用时 this 参数有四种赋值情况,
// 在这里,该匿名函数注册到 Number 原型后,最终被 num 通过 num.integer 调用
// 也就说是通过对象调用的,因此其指向就是对象本身,因此这里 this 即数字本身
return Math[this < 0 ? 'ceil' : 'floor'](this);
});

console.log((-10 / 3).integer()); // -3

JavaScript 缺少一个移除字符串首尾空白的方法。这个小疏忽很容易弥补:

1
2
3
4
5
String.method('trim', function () {
return this.replace(/^\s+|\s+$/g, '');
});

console.log("\"" + " neat ".trim() + "\"");

我们的 trim 方法使用了一个正则表达式。我们将在后面看到更多关于正则表达式的内容。

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

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

1
2
3
4
5
6
Function.prototype.method = function (name, func) {
if (!this.prototype[name]) {
this.prototype[name] = func;
}
return this;
}

另一个要注意的就是 for in 语句用在原型上时表现很槽糕。我们在前面已经看到了几个减轻这个问题的影响的办法:我们可以使用 hasOwnProperty 方法筛选出继承而来的属性,或者我们可以查找特定的类型。

递归

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

递归函数可以非常高效地操作树形结构,比如浏览器端的文档对象模型(DOM)。每次递归调用时处理指定的树的一小段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 定义 walk_the_DOM 函数,它从某个指定的节点开始,按 HTML 源码中的顺序
// 访问该树的每个节点。它会调用一个函数,并依次传递每个节点给它。
// walk_the_DOM 调用自身去处理每一个子节点
var work_the_DOM = function walk(node, func) {
func(node);
node = node.firstChild;
while (node) {
walk(node, func);
node = node.nextSibling;
}
};

// 定义 getElementsByAttribute 函数。它以一个属性名称字符串
// 和一个可选的匹配值作为参数。
// 它调用 walk_theDOM,传递一个用来查找节点属性名的函数作为参数。
// 匹配的节点会系加到一个结果数组中。
var getElementsByAttribute = function (att, value) {
var retult = [];

work_the_DOM(document.body, function (node) {
var actual = node.nodeType === 1 &&
node.getElementsByAttribute(att);
if (typeof actual == 'string' &&
(actual === value || typeof value !== 'string')) {
retult.push(node);
}
});
}

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

1
2
3
4
5
6
7
8
9
10
11
12
// 构建一个带尾递归的函数,因为它会返回自身调用的结果,所以是尾递归

// JavaScript 当前没有对这种形式的递归做出优化
var factorial = function factorial(i, a) {
a = a || 1;
if (i < 2)
return a;

return factorial(i - 1, a * i);
}

document.writeln(factorial(4)); // 24

作用域

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

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

精糕的是,尽管 JavaScript 的代码块语法貌似支持块级作用域,但实际上 JavaScript 并不支持。这个混淆之处可能成为错误之源。

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

很多现代语言都推荐尽可能延迟声明变量。而用在 JavaScript 上的话却会成为糟糕的建议,因为它缺少块级作用域。所以,最好的做法是在函数体的顶部声明函数中可能用到的所有变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var foo = function() {
var a = 3, b = 5;
let d = 7;

var bar = function() {
// 在函数作用域中重新定义 a,外层 a 不会被覆盖
var a = 4;

// 在函数作用域中定义的全新变量函数返回后不可访问
var c = 10;

// 在函数作用域中可访问外部变量
b = 12;
};

bar();
console.log(a + " " + b); // 3 12
console.log(typeof c); // undefined

if(true) {
// 由于缺少块级作用域,在前面定义的变量 a 将被覆盖
var a = 6;

// 在块级作用域中定义的全新变量将在整个函数共享,即出了块级作用域变量依旧存在
var c = 10;

// ES6 引入的 let 解决了块级作用域变量的覆盖和共享问题
let d = 9;
let e = 1;
}

console.log(a + " " + b + " " + d); // 6 12 7
console.log(typeof c); // number
console.log(typeof e); // undefined
};

foo();

注:对于没有声明就直接赋值的变量,默认为全局变量,不论这个变量在哪被使用,比如 f = 12;

闭包

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

我们的 getElementsByAttribute 函数可以工作,是因为它声明了一个 results 变量,而传递给 walk_the_DOM 的内部函数也可以访问 results 变量。

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

注:例如假设只是注册了一个匿名函数后立刻返回了,而匿名函数引用了外部函数的变量,这时外部函数已经结束返回了,而该匿名函数后来才被调用,那么它引用的那个其内存没被释放,变量还能访问

之前,我们构造了一个 myObject 对象,它拥有一个 value 属性和一个 increment 方法。假定我们希望保护该值不会被非法更改。

和以对象字面量形式去初始化 myobject 不同,我们通过调用一个函数的形式去初始化 myObject,该函数会返回一个对象字面量。

函数里定义了一个 value 变量。该变量对 increment 和 getValue 方法总是可用的,但函数的作用域使得它对其他的程序来说是不可见的。

1
2
3
4
5
6
7
8
9
10
11
12
var myObject = (function () {
var value = 0;

return {
increment: function (inc) {
value += typeof inc === 'number' ? inc : 1;
},
getValue: function() {
return value;
}
};
}());

前面说过,函数字面量就是一个表达式,该表达式的值为一个函数对象。

这里使用匿名的方式定义了一个函数字面量,然后利用括号将整个函数定义作为表达式整体,即 (表达式)(),而该表达式返回一个函数对象,因此可以直接使用 () 来对匿名函数对象进行调用。从而将函数中的返回的对象直接赋值给 myObject

我们并没有把一个函数赋值给 myObject。. 我们是把调用该函数后返回的结果赋值给它。注意最后一行的 ()。该函数返回一个包含两个方法的对象,并且这些方法继续享有访问 value 变量的特权。

本章之前的 Quo 构造器产生一个带有 status 属性和 get_status 方法的对象。但那看起来并不是十分有趣。为什么要用一个 getter 方法去访问你本可以直接访问到的属性呢?如果 status 是私有属性,它才是更有意义的。所以,让我们定义另一种形式的 quo 函数来做此事:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建一个名为 quo 的构造器函数
// 它构造出带有 get_status 方法和 status 私有属性的一个对象
var quo = function (status) {
return {
get_status: function () {
return status;
}
};
};

// 构造一个 quo 实例
var myQuo = quo("amazed");
console.log(myQuo.get_status());

这个 quo 函数被设计成无须在前面加上 new 来使用,所以名字也没有首字母大写。

当我们调用 quo 时,它返回包含 get_status 方法的一个新对象。该对象的一个引用保存在 myQuo 中。即使 quo 已经返回了,但 get_status 方法仍然享有访问 quo 对象的 status 属性的特权。

get_status 方法并不是访问该参数的一个副本,它访问的就是该参数本身。这是可能的,因为该函数可以访问它被创建时所处的上下文环境。这被称为闭包

让我们来看一个更有用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义一个函数,它设置一个 DOM 节点为黄色,然后把它渐变为白色

var fade = function (node) {
var level = 1;
var setp = function() {
var hex = level.toString(16);
node.style.backgroundColor = '#FFFF' + hex + hex;
if(level < 15) {
level += 1;
setTimeout(setp, 100);
}
};
setTimeout(setp, 100);
};

fade(document.body);
我们调用 fade,把 document.body 作为参数传递给它(HTML

标签所创建的节点)。

fade 函数设置 level 为 l。它定义了一个 step 函数,接着调用 setTimeout,并传递 step 函数和一个时间(100 毫秒)给它。然后它返回,fade 函数结束。

在大约十分之一秒后,step 函数被调用。它把 fade 函数的 level 变量转化为 16 位字符。

接着,它修改 fade 函数得到的节点的背景颜色。然后查看 fade 函数的 level 变量。如果背景色尚未变成白色,那么它增大 fade 函数的 level 变量,接着用 setTimeout 预定让它自己再次运行。

step 函数很快再次被调用。但这次,fade 函数的 level 变量值变成 2。

fade 函数在之前已经返回了,但只要 fade 的内部函数需要,它的变量就会持续保留

为了避免下面的问题,理解内部函数能访问外部函数的实际变量而无须复制是很重要的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 糟糕的例子

// 构造一个函数,用错误的方式给一个数组中的节点设置事件处理程序
// 当点击一个节点时,按照预期,应该弹出一个对话框显示节点的序号,
// 但它总是会显示节点的数目。

var add_the_handlers = function (nodes) {
for (var i = 0; i < nodes.length; i++) {
nodes[i].onclick = function (e) {
alert(i);
};
}
};

var buttons = (function() {
var buttons = [];
for (var i = 0; i < 5; i++) {
var btn = document.createElement("input");
btn.type = "button";
btn.value = "Button " + i;
buttons.push(btn);
document.body.appendChild(btn);
}
return buttons;
})();

add_the_handlers(buttons);

add_the_handlers 函数的本意是想传递给每个事件处理器一个唯一值(i)。但它未能达到目的,因为事件处理器函数绑定了变量 i 本身,而不是函数在构造时的变量 i 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 改良后的例子

// 构造一个函数,用正确的方式给数组中的节点设置事件处理程序
// 点击一个节点,将会弹出一个对话框显示节点的序号

var add_the_handlers = function (nodes) {
var helper = function (i) {
return function (e) {
alert(i);
};
};
for (var i = 0; i < nodes.length; i += 1) {
nodes[i].onclick = helper(i);
}
};

避免在循环中创建函数,它可能只会带来无谓的计算,还会引起混滑,正如上面那个槽糕的例子。我们可以先在循环之外创建一个辅助函数,让这个辅助函数再返回一个绑定了当前 1 值的函数,这样就不会导致混滑了。

以下是个人理解

第一种方法中只是定义一个匿名函数字面量,并没有实际执行函数。i 没有参与语句执行过程,因此这时字面量中仅仅是对 i 的引用,没有直接取得 i 的值。

倘若赋值过程执行了函数,情况就会不同:

1
2
3
4
5
6
7
8
9
var add_the_handlers = function (nodes) {
for (var i = 0; i < nodes.length; i++) {
nodes[i].onclick = function (i) {
return function (e) {
alert(i);
};
} (i);
}
};

同样是匿名函数,但这次匿名函数不仅仅只是定义,而是定义的同时执行了语句,i 的值被传进去,参与了语句执行过程,这时发生了赋值操作,因此这时 alert(i); 得到的 i 值实际上是匿名函数的参数 i, 这个 i 是复制自每一次循环中的 i 值。

第二种方法只是把上面匿名函数实名化,其原理是一致的。

若把匿名函数中的 i 去掉,这时因为 i 并没有参与语句执行过程,匿名函数中返回的函数也只是字面量定义,没有实际执行。所以点击后 i 值是同一个。

但是若在函数中定义一个变量,并对 i 进行取值操作情况又会有所不同:

1
2
3
4
5
6
7
8
9
10
var add_the_handlers = function (nodes) {
for (var i = 0; i < nodes.length; i++) {
nodes[i].onclick = function () {
var m = i;
return function (e) {
alert(m);
};
} ();
}
};

这次因为在赋值过程中 i 参与了执行,m 取得了 i 的当前值并保留在自身函数作用域中,返回的匿名函数字面量定义中再对 m 进行引用,但这次所有 m 值都不同,点击时每次显示的值也就不同了。

回调

函数使得对不连续事件的处理变得更容易。例如,假定有这么一个序列,由由用户交互行为触发,向服务器发送请求,最终显示服务器的响应。最自然的写法可能会是这样的:

1
2
3
request = prepare_the_request();
response = send_request_synchronously(request);
display(response);

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

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

1
2
3
4
request = prepare_the_request();
send_request_asynchronously(request, function (response) {
display(response);
});

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

模块

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

举例来说,假定我们想要给 string 增加一个 deentityify 方法。它的任务是寻找字符串中的 HTML 字符实体并把它们替换为对应的字符。这就需要在一个对象中保存字符实体的名字和它们对应的字符。但我们该在哪里保存这个对象呢?

我们可以把它放到一个全局变量中,但全局变量是魔鬼。我们可以把它定义在该函数的内部,但是那会带来运行时的损耗,因为每次执行该函数的时候该字面量都会被求值一次。理想的方式是把它放入一个闭包,而且也许还能提供一个增加更多字符实体的扩展方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
String.method('deentityify', function() {
var entity = {
quot: '"',
lt : '<',
gt : '>',
};

// 返回 deentityify 方法
return function () {
// 这才是 deentityify 方法。它调用字符串的 replace 方法,
// 查找 & 开头和 ; 结束的子字符串。如果这些字符可以在字符实体表中找到,
// 那么就将该字符实体替换为映射表中的值。

return this.replace(/&([^&;]+);/g, function (a, b) {
var r = entity[b];
return typeof r == 'string' ? : r : a;
}
};
}());

document.writeln('&lt;&quot;&gt;'.deentityify()); // <">

请注意最后一行。我们用 () 运算法立刻调用我们刚刚构造出来的函数。这个调用所创建并返回的函数才是 deentityify 方法。

模块模式利用了函数作用域和闭包来创建被绑定对象与私有成员的关联,在这个例子中,只有 deentityify 方法有权访问字符实体表这个数据对象。

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

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

模块模式也可以用来产生安全的对象。假定我们想要构造一个用来产生序列号的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var serial_maker = function() {

// 返回一个用来产生唯一字符串的对象。
// 唯一字符串由两部分组成:前级+序列号。
// 该对象包含一个设置前级的方法,一个设置序列号的方法
// 和一个产生唯一字符串的 gensym 方法。

var prefix = '';
var seq = 0;

return {
set_prefix: function (p) {
prefix = String(p);
},
set_seq: function (s) {
seq = s;
},
gensym: function () {
var result = prefix + seq;
seq += 1;
return result;
}
};
};

var seqer = serial_maker();
seqer.set_prefix('Q');
seqer.set_seq(1000);
var unique = seqer.gensym(); // unique 是 "Q1000"

seger 包含的方法都没有用到 this 或 that,因此没有办法损害 seqer.。除非调用对应的方法,否则没法改变 prefi×或 seg 的值。seqer 对象是可变的,所以它的方法可能会被替换掉,但替换后的方法依然不能访问私有成员。seqer 就是一组函数的集合,而且那些函数被授予特权,拥有使用或修改私有状态的能力。

如果我们把 seqer.gensym 作为一个值传递给第三方函数,那个函数能用它产生唯一字符串,但却不能通过它来改变 prefix 或 seg 的值。

级联

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
getElement('myBoxDiv')
.move(350, 150)
.width(100)
.height(100)
.color('red')
.border('10px outset')
.padding('4px')
.appendText("Please stand by")
.on('mousedown', function (m) {
this.startDrag(m, this.getNinth(m));
})
.on('mousemove', 'drag')
.on('mouseup', 'stopDrag')
.later(2000, function () {
this
.color('yellow')
.setHTML("What hath God wraught?")
.s11de(400, 40, 200, 200);
})
.tip('This box is resizeable');

在这个例子中,getElement 函数产生一个对应于 id="myBoxDiv" 的 DOM 元素且给其注入了其他功能的对象。该方法允许我们移动元素,修改它的尺寸和样式,并添加行为。这些方法每一个都返回该对象,所以每次调用返回的结果可以被下一次调用所用。

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

柯里化 (局部套用)

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

1
2
var add1 = add.curry(1); 
document.writeln(add1(6)); // 7

add1 是把 1 传递给 add 函数的 curry 方法后创建的一个函数。add1 函数把传递给它的参数的值加 1。JavaScript 并没有 curry 方法,但我们可以给 Function.prototype 扩展此功能:

1
2
3
4
5
6
Function.method('curry',function () {
var args = arguments, that = this;
return function () {
return that.apply(null, args.concat(arguments));
};
}); //有些事好像看起来不太对头…

curry 方法通过创建一个保存着原始函数和要被套用的参数的闭包来工作。它返回另一个函数,该函数被调用时,会返回调用原始函数的结果,并传递调用 curry 时的参数加上当前调用的参数。它使用 Array 的 concat 方法连接两个参数数组。

糟糕的是,就像我们先前看到的那样,arguments 数组并非一个真正的数组,所以它并没有 concat 方法。要避开这个问题,我们必须在两个 arguments 数组上都应用数组的 slice 方法。这样产生出拥有 concat 方法的常规数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Function.method('curry', function () {
var slice = Array.prototype.slice,
args = slice.apply(arguments),
that = this;

return function () {
return that.apply(null, args.concat(slice.apply(arguments)));
};
});

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

var add1 = add.curry(1);
document.writeln(add1(6)); // 7

记忆

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

比如说,我们想要一个递归函数来计算 Fibonacci 数列。一个 Fibonacci 数字是之前两个 Fibonacci 数字之和。最前面的两个数字是 0 和 1.

1
2
3
4
5
6
7
var fibonacci = function (n) {
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n -2);
};

for(var i = 0; i <= 10; i += 1) {
document.writeln('// ' + i + ': ' + fibonacci(i));
}

这样是可以工作的,但它做了很多无谓的工作。fibonacci 函数被调用了 453 次。我们调用了 11 次,而它自身调用了 442 次去计算可能已被刚计算过的值。如果我们让该函数具备记忆功能,就可以显著地减少运算量。

我们在一个名为 memo 的数组里保存我们的存储结果,存储结果可以隐藏在闭包中。当函数被调用时,这个函数首先检查结果是否已存在,如果已经存在,就立即返回这个结果。

1
2
3
4
5
6
7
8
9
10
11
var fibonacci = function () {
var memo = [0, 1];
return function fib (n) {
var result = memo[n];
if (typeof result !== 'number') {
result = fib(n - 1) + fib(n - 2);
memo[n] = result;
};
return result;
};
}();

这个函数返回同样的结果,但它只被调用了 29 次。我们调用了它 11 次,它调用了自己 18 次去取得之前存储的结果。

我们可以把这种技术推而广之,编写一个函数来帮助我们构造带记忆功能的函数。memoizer 函数取得一个初始的 memo 数组和 formula 函数。它返回一个管理 meno 存储和在需要时调用 formula 函数的 recur 函数。我们把这个 recur 函数和它的参数传递给 formula 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var memoizer = function (memo, formula) {
return function recur(n) {
var result = memo[n];
if (typeof result !== 'number') {
result = formula(recur, n);
memo[n] = result;
};
return result;
};
};

var fibonacci = memoizer([0, 1], function (recur, n) {
return recur (n - 1) + recur (n - 2);
});

通过设计这种产生另一个函数的函数,极大地减少了我们的工作量。例如,要产生一个可记忆的阶乘函数,我们只需提供基本的阶乘公式即可:

1
2
3
var factorial = memoizer([1, 1], function (recur, n) {
return n * recur (n - 1);
});

注: 调用函数 memoizer 传了一个匿名数组,该匿名数组在返回的闭包中被引用,生命周期也会变长

源文件来自于

 评论