JavaScript权威指南 - 函数

函数自己就是一段JavaScript代码,定义一次但可能被调用任意次。若是函数挂载在一个对象上,做为对象的一个属性,一般这种函数被称做对象的方法。用于初始化一个新建立的对象的函数被称做构造函数。javascript

相对于其余面向对象语言,在JavaScript中的函数是特殊的,函数便是对象。JavaScript能够把函数赋值给变量,或者做为参数传递给其余函数,甚至能够给它们设置属性等。html

JavaScript的函数能够嵌套在其余函数中定义,这样定义的函数就能够访问它们外层函数中的任何变量。这也就是所谓的“闭包”,它能够给JavaScript带来强劲的编程能力。java

1.函数定义

函数使用function关键字定义,有函数语句函数表达式两种定义方式。node

//一.函数语句类: //打印对象全部属性名称和值。 function printprops(obj) { for (var key in obj) { console.log(key + ":" + obj[key]); } } //计算阶乘的递归函数,函数名称将成为函数内部的一个局部变量。 function factorial(n) { if (n <= 1) return 1; return n * factorial(n); } //二.函数表达式类: //计算n的平方的函数表达式。这里将一个函数赋给一个变量。 var square = function (x) { return x * x; } //兔子数列。函数表达式也能够包含名称,方便递归。 var foo = function foo(n) { if (n <= 1) return 1; else foo(n - 1) + foo(n - 2); } //数组元素升序排列。函数表达式也能做为参数传递给其余函数。 var data = [5, 3, 7, 2, 1]; data.sort(function (a, b) { return a - b; }); //函数表达式有时定义后当即调用。 var tensquared = (function (x) { return x * x; }(10));

函数命名
函数名称要求简洁、描述性强,由于这样能够极大改善代码的可读性,方便别人维护代码;函数名称一般是动词或以动词开头的词组。一般来讲,函数名编写有两种约定:算法

  1. 一种约定是函数名第一个单词首字母小写,后续单词首字母大写,就像likeThis()
  2. 当函数名包含多个单词时,另外一种约定是用下划线来分割单词,就像like_this()

项目中编写方法名时尽可能选择一种保持代码风格一致。还有,对于一些私有函数(不做为公用API的一部分),这种函数一般以一条下划线做为前辍。编程

2.函数调用

函数声明后须要经过调用才能被执行。JavaScript中一般有4种方式来调用函数:数组

  1. 做为普通函数;
  2. 做为对象方法;
  3. 做为构造函数;
  4. 经过它们的call()apply()方法间接调用。

下面就经过一些具体示例来演示上述4中函数的调用方式。浏览器

1.对于普通函数,经过调用表达式就可直接调用,这种方式很直接也很常见。缓存

//定义一个普通函数。 var strict = function () { return !this; }; //检测当前运行环境是否为严格模式。 //经过函数名直接调用。 console.log(strict()); 

注:根据ES3和非严格的ES5对普通函数调用的规定,调用上下文(this)是全局对象;在严格模式下,调用上下文则是undefined。闭包

2.一般,保存在对象属性里的JavaScript函数被称做“方法”。

//定义一个对象直接量。 var calc = { a: null, b: null, add: function () { //将函数保存在对象属性中。 return this.a + this.b; } }; //经过对象名调用方法。 calc.a = 1, calc.b = 2; console.log(calc.add()); 

注:对象方法中的调用上下文(this)不一样于普通函数中的上下文。这里this指代当前对象。

方法链:当方法返回值是一个对象,那么这个对象还能够再调用它的方法。每次调用的结果都是另一个表达式的组成部分,这种方法调用方式最终会造成一个序列,也被称为“方法链”。因此,在本身设计API的时候,当方法并不须要返回值时,最好直接返回this。这样之后使用API就能够进行“链式调用”风格的编程。

须要注意的是,this是一个关键字,Javascript语法不容许给它赋值。再者,关键字this没有做用域的限制,嵌套的函数不会从外层调用它的函数中继承this。也就是说,若是嵌套函数做为方法调用,其this指向为调用它的对象。若是嵌套函数做为函数调用,其this值不是全局对象就是undefined。下面经过一段代码来具体说明。

