1. 简介
闭包:
函数执行时形成一个私有作用域,保护里面的私有变量不受外界干扰,这种保护机制称之为闭包
市面上开发者认为的闭包是:函数执行形成一个不销毁的私有作用域(私有栈内存),不仅可以保护私有变量不被外界干扰,还可以保存一些内容,供后期调取使用,才是闭包
高级程序设计:
闭包是指有权访问另一个函数作用域中的变量的函数,只有函数内部的子函数才能读取函数中的私有变量,所以闭包可以理解成定义在一个函数内部的函数。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
销毁和不销毁主要看它还有没有作用,内部的函数创建的作用域执行完毕后销毁,但是在外部函数的作用域不会销毁,因为存在私有变量被其内部的函数引用,所以不会销毁。
创建闭包的常见方式,就是在一个函数内部创建另一个函数。
// 形式1,函数中返回函数
/* 柯里化函数 */
function fn() {
var a = 12;
return function() {
console.log(a);
}
}
var f = fn();
// 形式2,高级单例模式
/* 惰性函数 */
var fn = (function() {
var a = 12;
function fn() {
console.log(a);
}
return {
fn: fn
}
})();
// 形式3,函数中创建函数
function fn() {
var element = document.getElementById('box');
element.onclick = function() {
//=> 可以访问外部的 element
console.log(element.id);
}
}//=> 这里的 element 永远不会销毁,除非手动销毁
在函数的内部返回的匿名函数,访问了外部函数的变量。即使这个函数被返回,而且在其他地方调用,它仍然可以访问该变量。这是因为内部函数的作用域链中包含外部函数的变量对象。
解析题:
function fn() {
var i = 5;
return function (n) {
console.log(n*(--i));
}
}
var f = fn();
f(10); //=> 10*4 = 40
fn()(10); //=> 10*4 = 40
fn()(20); //=> 20*4 = 80
f(20); //=> 20 * 3 = 60
调用 f 时,使用的是同一个作用域的 i,每次调用后,i 都变化
调用 fn()() 时,每次都是不同的 i,初始值都是 5
var i = 2;
function fn() {
i += 2;
return function (n) {
console.log(n + (--i));
}
}
var f = fn(); //=> 只执行 fn,全局 i+=2,执行后 i = 4
f(2); //=> 此时执行的是 fn 内部的函数,只执行--i,此时 i = 3,结果为 2 + 3 = 5
f(3); //=> 此时执行的是 fn 内部的函数,只执行--i,此时 i = 2,结果为 3 + 2 = 5
fn()(2); //=> 此时执行的是 fn 以及内部函数,执行了 i+=2 和 --i,此时 i = 3,结果为 2 + 3 = 5
fn()(3); //=> 此时执行的是 fn 以及内部函数,执行了 i+=2 和 --i,此时 i = 4,结果为 3 + 4 = 7
f(4); //=> 此时执行的是 fn 内部函数,只执行 --i,执行之后 i = 3,结果为 4 + 3 = 7
2. 函数作用域链
函数的作用域链是指函数执行时,查找变量的机制:
当前作用域的变量对象 -> 上一级作用域的变量对象 -> … -> 全局作用域的变量对象
这就是作用域链。作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。
函数的作用域链的形成机制:
定义函数时,创建一个包含上一级作用域变量对象至全局变量对象的作用域链,保存在内部的
[[Scope]]
属性中调用函数时,为函数创建一个执行环境,然后通过复制函数的
[[Scope]]
属性中的对象构建起执行环境的作用域链再创建一个函数本身的变量对象并推入到执行环境作用域链的前端
也就是说,函数的作用域链的创建,是跟函数定义的位置有关,而跟函数执行的位置无关
var a = 12;
function fn() {
//=> arguments:实参集合
//=> arguments.callee:函数本身
//=> arguments.callee.caller:当前函数在哪执行,caller就是谁(记录的是它执行的宿主环境),在全局下执行 caller 的结果是 null
console.log(a);
}
function sum() {
var a = 120;
fn(); //=> 12,其作用域链只与其定义的位置有关,其上一级作用域是全局作用域
}
sum(); //=> 12
3. this 对象
在闭包中使用 this
对象也可能导致问题。this
对象是在运行时基于函数的执行环境绑定的:在全局函数中,this
等于 window
,而当函数被作为某个对象的方法调用时,this
等于那个对象。不过,匿名函数的执行环境具有全局性,因此其 this
对象通常指向 window
。
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function() {
return function() {
return this.name;
}
}
}
// 由于 getNameFunc() 返回一个函数,所以调用 getNameFunc()() 会立即调用它返回的函数。
alert(object.getNameFunc()()); //"The Window"
每个函数在被调用时都会自动取得两个特殊变量:this
和 arguments
,内部函数在搜索这两个变量时,只会搜索到其变量对象为止。所以永远不可能直接访问外部函数中的这两个变量。
可以把外部作用域中的 this
对象保存在一个闭包能够访问到的变量中,就可以让闭包访问该对象了。
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function() {
var that = this;
return function() {
return that.name;
}
}
}
alert(object.getNameFunc()()); //"My Object"
在几种特殊情况下,this
的值可能意外发生改变。
var name = "The Window";
var object = {
name: "My Object",
getName: function() {
return this.name;
}
}
object.getName(); //"My Object"
(object.getName)(); //"My Object"
(object.getName = object.getName)(); //"The Window"
第一种调用是正常调用,一般不会使用下面两种方式,这里只是用于说明;
第二种调用,就好像只是在引用一个函数,但 this
的值得到了维持;
第三种调用,先执行了一条赋值语句,然后再调用赋值后的结果,因为这个赋值表达式的值是函数本身,所以 this
的值不能维持。
4. 闭包的内存泄漏
全局作用域在页面关闭的时候销毁,函数作用域一般在代码执行完毕之后销毁。
但是闭包的情况又有所不同。外部函数的私有作用域(栈内存)被内部的函数占用着,所以外部函数在执行完毕后也不会销毁其私有作用域
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 compare = creatComparisonFunction("name");
var result = compare({ name: "Nichoals" }, { name: "Greg" });
手动解除引用
//=> 手动解除引用,以便释放内存
//=> 释放内部函数的引用,那么内部函数会被销毁,那么外部函数执行时的私有作用域就没有被谁占用着,那么也会被销毁
compare = null;
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存,过度使用可能会导致内存占用过多。使用闭包时,需要注意手动接触引用
5. 闭包的作用
(1)闭包具有保护作用:保护私有变量不受外界干扰
在真实项目中,尤其是团队协作开发的时候,应当尽可能的减少全局变量的使用,以防止相互之间的冲突(全局变量污染),那么此时我们完全可以把自己这一部分内容封装到一个闭包中,让全局变量转换为私有变量
//=> 形成块级作用域
(function(){
//=> 把你的代码全部复制进来
})();
不仅如此,我们封装类库插件的时候,也会把自己的程序都存放到闭包中保护起来,防止和用户的程序冲突,但是我们又需要暴露一些方法给客户使用。
- jQuery 方式:把需要暴露的方法通过 window 抛到全局
(function(){
function jQuery() {
//...
}
//...
//=> 把需要供外面使用的方法,通过给 window 设置属性的方式暴露出去
window.jQuery = window.$ = jQuery
})();
jQuery();
$();
- Zepto 方式:基于
return
一个对象把需要供外面使用的方法暴露出去
//=> 高级单例模式
var Zepto = (function() {
//...
return {
xxx: function () {
}
};
})();
(2)闭包具有保存作用:形成不销毁的栈内存,把一些值保存下来,方便后面的调取使用
- 在函数外部永远不可能直接访问到函数内的
this
对象
function () {
var that = this;
return function() {
that //=> 获得上一级函数的 this 对象
}
}
for
循环中
for (var i = 0; i < tabList.length; i++) {
tabList[i].onclick = function () {
changTab(i);
//=> 执行方法,形成一个私有栈内存,遇到变量,不是私有变量,向上一级作用域查找(上级作用域 window)
//=> 当我们点击的时候,外层循环已经结束(能点击的时候页面已经加载完成,页面加载完成预示着 JS 代码都已经执行完成,也就是循环也都执行完成),外层循环结束已经让全局的 i = li 的总长度
//=> 所有时间绑定都是异步编程(同步编程:一件事一件事的做,当前这件事没完成,下一个任务不能处理。异步编程:当前这件事没有彻底完成,不再等待,继续执行下面的任务),绑定事件后,不需要等待执行,继续执行下一个循环任务,所以当我们点击执行方法的时候,循环早已结束(让全局的 i 等于循环最后的结果)
}
}
//=> 解决方案
//=> 1. 提前把 i 保存在自定义属性,点击时,使用这个自定义属性,而不使用 i
//=> 2. 闭包,把它的上一级作用域改变
tabList[i].onclick = (function(i) {
return function () {
changeTab(i) //=> 上级作用域:自执行函数形成的作用域
}
})(i);
//=> 循环几次就形成几个不销毁的作用域,而每一个不销毁的栈内存中,都存储了一个私有变量 i,而这个值分别是每一次执行传递进来的全局 i 的值。当点击的时候,执行返回的小函数,遇到变量,向它自己的上级作用域查找,找到前面不销毁作用域的 i 的值,达到我们想要的效果。这就是闭包的保存效果。但是真实项目中不要使用,因为会消耗性能。
//=> 3. 基于 ES6 的 let 创建变量,是存在块级作用域的(类似于私有作用域)
for (let i = 0; i < tabList.length; i++) {
tabList[i].onclick = function () {
changTab(i);
}
}
6. 模仿块级作用域
JavaScript 没有块级作用域的概念(ES6 已经有了),这意味着在块语句中定义的变量,实际上是在包含函数中而非语句中创建的。即使在后面重复声明(没有赋值),一样会是以前的值。JavaScript 从来不会告诉你是否多次声明了同一个变量。它会忽略后续的声明,但是会执行声明中变量的初始化。
for (var i = 1; i < 9; i++) {
//...
}
var i;
alert(i); //9
立即执行匿名函数可以用来模仿块级作用域并避免这个问题。用作块级作用域(私有作用域)的匿名函数如下:
(function() {
//这里是块级作用域
})();
// 其他写法
~function(){
}();
!function(){
}();
void function() {
}();
将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式。而紧随其后的另一对圆括号会立即调用函数。
这种技术经常用在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。一般来说,应该尽量减少向全局作用域中添加变量和函数,过多的变量和函数很容易导致命名冲突。
这种做法也可以减少闭包占用内存的问题,因为没有指向匿名函数的引用,只要函数执行完毕,就可以立即销毁其作用域链。
7. 私有变量
在私有作用域中,只有以下两种情况是私有变量:
声明过的变量(带 var / function)
形参也是私有变量
剩下的都不是私有变量,需要基于作用域链向上查找
/*
* 变量提升:
* var a,var b,var c,function fn = xxx
*/
var a = 12,
b = 13,
c = 14;
function fn(a) {
/*
* fn 执行:形成私有作用域
* 形参赋值 a = 12,变量提升 var b
*/
console.log(a, b, c); // 12, undefined, 14,c 是全局变量
var b = c = a = 20; //=> 把全局变量 c 改为 20,其他都是私有变量赋值
console.log(a, b, c); //=> 20, 20, 20
}
fn(a);
console.log(a, b, c) //=> 12, 13, 20
360 面试题
var ary = [12,23];
function fn(ary) {
console.log(ary); //=> [12,23],地址与全局一样
ary[0] = 100; //=> 改变私有变量,地址一样,全局也改变
ary = [100]; //=> 改变了私有变量的地址
ary[0] = 0; //=> 改变私有变量,地址不一样与全局无关
console.log(ary); //=> [0]
}
fn(ary);
console.log(ary); //=> [100, 23]
7.1 特权方法
利用闭包可以通过作用域链访问私有变量的特性,可以创建用于访问私有变量的公有方法,我们称有权访问私有变量和私有函数的公有方法为特权方法。
在构造函数中定义特权方法
function MyObject() {
//私有变量和私有函数
var privateVarible = 10;
function privateFunction() {
return false;
}
//特权方法
this.publicMethod = function() {
privateVarible++;
return privateFunction();
}
}
在创建 MyObject
的实例后,除了 publicMethod()
方法外,没有任何办法访问内部的私有变量。但是这里每创建一个实例,都重复创建了相同的私有变量和私有函数。
利用私有和特权成员,可以隐藏那些不应该被直接修改的数据
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"
每次的私有变量 name
在不同的实例中是不同的,跟前面的方法有所区别。
在构造函数中定义特权方法也有一个缺点,那就是你必须使用构造函数模式来达到这个目的。构造函数模式的缺点是针对每个实例都会创建同样一组新方法。
7.2 静态私有变量
通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法。
(function() {
var privateVariable = 10;
function privateFunction() {
return false;
}
//构造函数 没有 var 暴露给全局
MyObject = function() {
};
另一种方式:暴露给全局
window.MyObject = MyObject;
MyObject.prototype.publicMethod = function() {
privateVariable++;
privateFunction();
};
})();
这里的构造函数使用函数表达式,同样没有用 var
关键字,是为了使其创建为全局函数。注意,这在严格模式下会导致错误。
这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,有保存着对包含作用域的引用。
但是这种方法创建会因为使用原型而增进代码复用,但每个实例都没有属于自己的私有变量,都会共享。
注意:多查找作用域链中的一个层次,就会在一定程度上影响查找速度,这是闭包和私有变量的一个明显的不足之处。
7.3 模块模式
前面的模式是用于自定义类型创建私有变量和特权方法的。而道格拉斯所说的模块模式则是为单例创建私有变量和特权方法。所谓单例,指的就是只有一个实例的对象。
按照惯例,JavaScript 是以对象字面量来创建单例对象的。
var singleton = {
name: value,
method: function() {
//方法代码
}
};
模块模式通过为单例添加私有变量和特权方法能够使其增强。
var singleton = function() {
var privateVariable = 10;
function privateFunction() {
return false;
}
return {
publicProperty: true,
publicMethod: function() {
privateVariable++;
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);
}
}
};
}();
在 Web 应用程序中,经常需要使用一个单例来管理应用程序级的信息。简而言之,如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。
7.4 增强的模块模式
这种增强的模块模式适合那些单例必须是某种类型的实例,同时必须添加某些属性和方法对其加以增强的情况。
这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和方法对其加以增强的情况。
var singleton = function() {
//私有变量和私有函数
var privateVariable = 10;
function privateFunction() {
return false;
}
//创建对象,这里是 CustomType 的一个实例
var object = new CustomType();
//添加特权属性和方法
object.publicProperty = true;
object.publicMethod = function() {
privateVariable++;
privateFunction();
};
//返回这个对象
return object;
}();