第三章 函数
一、函数相关概念
1、函数定义
函数也是一种特殊的数据类型。在javaScript中函数没有重载的概念。同名函数之间会覆盖,而与参数没有关系。
函数定义三种方式:
1)使用构造函数直接创建
var function_name = new Function(arg1,arg2,...,argn,function_body);
//不推荐使用Function构造函数创建函数
2)创建函数声明
Function multiply(x,y){return x*y}
//代码块,对他的调用木有顺序问题,这种也通常称之为构造函数
这会涉及一个重要特征:函数声明提升-->在执行代码之前会先读取函数声明。例如:
sayHi();
function sayHi(){
Alert(‘Hi’);
}
注:这里并不会因为在函数之前调用就会报错。因为在代码执行前会先读取函数声明。
3)函数表达式
var multiply = function(x,y){return x*y}
注:一个函数是可以通过外部代码调用的一个子程序。他们是Function对象。在函数执行时,this关键字并不会指向函数本身,而是指向调用该函数的对象。(一个函数最多255个参数)
2、匿名函数应用:
<1>回调函数:用函数A传递给函数B,由B来执行A时,A就是一个回调函数。若A是匿名的,则称为匿名回调函数。
<2>自调函数:例如:(function(){alert(‘1’)})() 使用自调函数的好处在于不会产生任何的全局变量。
<3>内部(私有)函数:闭包应用
3、函数的调用
<1>作为函数 普通的调用方式(纯函数fn())
<2>作为方法 方法调用(o.fn())
<3>作为构造函数 使用new关键字
<4>通过call()和apply()方法间接调用
调用方式,会影响this所指代的对象。
4、函数的形参和实参
ECMAScript函数不介意传递多少个参数,也不在乎传递什么数据类型。即便定义了值接收三个参数也未必一定要传递三个参数。
<1>当函数的实参个数比形参个数少时,剩下的形参都将设置为undefined
<2>实参对象:arguments
当调用的函数传入的实参个数超过函数定义的形参个数时,没有办法直接获得为命名值的引用。为了解决这个问题,在函数体内,标识符arguments是指向实参对象的引用。通过arguments对象的length属性,可以知道参数的个数,arguments[0]...arguments[i]可以访问参数值
注:arguments类数组,但不是数组,是实参对象
<3>函数中的参数传递是值传递
例如:
function setName(obj){
obj.name = “Mike”;
obj = new Object();
obj.name = “Jack”;
}
Var person = new Object();
setName(person);
Console.log(person.name); //Mike
如果person是引用传递,那么person就会被修改为指向其name为Jack的新对象。但是没有。实际上,当函数在内部重写obj时,这个变量的引用就是一个局部对象了。而这个局部对象再函数执行完毕后立即被销毁了。
<4>arguments的另一个属性-- callee 指向拥有这个arguments的函数。
function factorial(num){
if(num <=1){
retrun 1;
}else{
Return num*arguments.callee(num - 1);//这里没有用num*factorial(num-1)降低了耦合度
}
}
5、函数的属性、方法
<1>length属性--返回参数的个数
<2>prototype属性
<3>call()和apply()
这两个方法的用途都是在特定的作用域中调用函数。call()和apply()的第一个参数用作this的对象(即设置函数体内this对象的值)。Call(设置this的对象,其他参数...)而apply(设置this的对象,数组或者arguments对象)。
例如:
function sayColor(sPrefix,sSuffix) {
alert(sPrefix + this.color + sSuffix);//The color is blue a very nice color indeed.
};
//若是直接看sayColor这个函数的话,这里的this就得看这个函数的调用方式来决定this指代的是谁。
var obj = new Object();
obj.color = "blue";
sayColor.call(obj, "The color is ", " a very nice color indeed.");
//而我使用sayColor.call()去调用的话,指定了sayColor函数中的this指代的是obj这个对象,所以才会有“The color is blue a very nice color indeed.”
<4>bind()
<5>toString()
二、js中的全局函数
即Global对象的方法:
1、escape(String) 将参数中所有的非字母字符转换成XX%表示的等价数字
2、eval(String) 将传入的字符串,作为js源代码执行
3、isFinite(变量) 确定变量是否有边界,有则返回true无则返回false
4、isNaN(变量) 确定一个变量是否是非数字,是非数字返回true,否则false
5、parseFloat(字符串数字) 将字符串开头的整数或浮点数字分离出来
6、parseInt(字符串数字) 将字符串开头的整数分离出来
7、unescape(String) escape的逆向操作,将16进制--ASCII码
8、encodeURI(url) 传递一个可用的URL
9、decodeURI(url) encodeURI(url)的反转函数
10、encodeURIComponent(url) 传递URL一部分
11、decodeURIComponent(url) encodeURIComponent(url)的反转函数
三、变量的作用域
1、作用域链
作用域:在某些空间或范围内进行读写
域:
<1>全局作用域:<script>这里的变量是全局变量、这里的函数是全局函数</script>
在js中每遇到一个域,就会进行这样一些步骤:
a、预解析
找一些东西:根据--var function 参数....
对于var 声明的变量最初只会读取变量名,并未赋值,默认都是undefined
对于函数,在正式运行代码前,都是整块函数。
b、逐行读取(从上至下)
表达式: = + - * / % ++ -- ! 参数 可以修改预解析值
遇到函数声明,未被调用什么都不会发生。
注:多个script标签,每个都是一个域,只会一个一个来。例如:
<script>
alert(a); // function a(){alert(4);}
var a = 1; // 遇到表达式 预解析中的值被改变
alert(a); // 1
function a(){alert(2);} //逐步执行,函数块只有被调用了才会被执行
var a = function(){alert(5);}//遇到表达式 预解析中的值再次被改变
alert(a); // function(){alert(5);}
a(); // 5 调用函数,此时的函数function(){alert(5);}
function a(){alert(4);}//函数在预解析中不受位置先后影响,优先级高于变量
var a = 6; //遇到表达式 预解析中的值再次被改变
alert(a); //6
a(); //报错,调用函数,而此时a已被表达式替换,不是函数了。此处相当于6();
</script>
<!--解释:首先在<script>标签中作为一个执行环境,首先进行预解析:会先预解析var 变量声明和函数,变量声明时默认都是undefined,当遇到同名的变量和函数时,不管其位置先后,会优先保留函数,这里有三个变量声明和两个函数,但未预见表达式时,执行的是预解析后的,这也是为什么第一次弹出的是function a(){alert(4);}构造函数的原因。而预解析完成后,则按从上到下的顺序开始执行代码,
但是表达式会改变预解析中的值,所以当遇到第二行代码时,原本的函数被替换为表达式值,所以第二行
弹出的是1.-->
<2>局部作用域:函数、 对象{}
执行环境:每个函数都有自己的执行环境(函数的内部也是一个域)。当执行进入一个函数是,函数的环境就会被推入一个环境栈中,而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。全局环境是最外围的一个执行环境。全局环境直到应用程序退出时才被销毁。
当代码在一个环境中执行时,会创建变量对象的一个作用域。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。活动对象在最开始时只包含一个变量,即arguments参数对象(这个对象在全局函数中是不存在的),并且由上至下,由里至外。具体案例再局部变量中。
javaScript它不存在大括号级的作用域,但是它有函数作用域。在函数内定义的变量在函数外是不可见的。但是如果该变量在某个代码块中定义的(如:if或者for语句),它在代码块外是可见的。例如:
<script>
alert(a) //undefined,并不是not defined,说明是解析了,但是由于再变量上面,还没进行赋值
if(true){
var a = 1; //最初预解析默认值是undefined
}
alert(a); //1 虽然在{}但是不影响取值
</script>
注:目的为了说明,写在条件语句中的变量是全局变量,函数全局函数,但是在所有浏览器中,火狐比较特殊是不会预解析{}中的东西的,所以尽量不用把变量或者函数定义在if/for循环中。
2、全局变量
声明在函数之外的(在if/for语句中定义的变量,它在代码块外是可见的),对于全局变量来说加不加var都无所谓
3、局部变量
声明在函数内的。局部变量无法在外部直接访问。局部对象会在函数执行完毕后立即被销毁。
<1>另外局部变量始终优先于全局域。(js中不存在大括号作用域,但是存在函数作用域)
<script>
var a =123;
function fn(){
alert(a); //并不是123,而是undefined
var a=1;
alert(a); //1,局部变量会覆盖与之同名的全局变量
}
fn();
</script>
解释:先进行预解析。此时预解析中有a变量是未定义和函数块f()。当函数被调用时,这时会创建一个新的作用域--函数作用域。函数作用域的优先级高于局部。在函数中,同样会经历预解析,逐步执行。首先预解析中会有a变量,因为优先级高于全局,并赋予a为undefined,所以逐步执行时,会弹出undefined。当执行到var a = 1 ;遇到表达式,这是会改变预解析中的值,此时为1。
<2>注意局部变量的声明方式有var 和没有var的区别。例如:
<script>
var a = 1;
function fn1(){
alert(a); //1 从上至下,由里及外找,局部未找到找全局(涉及作用域链)
a = 2; //在函数中,没有var 声明的变量,在被创建时会被赋予全局作用域
}
alert(a); //1 函数未被执行,a = 2未被创建,渠道的是var a = 1;
fn1(); //执行函数
alert(a); //2
</script>
解释:首先进行预解析。在函数还没有被调用的时候,预解析中只有a和fn1()函数块。而当函数被调用时,就有了一个新的作用域--函数域。函数域的优先级高于全局。此时在函数中又会经历预解析、逐步执行,此时在局部作用域中并未找到a,就会继续往上找,直到找到全局为止。再全局中发现了a =1,继续执行,a = 2 由于未被var声明,还是全局中的a(若变量名不一样,依然被赋予的是全局属性),表达式修改了预解析中a的值,所以下面再弹出a就是2了
<3>带参数的情况。例如:
<script>
var a = 1;
function fn2(a){//参数也会在预解析中创建,此时a被赋予undefined
alert(a); //undefined
a = 2; //参数只会在函数中存在,是局部的,也被当做是一种局部变量
}
fn2(); //执行函数
alert(a); //1 因为在此作用域中a的值就是1
</script>
解释:这里的参数可以视作为fn2(var a).如果我们把调用函数改为fn1(a)这时第一个就不是弹出undefined而是1了,这里相当于fn1(var a=1).
说到参数,这里会涉及一个arguments实参集合,使用环境,其实每次传进来的参数,都会有一个arguments集合,但它不是数组。使用情况,在我们不确定参数个数时可以使用它。其实我们调用时,函数中可以不写参数,在函数中可以用arguments去取传进来的参数。例如:
function fn2(){
arguments[0] ---> 1
arguments[2] ---> 3
};
fn2(1,2,3)
原理分析:
全局变量作为window的一个作用域,当代码进入一个函数时,js会创建一个新的作用域,来作为当前作用域的子作用域。此时,子作用域(变量:局部变量)优先于全局变量,局部变量会覆盖同名的全局变量,当变量与函数同名,只会保留函数。
整个的执行过程分析:每遇到一个域,都会执行这样一个过程。首先第一步预编译会将所有的函数创建为作用域上的函数,建立可调用的函数,并且所有的var变量也会在预编译中创建,此时的变量初始值均被设置为undefined。
然后再逐步执行。当js开始解释执行代码,遇到表达式时会改变预编译中的值。当js遇到函数调用时,js会首先在当前作用域查找函数或者变量,如果没有就会到上一层查找,直到找到window作用域为止。
四、闭包
1、垃圾回收机制
ECMAScript 要求使用自动垃圾收集机制。相当一部分实现对它们的垃圾收集操作只赋予了很低的优先级。即如果对象不再“可引用(由于不存在对它的引用,使执行代码无法再访问到它)”时,该对象就成为垃圾收集的目标。因而,在将来的某个时刻会将这个对象销毁并将它所占用的一切资源释放,以便操作系统重新利用。
正常情况下,当退出一个执行环境时就会满足类似的条件(或者说是函数执行完毕)。此时,作用域链结构中的活动(可变)对象以及在该执行环境中创建的任何对象--包括函数对象,都不再“可引用”,因此将成为垃圾收集的目标。都是闭包的出现,使得函数内的变量并没有立刻被回收。
标记清除是目前主流的垃圾收集算法,对于不在使用的值加上标记。另一种收集算法是“引用计数”,不过这种目前很少使用了;
2、闭包解释1
闭包的差别在于局部变量可以在函数执行结束后仍然被函数外的代码访问。这意味着函数必须返回一个指向闭包的“引用”,或将这个”引用”赋值给某个外部变量,才能保证闭包中局部变量被外部代码访问。当然包含这个引用的实体应该是一个对象,因为在Javascript中除了基本类型剩下的就都是对象了。可惜的是,ECMAScript并没有提供相关的成员和方法来访问闭包中的局部变量。但是在ECMAScript中,函数对象中定义的内部函数是可以直接访问外部函数的局部变量。
让我们说的更透彻一些。所谓“闭包”,就是在构造函数体内定义另外的函数作为目标对象的方法函数,而这个对象的方法函数反过来引用外层函数体中的临时变量。这使得只要目标 对象在生存期内始终能保持其方法,就能间接保持原构造函数体当时用到的临时变量值。尽管最开始的构造函数调用已经结束,临时变量的名称也都消失了,但在目标对象的方法内却始终能引用到该变量的值,而且该值只能通这种方法来访问。即使再次调用相同的构造函数,但只会生成新对象和方法,新的临时变量只是对应新的值,和上次那次调用的是各自独立的。例如:
function a() {
var i = 0; // local variable
// 每次调用时,产生闭包,并返回内部函数对象给调用者
function b() { alert(++i);
//在这个函数中他也是一个新的作用域,在这个作用于中她可以访问a()函数中的所有变量,自然也可以访问全局window中的变量。
//因为这个作用域是一级一级往上找的嘛。但是在a()函数中就不一样了他只能访问到a()自己的和全局window的。这就是作用域。也就是对于变量来说,他们的访问权限。
}
return b;
}
var c = a(); //这里已经执行完了a函数
c(); //1 通过闭包访问到了局部变量i,在函数b中依然保存着对应的i
c(); //2
//这里因为a()说明a()函数已经执行完毕。a()函数中的变量都会被回收了。它已经不是0了。在当然样直接在外面也访问不到。但是a()函数执行完毕之后他返回b函数当执行c,他是对b()函数的一个引用(一切皆对象,函数也可以看成是对象)。
//因为被c引用着,所以b作为c的引用不会被回收。b()函数中的变量i还活着。这时他是1了。因为没有被回收,我们再次执行c,因为之前还活着没有被回收,自然不是从0开始加,因为他保存时已经是1了。所以才会变成2.不然你再次执行++i,他都不知道i是什么,++i又弹出什么。
<1>当定义函数a的时候,js解释器会将函数a的作用域链(scope chain)设置为定义a时a所在的“环境”,如果a是一个全局函数,则scope chain中只有window对象。
<2>当执行函数a的时候,a会进入相应的执行环境(excution context)。
<3>在创建执行环境的过程中,首先会为a添加一个scope属性,即a的作用域,其值就为第1步中的scope chain。即a.scope=a的作用域链。
<4>然后执行环境会创建一个活动对象(call object)。活动对象也是一个拥有属性的对象,但它不具有原型而且不能通过JavaScript代码直接访问。创建完活动对象后,把活动对象添加到a的作用域链的最顶端。此时a的作用域链包含了两个对象:a的活动对象和window对象。
<5>下一步是在活动对象上添加一个arguments属性,它保存着调用函数a时所传递的参数。
<6>最后把所有函数a的形参和内部的函数b的引用也添加到a的活动对象上。在这一步中,完成了函数b的的定义,因此如同第3步,函数b的作用域链被设置为b所被定义的环境,即a的作用域。
到此,整个函数a从定义到执行的步骤就完成了。此时a返回函数b的引用给c,而函数b的作用域链包含了对函数a的活动对象的引用,也就是说b可以访问到a中定义的所有变量和函数。函数b被c引用,函数b又依赖函数a,因此函数a在返回后不会被GC回收。
当函数b执行的时候亦会像以上步骤一样。因此,执行时b的作用域链包含了3个对象:b的活动对象、a的活动对象和window对象.
当在函数b中访问一个变量的时候,搜索顺序是:
先搜索自身的活动对象,如果存在则返回,如果不存在将继续搜索函数a的活动对象,依次查找,直到找到为止。
如果函数b存在prototype原型对象,则在查找完自身的活动对象后先查找自身的原型对象,再继续查找。这就是Javascript中的变量查找机制。
如果整个作用域链上都无法找到,则返回undefined。例如:
function f(x) {
var g = function () { return x; }
return g;
}
var h = f(1);
alert(h());
分析:结果弹出 1
变量h,指向了f中返回的函数g.也就相当于h是g的引用。此时h的作用域链为:h的活动对象/[g]-->f的活动对象-->window对象。
3、闭包解释2
闭包就是函数的局部变量集合,只是这些局部变量在函数返回后会继续存在。
闭包就是就是函数的“堆栈”在函数返回后并不释放,我们也可以理解为这些函数堆栈并不在栈上分配而是在堆上分配
最简单的描述:当函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。也就是说,内部函数会在外部函数返回后被执行。而当这个内部函数执行时,它仍然必需访问其外部函数的局部变量、参数以及其他内部函数。这些局部变量、参数和函数声明(最初时)的值是外部函数返回时的值,但也会受到内部函数的影响。
例如:
<script>
function exampleClosureForm(arg1,arg2){
var localVar = 8;
function exampleReturned(innerArg){
return ((arg1+arg2)/(innerArg + localVar));
}
/*返回一个定义为exampleReturned的内部函数的引用*/
return exampleReturned;
}
var globalVar = exampleClosureForm(2,4); //这个返回的exampleReturned函数的引用
console.log(globalVar(2)); //0.6 指向上面的函数后,变量值还保存着
</script>
这种情况下,在调用外部函数 exampleClosureForm 的执行环境中所创建的函数对象就不会被当作垃圾收集,因为该函数对象被一个全局变量(globalVar)所引用,而且仍然是可以访问的,甚至可以通过 globalVar(n) 来执行。
总结:
闭包的特点:函数嵌套函数,内部函数可以引用外部函数的参数和变量。参数和
变量不会被垃圾回收机制收回。
好处:希望一个变量长期驻扎在内存当中(不会被回收);避免全局变量的污染;
私有成员的存在。