闭包
匿名函数经常被人误认为是闭包(closure)。闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。比如下面是之前展示的 createComparisonFunction()函数:
function createComparisonFunction(propertyName) { return function(object1, object2) { let value1 = object1[propertyName]; let value2 = object2[propertyName];if (value1 < value2) { return -1; } else if (value1 > value2) { return 1; } else { return 0; } };
}
在 createComparisonFunction()返回匿名函数后,它的作用域链被初始化为包含 createComparisonFunction()的活动对象和全局变量对象。这样,匿名函数就可以访问到 createComparisonFunction()可以访问的所有变量。另一个有意思的副作用就是,createComparisonFunction()的活动对象并不能在它执行完毕后销毁,因为匿名函数的作用域链中仍然有对它的引用。在createComparisonFunction()执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁:
// 创建比较函数
let compareNames = createComparisonFunction('name');
// 调用函数
let result = compareNames({ name: 'Nicholas' }, { name: 'Matt' });
// 解除对函数的引用,这样就可以释放内存了
compareNames = null;
因为闭包会保留它们包含函数的作用域,所以比其他函数更占用内存。过度使用闭包可能导致内存过度占用,因此建议仅在十分必要时使用。V8 等优化的 JavaScript 引擎会努力回收被闭包困住的内存,不过我们还是建议在使用闭包时要谨慎。
this 对象
在闭包中使用 this 会让代码变复杂。如果内部函数没有使用箭头函数定义,则 this 对象会在运行时绑定到执行函数的上下文。如果在全局函数中调用,则 this 在非严格模式下等于 window,在严格模式下等于 undefined。如果作为某个对象的方法调用,则 this 等于这个对象。匿名函数在这种情况下不会绑定到某个对象,这就意味着 this 会指向 window,除非在严格模式下 this 是 undefined。不过,由于闭包的写法所致,这个事实有时候没有那么容易看出来。来看下面的例子:
window.identity = 'The Window';
let object = { identity: 'My Object', getIdentityFunc() { return function() { return this.identity; }; }
};
console.log(object.getIdentityFunc()()); // 'The Window'
这里先创建了一个全局变量 identity,之后又创建一个包含 identity 属性的对象。这个对象还包含一个 getIdentityFunc()方法,返回一个匿名函数。这个匿名函数返回 this.identity。因为getIdentityFunc()返回函数,所以 object.getIdentityFunc()()会立即调用这个返回的函数,从而得到一个字符串。可是,此时返回的字符串是"The Winodw",即全局变量 identity 的值。为什么匿名函数没有使用其包含作用域(getIdentityFunc())的 this 对象呢?
前面介绍过,每个函数在被调用时都会自动创建两个特殊变量:this 和 arguments。内部函数永远不可能直接访问外部函数的这两个变量。但是,如果把 this 保存到闭包可以访问的另一个变量中,则是行得通的。比如:
window.identity = 'The Window';
let object = { identity: 'My Object', getIdentityFunc() { let that = this; return function() { return that.identity; }; }
};
console.log(object.getIdentityFunc()()); // 'My Object'
内存泄漏
function assignHandler() { let element = document.getElementById('someElement'); element.onclick = () => console.log(element.id);
}
以上代码创建了一个闭包,即 element 元素的事件处理程序,而这个处理程序又创建了一个循环引用。匿名函数引用着 assignHandler()的活动对象,阻止了对element 的引用计数归零。只要这个匿名函数存在,element 的引用计数就至少等于 1。也就是说,内存不会被回收。其实只要这个例子稍加修改,就可以避免这种情况,比如:
function assignHandler() { let element = document.getElementById('someElement'); let id = element.id; element.onclick = () => console.log(id);element = null;
}
立即调用的函数表达式
立即调用的匿名函数又被称作立即调用的函数表达式(IIFE,Immediately Invoked Function Expression)。
(function() { // 块级作用域
})();
在 ECMAScript 6 以后,IIFE 就没有那么必要了,因为块级作用域中的变量无须 IIFE 就可以实现同样的隔离。下面展示了两种不同的块级作用域形式:
// 内嵌块级作用域
{ let i; for (i = 0; i < count; i++) { console.log(i); }
}
console.log(i); // 抛出错误
// 循环的块级作用域
for (let i = 0; i < count; i++) { console.log(i);
}
console.log(i); // 抛出错误
私有变量
严格来讲,JavaScript 没有私有成员的概念,所有对象属性都公有的。不过,倒是有私有变量的概念。任何定义在函数或块中的变量,都可以认为是私有的,因为在这个函数或块的外部无法访问其中的变量。私有变量包括函数参数、局部变量,以及函数内部定义的其他函数。
function add(num1, num2) { let sum = num1 + num2; return sum;
}
特权方法(privileged method)是能够访问函数私有变量(及私有函数)的公有方法。在对象上有两种方式创建特权方法。第一种是在构造函数中实现,比如:
function MyObject() { // 私有变量和私有函数 let 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; };
}
let person = new Person('Nicholas');
console.log(person.getName()); // 'Nicholas'
person.setName('Greg');
console.log(person.getName()); // 'Greg'
静态私有变量
(function() { let name = ''; Person = function(value) { name = value; }; Person.prototype.getName = function() { return name; }; Person.prototype.setName = function(value) { name = value; };
})();
let person1 = new Person('Nicholas');
console.log(person1.getName()); // 'Nicholas'
person1.setName('Matt');
console.log(person1.getName()); // 'Matt'
let person2 = new Person('Michael');
console.log(person1.getName()); // 'Michael'
console.log(person2.getName()); // 'Michael'
这里的 Person 构造函数可以访问私有变量 name,跟 getName()和 setName()方法一样。使用这种模式,name 变成了静态变量,可供所有实例使用。这意味着在任何实例上调用 setName()修改这个变量都会影响其他实例。调用 setName()或创建新的 Person 实例都要把 name 变量设置为一个新值。而所有实例都会返回相同的值。
模块模式
按照惯例,JavaScript 是通过对象字面量来创建单例对象的,如下面的例子所示:
let singleton = { name: value,method() { // 方法的代码}
};
模块模式是在单例对象基础上加以扩展,使其通过作用域链来关联私有变量和特权方法。模块模式的样板代码如下:
let singleton = function() { // 私有变量和私有函数let privateVariable = 10; function privateFunction() { return false; } // 特权/公有方法和属性return { publicProperty: true, publicMethod() { privateVariable++; return privateFunction(); } };
}();
模块模式使用了匿名函数返回一个对象。在匿名函数内部,首先定义私有变量和私有函数。之后,创建一个要通过匿名函数返回的对象字面量。这个对象字面量中只包含可以公开访问的属性和方法。因为这个对象定义在匿名函数内部,所以它的所有公有方法都可以访问同一个作用域的私有变量和私有函数。本质上,对象字面量定义了单例对象的公共接口。如果单例对象需要进行某种初始化,并且需要访问私有变量时,那就可以采用这个模式:
let application = function() { // 私有变量和私有函数 let components = new Array(); // 初始化components.push(new BaseComponent()); // 公共接口return { getComponentCount() { return components.length; }, registerComponent(component) { if (typeof component == 'object') { components.push(component); } } };
}();
在模块模式中,单例对象作为一个模块,经过初始化可以包含某些私有的数据,而这些数据又可以通过其暴露的公共方法来访问。以这种方式创建的每个单例对象都是 Object 的实例,因为最终单例都由一个对象字面量来表示。
小结
- 函数表达式与函数声明是不一样的。函数声明要求写出函数名称,而函数表达式并不需要。没有名称的函数表达式也被称为匿名函数。
- ES6 新增了类似于函数表达式的箭头函数语法,但两者也有一些重要区别。
- JavaScript 中函数定义与调用时的参数极其灵活。arguments 对象,以及 ES6 新增的扩展操作符,可以实现函数定义和调用的完全动态化。
- 函数内部也暴露了很多对象和引用,涵盖了函数被谁调用、使用什么调用,以及调用时传入了什么参数等信息。
- JavaScript 引擎可以优化符合尾调用条件的函数,以节省栈空间。
- 闭包的作用域链中包含自己的一个变量对象,然后是包含函数的变量对象,直到全局上下文的变量对象。
- 通常,函数作用域及其中的所有变量在函数执行完毕后都会被销毁。
- 闭包在被函数返回之后,其作用域会一直保存在内存中,直到闭包被销毁。
- 函数可以在创建之后立即调用,执行其中代码之后却不留下对函数的引用。
- 立即调用的函数表达式如果不在包含作用域中将返回值赋给一个变量,则其包含的所有变量都会被销毁。
- 虽然 JavaScript 没有私有对象属性的概念,但可以使用闭包实现公共方法,访问位于包含作用域中定义的变量。
- 可以访问私有变量的公共方法叫作特权方法。
- 特权方法可以使用构造函数或原型模式通过自定义类型中实现,也可以使用模块模式或模块增强模式在单例对象上实现。
又快过年了,欣赏一首
除夜雪
宋·陆游
北风吹雪四更初,嘉瑞天教及岁除。
半盏屠苏犹未举,灯前小草写桃符。