【读书笔记】读《JavaScript高级程序设计-第2版》 - 函数部分
1. 定义
函数实际上是对象,每个函数都是Function类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。
对于函数的定义有以下三种类型:
函数声明式定义——如
1 function sum(num1, num2) {return num1 + num2;}
函数表达式定义——如
1 var sum = function(num , num2) {return num1 + num2;}; //注意函数末尾有一个分号,就像声明其他变量一样
Function构造函数——如
1 var sum = Function("num1", "num2", "return num1 + num2"); //不推荐,但是这种写法对于理解“函数是对象,函数名是指针”的概念倒是非常直观的。
由于函数名仅仅是指向函数的指针,因此函数名与包含对象指针的其他变量没有区别,也就是说,一个函数可能会有多个名字,如
1 function sum(num1, num2) {return num1 + num2;} 2 var anotherSum = sum; //注意:使用不带圆括号的函数名是访问函数的指针 3 sum = null; 4 alert(anotherSum(10, 10)); //20
2. 没有重载
将函数名作为指针,很容易理解为什么js中没有函数重载的概念。
1 function addNum(num) {return num + 100;} 2 function addNum(num) {return num + 200;} 3 var result = addNum(100); //300
显然后面的函数覆盖了前面的函数。
以下做以等效替换——
1 var addNum = function(num) {return num + 100;} 2 addNum = function(num) {return num + 200;} 3 var result = addNum(100); //300
因此说,在创建第二个函数的时候,实际上覆盖了引用第一个函数的变量addNum。
3. 函数声明与函数表达式
解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。或者理解为第一种定义方式会在代码执行之前被加载到作用域中,而后者则是在代码执行到那一行的时候才会有定义。
4. 作为值的函数
因为js中的函数名本身就是变量,所以函数可以当做值来使用。也就是说,不仅可以向传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。
5. 函数内部属性(函数被调用后所具有的属性)
每个函数在被调用时,其活动对象都会自动取得两个特殊的对象:arguments和this。
arguments是一个类数组对象,包含着传入函数中的所有参数,这个对象还有一个名为callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数。
this引用的是函数据以执行操作的对象,或者说是函数在执行时所处的作用域。(当在网页的全局作用域中调用函数时,this对象引用的是window)
1 window.color = "red"; 2 var o = { color:"blue"}; 3 function saycolor() { 4 alert(this.color); 5 } 6 saycolor(); //red:在全局作用域中调用 7 o.saycolor = saycolor; 8 o.saycolor(); //blue //此时this引用的是对象o
注意:函数的名字仅仅是一个包含指针的变量而已,因此,即使是在不同的环境中执行,全局的saycolor()函数与o.saycolor()指向的仍然是同一个函数。
6. 函数本身(固有)属性和方法
每一个函数都有两个属性:length和prototype。其中,length属性表示的是函数希望接受的命名参数的个数,如
1 function sum(num1, num2) {return num1 + num2;} 2 alert(sum.length) //2
prototype属性是保存它们所有实例方法的真正所在。换句话说,诸如toString()和valueOf()等方法实际上都保存在prototype名下,只不过是通过各自对象的实例访问罢了。
每个函数都包含两个非继承而来的方法:apply()和call()。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值。先看apply()方法的使用——
1 function sum(num1, num2) { 2 return num1 + num2; 3 } 4 function callSum1(num1, num2) { 5 return sum.apply(this, arguments); //传递arguments对象 6 } 7 function callSum2(num1, num2) { 8 return sum.apply(this, [num1, num2]); //传递数组 9 } 10 alert(callSum1(10, 10)); //20这里的this作用域是window对象 11 alert(callSum2(10, 10)); //20这里的this作用域是window对象
call()和apply()的作用相同,区别在于接受参数的方式不同。如
1 function callSum3(num1, num2) { 2 return sum.call(this, num1, num2); //传递的参数必须逐个列举出来 3 // return sum.call(this, arguments[0], arguments[1]); 4 }
对于是使用apply()还是call(),完全取决于采取哪种给函数传递参数的方式最方便。
对于这两个方法的真正强大之处在于能够扩充函数赖以运行的作用域。如
1 window.color = "red"; 2 var o = { color:"blue"}; 3 function saycolor() { 4 alert(this.color); 5 } 6 saycolor(); //red 7 saycolor.call(this); //red 8 saycolor.apply(window); //red 9 saycolor.apply(o); //blue 10 11 //o.saycolor = saycolor; //对象和方法具有一定的耦合 12 //o.saycolor(); //blue
因此,使用call()(或者apply())来扩充作用域的最大好处是对象不需要与方法有任何耦合关系。
7. 理解返回值
函数在定义时不必指定是否返回值。如果函数具有返回值,函数会在执行完return语句之后停止并立即退出,因此,位于return语句之后的任何代码都永远不会执行。另外,return语句也可以不带有任何返回值,在这种情况下,函数在停止执行后将返回undefined值,这种做法一般用在需要提前停止函数执行而又不需要返回值的情况下。如
1 function sayHi(name, msg) { 2 return; 3 alert("Hello " + name + "," + msg); //永远不会被调用 4 }
推荐的做法是要么让函数始终都返回一个值,要么永远都不要返回值。否则,如果函数有时候返回值,有时候不返回值,会给调试代码带来不便。
8. 理解参数
JavaScript函数不介意传递进来多少个参数,也不在乎传进来的参数是什么数据类型。如
1 //常规写法 2 function sayHi(name, msg) { 3 alert("Hello " + name + "," + msg); //永远不会被调用 4 } 5 //变通写法 6 function syaHi() { 7 alert("Hello " + arguments[0] + "," + arguments[1]); 8 } 9 sayHi("King", "How are you?"); //两个函数的执行结果是一致的
之所以会这样,原因是JavaScript中的参数在内部是用一个数组来表示的。函数接收到的始终是一个数组,而不关心数组汇总包含哪些参数。如果这个数组不包含任何元素,无所谓;如果包含多个参数,也没有问题。
可以通过arguments.length属性可以获知有多少个参数传递了函数。
当然,还可以arguments对象和命名参数一起使用,如
1 function doAdd(num1, num2) { 2 if (arguments.length == 1) { 3 alert(num1 + 10); 4 } else if (arguments.length == 2) { 5 alert(num1 + num2); 6 } 7 }
没有传递值的命名参数将自动被赋予undefined值,这就跟定义了变量但又没有初始化一样。
9. 匿名函数——递归
一个非常经典的递归阶乘函数:
1 function factorial(num) { 2 if (num <= 1) { 3 return 1; 4 } else { 5 return num * factorial(num - 1); 6 } 7 } 8 var anotherFactorial = factorial; 9 factorial = null; 10 alert(anotherFactorial(4)); //出错,源于内部定义已经引用方法factorial
方法改进如下:
1 function factorial(num) { 2 if (num <= 1) { 3 return 1; 4 } else { 5 return num * arguments.callee(num - 1); //arguments.callee指代的是被调用者 6 } 7 } //方法经改进后alert(anotherFactorial(4));就不会出错了
10. 匿名函数——闭包
闭包指有权访问另一个函数作用域中的变量的函数。(最核心的目的是从函数的外部读到函数的内部变量)
1 function f1(){ 2 var n=999; 3 //它本身是一个匿名函数,同时也是一个闭包 4 //这里的变量nAdd是一个全局变量 5 nAdd=function(){ 6 n+=1; 7 } 8 function f2(){ 9 //可以对上层函数f1的局部变量进行任何操作 10 alert(n); 11 } 12 return f2; 13 } 14 //闭包是一个函数,这个函数可以访问到另一个函数的局部变量 15 var result=f1(); //由于闭包函数的存在,使得f1()函数中的局部变量被保存在内存中,使得当f1()函数被调用后,其内部的变量n依旧存在,并没有在f1调用后被自动清除 16 //n一直保存在内存中的原因剖析: 17 //f1是f2的父函数,而f2被赋给了一个全局变量result,这导致f2始终在内存中, 18 //而f2的存在依赖于f1,因此f1也始终在内存中,不会再调用结束后,被垃圾收集机制回收 19 result(); // 999 20 nAdd(); //因为n一直保存在内存中,nAdd()本身就是一个闭包,nAdd()是对其上层父函数f1中局部变量的操作,所以n的值变为了1000 21 result(); // 1000
思考——
1 var name = "The Window"; 2 var object = { 3 name : "My Object", 4 getNameFunc : function(){ 5 return function(){ 6 return this.name; // The Window 7 }; 8 } 9 }; 10 alert(object.getNameFunc()()); //The Window ??
this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被当做某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象通常指向window。
为什么会这样呢?原因是在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。每个函数再被调用的时候,其活动对象都会自动取得两个特殊变量:this和arguments。内部函数在搜索者两个变量时,只会搜索到其活动对象为止,因此不可能直接访问到外部函数中的这两个变量。
改进:把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了,如下所示:
1 var name = "The Window"; 2 var object = { 3 name : "My Object", 4 getNameFunc : function(){ 5 var that = this; 6 return function(){ 7 return that.name; // My Object 8 }; 9 } 10 }; 11 alert(object.getNameFunc()()); // My Object
注意:由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能导致内存占用过多,因此建议只在绝对必要时再考虑使用闭包。
11. 匿名函数——模仿块级作用域
1 function output(count) { 2 for(var i = 0; i < count; i++){ 3 alert(i); 4 } 5 var i;//JavaScript不会告诉是否多次声明了变量,只会对后续的声明视而不见,但会改变其值 6 }
从i有了定义开始,就可以在函数内部随处访问它。
我们想要在i使用完之后立即销毁掉。可以用匿名函数来模仿块级作用域来实现。
1 (function(){ 2 //块级作用域 3 })()
注意:不可以丢掉左边的小括号,即需要将函数封装在一对括号中。否则js将function关键字当做一个函数声明的开始,而函数声明后面不能跟圆括号。对于函数表达式的写法,如
1 var someFunc = function(){}; 2 someFunc();
其表达式变量someFunc随后跟了一个小括号,代表函数的调用。因此,同理而言,(function(){//块级作用域})()的左侧小括号代表一个匿名函数表达式,而后边的小括号代表该匿名函数的执行。因此,其内部的变量在执行后随即被销毁。
所以,无论在什么地方,只要临时需要一些变量,就可以使用块级作用域(也称为私有作用域)。对于上面的例子可改造如下:
1 function output(count) { 2 (function(){ 3 //能够访问到count变量是因为块级作用域本身是一个闭包 4 for(var I = 0; I < count; i++){ 5 alert(i); 6 } 7 )(); 8 alert(i); //报错 9 }
这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。一般来说,我们应该尽量减少向全局作用域中添加变量和函数。在一个由很多人开发人员共同参与的大型应用程序中,过多的全局变量和函数很容易导致命名冲突。通过创建私有作用域,每个开发人员既可以使用自己的变量,又不必担心搞乱全局作用域。而且,这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。
12.关于匿名函数,总结如下:
1>任何函数表达式从技术上说都是匿名函数,因为没有引用它们的确定的方式;
2>递归函数应该始终使用arguments.callee来递归地调用自身,不要使用函数名,因为函数名可能会发生变化;
3>当在函数内部定义了其他函数时,就创建了闭包。闭包有权访问包含函数内部的所有变量;是源于:
a>在后台执行环境中,闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域;
b>通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。但是,当函数返回了一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止;
4>使用闭包可以在JavaScript中模仿块级作用域,要点如下:
a>创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用;
b>结果就是函数内部的所有变量都会被立即销毁,除非将某些变量赋值给了包含作用域中的变量;