JavaScript语言精粹 笔记02 函数
函数
函数对象
函数字面量
调用
参数
返回
异常
给类型增加方法
递归
作用域
闭包
回调
模块
级联
套用
记忆
函数
在JS中函数就是对象。对象是“名/值”对的集合并拥有一个连接到原型对象的隐藏连接。对象字面量产生的对象连接到Object.prototype。函数对象连接到Function.prototype(该原型本身连接到Object.prototype)。每个函数在创建时附有两个附加的隐藏属性:函数上下文和实现函数行为的代码。
因为函数是对象,所以它可以像任何其他的值一样被使用。函数可以存放在变量、对象和数组中。函数可以被当做变量传递给其他函数,函数亦可以再返回函数。函数可以拥有方法。
函数的与众不同之处在于它们可以被调用。
函数对象可以通过函数字面量来创建:
//创建一个名为add的变量,并用两个数字相加的函数赋值给它 var add = function(a,b){ return a+b; };
函数字面量可以出现在任何允许表达式出现的地方。函数也可以被定义在其他函数中。
调用一个韩式时将暂停当前函数的运行,传递控制权和参数给新函数。除了声明时定义的形式参数,每个函数接收两个附加的参数:this和arguments。参数this在面型对象编程中非常重要,它的值取决于调用的模式。JS中一共有4种调用模式:方法调用模式、函数调用模式、构造器调用模式和apply调用模式。这些模式在如何初始化关键参数this上存在差异。
调用时,当实际参数个数与形式参数个数不匹配时不会导致运行时错误。如果实际参数值过多了,超出的参数值将被忽略。如果实际参数值过少,缺失的值将会替换为undefined。对参数值不会进行类型检查,任何类型的值都可以被传递给参数。
方法调用模式
当一个函数被保存为对象的一个属性时,我们称它为一个方法。this被绑定到该对象。如果一个调用表达式包含一个属性存取表达式(.表达式或[subscript]下标表达式),那么它被当做一个方法来调用。
//创建一个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到对象的绑定发生在调用的时候。这个超级迟绑定使得函数可以对this高度复用。通过this可以取得它们所属上下文的方法称为公共方法。
函数调用模式
当一个函数并非一个对象属性时,那么它被当作一个函数来调用:
var sum = add(3,4);//sum的值为7
当函数以此模式调用时,this被绑定到全局对象。这是语言设计上的一个错误。倘若语言设计正确,当内部函数调用时,this应该仍然绑定到外部函数的this变量。这个设计的错误后果是方法不能利用内部函数来帮助它工作,因为内部函数的this被绑定了错误的值,所以不能共享该方法的访问权。幸运的是,有一个很容易的解决方案:如果该方法定义一个变量并给它赋值为this,那么内部函数就可以通过那个变量访问到this。
//给myObject增加一个double方法 myObject.double=function(){ vat that=this;//解决方法 var helper=function(){ that.value=add(that.value,that.value); } helper();//以函数的形式调用helper。 } //以方法的形式调用double。 myObject.double(); document.writeln(myObject.getValue());//6
构造器调用模式
JS是一门基于原型继承的语言。这意味着对象可以直接从其他对象继承属性。该语言是无类别的。这偏离了当今编程语言的主流。当今大多数语言都是基于类的语言。尽管原型继承有着强大的表现力,但它并不能被广泛理解。
如果在函数前面带上new来调用,那么将创建一个隐藏连接到该函数的prototype成员的新对象,同时this将会被绑定到那个新对象上。
//创建一个名为Quo的构造器函数,它构造一个带有status属性的对象。 var Quo = function(string){ this.status=string; } //给Quo的所有实例提供一个名为get_status的公共方法。 Quo.prototype.get_status=function(){ return this.status; } //构造一个Quo实例 var myQuo=new Quo("confused"); document.writeln(myQuo.get_status());//confused
目的就是结合new前缀调用的函数被称为构造器函数。按照约定,它们保存在以大写格式命名的变量里。如果调用构造器函数时没有在前面加上new,可能会方式非常糟糕的事情,即没有编译警告,也没有运行时警告,所以大写约定非常重要。不推荐使用这种形式的构造器函数。
Apply调用模式
JS是一门函数式的面向对象编程语言,所以函数可以拥有方法。
apply方法让我们构建一个参数数组并用其去调用函数。它允许我们选择this的值。apply方法接收两个参数。第一个是将绑定给this的值。第二个是一个参数数组。
//构造一个包含两个数字的数组,并将它们相加。 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 = Quo.prototype.get_status.apply(statusObject); // status is 'A-OK'
当函数调用时,会得到一个免费奉送的参数,那就是arguments数组。通过它函数可以访问所有它被调用时传递给它的参数列表,包括那些没有被分配给函数声明时定义的形式参数的多余参数。这使得编写一个无需指定参数个数的函数成为可能:
var sum = function ( ) { var i, sum = 0; for (i = 0; i < arguments.length; i += 1) { sum += arguments[i]; } return sum; }; document.writeln(sum(4, 8, 15, 16, 23, 42)); // 108
arguments并不是一个真正的数组。它是一个类似数组的对象。arguments拥有一个length属性,但它缺少所有数组的方法。
一个函数总是会返回一个值。如果没有指定返回值,则返回undefined。
如果函数在前面加上new前缀的方式来调用,且返回值不是一个对象,则返回this(该新对象)。
var add = function (a, b) { if (typeof a !== 'number' || typeof b !== 'number') { throw { name: 'TypeError', message: 'add needs numbers' }; } return a + b; }
throw语句中断函数的执行,它应该抛出一个exception对象,该对象包含name属性和message属性,你也可以添加其他属性。
该exception对象被传递到一个try语句的catch从句:
//构造一个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属性以确定异常的类型。
JS允许给语言的基本类型添加方法。通过给Object.prototype添加方法来使得该法对所有对象可用。这样的方式对函数、数组、字符串、数字、正则表达式、布尔值同样适用。
举例来说,我们可以通过给Function.prototype增加方法使得该方法对说有函数可用:
Function.prototype.method = function (name, func) { this.prototype[name] = func; return this; };
通过给Function.prototype增加一个method方法,我们就不必键入prototype这个属性名。
JS没有单独的整数类型,我们给Number.prototype添加一个integer方法。
Number.method('integer',function(){ return Math[this<0?'ceil':'floor'](this); }); document.writeln((-10/3).integer());//-3
再举一个例子,给String添加一个trim方法:
String.method('trim', function ( ) { return this.replace(/^\s+|\s+$/g, ''); });
document.writeln('"' + " neat ".trim( ) + '"')
基本类型的原型是公共的结构,所以在类库混用时务必要小心。一个保险的做法就是在确定没有该方法时才添加它。
Function.prototype.method = function (name, func) { if (!this.prototype[name]) { this.prototype[name] = func; } };
一些语言提供了尾递归优化。这意味着如果一个函数返回自身递归调用的结果,那么调用过程会被替换为一个循环,它可以显著提高速度。但是,JS并没有提供尾递归优化。深度递归的函数可能会因为返回堆栈溢出而运行失败。
尽管代码块的语法似乎表现出JS支持块级作用域,但实际上JS并不支持。
JS却实有函数作用域。在函数中定义的参数和变量在函数外部是不可见的,而且在一个函数中的任何位置定义的变量在该函数中的任何地方都是可见的。
很多现代语言都推荐尽可能迟地声明变量。而使用在JS上的话却会成为糟糕的建议,因为它缺少块级作用域。所以,最好的做法是在函数体的顶部声明函数中可能用到的所有变量。
闭包,指的是词法表示包括不被计算的变量的函数,也就是说,函数可以使用函数之外定义的变量。
简单的闭包实例
在 ECMAScript 中使用全局变量是一个简单的闭包实例。请思考下面这段代码:
var sMessage = "hello world"; function sayHelloWorld() { alert(sMessage); } sayHelloWorld();
在上面这段代码中,脚本被载入内存后,并没有为函数 sayHelloWorld() 计算变量 sMessage 的值。该函数捕获 sMessage 的值只是为了以后的使用,也就是说,解释程序知道在调用该函数时要检查 sMessage 的值。sMessage 将在函数调用 sayHelloWorld() 时(最后一行)被赋值,显示消息 "hello world"。
复杂的闭包实例
在一个函数中定义另一个会使闭包变得更加复杂。例如:
var iBaseNum = 10; function addNum(iNum1, iNum2) { function doAdd() { return iNum1 + iNum2 + iBaseNum; } return doAdd(); }
这里,函数 addNum() 包括函数 doAdd() (闭包)。内部函数是一个闭包,因为它将获取外部函数的参数 iNum1 和 iNum2 以及全局变量iBaseNum 的值。 addNum() 的最后一步调用了 doAdd(),把两个参数和全局变量相加,并返回它们的和。
这里要掌握的重要概念是,doAdd() 函数根本不接受参数,它使用的值是从执行环境中获取的。
可以看到,闭包是 ECMAScript 中非常强大多用的一部分,可用于执行复杂的计算。
提示:就像使用任何高级函数一样,使用闭包要小心,因为它们可能会变得非常复杂。
以上关于闭包的内容摘自W3School
内部函数可以访问定义它们的函数外部的参数和变量(除了this和arguments)。
var myObject = function ( ) { var value = 0; return { increment: function (inc) { value += typeof inc === 'number' ? inc : 1; }, getValue: function ( ) { return value; } }; }( );
和对象字面量形式去初始化myObject不同,我们通过调用一个函数的形式去初始化myObject,该函数将返回一个对象字面量。此函数定义了一个value变量。该变量对increment和getValue方法总是可用的,但函数的作用域使得它对其他的程序来说是不可见的。
var quo = function (status) { return { get_status: function ( ) { return status; } }; }; //构造一个quo实例 var myQuo = quo("amazed"); document.writeln(myQuo.get_status( ));
当调用quo时,它返回博阿含get_status方法的一个新对象。该方法的一个引用保存在myQuo中。即使quo已经返回了,但get_status方法仍然享有访问quo对象的status属性的特权。get_status方法并不是访问该参数的一个拷贝,它访问的就是该参数本身。该参数可以访问它被创建时所处的上下文环境。这被称为闭包。
为了避免下面的问题,理解内部函数能访问外部函数的实际变量而无须复制是很重要的:
//糟糕的例子
var nodes=document.getElementsByTagName("h3");
var i;
for (i = 0; i < nodes.length; i++) {
nodes[i].onclick = function (i) {
console.log(i);//想要输出nodes[i]的序号,但实际上输出的是nodes个数
};
};
}
解决方案:
var nodes=document.getElementsByTagName("h3"); var i; for (i = 0; i < nodes.length; i += 1) { nodes[i].onclick = function (e) { return function () { console.log(e); }; }(i);//将i传入函数中 }
网络上的同步请求将导致客户端进入假死状态。如果网络传输或服务器很慢,响应性的降低将是不可能接受的。所以要发起异步请求,提供一个当服务器响应到达时将被调用的回调函数。异步的函数立即返回,这样客户端就不会被阻塞。
可以用函数和闭包来构造模块。模块是一个提供接口却隐藏状态与实现的函数或对象。通过使用函数去产生模块,我们几乎可以完全摒弃全局变量的使用。举例来说,给Stirng增加一个方法deentityify方法,它的任务是寻找字符串中的HTML字符实体并替换为它们对应的字符。
String.prototype.deentityify= function ( ) { // 字符实体表,它映射字符实体的名字到对应的字符 var entity = { quot: '"', lt: '<', gt: '>' }; //返回deentityify方法 return function ( ) { //这才是deentityify方法,它调用字符串的replace方法。 return this.replace(/&([^&;]+);/g, function (a, b) {//a对应匹配/&([^&;]+);/的字符串,b对应匹配([^&;]+)的字符串 var r = entity[b]; return typeof r === 'string' ? r : a; } ); }; }( ); console.log('<">'.deentityify( ));//输出:<">
模块模式利用了函数作用域和闭包来创建绑定对象与私有成员的关联。这个例子中,只有deentityify方法有权访问字符实体表这个数据对象。
模块模式的一般形式是:一个定义了私有变量和函数的函数;利用闭包创建可以访问私有变量和函数的特权函数;最后返回这个特权函数,或者把它们保存到一个可以访问到的地方。使用模块模式可以摒弃全局变量的使用,它促进了信息隐藏和其他优秀的设计实践。
有一些方法没有返回值。如果我们让这些方法返回this而不是undefined,就可以启动级联。在一个级联中,我们可以在单独一条语句中依次调用同一个对象的多个方法。这在jQuery中很常见。
套用允许我们将函数与传递给它的参数结合去产生一个新的函数。不知道有什么用。
函数可以用对象去记住前面操作的结果,从而避免无谓的运算。这种优化被称为记忆。
优化后的Fibonacci数列函数:
var fibonacci = function ( ) { var memo = [0, 1]; var fib = function (n) { var result = memo[n]; if (typeof result !== 'number') { result = fib(n - 1) + fib(n - 2); memo[n] = result; } return result; }; return fib; }( );
减少了不必要的运算。
参考:《JavaScript语言精粹》Douglas Crockford著 赵泽欣 鄢学鹍 译
转载请注明出处: