常规方式定义函数
定义函数有两种方式,第一种方式为常规声明方式,该方式下函数可以先使用,后声明,即"函数声明提升"特性。
//函数提升
sayHi();
function sayHi(){
alert("Hi!");
}
常规声明方式方式定义的函数有 name 属性,可以获取函数的名字
// name 属性
function functionName(arg0, arg1, arg2) {
//function body
}
alert(functionName.name); //"functionName"
匿名方式定义函数
第二种方式为匿名函数方式,即函数没有名字。但是函数对象可以赋值给变量,可以作为参数,也可以作为返回值
var functionName = function(arg0, arg1, arg2){
//function body
};
7.1 递归
递归就是函数自己调用自己,下面求阶乘的函数是个典型的递归函数
function factorial(num){
if (num <= 1){
return 1;
} else {
return num * factorial(num-1);
}
}
但是上面这种写法在 Javascirpt 中可能会出现错误,因为在 Javascript 中,函数的名字可能发生变化
var anotherFactorial = factorial;
factorial = null;//这个名字不再指向函数对象
alert(anotherFactorial(4)); //出错
可以用 arguments.callee 来解决这个问题,arguments.callee 始终指向当前正在执行的函数对象
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 * f(num-1);
}
});
7.2 闭包
闭包也是一个函数,但是这个函数保存了另外一个函数作用域中的变量。通常创建闭包的方式
- 在一个函数中创建另外一个函数
- 内部函数访问了外部函数中的局部变量
- 外部函数执行完毕后被销毁,而内部函数对象因为有更长的生命周期而被保留,因而变成了闭包
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;
}
};
}
执行环境
- 执行环境是一组数据,这组数据定义了代码运行的边界,防止出现混乱。
- 执行环境在函数执行时候被创建,函数执行完毕后,被销毁。
-
window
对象是一个全局执行环境,不会被销毁。
变量对象
每一个执行环境都有一个对应的变量对象,变量对象中保存着函数能访问到的局部变量和函数的名字。
作用域链
每个执行环境都会包含一个作用域链,作用域链的顶端指向当前环境的变量对象,下一个指向外部环境的变量对象,最后指向全局环境的变量对象。作用域链保存在函数对象的 [[Scope]] 属性中
查找变量
函数查找一个变量的时候,先从作用域链顶端的变量对象中查找,找不到则到下一个变量对象中查找,一直查找到最后一个全局环境的变量对象为止。
作用域链创建的流程
函数运行时,会:
- 先创建函数自己的局部执行环境
- 向执行环境中加入一个默认的作用域链,该作用域链只包含全局环境的变量对象
- 将函数自己的变量对象添加到作用域链的顶端
下面的代码运行后,会产生如图的数据结构
function compare(value1, value2){
if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
}
var result = compare(5, 10);
闭包为什么能保存外部环境中的变量
当内部环境访问外部环境的时候,外部环境的变量对象会被添加到内部环境的作用域链中,因此内部环境可以访问外部环境。
一般情况下,内部环境总是先于外部环境执行完毕,因此先被销毁
特殊的情况下,外部环境先被销毁,而内部环境被长期保留。这时候,由于内部环境的作用域链中引用了外部环境的变量对象,因此外部环境的变量对象会被保留下来,被销毁的只有外部环境本身和他的作用域链。
适度使用闭包并及时释放闭包占用的内存
- 因为闭包会阻止外部环境销毁其变量对象,因此闭包会占用额外内存,过度的闭包会导致性能下降
- 可以闭包使用完毕后,及时将其设置为 null ,手动释放内存
7.2.1 闭包与变量的状态
闭包中引用的外部变量,其值可能会在闭包创建以后继续发生变化,而闭包并不能记住创建时外部变量的状态。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
//所有闭包引用的都是同一个外部变量对象中的同一个 i ,因此只能得到最后的 i 的状态
result[i] = function(){
return i;
};
}
return result;
}
要想保持闭包创建时外部变量的状态,可以将外部变量作为闭包的参数传进去,这样闭包中只有对本地变量使用,不受外部变量变化的影响
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(num){
return function(){
return num;
};
}(i);//将状态作为参数穿进去
}
return result;
}
7.2.2 关于 this 指针
- 在全局作用于下,this 指向 window 对象
- 在方法中,this 指向方法所在的对象
- 在闭包中,this 指向 window 对象
在下面的例子中,object.getNameFunc()()
将返回一个闭包。这是因为,最终返回的匿名函数的外部环境已经不存在了,因此匿名函数变成了闭包。所以,其中的 this 将指向 window 对象
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //"The Window" (in non-strict mode)
要想在这种情况下让 this 指向对象本身,可以先用一个临时变量保存 this ,然后利用闭包的特性引用临时变量,而不是直接使用 this
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
//用一个临时变量保存 this
var that = this;
return function(){
//在内部引用临时变量
return that.name;
};
}
};
alert(object.getNameFunc()()); //"My Object"
7.2.3 闭包的内存泄漏:
一般情况下 HTML 对象的事件属性会指向一个函数对象, HTML 对象被销毁,函数对象也会被销毁。
某些特殊情况下,事件处理函数中,又反过来引用了 HTML 对象,事件处理函数就变成了一个闭包。这是一种循环引用,导致 HTML 对象将不会被销毁
function assignHandler(){
var element = document.getElementById("someElement");
//事件处理函数引用了外部变量,且生存周期比外部环境长,因此变成了闭包
//闭包中引用的对象将不会被释放
element.onclick = function(){
alert(element.id);
};
}
解决办法:可以先将对象的属性赋值给外部函数的一个局部变量,闭包中引用这个局部变量而不引用 HTML 对象。 HTML 对象使用完成后应该赋值 null
function assignHandler(){
var element = document.getElementById("someElement");
//将 HTML 对象复制给一个局部变量,闭包中引用这个局部变量
var id = element.id;
element.onclick = function(){
alert(id);
};
//释放这个 HTML 对象
element = null;
}
7.3 块级作用域
Javascript 中没有块级作用域的概念。下面的代码中,临时变量 i 超出了作用于仍然存在
function outputNumbers(count){
for (var i=0; i < count; i++){
alert(i);
}
alert(i); //count
}
可以将使用临时变量的代码放到块级作用域中,这样可以防止命名空间污染,减少内存占用
function outputNumbers(count){
(function () {
for (var i=0; i < count; i++){
alert(i);
}
})();
alert(i); //causes an error
}
7.4 私有变量
函数本身就具有封装性,在函数内部定义的变量和函数都是私有的,外部不能访问
///外部看不见 num1 num2 等变量
function add(num1, num2){
var sum = num1 + num2;
return sum;
}
访问接口
完全私有的变量是没有用处的,可以留一个公共接口供外部访问
function MyObject(){
//私有变量
var privateVariable = 10;
//私有函数
function privateFunction(){
return false;
}
//特权方法,外部可以访问
this.publicMethod = function (){
privateVariable++;
return privateFunction();
};
}
实例成员私有模式
这是最基本的模式,每个实例都会创建自己独立的成员,不同实例之间互不影响;通过特权函数可以访问实例的私有成员。
缺点是:特权函数也会在每个实例创建一份,浪费资源。
function Person(name){
this.getName = function(){
return name;
};
this.setName = function (value) {
name = value;
};
}
var person = new Person("Nicholas");
alert(person.getName()); //"Nicholas"
person.setName("Greg");
alert(person.getName()); //"Greg"
7.4.1 静态私有变量
实现方式:
- 所有成员,包括私有成员、特权函数和构造函数都在一个自调用的匿名函数中定义
- 通过匿名函数的自调用创建所有成员
- 其中构造函数不使用 var 关键字,因此可以全局访问
- 访问接口在原型中定义
(function(){
//私有成员和私有方法
var privateVariable = 10;
function privateFunction(){
return false;
}
//构造函数,没有用 var 关键字,所以可以全局调用
MyObject = function(){
};
//特权方法
MyObject.prototype.publicMethod = function(){
privateVariable++;
return privateFunction();
};
})();//通过自调用创建成员
模式特点:
- 所有成员,包括私有成员、特权函数和构造函数只在匿名函数自调用的时候创建一次
- 私有成员因为被构造函数和特权函数引用,因此得以在匿名函数自调用完成以后,仍然保留在内存中
- 构造函数可以多次调用,创建多个实例,但是私有成员不会被再次创建
- 所有实例共享私有变量,就好像其他语言中的静态变量
(function(){
//name 本来是匿名函数的私有变量,在匿名函数自调用的时候创建
var name = "";
//构造函数闭包引用了外部变量,并且生命周期比外部环境长,因此变成了闭包
//因此,匿名函数自调用完毕后,私有变量不会被销毁
Person = function(value){
name = value;
};
Person.prototype.getName = function(){
return name;
};
Person.prototype.setName = function (value){
name = value;
};
})();
var person1 = new Person("Nicholas");
alert(person1.getName()); //"Nicholas"
person1.setName("Greg");
alert(person1.getName()); //"Greg"
var person2 = new Person("Michael");
alert(person1.getName()); //"Michael"
alert(person2.getName()); //"Michael"
7.4.2 模块模式(单例模式)
在匿名函数中没有定义构造函数,而是在匿名函数自调用以后返回一个对象,对象中包含了公共接口
var singleton = function(){
//私有成员
var privateVariable = 10;
function privateFunction(){
return false;
}
//返回对象,
return {
publicProperty: true,
publicMethod : function(){
privateVariable++;
return privateFunction();
}
};
}();
这种模式下,私有成员同样只创建一次,通过公共接口访问
var application = function(){
//私有成员
var components = new Array();
//一些其他的初始化动作
components.push(new BaseComponent());
//公共接口
return {
getComponentCount : function(){
return components.length;
},
registerComponent : function(component){
if (typeof component == "object"){
components.push(component);
}
}
};
}();
7.4.2 增强单例模式
和上面的单例模式差不多,只是将返回值由字面量对象改成普通对象
var singleton = function(){
//私有成员
var privateVariable = 10;
function privateFunction(){
return false;
}
//创建一个对象
var object = new CustomType();
//添加特权接口
object.publicProperty = true;
object.publicMethod = function(){
privateVariable++;
return privateFunction();
};
//返回对象
return object;
}();
例程
var application = function(){
//private variables and functions
var components = new Array();
//initialization
components.push(new BaseComponent());
//create a local copy of application
var app = new BaseComponent();
//public interface
app.getComponentCount = function(){
return components.length;
};
app.registerComponent = function(component){
if (typeof component == "object"){
components.push(component);
}
};
//return it
return app;
}();