var o = { m: function () { //对象中的方法 var self = this; //将this的值保存在一个变量中 console.log(this === o); //输出true,代表this就是这个引用对象o f(); //调用嵌套函数f() function f() { //定义一个嵌套函数(**普通函数,非对象方法) console.log(this === o); //输出false,this的值为全局对象或undefined console.log(self === o); //输出true,变量self指外部函数的this值 } } }

3.若是函数或者防方法调用以前带有关键字new,它就构成构造函数调用。构造函数调用会建立一个新的对象,构造函数一般不使用return,函数体执行完毕它会显示返回。还有,建立的对象继承自构造函数的prototype属性,构造函数中使用this关键字来引用这个新建立的对象。

//与普通函数同样的定义方式。 function Person(name, age) { this.name = name; this.age = age; this.say = function () { console.log("My name is " + this.name + ", I am " + this.age + " years old."); } } //用关键字new调用构造函数,实例化对象。 var obj = new Person("Lamb", "21"); obj.say();//调用对象方法。

4.咱们知道Javascript中的函数也是对象,因此函数对象也是能够包含方法的,其中call()apply()两个方法能够用来间接地调用函数,这两个方法均可以显式指定调用函数里面的调用上下文this

//定义一个打印函数。 function print() { if (this.text) { alert(this.text); } else { alert("undefined"); } } //call方法间接调用方法,并指定其调用上下文。 print.call({ text: "hello" });

关于call()apply()两个方法的用法以及区别下面详细讨论。

3.函数的实参和形参

JavaScript中的函数定义不须要指定函数形参的类型,调用函数时也不检查传入形参的个数。这样,同时也会留下两个疑问给咱们:

  1. 当调用函数时的实参个数和声明的形参个数不匹配的时候如何处理;
  2. 如何显式测试函数实参的类型,以免非法的实参传入函数。

下面就简单介绍JavaScript是如何对上述两个问题作出处理的。

可选参数
当调用函数的时候传入的实参比函数定义时指定的形参个数要少,剩下的形参都将设置为undefined。通常来讲,为了保持函数较好的适应性,都会给省略的参数设置一个合理的默认值。

function getPropertyNames(obj,/*optional*/arr) { arr=arr||[]; for (var property in obj) { arr.push(property); } return arr; }

须要注意的是,当使用这种可选实参来实现函数时,须要将可选实参放在实参列表的最后。通常来书,函数定义中使用注释/*optional*/来强调形参是可选的。

实参对象
当调用函数时传入的参数个数超过了本来函数定义的形参个数,那么方法中能够经过实参对象来获取,标识符arguments是指向实参对象的引用。实参对象是一个类数组对象,能够经过数字下标来访问传入函数的实参值。实参对象有一个重要的用处,就是让函数能够操做任意数量的实参,请看下面的例子:

//返回传入实参的最大值。 function max(/* ... */) { var max = Number.NEGATIVE_INFINITY; //该值表明负无穷大。 for (var i = 0; i < arguments.length; i++) { if (arguments[i] > max) { max = arguments[i]; } } return max; } //调用。 var largest = max(10, 45, 66, 35, 21); //=>66

还有重要的一点,若是函数中修改arguments[]元素,一样会影响对应的实参变量。

除以上以外,实参对象还包含了两个属性calleecaller

  • callee是ECMAScript标准规范的,它指代当前正在执行的函数。
  • caller是非标准属性可是大多数浏览器都支持,它指代当前正在执行函数的函数。
//callee能够用来递归匿名函数。 var sum = function (x) { if (x <= 1) return 1; return x + arguments.callee(x - 1); } //调用函数b,方法a中打印结果为函数b。 var a = function () { alert(a.caller); } var b = function () { a(); }

注意,在ECMAScript 5严格模式下,对这两个属性进行读写会产生一个类型错误。

实参类型
声明JavaScript函数时形参不须要指定类型,在形参传入函数体以前也不会作任何类型检查,可是JavaScript在必要的时候会进行类型转换,例如:

function mult(a, b) { return a * b; } function conn(x, y) { return x + y; } console.log(mult(3, "2")); //字符串类型自动转为数字类型,输出结果:6 console.log(conn(3, "2")); //数字类型自动转为字符串类型,输出结果:"32"

上述的两种类型存在隐式转换关系因此JS能够自动转换,可是还存在其余状况:好比,一个方法指望它第一个实参为数组,传入一个非数组的值就可能引起问题,这时就应当在函数体中添加实参类型检查逻辑。

4.做为值的函数

开篇提到过,在JavaScript中函数不只是一种语法,函数便是对象,简单概括函数具备的几种性质:

1.函数能够被赋值给一个变量;

function square(x) { return x * x; } var s = square; //如今s和square指代同一个函数对象 square(5); //=>25 s(5); //=>25

2.函数能够保存在对象的属性或数组元素中;

var array = [function (x) { return x * x; }, 20]; array[0](array[1]); //=>400

3.函数能够做为参数传入另一个函数;

//这里定义一些简单函数。 function add(x, y) { return x + y; } function subtract(x, y) { return x - y; } function multipty(x, y) { return x * y; } function divide(x, y) { return x / y; } //这里函数以上面某个函数作参数。 function operate(operator, num1, num2) { return operator(num1, num2); } //调用函数计算(4*5)-(2+3)的值。 var result = operate(subtract, operate(multipty, 4, 5), operate(add, 2, 3)); console.log(result); //=>15

4.函数能够设置属性。

//初始化函数对象的计数器属性。 uniqueInteger.counter = 0; //先返回计数器的值,而后计数器自增1。 function uniqueInteger() { return uniqueInteger.counter+=1; }

当函数须要一个“静态”变量来在调用时保持某个值不变,最方便的方式就是给函数定义属性,而不是定义全局变量,由于定义全局变量会让命名空间变的杂乱无章。

5.做为命名空间的函数

函数中声明的变量只在函数内部是有定义,不在任何函数内声明的变量是全局变量,它在JavaScript代码中的任何地方都是有定义的。JavaScript中没有办法声明只在一个代码块内可见的变量的。基于这个缘由,经常须要定义一个函数用做临时的命名空间,在这个命名空间内定义的变量都不会污染到全局变量。

//该函数就可看做一个命名空间。 function mymodule() { //该函数下的变量都变成了“mymodule”空间下的局部变量,不会污染全局变量。 } //最后须要调用命名空间函数。 mymodule();

上段代码仍是会暴露出一个全局变量:mymodule函数。更为常见的写法是,直接定义一个匿名函数,并在单个表达式中调用它:

//将上面mymodule()函数重写成匿名函数,结束定义并当即调用它。 (function () { //模块代码。 }());

6.闭包

闭包是JavaScript中的一个难点。在理解闭包以前先要明白变量做用域函数做用域链两个概念。

  • 变量做用域:无非就是两种,全局变量和局部变量。全局变量拥有全局做用域,在任何地方都是有定义的。局部变量通常是指在函数内部定义的变量,它们只在函数内部有定义。

  • 函数做用域链:咱们知道JavaScript函数是能够嵌套的,子函数对象会一级一级地向上寻找全部父函数对象的变量。因此,父函数对象的全部变量,对子函数对象都是可见的,反之则不成立。须要知道的一点是,函数做用域链是在定义函数的时候建立的。

关于“闭包”的概念书本上定义很具体,可是也很抽象,很难理解。简单的理解,“闭包”就是定义在一个函数内部的函数(这么说并不许确,应该说闭包是函数的做用域)。

var scope = "global scope"; //全局变量 function checkscope() { var scope = "local scope"; //局部变量 function f() { return scope; } //在做用域中返回这个值 return f(); } checkscope(); //=>"local scope"

上面一段代码就就实现了一个简单的闭包,函数f()就是闭包。根据输出结果,能够看出闭包能够保存外层函数局部变量,经过闭包能够把函数内的变量暴露在全局做用域下。

闭包有什么做用呢?下面一段代码是上文利用函数属性定义的一个计数器函数,其实它存在一个问题:恶意代码能够修改counter属性值,从而让uniqueInteger函数计数出错。

//初始化函数对象的计数器属性。 uniqueInteger.counter = 0; //先返回计数器的值,而后计数器自增1。 function uniqueInteger() { return uniqueInteger.counter+=1; }

闭包可捕捉到单个函数调用的局部变量,并将这些局部变量用做私有状态,故咱们能够利用闭包的特性来重写uniqueInteger函数。

//利用闭包重写。 var uniqueInteger = (function () { //定义函数并当即调用 var counter = 0; //函数的私有状态 return function () { return counter += 1; }; })(); //调用。 uniqueInteger(); //=>1 uniqueInteger(); //=>2 uniqueInteger(); //=>3

当外部函数返回后,其余任何代码都没法访问counter变量,只有内部的函数才能访问。根据输出结果能够看出,闭包会使得函数中的变量都被保存在内存中,内存消耗大,因此要合理使用闭包。

counter同样的私有变量在多个嵌套函数中均可以访问到它,由于这多个嵌套函数都共享同一个做用域链,看下面一段代码:

function counter() { var n = 0; return { count: function () { return n += 1; }, reset: function () { n = 0; } }; } var c = counter(), d = counter(); //建立两个计时器 c.count(); //=>0 d.count(); //=>0 能看出它们互不干扰 c.reset(); //reset和count方法共享状态 c.count(); //=>0 由于重置了计数器c d.count(); //=>1 而没有重置计数器d

书写闭包的时候还需注意一件事,this是JavaScript的关键字,而不是变量。由于闭包内的函数只能访问闭包内的变量,因此this必需要赋给that才能引用。绑定arguments的问题与之相似。

var name = "The Window"; var object = { name: "My Object", getName: function () { var that = this; return function () { return that.name; }; } }; console.log(object.getName()()); //=>"My Object"

到这里若是你还不明白我在说什么,这里推荐两篇前辈们写的关于“闭包”的文章。
阮一峰,学习Javascript闭包(Closure)
russj,JavaScript 闭包的理解

7.函数属性、方法和构造函数

前文已经介绍过,在JavaScript中函数也是对象,它也能够像普通对象同样拥有属性和方法。

length属性
在函数体里,arguments.length表示传入函数的实参的个数。而函数自己的length属性表示的则是“形参”,也就是在函数调用时指望传入函数的实参个数。

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; }

prototype属性
每一个函数都包含prototype属性,这个属性指向一个对象的引用,这个对象也就是原型对象。当将函数用做构造函数的时候,新建立的对象会从原型对象上继承属性。

call()方法和apply()方法
上文提到,这两个方法能够用来间接调用函数。call()apply()的第一个实参表示要调用函数的母对象,它是调用上下文,在函数内经过this来引用母对象。假如要想把函数func()以对象obj方法的形式来调用,能够这样:

func.call(obj); func.apply(obj);

call()apply()的区别之处是,第一个实参(调用上下文)以后的全部实参传入的方式不一样。

func.call(obj, 1, 2); //实参能够为任意数量 func.apply(obj, [1, 2]); //实参都放在了一个数组中

下面看一个有意思的函数,他能将一个对象的方法替换为一个新方法。这个新方法“包裹”了原始方法,实现了AOP。

//调用原始方法以前和以后记录日志消息 function trace(o, m) { var original = o[m]; //在闭包中保存原始方法 o[m] = function () { //定义新方法 console.log(new Date(), "Entering:", m); //输出日志消息 var result = original.apply(o, arguments); //调用原始方法 console.log(new Date(), "Exiting:", m); //输出日志消息 return result; //返回结果 } }

这种动态修改已有方法的作法,也被称做“猴子补丁(monkey-patching)”。

bind()方法
bind()方法是ES5中新增的方法,这个方法的主要做用是将函数绑定至某个对象。该方法会返回一个新的函数,调用这个新的函数会将原始函数看成传入对象的方法来调用。

function func(y) { return this.x + y; } //待绑定的函数 var o = { x: 1 }; //将要绑定的对象 var f = func.bind(o);//经过调用f()来调用o.func() f(2); //=>3

ES3中能够经过下面的代码来实现bind()方法:

if (!Function.prototype.bind) { Function.prototype.bind = function (o /* , args */) { //将this和arguments保存在变量中,以便在嵌套函数中使用。 var self = this, boundArgs = arguments; //bind()方法返回的是一个函数。 return function () { //建立一个参数列表,将传入bind()的第二个及后续的实参都传入这个函数。 var args = [], i; for (var i = 1; i < boundArgs.length; i++) { args.push(boundArgs[i]); } for (var i = 0; i < arguments.length; i++) { args.push(boundArgs[i]); } //如今将self做为o的方法来调用,传入这些实参。 return self.apply(o,args); } } }

Function()构造函数
定义函数时须要使用function关键字,可是函数还能够经过Function()构造函数来定义。Function()构造函数能够传入任意数量字符串实参,最后一个实参字符串表示函数体,每两条语句之间也须要用分号分隔。

var f = Function("x", "y", "return x*y;"); //等价于下面的函数 var f = function f(x, y) { return x * y; }

关于Function()构造函数须要注意如下几点:

  • Function()构造函数容许Javascript在运行时动态建立并编译函数;
  • 每次调用Function()构造函数都会解析函数体并建立新的函数。若是将其放在循环代码块中执行,执行效率会受到影响;
  • 最重要的一点,它所建立的函数并非使用词法做用域,相反,函数体代码的编译老是会在顶层函数执行。好比下面代码所示:

    var scope = "global scope"; function checkscope() { var scope = "local scope"; return Function("return scope;"); //没法捕获局部做用域 } checkscope(); //=>"global scope"

    Function()构造函数能够看做是在全局做用域中执行的eval(),在实际开发中不多见到。

8.函数式编程

JavaScript中能够像操控对象同样操控函数,也就是说能够在JavaScript中应用函数式编程技术。

使用函数处理数组
假设有一个数组,数组元素都是数字,咱们想要计算这些元素的平均值和标准差。能够利用map()reduce()等数组方法来实现,符合函数式编程风格。

//首先定义两个简单的函数。 var sum = function (x, y) { return x + y; } var square = function (x) { return x * x } //将上面的函数和数组方法配合使用计算出平均数和标准差。 var data = [1, 1, 3, 5, 5]; 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));

高阶函数
所谓高阶函数就是函数操做函数,它接收一个或多个函数做为参数,并返回一个新的函数。

//返回传入函数func返回值的逻辑非。 function not(func) { return function () { var result = func.apply(this, arguments); return !result; }; } //判断传入参数a是否为偶数。 var even = function (x) { return x % 2 === 0; } var odd = not(even); //odd为新的函数,所作的事和even()相反。 [1, 1, 3, 5, 5].every(odd); //=>true 每一个元素都是奇数。

这里是一个更常见的例子,它接收两个函数f()g(),并返回一个新的函数用以计算f(g())

//返回一个新的函数,计算f(g(...))。 function compose(f, g) { return function () { //须要给f()传入一个参数,因此使用f()的call()方法。 //须要给g()传入不少参数,因此使用g()的apply()方法。 return f.call(this, g.apply(this, arguments)); } } var square = function (x) { return x * x; } var sum = function (x, y) { return x + y; } var squareofsum = compose(square, sum); squareofsum(2, 3); //=>25

记忆
能将上次计算的结果缓存起来,在函数式编程当中,这种缓存技巧叫作“记忆”。下面的代码展现了一个高阶函数,memorize()接收一个函数做为实参,并返回带有记忆能力的函数。

//返回f()的带有记忆功能的版本。 function memorize(f) { //将值保存在闭包中。 var cache = {}; return function () { //将实参转换为字符串形式,并将其用作缓存的键。 var key = arguments.length + Array.prototype.join.call(arguments, ","); if (key in cache) { return cache[key]; } else { return cache[key] = f.apply(this, arguments); } } }

memorize()所返回的函数将它的实参数组转换成字符串,并将字符串用作缓存对象的属性名。若是缓存中存在这个值,则直接返回它,不然调用既定的函数对实参进行计算,将计算结果缓存起来并保存。下面代码展现了如何使用memorize()

//返回两个整数的最大公约数。 function gcd(a, b) { var temp; if (a < b) { //确保 a >= b temp = b; b = a; a = temp; } while (b != 0) { //这里是求最大公约数的欧几里德算法 temp = b; b = a % b; a = temp; } return a; } var gcdmemo = memorize(gcd); gcdmemo(85, 187); //当写一个递归函数时,每每须要实现记忆功能。 var factorial = memorize(function (n) { return (n <= 1) ? 1 : n * factorial(n - 1); }); factorial(5); //=>120

9.参考与扩展

本篇内容源自我对《JavaScript权威指南》第8章 函数 章节的阅读总结和代码实践。总结的比较粗糙,你也可经过原著或MDN更深刻了解函数。

相关文章
相关标签/搜索