JavaScript权威指南--函数
要点概述
函数是这样一段代码,它只定义一次,但可能被执行或调用任意次。
javascript函数是参数化的:函数定义会包括一个形参(parmeter)标识符列表。这些参数在函数中像局部变量一样工作。函数调用会给形参提供实参的值。函数使用它们实参的值计算返回值,成为该函数的调用表达式的值。除了实参之外,每次调用还会拥有一个值——本地调用的上下文——这就是this关键字值。
如果函数挂载在一个对象上,作为对象的一个属性,就称为它为对象的方法。当通过这个对象来调用函数时,该对象就是此次调用的上下文(context),也就是该函数的this值。用于初始化一个新创建对象的函数称为构造函数(constructor)。
在javascript中,函数即对象,程序可随意操作它们。比如,javascript可以把函数赋值给变量,或者作为参数传递给其他函数。因为函数就是对象,所以可以给他们设置属性,甚至调用它们的方法。
javascript的函数可以嵌套在其他函数中定义,这样他们就可以访问它们被定义时所处的作用域变量。这意味着javascript函数构成了一个闭包(closere),它给javascript带来了非常强劲的编程能力。
1.函数定义
函数使用function关键字来定义。它可以用在函数定义表达式或者函数声明语句里。在这两种形式中,函数定义都从function关键字开始,其后跟随这些部分:
- 函数名称标识符:函数名称是函数声明语句必须的部分。它的用途就像是变量的名字,新定义的函数对象会赋值给这个变量。对函数定义表达式来说,这个名字是可选的:如果存在,该名字只存在函数中,并代指函数对象本身。
- 一对圆括号:其中包含由0个或者多个逗号隔开的标识符组成的列表。这些标识符是函数的参数名称,它们就像函数体中的局部变量一样。
- 一对花括号,里边包含0条或者多条javascript语句。这些语句构成了函数体:一旦调用函数,就会执行这些语句。
//定义javascript函数 //输出o的每个属性的名称和值,返回undefined function printprops(o) { for (p in o) console.log(p + ":" + o[p] + "\n") } //计算两个迪卡尔坐标(x1,y1)和(x2,y2)之间的距离 function distance(x1, y1, x2, y2) { var dx = x2 - x1; var dy = y2 - y1; return Math.sqrt(dx * dx + dy * dy) } //计算递归函数(调用自身的函数) //x!的值是从x到x递减(步长为1)的值的累乘 function factorial(x) { if (x <= 1) return 1; return x * factorial(x - 1); } //这个函数表达式定义了一个函数用来求传入参数的平方 //注意我们把它赋值了给一个变量 var square = function(x) { return x * x } //函数表达式可以包含名称,这在递归时很有用 var f = function fact(x) { if (x <= 1) return 1; else return x * fact(x - 1); }; //f(7)=>5040 //函数表达式也可以作为参数传给其它函数 data.sort(function(a, b) { return a - b; }); //函数表达式有时定义后立即使用 var tensquared = (function(x) { return x * x; }(10))
注意:以表达式定义的函数,函数的名称是可选的。一条函数声明语句实际上声明了一个变量。并把一个函数对象赋值给它。相对而言,定义函数表达式时并没有声明一个变量。函数可以命名,就像上面的阶乘函数,它需要一个名称来指代自己。
如果一个函数定义表达式包含名称,函数的局部变量作用域将会包含一个绑定到函数对象的名称。实际上,函数的名称将成为函数内部的一个局部变量。通常而言,以表达式方式定义函数时不需要名称,这会让定义它们的代码更紧凑。函数定义表达式特别适合用来那些只用到一次的函数,比如上面展示的最后两个例子。
函数声明提前。函数表达式就不一样了,赋给一个变量后变量声明提前了,但是函数无法调用。
return语句导致函数停止执行。并返回它的表达式(如果有的话)的值给调用者。如果return语句没有一个与之相关的表达式,则它返回undefined值。如果一个函数不包含return语句。那它就执行函数体内的每条语句,并返回undefined值给调用者。
嵌套函数
function hyuse(a, b) { function square(x) { return x * x } return Math.sqrt(square(a) + square(b)); }
嵌套函数的有趣之处在于它的变量作用域规则:它们可以访问嵌套它们(或者多重嵌套)的函数的参数和变量。
函数声明语句并非真正的语句。ECMAScript规范只是允许它们作为顶级语句,它们可以出现在全局代码里,或者内嵌在其他函数中,但它们不能出现在循环、条件判断、或者try/cache/finally及with语句中(有些javascript并为严格遵循这条规则,比如Firefox就允许在if语句中出现条件函数声明)。注意:此限制仅适用于以语句形式定义的函数。函数定义表达式可以出现在javascript的任何地方。
2.函数调用
构成函数主题的javascript代码在定义之时并不会执行,只有调用该函数是,它们才会执行。有4种方式来调用javascript函数。
- 作为函数
- 作为方法
- 作为构造函数
- 通过它们的call()或apply()方法间接调用
2.1.函数调用
使用调用表达式可以进行普通的函数调用也可以进行方法调用。一个调用表达式由多个函数表达式组成,每个函数表达式都是由一个函数对象和左圆括号、参数列表和右圆括号组成,参数列表是由逗号分隔的逗号的零个或多个参数表达式组成。如果函数表达式是一个属性访问表达式,即该函数是一个对象的属性或数组中的一个元素。那么它就是一个方法调用表达式。
在一个调用中,每个参数表达式(圆括号之间的部分)都会计算出一个值,计算的结果作为参数传递给另外一个函数。这些值作为实参传递给声明函数时定义的形参。在函数体中存在一个形参的调用,指向当前传入的实参列表,通过它可以获得参数的值。
对于普通的函数调用,函数的返回值成为调用表达式的值。如果该函数返回是因为解释器到达结尾,返回值就是undefined。如果函数返回是因为解释器执行到一条return语句,返回的值就是return之后的表达式值,如果return语句没有值,则返回undefined。
根据ECMAScript3和非严格的ECMAScript5对函数的调用规定,调用上下文(this的值)是全局对象。然后在严格模型下,调用上下文则是undefined、
以函数的形式调用的函数通常不使用this关键字。不过 ,“this”可以用来判断当前是否为严格模式。
//定义并调用一个函数来确定当前脚本运行是否为严格模式 var strict = (function() {return !this;}())
2.2.方法调用
一个方法无非是个保存在一个对象的属性里的javascript函数。如果有一个函数f和一个对象o,则可以用下面的代码给o定义一个名为m()的方法:o.m = f;
对方法调用的参数和返回值的处理,和上面所描述的普通函数调用完全一致。但是方法调用和函数调用有一个重要的区别,即:调用上下文。属性访问表达式由两部分组成:一个对象(本例中的o)和属性名称(m)。像这样的方法在调用表达式里,对象o成为调用上下文,函数体可以使用关键字this引用该对象。
大多数方法调用使用点符号来访问属性,使用方括号(的属性访问表达式)也可以进行属性访问操作。下面两个例子都是函数的调用:
o["m"](x,y) //o.m(x,y)的另外一种写法 a[0](z)//同样是一个方法调用(这里假设a[0]是一个函数)
方法调用可能包含更复杂的函数属性访问表达式:
customer.surname.toUpperCase(); //调用customer.surname方法 f().m(); //在f()调用结束后继续调用返回值中的方法m()
方法和this关键字是面向对象编程范例的核心。任何函数只要作为方法调用实际上都会传入一个隐式的实参——这个实参是一个对象,方法调用的母体就是这个对象。
方法链
当方法的返回值是一个对象,这个对象还可以再调用它的方法。这种方法调用序列中(通常称为“链”或者“级联”)每次的调用结果都是另外一个表达式组成部分。比如基于jQuery(19章会讲到),我们常这样写代码:
//找到所有的header,取得他们的id的映射,转换为数组并给它们进行排序 $(":header").map(function(){return this.id}).get().sort();
当方法并不需要返回值时,最好直接返回this。如果在设计的API中一直采用这种方式(每个方法都返回this),使用API就可以进行“链式调用”风格的编程,在这种编程风格中,只要指定一次要调用的对象即可。余下的方法都看一基于此进行调用:
shape.setX(100).setY(100).setSize(50).setOutline("red").setFill("blue").draw();
不要讲方法的链式调用和构造函数的链式调用混为一体。
链式调用:函数式编程
const utils = { chain(a) { this._temp = a; return this; }, sum(b) { this._temp += b; return this; }, sub(b) { this._temp -= b; return this; }, value() { const _temp = this._temp; this._temp = undefined; return _temp; } }; console.log(utils.chain(1).sum(2).sum(3).sub(4).value());
需要注意的是,this是一个关键字,不是变量,也不是属性名。javascript的语法不允许给this赋值。
和变量不同,关键字this没有作用域的限制,嵌套的函数不会从调用它的函数中继承this。如果嵌套函数作为方法调用,其this的值只想调用它的对象。如果嵌套函数作为函数调用,其this值不是全局对象(非严格模式下)就是undefined(严格模式下)。很多人误以为调用嵌套函数时this会指向调用外层函数的上下文。如果你想访问这个外部函数的this值,需要将this值保存在一个变量里,这个变量和内部函数都在一个作用域内。通常使用变量self来保存this。比如:
var o = { //对象o m: function() { //对象中的方法m() var self = this; //将this的值保存在一个变量中 console.log(this === o); //输出true,this就是这个对象o f(); //调用辅助函数f() function f() { //定义一个嵌套函数f() console.log(this === o); //"false":this的值是全局对象undefied console.log(self === o); //"true": slef指外部函数this的值 } } }; o.m();//调用对象o的方法m
2.3.构造函数调用
如果函数或者方法之前带有关键字new,它就构成构造函数调用(构造函数掉在4.6节和6.1.2节有简单介绍,第9章会对构造函数做更详细的讨论)。构造函数调用和普通的函数调用方法以及方法调用在实参处理、调用上下文和返回值各方面都不同。
如果构造函数调用圆括号内包含一组实参列表,先计算这些实参表达式,然后传入函数内,这和函数调用和方法调用是一致的。但如果构造函数没有形参,javascript构造函数调用的语法是允许省略形参列表和圆括号的。如下文两个代码是等价的:
var o = Object(); var o = Object;
构造函数调用创建一个新的 空对象,这个对象实例继承自构造函数prototype属性。构造函数试图初始化这个新创建的对象,并将这个对象用做其调用上下文。因此,构造函数可以使用this关键字来引用这个新创建的对象。
注意:尽管构造函数看起来像一个方法调用,它依然会使用这个新对象作为调用上下文。也就是说,在表达式new o.m()中,调用上下文并不是o。
构造函数通常不使用return关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它显式返回。这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。然而,如果构造函数显式的使用了return语句返回一个对象,那么调用表达式的值就是这个对象。如果构造函数使用return语句但没有指定返回值。或者返回一个原始值,那么这时将忽略返回值。同时使用这个新对象作为调用结果。
2.4.间接调用
javascript中的函数也是对象,和其它javascript对象没有什么两样,函数对象也可以包含方法。其中的两个方法call()和apply()可以用来间接的调用函数。两个方法都允许间接的调用函数。两个方法都允许显式指定调用所需的this值,也就是说,任何函数可以作为任何对象的方法来调用,哪怕这个函数不是那个对象的方法。
3.函数的实参和形参
javascript中的函数定义并未指定函数的形参类型,函数调用也未对实参做任何类型的检测。实际上javascript甚至不检查传入的形参的个数。下面几节将会讨论当调用函数时实参个数和声明的形参个数不匹配时出现的状况。同样说明了如何显式测试函数实参的类型,避免非法的实参传入函数。
3.1可选参数
当调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的的形参都将设置为undefined值。因此,在调用函数时,形参是否可选以及是否可以省略应当保持 较好适应性。为了做到这一点,应当给省略的参数赋一个合理的默认值、来看这个例子:
//将对象o中的可枚举属性名追加到数组a中,并返回这个数组a //如果省略a,则创建一个新数组并返回这个新数组 function getPropertyNames(o, /*optional*/ a) { if (a === undefined) a = []; //如果a未定义,则使用新数组 for (var property in o) a.push(property); return a; } //这个函数调用时可以使用两使用1或2个实参 var a = getPropertyNames(o); //将o的属性存储到一个新的数组中 getPropertyNames(p, a); //将p的属性追加到数组a中
如果第一行代码中不使用if语句,可以使用“||”运算符:a = a || []。
需要注意的是,使用“||”运算符代替if语句的前提是a必须先声明,否则表达式会报引用错误,在这个例子中a是作为形参传入的,相当于var a,既然已经声明a,所以这样用是没有问题的。
需要注意的是,当用这种可选实参来实现函数时,需要将可选实参放在参数列表的最后。那行调用你的函数的人是没办法省略第一个实参传入第二个实参的它必须将undefined显式传入【注意:函数的实参可选时往往传入一个无意义的占位符,惯用的做法是传入null作为占位符,当然也可以使用undefined】,同样要注意在函数定义中,使用注释/*optional*/来强调形参是可选的。
3.2.可变长的实参列表:实参对象
当调用函数的时候,传入的实参的个数大于函数定义的形参个数时,没有办法获得未命名值的引用。参数对象解决了这个问题。在函数体内,arguments是指向实参对象的引用,实参对象是一个类数组的对象(参照7章11节),这样可以通过数字下标就能访问传入函数的实参值。而不用非要通过名字来得到实参。
假设定义函数f,它只有一个实参x。如果调用这个函数时需要传入两个实参,第一个实参可以通过参数名x来获得,也可以通过arguments[0]来得到。第二个实参只能通过arguments[1]来得到。此外和真正的数组一样,arguments也包含一个length属性,用以表示其所包含元素的个数。因此,调用函数f()时传入两个参数,arguments.length的值就是2.
function f(x, y, z) { //首先验证传入实参的个数是否正确 if (arguments.leng != 3) { throw new Error("function f() called with" + arguments.length + "arguments,but it ecxpects 3 arguments"); } //再执行函数的其它逻辑 }
需要注意的是,通常不必这样检查实参个数。大多数情况下,javascript的默认行为可以满足需要的:省略的实参都是undefined,多出的实参会自动省略。
实参对象有一个重要的用处,就是让函数操作任意数量的实参。如下的不定实参函数
function max( /*...*/ ) { var max = Number.NEGATIVE_INFINITY; //遍历实参,查找并记住最大值 for (var i = 0; i < arguments.length; i++) if (arguments[i] > max) max = arguments[i]; //返回最大值 return max; } max(10, 100, 22, 1000); //=>1000
注意:不定实参函数的实参个数不能为零。arguments[]对象最适合的场景是在这样一类函数中,这类函数包含固定个数的命名和必须参数,以及随后个数不定的可选实参。
记住,arguments并不是真正的数组。它是一个实参对象,碰巧有以数字索引的属性。
数组对象包含一个非同寻常的特性。在非严格模式下,当一个函数包含若干形参,实参对象的数组元素是函数形参所对应实参别名,实参对象以数字索引,实参对象中以数字索引,并且形参名称可以可以认为是相同变量的不同命名。通过实参名字来修改实参值的话,通过arguments[]数组也可以获取到更改后的值,下面的这个例子清楚的说明了这一点。
function f(x) { console.log(x); //输出实参的初始值 arguments[0] = null; //修改实参组的元素同样会修改x的内容 console.log(x); //输“null” }
如果实参对象是一个普通的数组的话,第二条console.log(x)语句结果绝对不是null.这个例子中,arguments[]和x指代同一个值。
在ECMAScript5中移除了实参对象的这个特殊属性。在严格模型下还有一点(和非严格模式不同),在非严格模式下,函数里的arguments仅仅是一个标识符,在严格模式中,它变成了一个保留字。严格模式下 函数无法使用arguments作为形参名或局部变量名,也不能给arguments赋值。
callee和caller属性
除了数组元素,实参对象还定义了callee和caller属性。在非严格模式下(严格模式下会有一系列错误),ECMAScript标准规范规定callee属性指代当前正在执行的函数。caller属性是非标准的,但大多数浏览器都实现这个属性。它指代调运当前正在执行的函数的函数。通过方法caller属性可以访问调运栈。callee属性在某些时候非常有用,比如在匿名函数中通过callee来递归调用自身。
var factorial = function(x) { if (x <= 1) return 1; return x * arguments.callee(x - 1); }
区别:
//1 :caller 返回一个调用当前函数的引用 如果是由顶层调用的话 则返回null var callerTest = function() { console.log(callerTest.caller) ; } ; function a() { callerTest() ; } a() ;//输出function a() {callerTest();} callerTest() ;//输出null //2 :callee 返回一个正在被执行函数的引用 (这里常用来递归匿名函数本身 但是在严格模式下不可行) //callee是arguments对象的一个成员 表示对函数对象本身的引用 它有个length属性(代表形参的长度) var c = function(x,y) { console.log(arguments.length,arguments.callee.length,arguments.callee) } ; c(1,2,3) ;//输出3 2 function(x,y) {console.log(arguments.length,arguments.callee.length,arguments.callee)}
3.3.将对象属性用作实参
当一个函数包含超过3个形参时,对于程序员来说,要记住调用函数中实参的正确顺序实在让人头疼。每次调用这个函数时都不厌其烦的查阅文档,为了不让程序员每次都要梳理,最好通过名/值对的形式传入参数。这样参数的顺序就无关紧要了。为了实现这样风格的方法调用,定义函数的时候,传入的实参都写入一个单独的对象之中,在调用的时候传入一个对象,对象中的名/值才是真正需要的实参数据,如下例子,这样的写法允许在函数中设置省略参数的默认值。
//将原始值数组的length元素复制至目标数组 //开始复制原始数组的from_start元素 //并且将其复制到目标数组to_start中 //要记住实现的顺序并不容易 function arrayCopy( /*array*/ from, /*index*/ from_start, /*array*/ to, /*index*/ to_start, /*integer*/ length) { //逻辑代码 } //这个版本的实现效率有些低,但你不必再记住实参的顺序 //并且from_start和to_start都默认为0 function easyCopy(args) { arrayCopy(args.form, args.form_start || 0, //注意,这里设置了默认值 args.to, args.to_start || 0, args.length); } //来看如何调用easyCopy var a = [1, 2, 3, 4], b = []; easyCopy({ from: a, to: b, length: 4 });
3.4.实参类型
对其进行类型检测。
可以添加注释、类型转换、代码检测
。。。。。。
4.作为值的函数
在javascript中,函数不仅是一种语法,也是值。也就是说,可以将函数赋值给变量。存储在对象的属性或数组的元素中,作为参数传入另外一个函数等。
为了便于理解javascript中的函数是如何做数据的以及javascript语法,来看一个函数定义:
function square(x) { return x * x }
这个定义创建一个新的函数对象,并将其赋值给square。函数的名字实际上是看不见的,它(square)仅仅是变量的名字。这个变量指代函数对象。函数还可以赋值给其它的变量,并且仍可以正常工作:
var s = square; //现在s和sqare指代同一个函数 square(4); //=>16 s(4); //=>16
除了可以将函数赋值给变量,统一可以将函数赋值给对象的属性。当函数作为对象的属性调用时,函数就称为方法。
var o = { square: function(x) {return x * x} }; //对象直接量 var y = o.square(16);
函数甚至不需要名字,当把他们赋值给数组元素时:
var a = [function(x) {return x * x},20]; console.log(a[0](a[1])) //=>400
例子:
//在这里定义一些简单的函数 function add(x, y) {return x + y;} function subtract(x, y) {return x - y;} function multiply(x, y) {return x * y;} function divide(x, y) {return x / y;} //这里的函数以上面的某个函数作为参数 //并给它传入两个操作数然后调用它 function operate(operator, operand1, operand2) { return operator(operand1, operand2) } //这行代码所示的函数调用了实际上计算了(2+3)+(4*5)的值 var i = operate(add,operate(add,2,3) , operate(multiply,4,5)); //我们为这个例子重复实现了一个简单的函数 //这次实现使用函数量,这些函数直接量定义在一个对象直接量中 var operators = { add: function(x, y) {return x + y;}, subtract: function(x, y) {return x - y;}, multiply: function(x, y) {return x * y;}, divide: function(x, y) {returnx / y}, pow:Math.pow()//使用预定义的函数 }; //这个函数接受一个名字作为运算符,在对象中查找这个运算符 //然后将它作用于锁提供的操作数 //注意这里调用运算符函数语法 function operate2(operation,operand1,operand2){ if(typeof operators[operation] === "function") return operators[operation](operand1,operand2); else throw "unkown operators"; } //这样来计算("hello" + "" + "world")的值 var j = operate2("add","hello",operate2("add","","world") ); //使用预定义的函数Math.pow() var k = operate2("pow",10,2);
这里是将函数做值的另外一个例子,考虑下Array.sort()方法,sort()方法可以接受一个函数作为参数,用来处理具体的排序操作。
自定义函数属性
javascript中的函数并不是原始值,而是一种特殊的对象,也就是说,函数可以拥有属性。当函数需要一个“静态”的变量来调用时保持某个值不变,最方便的方法就是给函数定义属性,而不是全局变量。显然定义全局变量会让命名空间变得更杂乱无章。
比如:你想写一个返回一个唯一整数的函数,不管在哪里调用的函数都会返回这个整数。而函数不能两次返回同一个值。为了做到这一点,函数必须能够跟踪它每次返回的值,而且这些值的信息需要在不同函数调用过程中持久化。可以将这些信息存放到全局变量中,但这并不是必需的,因为这个信息仅仅是函数本身用到的。最好将这个信息保存到函数的一个属性中,下面这个例子就实现了这样的一个函数,每次调用函数都会返回一个唯一的整数:
//初始化函数对象的计数器属性 //由于函数声明被提前了,因此这个是可以在函数声明 //之前给它的成员赋值的 unInterger.counter = 0; //每次调用这个函数都会返回一个不同的整数 //它使用一个属性来记住下一次将要返回的值 function unInterger() { unInterger.counter++ ; //先返回计数器的值,然后计数器自增1 }
来看另外一个例子,下面这个函数factorrial()使用了自身属性(将自身当做数组来对待)来缓存上一次的计算结果:
//计算阶乘,并将结果缓存在函数的属性中 function factorrial(n) { if (isFinite(n) && n > 0 && n == Math.round(n)) { //有限的正整数 if (!(n in factorrial)) //如果没有缓存结果 factorrial[n] = n * factorrial(n - 1); //计算并缓存之 return factorrial[n]; } else return NaN; //如果输入有误 } factorrial[1] = 1; //初始化缓存以保存这种基本情况 console.log(factorrial(5))
5.作为命名空间的函数
在javascript中是无法声明只在一个代码块内可见的变量的(在客户端javascript中这种说法不完全正确,在有些javascript扩展中就可以使用let声明语句块内的变量,详细内容见11章),基于这个原因,我们常常简单定义一个函数用做临时命名空间,在这个命名空间内定义的变量不会污染的全局命名空间。
比如,假设你写了一段javascript模块代码,这段代码将要用在不同的javascript程序中,会出现已经存在的变量和代码发生冲突。解决的办法当然是将代码放入一个函数内,然后调用这个函数。这样全局变量就编程了函数内的局部变量:
function mymodule() { //模块代码 //这个模块所有使用的所有变量是局部变量 //而不是污染全局命名空间 } mymodule(); //不要忘了还要调用的这个函数
这段代码仅仅定义了一个单独的全局变量,名叫“mymodule”的函数。这样还是太麻烦了,可以直接定义一个匿名函数,并在单个表达式中调用它:
(function() { //mymodule函数重写为匿名函数表达式 //模块代码 }()); //结束函数定义并立即调用它
function之前的左括号是必须的,因为如果不写这个左圆括号,javascript解释器会试图将其解析为函数定义表达式。使用了它javascript解释器才会正确地将其解析为函数定义表达式。
例子展示了这种命名空间技术:
。。。。。。
6.闭包
和大多数现代编程语言一样,javascript也采用词法作用域(lexical scoping),也就是说,函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。
为了实现这种词法作用域,javascript函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域。
函数对象可以通过作用域相互关联起来,函数体内部的变量都可以保持在函数的作用域内,这种特性在计算机科学文献中称为“闭包”。(这种叫法非常古老,是指函数的变量可以隐藏于作用域链之内,因此看起来是函数将变量包裹了起来。)
调用函数时闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域链。
理解闭包首先需要了解嵌套函数的词法作用域规则,看一下这段代码:
var scope = "global scope"; //全局变量 function checkscope() { var scope = "local scope"; //局部变量 function f() { return console.log(scope); } //在作用域中返回这个值 return f(); } checkscope(); // local scope时
改动:
var scope = "global scope"; //全局变量 function checkscope() { var scope = "local scope"; //局部变量 function f() { return console.log(scope); } //在作用域中返回这个值 return f; } checkscope()(); //
checkscope()现在仅仅返回函数内嵌套的一个函数对象,而不是直接返回结果。
回想一下这个词法作用域的基本规则:javascript函数的执行用到了作用域链。这个作用域链是函数定义的时候创建的。嵌套的函数f()定义在这个作用域链里,其中的变量scope一定是局部变量,不管在何时何地都执行函数f(),这种绑定在执行f()时依然有效。因此,最后一行返回"local scope",而不是“global”.简言之,闭包的这个特性强大到让人吃惊:它可以捕捉到局部变量(和参数),并一直保存下来,看起来像这些变量绑定到在其中定义他们的外部函数。
实现闭包
我们将作用域链描述为一个对象列表,不是绑定的栈。每次调用javascript函数的时候,都会为之创建一个新的对象用来保存局部变量,把这个对象添加至作用域链中。当函数返回的时候,就从作用域链中将这个绑定的变量的对象删除。如果不存在嵌套的函数,也没有其它引用指向这个绑定的对象,它就会被当做垃圾回收掉。如果定义了嵌套的函数,每个嵌套的函数都各自对应一个作用域链,并且这个作用域链指向一个变量绑定对象。但如果这些嵌套的函数对象在外部函数中保留了下来,那么它们也会和所指向的变量绑定对象一样当做垃圾回收。但是如果这个函数定义了嵌套函数,并将它作为返回值返回或者存储在某处的属性里,这时就会有一个外部引用指向这个嵌套的 函数,它就不会被当做垃圾回收,并且它所指向的变量绑定也不会被当做垃圾回收(作者在这里清楚地解释了闭包和垃圾回收之前的关系,如果使用不慎,闭包很容易造成“循环引用”,当DOM对象和javascript对象之前存在循环引用时需要格外小心,在某些浏览器下会造成内存泄漏)。
前面定义的unInterger()函数,这个函数使用自身的一个属性来保存每次返回的值,以便每次调用都能跟踪上次的返回值。但是这种做法有一个问题,就是恶意代码可能将计数器重置或者把一个非整数赋值给它,导致unInterger()函数不一定能产生“唯一”的“整数”。而闭包可以捕捉到单个函数调用的局部变量,并将这些局部变量用做私有状态,我们可以利用闭包重写这个函数:
var unInterger = (function() { //定义函数并立即调用 var counter = 0; //函数的私有状态 return function() {return counter++;}; }());
像counter一样的私有变量不是只能用在一个单独的闭包内,在同一个外部函数内定义多个嵌套函数可以访问它,这个嵌套函数都共享一个作用域链,看一下这短代码:
function counter(){ var n =0; return{ count:function(){return n++;}, reset:function(){n = 0;} }; } var c = counter(),d = counter(); //创建两个计数器 console.log(c.count()) //=>0 console.log(d.count()) //=>0 console.log(c.reset()) // reset()和count方法共享状态 undefined console.log(c.count()) //=>0 因为我们重置了c console.log(d.count()) //=>1 我们没有重置d console.log(d.count()) //=>2
每次调用counter()会创建一个新的作用域链和一个新的私有变量。因此,如果调用counter()两次会得到两个计数器对象,而且彼此包含不同的私有变量,调用其中一个计数器对象的count()或者reset()不会影响另外一个对象。
从技术角度看,其实可以将这个闭包合并为属性存取器方法,getter和setter.下面这段代码所示的counter()函数是6章6节中代码的变种,所不同的是,这里私有状态的实现是利用了闭包,而不是利用普通的对象属性来实现:
function counter(n) { //函数参数n是一个私有变量 return { //属性getter方法返回并给私有计数器var递增1 get count() { return n++; }, //属性setter方法不允许n递减 set count(m) { if (m >= n) n = m; else throw Error("count can only be set to a larger value"); } }; } var c = counter(1000); console.log(c.count) //=>1000 console.log(c.count) //=>1001 console.log(c.count) //=>1002 console.log(c.count = 2000) console.log(c.count) //=>2000 console.log(c.count) //=>2001 console.log(c.count = 2000) //Error: count can only be set to a larger value
需要注意的是,这个版本的counter()函数并未声明局部变量,而只是使用参数n来保存私有状态,属性存取器方法可以访问n。这样的话,调用counter()的函数就可以指定私有变量的初始值了。
下面的这个例子,利用闭包技术来共享私有状态的通用做法。这个例子定义了一个addPrivateProperty()函数,这个函数定义了一个私有变量,以及两个嵌套的函数来获取和设置这个私有变量的值。它将这些嵌套函数添加为所指定对象的方法。
。。。。。。
我们已经给出了很多例子,在同一个作用域链中定义两个闭包,这两个闭包共享同样的私有变量或变量。这是一种非常重要的技术,但还是要小心那些不希望共享的变量往往不经意间共享给了其它的闭包,了解这一点非常重要。看一下下面的这段代码:
//这个函数返回一个总是返回v的函数 function constfunc(v) { return function() {return v;} }; //创建一个数组用来常数函数 var funcs = []; for (var i = 0; i < 10; i++) funcs[i] = constfunc(i); //在第5个位置的元素所表示的函数返回值为5 funcs[5]() //=>5
这段代码利用循环创建了很多闭包 ,当写类似这种代码的时候往往会犯一个错误:那就是试图将循环代码移入定义这个闭包的函数之内,看一下这段代码:
//返回一个函数组成的数组,它们的返回值是0-9 function constfuncs() { var funcs = []; for (var i = 0; i < 10; i++) funcs[i] = function() { return i; }; return funcs; } var funcs = constfuncs(); console.log(funcs[5]()) //10
上面的这段代码创建了10个闭包,并将它们存储到一个数组中。这些闭包都是在同一个函数调用中定义的,因此它们可以共享变量i。当constfuncs()返回时,变量i的值是10,所有的闭包都共享这一个值,因此,数组中的函数返回值都是同一个值,这不是我们想要的结果。关联到闭包的作用域链都是“活动的”,记住这一点非常重要。嵌套的函数不会将作用域内的私有成员负责一份,也不会对所绑定的变量生成静态快照(static snapshot)。
书写闭包的时候还需要注意一件事情,this是javascript的关键字,而不是变量。正如之前讨论的,每个函数调用都包含一个this值,如果闭包在外部的函数里是无法访问this【严格将,闭包内的逻辑是可以使用this的,但这个this和当初定义函数的this不是同一个,即便是同一个this,this的值是随着调用栈的变化而变化的,而闭包里的逻辑所取到的this的值也是不确定的,因此外部函数内的闭包是可以使用this的,但要非常小心的使用才行,作者在这里提到的将this转存为一个变量的做法就可以避免this的不确定性带来的歧义】,除非外部函数将this转存为一个变量:
var self = this; //将this保存到一个变量中,以便嵌套的函数能够访问它
绑定arguments的问题与之类似。arguments并不是一个关键字,但在调用每个函数时都会自动声明它,由于闭包具有自己所绑定的arguments,因此闭包内无法直接访问外部函数的参数数组,除非外部函数将参数数组保存到另外一个变量中:
var outerArguments = arguments; //保存起来以便嵌套的函数能使用它
在本章接下来的例子中就利用了这种编程技巧来定义闭包,以便在闭包中可以访问外部函数的this和arguments值。
7.函数属性、方法和构造函数
我们看到在javascript程序中,函数是值。对函数执行typeof运算会返回字符串“function”,但是函数是javascript特殊对象。因为函数也是对象,它们也可以拥有属性和方法,就像普通的对象可以拥有属性和方法一样。甚至可以用Function()构造函数来创建新的函数对象。
7.1.length属性
在函数体里,arguments.length表示传入函数的实体的个数。而函数本身的length属性则有不同的含义。函数的length属性是只读属性,它代表实参的数量,这里的参数是值“形参”而非“实参”,也就是定义函数时给出的实参个数,通常也是在函数调用时期望传入函数的实参个数。
下面代码定义一个名叫check()的函数,从另外一个函数给它传入arguments数组,它比较arguments.length(实际传入的实参个数)和arguments.callee.length(期望传入的实参个数)来判断所传入的实参个数是否正确。如果个数不正确,则抛出异常。check()函数之后定义一个测试函数f(),用来展示check()用法:
//这个函数使用arguments.callee,因此它不能再严格模式下工作 function check(args) { var actual = args.length; //实参的真实个数 var expected = args.callee.length; //期望的实参个数 if (actual !== expected) //如果不同则抛出异常 throw Error("Expected" + expected + "args; got" + actual) } function f(x, y, z) { check(arguments); //检查实参个数和期望的实参个数是否一致 return x + y + z; //再执行函数的后续逻辑 }
7.2.prototype属性
每一个函数都包含prototype属性,这个属性是指向一个对象的引用,这个对象称为原型对象(prototype object).每一个函数都包含不同原型对象。当将函数用作构造函数的时候,新创建的对象会从原型对象上继承属性。
7.3.call()和apply()方法
可以将call()和apply()看做是某个对象的方法,通过调用方法的形式来间接调用。
call()和apply()的第一个实参是要调用函数的母对象,它是调用上下文,在函数体内通过this来获得对它的引用。
f.call(o);
f.apply(o);
上面的例子每行代码和下面代码的功能类型(假设对象o中预先不存在名为m的属性):
o.m = f; //将f存储为o的临时方法 o.m(); //调用它不传入参数 delete o.m; //将临时方法删除
在ECMAScript5的严格模式中,call()和apply()的第一个实参都会变为this的值,哪怕传入的参数是原始值甚至是null或undefined。在ECMAScript3和非严格模式中,传入的null和undefined都会被全局变量替代,而其它原始值会被相应的包装对象(wrapper object)所替代。
调用上下文实参之后的所有实参就是要传入待调用的函数的值。call与apply不同,apply的实参都放入一个数组中。
如果一个函数的实参可以是任意数量,给apply()传入的参数数组可以是任意长度的。比如:为了找出数组中最大数组的元素,调用Math.max()方法的时候可以给apply()传入一个包含任意个元素的数组:
var biggest = Math.max.apply(Math, array_of_numbers);
需要注意的是给apply()的参数数组可以是类数组对象也可以是真实数组。
实际上,可以将当函数的arguments数组直接传入(另一个函数的)apply()来调用另一个函数,参照如下代码:
//将对象o中名为m()的方法替换为令一个方法 //可以在调用原始的方法之前和之后记录日志消息 function trace(o, m) { var original = o[m]; //在闭包中保存原始方法 o[m] = function() { //定义新的方法 console.log(new Date(), "entering:", m); //输出消息 var result = original.apply(this, arguments); //调用原始函数 console.log(new Date(), "exiting:", m); return result; }; }
trace()函数接收两个参数,一个对象和一个方法名,它将一个指定的方法替换为一个新方法,这个新方法是“包裹”原始方法的令一个泛函数(泛函数也叫泛函,在这里特指一个函数,以函数输入,输出的可以是值也可以是函数)。这种动态修改已有方法有时候叫做"monkey - patching".
7.4.bind()方法
bind()方法是ECMAScript5中新增的方法,但是ECMAScript3中可以轻易模拟bind().从名字就可以看出,此方法的作用就是将函数绑定至某个对象。
当函数f()上调用bind()方法传入一个对象o作为参数,这个方法将返回一个新的函数。(以函数调用的方式)调用新的函数会把原始的函数f()当o的方法来调用。传入新函数的任何实参都将传入原始函数,比如:
function f(y) {return this.x + y;} //这个是待绑定的函数 var o = {x: 1}; //将要绑定的函数 var g = f.bind(o); //通过g(x)来调用o.f(x) console.log(g(4)) // => 5
也可以通过以下代码实现轻松绑定:
//返回一个函数,通过它来调用o中的方法f(),传递它所有的实参 function bind(f,o){ if(f.bind) return f.bind(o);//如果bind()方法存在的话,使用bind()方法 else return function(){//否则这样绑定 return f.apply(o,arguments); } }
ECMAScript5中的bind()方法不仅仅是将函数绑定至一个对象,还附带一些其它的应用:除了第一个实参之外,传入bind()实参也会绑定至this,这个附带的应用是一种常见的函数编程技术,有时也被称为“柯里化”(currying)。参照下面的这个例子中的bind()方法的实现:
var sum = function(x,y){return x + y};//返回练个个实参的值 //创建一个类似sum的新函数,但this的值绑定到null //并且第一个参数绑定到1,这个新的参数期望只传入一个实参 var succ = sum.bind(null,1); succ(5) // =>6 x绑定到1,并传入2作为实例y function f(y,z) {return this.x + y + z}; //另外一个左累加计算的函数 var g = f.bind({x:1},2); //绑定this和y g(3) //=>6:this.x绑定到1,y绑定到2,z绑定到3
我们可以绑定this的值并在ECMAScript3中实现这个附带应用。例如下面的中的示例代码就模拟实现了标准的bind()方法。
ECMAScript3的Function.bind()方法:
if(!Function.prototype.bind){ Function.prototype.bind() = function(o /*,args*/){ //将this和arguments的值保存至变量中 //以便在后面的嵌套函数中可以使用他们 var self = this,boundArgs = arguments; //bind()返回值是一个函数 return function(){ //创建一个实参列表,将传入bind()的第二个及后续的实参都传入这个函数 var arg = [],i; for(i=1;i<boundArgs.length;i++) args.push(boundArgs[i]); for(i=0;i<arguments.length;i++) args.push(arguments[i]); //现在将self作为o的方法来调用,传入这些实参 return self.apply(o,args); }; }; }
我们注意到,bind()方法返回的函数是一个闭包,在这个闭包的外部函数中声明了self和boundArgs变量,这两个变量在闭包里用到。尽管定义闭包的内部函数已经从外部函数中返回,而且调用这个闭包逻辑的时刻要在外部函数返回之后(在闭包中照样可以争取访问这两个变量)。
ECMAScript5定义的bind()方法也有一些特性是上述ECMAScript3代码无法模拟的。首先,真正的的bind()方法返回一个函数对象,这个对象的length属性是绑定函数的形参减去绑定实参的个数(length值不能小于0)。再者,ECMAScript5的bind()方法可以顺带做构造函数,将忽略传入bind()的this,原始函数就会以构造函数的形式调用,其实参也已经绑定(意思是在运行时将bind()所返回的函数用做构造函数时,所传入的实参会原封不动的传入原始函数)。由bind()方法返回的函数并不包含prototype属性(普通函数的固有的prototype属性是不能删除的),并且将这些绑定的函数用做构造函数时锁创建的对象从原始值的未绑定的构造函数中继承prototype。同样在使用instanceof运算符时,绑定构造函数和未绑定构造函数并无两样。
7.5.toString()方法
和所有的javascript对象一样,函数也有toString()方法,ECMAScript规范规定这个方法返回一个字符串,这个字符串和函数声明语句的语法相关。实际上,大多数(非全部)的toString()方法的实现都返回函数的完整源码。内置函数往往返回一个"[native code]"的字符串作为函数体。
7.6.Function()构造函数
函数还可以通过Function()构造函数来定义:
var f = new Function("x","y","return x*y");
Function()构造函数可以传入任意数量的字符串实参,最后一个实参所表示的文本就是函数体;它可以包含任意的javascript语句,每两条语句之间用分号分隔。传入构造函数的其他所有的实参字符是指定函数的形参名字的字符串。如果定义的函数不包括任何参数,只须给构造函数简单地传入一个字符串--函数体--即可。
注意:Function()构造函数并不需要通过传入实参以指定函数名。就像函数直击量一样,Function()构造函数创建一个匿名函数。
关于Function()构造函数有几点需要注意:
- Function()构造函数允许javascript在运行时动态的创建并编译函数。
- 每次Function()构造函数都会解析函数体,并创建新的函数对象。如果是在一个循环或者多次调用的函数中执行这个构造函数,执行效率会受影响。相比之下 ,循环制的嵌套函数和函数定义表达式则不会每次执行时都重新编译。
- 最后一点,也是关于Function()构造函数非常重要的一点,就是它所创建的函数并不是使用词法的作用域。想法,函数体代码的编译总是会在顶层函数(也就是全局作用域)执行,正如下面代码所示:
var scope = "global"; function constructFunction() { var scope = "local"; return new Function("return scope"); //无法捕捉局部作用域 } // 这行代码返回global,因为通过Function()构造函数所返回的战术使用的不是局部作用域 constructFunction()(); //=>"global"
我们可以将Function()构造函数认为是在全局作用域执行eval()(参照4.12.ii节),eval()可以在自己的私有作用域内定义新变量和函数,Function()构造函数在实际编程过程中很少用到。
7.7.可调用的对象
“类数组对象”并不是真正的数组,但大部分场景下可以将其当做数组来对待。对于函数也存在类似情况。“可调用的对象”(callable object)是一个对象,可以在函数调用表达式中调用这个对象。所有的函数都是可调用的,但非所有的可调用对象都是函数。
截止目前,可调用对象在两个javascript实现中不能算作函数。首先,IE web浏览器(ie8及以前的版本)实现了客户端方法(诸如window.alert()和document.getElementsById()),使用了可调用的宿主对象,而不是内置函数对象。IE的这个方法在其它浏览器中也都存在,但他们本质不是Function对象。IE9将它们实现为真正的函数,因此这类可调用的对象越来越罕见。
另外一个常见的可调用对象是RegExp对象(在众多浏览器中均有实现),可以直接调用RegExp对象,这比调用它的exec()方法更快捷一些。代码最好不要对可调用的RegExp对象有太多依赖,这个特性在不久的将来可能会废除并删除。对RegExp执行typeof运算结果并不统一,有些浏览器中返回“function”,有些返回“object”。
如果想检测一个对象是否是真值的函数对象(并且具有函数方法),可以参照代码检测它的class属性:
function isFunction(x) { return Object.prototype.toString.call(x) === "[object Function]" }
8.函数式编程
和lisp、Haskell不同,javascript并非函数式编程语言,但在javascript中可以像操作对象一样操控函数,也就是说可以在javascript中应用函数式编程成绩。ECMAScript5中的数组方法(诸如map()和reduce())就可以非常适合用于函数式编程风格。
推荐你使用一下(至少阅读一下)奥利弗·斯蒂尔(Oliver Steele)的函数式javascript库。
8.1.使用函数处理数组
假设有一个数组,数组的元素都是数字,我们想要计算这些元素的平均值和标准差。若使用非函数式编程风格的话,代码是这样:
var data = [1, 1, 3, 5, 5, 6]; //这里待处理的数组 //平均数是所有元素的累加值和除以元素的个数 var total = 0; for (var i = 0; i < data.length; i++) total += data[i] var mean = total / data.length; //=>3.5 //计算标准差,首先计算每个数减去平均数减去平均数之后偏差的平方然后求和 total = 0; for (var i = 0; i < data.length; i++) { var deviation = data[i] - mean; total += deviation * deviation; } var stddev = Math.sqrt(total / (data.length - 1)); // 2.16794833886788 标准差的值
可以使用数组方法,map()和reduce()来实现同样的计算,这种实现极其简洁:
//首先先简单定义两个简单函数 var sum = function(x,y){return x+y;}; var square = function(x) {return x*x;}; //然后将这些函数和数组方法配合使用计算出平均数和标准差 var data = [1, 1, 3, 5, 5, 6]; //这里待处理的数组 var mean =data.reduce(sum)/data.length; var deviations = data.map(function(x){return x-mean;}); var stddev = Math.sqrt(deviations.map(square).reduce(sum)/(data.length-1));
如果我们基于ECMAScript3来如何实现呢?因为ECMAScript3并不包含这些数组方法,如果不存在内置方法我们可以自定义map()和reduce()函数:
。。。。。。
8.2.高阶函数
所谓高阶函数(higer-order function)就是操作函数的函数,它接收一个或多个函数作为参数,并返回一个新函数,看这个例子:
//这个高阶函数返回一个新的函数,这个新函数将它的实参传入f() //并返回f的返回值逻辑非 function not(f){ return function(){//返回一个新的函数 var result = f.apply(this,arguments);//调用f() return !result; //对结果求反 }; } var even = function (x){//判断a是否为偶数的函数 return x % 2 === 0; }; var odd = not(even); //判断一个新函数,和even()相反 [1,1,3,5,5].every(odd); //=>true 每个元素为奇数
上面的not()函数就是一个高阶函数,因为它接收一个函数作为参数,并返回一个新函数。另外一个例子,来看下面的mapper()函数,它也是接收一个函数作为参数,并返回一个新函数,这个新函数 将一个数组映射到另一个使用这个函数的数组上。这个函数使用了之前定义的map()函数,但首先要理解这两个函数有所不同的地方,理解这一点至关重要。
。。。。。。
8.3.不完全函数
8.4.记忆
前面定义了一个阶乘函数,它可以将上次的计算结果缓存起来。在函数式编程当中,这种缓存技巧叫“记忆”(memorization)。