这一章我们介绍函数表达式,在开始的时候我们会复习到很多之前学过的知识。
我们之前学过函数提升相关的知识,定义的函数会提升,定义的函数作为变量则不会提升,我们还举例说过下面的代码才能达到效果
var condition = false;
var sayHi = null;
if(condition){
sayHi = function(){
alert('true');
}
}else{
sayHi = function(){
alert('false');
}
}
现在,我们给这种函数定义之后赋值给变量的写法叫函数表达式。
var func = function(arg1, arg2){
return arg1 + arg2;
}
这种写法是函数表达式最常见的写法,但不是唯一的写法,我们之后还会介绍别的函数表达式的写法。
我们知道递归就是自己调用自己,我们也学习了下面这种写法不好,最好使用arguements.callee()方法以防止函数名称的改变:
function A(){
A();
}
var B = A;
A = null;
B(); // 报错,这种方法不好,在函数中调用了调用了函数名造成了耦合
function A(){
arguements.callee();
}
var B = A;
A = null;
B(); // 可以运行,但是严格模式不允许使用callee
我们发现这两种写法一种不好,好的方法严格模式还不让用,这怎么办呢?我们使用下面的办法:
var func = (function A(){
A();
}); // 这里的括号写不写都可以,但是写上更好
var B = func;
func = null;
B();
闭包是有权访问另一个函数作用域中的变量的函数。
我们之前在函数的学习中,学过函数的调用原理,如果不记得了可以复习一下,我们这里看看闭包的调用原理
function createComparisonFunction(propertyName){
return function(obj1, obj2){
var value1 = obj1[propertyName];
var value2 = obj2[propertyName];
if(value1 < value2){
return -1;
}else if(value1 > value2){
return 1;
}else{
return 0;
}
};
}
var compare = createComparisonFunction('name');
var result = compare({name : 'Nic'}, {name : 'Bob'});
alert(result);
compare = null;
我们看到实现的效果是createComparisonFunction执行完毕之后,还可以调用compare,compare会记住之前初始化的name值。我们说,一般在函数执行完毕之后,其活动对象就会被销毁,propertyName只是一个局部变量,为什么一直存在了呢?这就是闭包跟普通函数调用的不同。
下面我们来描述一下上面代码中函数的调用:
注意:
书中写的是作用域链是在函数调用的时候生成的,按照上面的实验,createComparisonFunction只是返回了函数,其活动对象就无法被销毁了,这是为什么呢?
因为其定义使用的是函数表达式而非函数定义,在使用函数表达式创建函数的时候,因为变量名必须有所指向,所以作用域链,活动对象就都创建出来了。
我们看下面的代码返回什么:
function createFunctions(){
var result = new Array();
for(var i = 0;i < 10; i++){
result[i] = function(){
return i;
}
}
return result;
}
for(var i = 0;i < 10; i++){
alert(createFunctions()[i]()); // 全是10
}
分析一下:
那么如何返回正确的值呢。其关键在如何让函数返回的i的值存在于各自的活动对象中而不是createFunctions的活动对象中。我们可以使用下面的方法:
function createFunctions(){
var result = new Array();
for(var i = 0;i < 10; i++){
result[i] = function(num){
return num;
}(i);
}
return result;
}
for(var i = 0;i < 10; i++){
alert(createFunctions()[i]); // 返回0-10
}
结合上面说的问题,这种方法从根本上解决问题,根本问题是变量在哪个活动对象的问题,通过函数的赋值,我们将i作为变量传给了内部函数的活动对象,由于i是基本类型,传值,所以i被传进了内部函数的活动对象中。
由于活动对象中的this指向的是调用的环境对象,所以调用闭包的函数,this的指向问题很大。我们分析下面的例子:
var name = 'The Window';
var object = {
name: 'My Object',
getNameFunc: function(){
return function(){
return this.name;
}
}
};
alert(object.getNameFunc()()); // The Window
分析:
this返回的是调用的对象,因为这里object.getNameFunc()获取了内部方法,再加一个(),等于在全局对象上调用了方法,所以指向了全局对象window
而实际上,我们认为内部函数是在object中被调用的,我们希望this指向object,为了实现这种情况,我们可以使用that
var name = 'The Window';
var object = {
name: 'My Object',
getNameFunc: function(){
var that = this;
return function(){
return that.name;
}
}
};
alert(object.getNameFunc()()); // My Object
分析一下:
IE9之前如果闭包中存着一个HTML元素,那么这个元素不能被销毁。在IE9之后和其他浏览器中没有问题,我们在这里不展开说
在实际的开发中,所有的程序员都在同一个全局作用域下编程,很容易带来潜在的命名冲突,我们应该尽量少的在全局作用域中添加变量,再者,在全局作用域下添加的闭包如果不即时释放,会消耗额外的资源。
我们知道其他语言有块作用域而js没有,其他的语言在for循环的时候定义了i,for循环结束,i就失效了,而js不会。我们还知道,函数是js中唯一可以实现块作用域的方法,一个函数中的作用域才是自己的。这样我们就有了下面的方法来解决问题,模拟块作用域。
(function() {
// 这是一个模拟的块作用域
// 这是一个闭包
})();
经过这么一写,包起来的就是自己的作用域,在调用完成之后立即释放。那么这个是不是一个闭包呢?我们学过,闭包是有权访问另一个函数作用域变量的函数,我们看,把这段代码放到任何函数中去,都可以访问这个函数的作用域变量,所以这是一个闭包
我们来看几个应用
function outputNumber(count) {
(function() {
for (var i = 0; i < count; i++) {
alert(i);
}
})();
alert(i); //报错 i is not defined
}
outputNumber(3);
使用这种方法可以:
在ES6的let出现之后我们不需要这么模拟了,可以使用真正的块变量:
{
let a = 1;
}
function MyObj(name) {
this.getName = function() {
return name;
};
this.setName = function(value) {
name = value;
};
}
var person = new MyObj('wf');
alert(person.getName()); //wf
person.setName('s');
alert(person.getName()); //s
通过对于构造函数中添加get set方法,实现了对于name属性的封装。这个实践实现了私有变量但是这个模式基于构造函数模式,我们之前论证过构造函数模式并不是很好的实现对象的模式,每个实例都会重建get set方法,有额外的资源消耗。所以这个方式并不是很好的方式。
我们之前说了不想每次实例化都重建方法,所以我们很自然的想到之前的办法,将set get方法写到构造函数的prototype中,写道构造函数之外。
我们学了闭包,将这一系列操作写入一个闭包中只暴露构造函数不失为一个好办法。
(function() {
var name = ''; //这是一个私有变量
Person = function(value) {
name = value;
};
Person.prototype.getName = function() {
return name;
};
Person.prototype.setName = function(value) {
name = value;
};
})();
var person1 = new Person('Nic');
alert(person1.getName()); //Nic
模块模式就是一种为单例创建私有变量和特权方法。这种方法用于对全局单例对象定义私有变量,并且定义方法操作时候使用
var application = function() {
var component = new Array(); //定义
component.push(new BaseComponent()); // 初始化
return {
//返回单例对象
getComponent: function() {
return component.length;
},
registerComponent: function(com) {
if (typeof com == 'object') {
component.push(com);
}
}
};
}();
我们看到之前我们讲的模块模式返回的是一个字面量标识的对象,这个对象使用typeof判断就是个Object,如果我们要求,返回的单例必须是某类型的,这时候我们可以把字面量改为特定类型的实例,也不影响功能,看下面的的代码:
var application = (function() {
var component = new Array(); //定义
var app = new BaseComponent(); // 要求返回的必须是BaseComponent类型
app.getComponent = function() {
return component.length;
};
app.registerComponent = function(com) {
if (typeof com == 'object') {
component.push(com);
}
return app;
};
})();