最近学这块知识学得有些吃力。还有很多遗漏的地方,只能以后多看些书来弥补了。
第7章 函数表达式
函数定义的两种方式:函数声明,函数表达式。这个在第5章函数声明与函数表达式也有提到的
函数声明:
- 语法:
function functionName(){}
- 一些浏览器给函数定义了一个非标准的name属性,这个属性值=functionName
- 函数声明提升:执行代码之前会先读取函数函数声明。就是说可以把函数声明放在调用它的语句之后
函数表达式:
- 函数表达式有几种不同的语法形式
- 这个比较常见:
var functionName = function(){};
- 用以上创建的函数叫匿名函数(name属性是空字符串)。
- 既然是表达式,在使用前就要先赋值
7.1 递归
arguments.callee这是个很有用的属性,指向正在执行的函数的指针。可以用它实现对函数的递归调用,第5章函数内部属性也讲到过。贴一段经典代码!
function factorial(num){
if (num <= 1){
return 1;
}else{
return num * arguments.callee(num-1);
}
}
用arguments.callee
比用函数名更保险。不过严格模式下会有错误。
var factorial = (function f(num){
if (num <= 1){
return 1;
}else{
return num * arguments.callee(num-1);
}
});
以上代码就是把函数f赋值给factorial了
7.2 闭包
这里刚开始看的时候有许多概念不懂,导致前后不能连贯,对作用域链一直存在疑问。所以我首先列举一下一些重要的概念。之前在第4章提到过的执行环境。我看了当时做的笔记,记的不是很完整。 所以在这里也补充一下
执行环境:
定义了变量或函数有权访问的其他数据
每个执行环境都有一个与之关联的变量对象。这个变量对象是用来保存环境中定义的所有变量和函数的
-
执行环境可以分为全局执行环境和函数执行环境
- 全局执行环境直到应用程序退出时才会销毁
- 每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。执行完毕后栈将其环境弹出
当代码在一个环境中执行时,会创建变量对象的一个作用域链
作用域链:
- 作用域链的前端始终都是当前执行代码所在环境的变量对象
- 作用域链的下一个变量对象来租包含(外部)环境
- 如果当前执行代码所在环境是函数,则将其活动对象作为变量对象
- 活动对象最开始只包含一个变量,即arguments对象。
变量对象:
- 保存了环境中定义的所有变量和函数
- 每个执行环境都有一个变量对象
- 局部环境的变量对象只在函数执行的过程中存在
- 全局环境变量对象始终存在(这两点和执行环境相通)
- 变量对象存储着环境中的以下内容
- 函数的形参
- var声明的变量
- 函数声明(但不包含函数表达式)
活动对象:
- 活动对象就是作用域链上正在被执行和引用的变量对象
当创建一个函数时:
- 创建预先包含全局变量对象的作用域链
- 这个作用域链保存在函数内部的[[Scope]]属性中
当第一次调用函数时:
- 创建一个执行环境(注意要调用的时候才会有执行环境)
- 复制函数的[[Scope]]属性中的对象,构建这个执行环境的作用域链
- 创建一个活动对象,使用this、arguments和其他命名参数的值来初始化函数的活动对象,把这个活动对象推入执行环境作用域链的前端
- 外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象在第三位,以此类推,直到作为作用域链终点的全局执行环境
下面结合书上的例子来看一下上述的两个过程
function compare(value1, value2){
if(value1 < value2){
return -1;
}else if(value1 > value2){
return 1;
}else{
return 0;
}
}
var result = compare(5, 10);
-
创建这个函数的时候:
- 第一次调用
-
复制作用域链到执行环境中
-
把活动对象推入执行环境作用域链的前端
一般来说,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是闭包的情况要特殊一点。
在另一个函数内部定义的函数会将包含函数(外部函数)的活动对象添加到它的作用域链中。下面看另一个例子
function createComparisonFunction(propertyName){
return function(object1, object2){
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if (value1 < value2){
return -1;
}else if (value1 > value2){
return 1;
}else{
return 0;
}
}
}
//创建函数
var compareNames = createComparisonFunction("name");
//调用函数
var result = compareNames({name:"xjh"},{name:"xkld"});
//解除对匿名函数的引用(以便释放内存)
compareNames = null;
也就是说这个时候里面的匿名函数是有权访问propertyName的
7.2.1 闭包与变量
闭包保存的是整个变量对象,不是某个特殊的值
7.2.2 关于this对象
this对象
- 是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,当函数被作为某个对象的方法调用时,this等于那个对象。
- 匿名函数的执行环境具有全局性,因此this对象通常指向window。
7.2.3 内存泄漏
如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素无法被销毁
7.3 模仿块级作用域
JS中没有块级作用域的概念,任何变量都是在函数中创建的
当重复声明一个变量时,只会对后续的声明视为不见(不过可以执行后续声明的变量初始化)
模拟块级作用域(私有作用域):
(function(){ //在外面加()的目的是把函数声明转换成函数表达式。JS中函数声明后面不能加圆括号
//块级作用域
})();
这段代码定义并调用了一个匿名函数,将函数声明包含在()中,最后的()会立即调用这个函数
定义匿名函数可以减少闭包占用的内存问题,因为没有指向匿名函数的引用,只要函数执行完毕,就可以立即销毁其作用域链
7.4 私有变量
JS没有私有成员的概念:所有对象属性都是共有的,不过有一个私有变量的概念:不能在函数的外部访问这些变量
私有变量包括函数的参数、局部变量、在函数内部定义的其他函数
可以利用闭包创建用于访问私有变量的公有方法
特权方法:有权访问私有变量和私有函数的公有方法
创建特权方法的两种方式:
- 在构造函数中定义特权方法
function MyObject(){
var privateVar = 10;
function privateFunction(){
return false;
}
this.publicMethod = function(){
privateVar++;
return privateFunction();
};
}
这个privateVar和privateFunction可以看成是私有的,因为在别的地方无法访问到这些变量(可以参考私有作用域for循环里面的i)
publicMethod因为是对象的属性(前面有this的),这样的话用构造函数创建一个实例的时候就有了这个公有的方法。而这个publicMethod的作用域链包含着MyObject的作用域链,就可以访问到对应的私有变量了
看如下代码:
function Person(name){
this.getName = function(){
return name;
};
this.setName = function(value){
name = value
};
}
getName()和setName()作为闭包能通过作用域链访问name。私有变量name在Person的每一个实例中都不同,每次调用构造函数都会重新创建这两个方法。这个方法和通过构造函数创建对象一样有个缺点,就是每次都要重新创建get和set这两个方法
- 使用静态私有变量
7.4.1 静态私有变量
(function(){
var privateVar = 10;
function privateFunction(){
return false;
}
MyObject = function(){//定义了一个全局变量MyObject指向这个匿名构造函数
};
MyObject.prototype.publicMethod = function(){
...
};
})();
以上代码主要是在私有作用域里面定义了一个构造函数。利用这个构造函数的原型访问私有变量
4.2中提到过:使用var声明的变量会自动被添加到最接近的环境中。在函数内部就是局部环境,with语句中就是函数环境。不使用var声明,变量自动添加到全局环境。这里定义构造函数时不用函数声明(function MyObject()
)的主要原因是函数声明只能创建局部函数。而用函数表达式,变量前不加var就可以定义一个全局变量,能够在私有作用域外被访问到(严格模式下会报错)
但是这个方法每个实例都可以对name进行修改,name就成为一个静态私有变量
7.4.2 模块模式
前面两种模式主要用于为自定义类型创建私有变量和特权方法。模块模式是为单例创建私有变量和方法
JS中以对象字面量方式创建单例对象
var singleton = {
name:value;
method:function(){
}
};
模块模式通过为单例添加私有变量和特权方法能够使其得到增强
var singleton = function(){
var privateVar = 10;
function privateFunction(){
return false;
}
return {
publicProperty:true,
publicMethod:function(){
...
}
}
};
以上代码返回一个对象字面量,对象字面量里的函数有权访问私有变量和函数,在外部可以通过singleton.publicMethod()这种形式访问
从本质上讲,这个对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的(类比java中的单例模式)
var application = function(){
var components = new Array();
//初始化
components.push(new BaseComponent());//这两个语句在第一次执行后就不再执行,因为外面只会使用return的两个方法访问该私有变量
return {
getComponentCount:function(){
return components.length;
},
registerComponent:function(component){
if(typeof component == "Object"){
components.push(component);
}
}
};
}();
7.4.3 增强的模块模式
适合一些单例必须是某种类型的实例,同时必须添加某些属性或方法对其加以增强的情况
var singleton = function(){
var privateVar = 10;
function privateFunction(){
return false;
}
var object = new CustomType();
object.publicProperty = true;
object.publicMethod = function(){
...
}
return object;
}();
以上代码要求singleton对象必须是CustomType的实例(区别就是把公共属性和方法定义到CustomType实例中了)
7.5 小结
-
函数表达式的特点:
- 函数声明要有名字,但函数表达式不需要。即函数表达式可以是匿名函数
- 在无法确定如何引用函数的情况下,递归函数会变得比较复杂
- 递归函数应该始终使用arguments.callee来递归地调用自身,不要使用函数名——函数名可能会发生变化
-
闭包:在函数内部定义其他函数时,就创建了闭包,闭包有权访问包含函数内部的所有变量:
- 闭包的作用域链包含着自己的作用域、包含函数的作用域和全局函数的作用域
- 通常函数的作用域及其所有变量都会在函数执行结束后被销毁
- 但是当函数返回了一个闭包时,这个函数的作用域会一直在内存中保存到闭包不存在为止
-
使用闭包模仿块级作用域
- 思路就是创建并立即调用一个函数,这样会立即执行里面的代码,又不会在内存中留下对该函数的引用
- 结果就是函数内部的所有变量都会被立即销毁——除非将某些变量赋值给了包含作用域中的变量
-
使用闭包创建私有变量:
- JS中没有正式的私有对象的概念。这里的私有对象是指在外部访问不到的变量。可以用闭包来实现公有方法,从而访问到在包含作用域中定义的变量
- 有权访问私有变量的公有方法叫特权方法
- 自定义类型的特权方法有:构造函数模式、原型模式。单例的特权方法有:模块模式、增强的模块模式