js闭包

Posted wangyunzhong123

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了js闭包相关的知识,希望对你有一定的参考价值。

就像老Albert所说的,“如果你不能向一个六岁的孩子解释清楚,那么其实你自己根本就没弄懂。”好吧,我试着向一个27岁的朋友就是JS闭包(javascript closure)却彻底失败了。

你们会怎么把它解释给一个充满好奇心的六岁孩子听呢?

1.何为闭包

function foo(x) {
  var tmp = 3;
  function bar(y) {
    alert(x + y + (++tmp));
  }
  bar(10);
}
foo(2)

不管执行多少次,都会alert 16,因为bar能访问foo的参数x,也能访问foo的变量tmp。

但,这还不是闭包。当你return的是内部function时,就是一个闭包。内部function会close-over外部function的变量直到内部function结束。

function foo(x) {
  var tmp = 3;
  return function (y) {
    alert(x + y + (++tmp));
  }
}
var bar = foo(2); // bar 现在是一个闭包
bar(10);

上面的脚本最终也会alert 16,因为虽然bar不直接处于foo的内部作用域,但bar还是能访问x和tmp。

但是,由于tmp仍存在与bar闭包的内部,所以它还是会自加1,而且你每次调用bar时它都会自加1.

(考虑到六岁这个限制:我们其实可以建立不止一个闭包方法,比如return它们的数组,也可以把它们设置为全局变量。它们全都指向相同的x和相同的tmp,而不是各自有一份副本。)

注:现在来整点儿七岁的内容。

上面的x是一个字面值(值传递),和JS里其他的字面值一样,当调用foo时,实参x的值被复制了一份,复制的那一份作为了foo的参数x。

2.参数传递一个object

那么问题来了,JS里处理object时是用到引用传递的,那么,你调用foo时传递一个object,foo函数return的闭包也会引用最初那个object!

function foo(x) {
  var tmp = 3;
  return function (y) {
    alert(x + y + tmp);
    x.memb = x.memb ? x.memb + 1 : 1;
    alert(x.memb);
  }
}
var age = new Number(2);
var bar = foo(age); // bar 现在是一个引用了age的闭包
bar(10);

不出我们意料,每次运行bar(10),x.memb都会自加1。但需要注意的是x每次都指向同一个object变量——age,运行两次bar(10)后,age.memb会变成2.

这和html对象的内存泄漏有关,呃,不过貌似超出了答题的范围。

JohnMerlino 对Ali说:

这里有一个不用return关键字的闭包例子:

function closureExample(obj, text, timedelay) { 
    setTimeout(function() { 
        document.getElementById(objID).innerHTML = text; 
        }, timedelay); 
} 
closureExample(‘myDiv’, ‘Closure is created’, 500); 

深夜1:37 John Pick这样回答:

JS里的function能访问它们的:

  1. 参数

  2. 局部变量或函数

  3. 外部变量(环境变量?),包括

    3.1 全局变量,包括DOM
    3.2 外部函数的变量或函数。

如果一个函数访问了它的外部变量,那么它就是一个闭包。

注意,外部函数不是必需的。通过访问外部变量,一个闭包可以维持(keep
alive)这些变量。在内部函数和外部函数的例子中,外部函数可以创建局部变量,并且最终退出;但是,如果任何一个或多个内部函数在它退出后却没有退出,那么内部函数就维持了外部函数的局部数据。

一个典型的例子就是全局变量的使用。

mykhal这样回答:

Wikipedia对闭包的定义是这样的:

In computer science, a closure is a function together with a
referencing environment for the nonlocal names (free variables) of
that function.

从技术上来讲,在JS中,每个function都是闭包,因为它总是能访问在它外部定义的数据。

Since scope-defining construction in Javascript is a function, not a
code block like in many other languages, what we usually mean by
closure in Javascript is a fuction working with nonlocal variables
defined in already executed surrounding function.

闭包经常用于创建含有隐藏数据的函数(但并不总是这样)。

var db = (function() { // 创建一个隐藏的object, 这个object持有一些数据 // 从外部是不能访问这个object的
var data = {}; // 创建一个函数, 这个函数提供一些访问data的数据的方法
return function(key, val) {
if (val === undefined) { return data[key] } // get
else {
} // set } // 我们可以调用这个匿名方法 // 返回这个内部函数,它是一个闭包 })(); db(‘x’);
// 返回 undefined db(‘x’, 1);
// 返回 1 // 我们不可能访问data这个object本身 // 但是我们可以设置它的成员

看了这么多外国大牛的解答,不知道你懂还是不懂,反正我是懂了。

P.S. 发布文章之后看到@xiaotie的一篇文章,觉得有必要推荐一下,因为其剖析得更为深入。有人说应该在文章结尾对闭包进行总结,可惜小弟才疏学浅,不能给出一个精辟的总结。

@xiaotie对闭包的总结如下:

(1)闭包是一种设计原则,它通过分析上下文,来简化用户的调用,让用户在不知晓的情况下,达到他的目的;

(2)网上主流的对闭包剖析的文章实际上是和闭包原则反向而驰的,如果需要知道闭包细节才能用好的话,这个闭包是设计失败的;

(3)尽量少学习。

大家学习学习。

3.闭包的用处

我们来看看闭包的用途。事实上,通过使用闭包,我们可以做很多事情。比如模拟面向对象的代码风格;更优雅,更简洁的表达出代码;在某些方面提升代码的执行效率。

3.1匿名自执行函数

我们知道所有的变量,如果不加上var关键字,则默认的会添加到全局对象的属性上去,这样的临时变量加入全局对象有很多坏处,
比如:别的函数可能误用这些变量;造成全局对象过于庞大,影响访问速度(因为变量的取值是需要从原型链上遍历的)。
除了每次使用变量都是用var关键字外,我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护,
比如UI的初始化,那么我们可以使用闭包:

var datamodel = {  
    table : [],  
    tree : {}  
};  

(function(dm){  
    for(var i = 0; i < dm.table.rows; i++){  
       var row = dm.table.rows[i];  
       for(var j = 0; j < row.cells; i++){  
           drawCell(i, j);  
       }  
    }  

    //build dm.tree    
})(datamodel); 

我们创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,
因此在执行完后很快就会被释放,关键是这种机制不会污染全局对象。

3.2缓存

再来看一个例子,设想我们有一个处理过程很耗时的函数对象,每次调用都会花费很长时间,
那么我们就需要将计算出来的值存储起来,当调用这个函数的时候,首先在缓存中查找,如果找不到,则进行计算,
然后更新缓存并返回值,如果找到了,直接返回查找到的值即可。闭包正是可以做到这一点,因为它不会释放外部的引用,
从而函数内部的值可以得以保留。

var CachedSearchBox = (function(){  
    var cache = {},  
       count = [];  
    return {  
       attachSearchBox : function(dsid){  
           if(dsid in cache){//如果结果在缓存中  
              return cache[dsid];//直接返回缓存中的对象  
           }  
           var fsb = new uikit.webctrl.SearchBox(dsid);//新建  
           cache[dsid] = fsb;//更新缓存  
           if(count.length > 100){//保正缓存的大小<=100  
              delete cache[count.shift()];  
           }  
           return fsb;        
       },  

       clearSearchBox : function(dsid){  
           if(dsid in cache){  
              cache[dsid].clearSelection();    
           }  
       }  
    };  
})();  

CachedSearchBox.attachSearchBox("input1");  

这样,当我们第二次调用CachedSearchBox.attachSerachBox(“input1”)的时候,
我们就可以从缓存中取道该对象,而不用再去创建一个新的searchbox对象。

3.3实现封装

可以先来看一个关于封装的例子,在person之外的地方无法访问其内部的变量,而通过提供闭包的形式来访问:

var person = function(){  
    //变量作用域为函数内部,外部无法访问  
    var name = "default";     

    return {  
       getName : function(){  
           return name;  
       },  
       setName : function(newName){  
           name = newName;  
       }  
    }  
}();  

print(person.name);//直接访问,结果为undefined  
print(person.getName());  
person.setName("abruzzi");  
print(person.getName());  

得到结果如下:

undefined
default
abruzzi

3.4实现面向对象中的对象

闭包的另一个重要用途是实现面向对象中的对象,传统的对象语言都提供类的模板机制,
这样不同的对象(类的实例)拥有独立的成员及状态,互不干涉。虽然JavaScript中没有类这样的机制,但是通过使用闭包,
我们可以模拟出这样的机制。还是以上边的例子来讲:

function Person(){  
    var name = "default";     

    return {  
       getName : function(){  
           return name;  
       },  
       setName : function(newName){  
           name = newName;  
       }  
    }  
};  


var john = Person();  
print(john.getName());  
john.setName("john");  
print(john.getName());  

var jack = Person();  
print(jack.getName());  
jack.setName("jack");  
print(jack.getName());  

运行结果如下:

default
john
default
jack

由此代码可知,john和jack都可以称为是Person这个类的实例,因为这两个实例对name这个成员的访问是独立的,互不影响的。

4作用域与闭包:this,var,(function () {})

4.1 var 作用域

先来看个简单的例子:

var parent = function () {
  var name = "parent_name";
  var age = 13;

  var child = function () {
    var name = "child_name";
    var childAge = 0.3;

    // => child_name 13 0.3
    console.log(name, age, childAge);
  };

  child();

  // will throw Error
  // ReferenceError: childAge is not defined
  console.log(name, age, childAge);
};

parent();

直觉地,内部函数可以访问外部函数的变量,外部不能访问内部函数的变量。上面的例子中内部函数 child 可以访问变量 age,而外部函数 parent 不可以访问 child 中的变量 childAge,因此会抛出没有定义变量的异常。

有个重要的事,如果忘记var,那么变量就被声明为全局变量了。

function foo() {
  value = "hello";
}
foo();
console.log(value); // 输出hello
console.log(global.value) // 输出hello

这个例子可以很正常的输出 hello,是因为 value 变量在定义时,没有使用 var 关键词,所以被定义成了全局变量。在 Node 中,全局变量会被定义在 global 对象下;在浏览器中,全局变量会被定义在 window 对象下。

如果你确实要定义一个全局变量的话,请显示地定义在 global 或者 window 对象上。

这类不小心定义全局变量的问题可以被 jshint 检测出来,如果你使用 sublime 编辑器的话,记得装一个 SublimeLinter 插件,这是插件支持多语言的语法错误检测,js 的检测是原生支持的。

JavaScript 中,变量的局部作用域是函数级别的。不同于 C 语言,在 C 语言中,作用域是块级别的。 JavaScript 中没有块级作用域。

js 中,函数中声明的变量在整个函数中都有定义。比如如下代码段,变量 i 和 value 虽然是在 for 循环代码块中被定义,但在代码块外仍可以访问 i 和 value。

function foo() {
  for (var i = 0; i < 10; i++) {
    var value = "hello world";
  }
  console.log(i); //输出10
  console.log(value);//输出hello world
}
foo();

所以有种说法是:应该提前声明函数中需要用到的变量,即,在函数体的顶部声明可能用到的变量,这样就可以避免出现一些奇奇怪怪怪的 bug。

但我个人不喜欢遵守这一点,一般都是现用现声明的。这类错误的检测交给 jshint 来做就好了。

4.2闭包

闭包这个概念,在函数式编程里很常见,简单的说,就是使内部函数可以访问定义在外部函数中的变量。

假如我们要实现一系列的函数:add10,add20,它们的定义是 int add10(int n)。

为此我们构造了一个名为 adder 的构造器,如下:

var adder = function (x) {
  var base = x;
  return function (n) {
    return n + base;
  };
};

var add10 = adder(10);
console.log(add10(5));

var add20 = adder(20);
console.log(add20(5));

每次调用 adder 时,adder 都会返回一个函数给我们。我们传给 adder 的值,会保存在一个名为 base 的变量中。由于返回的函数在其中引用了 base 的值,于是 base 的引用计数被 +1。当返回函数不被垃圾回收时,则 base 也会一直存在。

我暂时想不出什么实用的例子来,如果想深入理解这块,可以看看这篇 http://coolshell.cn/articles/6731.html

4.3 闭包的一个坑

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 5);
}

上面这个代码块会打印五个 5 出来,而我们预想的结果是打印 0 1 2 3 4。

之所以会这样,是因为 setTimeout 中的 i 是对外层 i 的引用。当 setTimeout 的代码被解释的时候,运行时只是记录了 i 的引用,而不是值。而当 setTimeout 被触发时,五个 setTimeout 中的 i 同时被取值,由于它们都指向了外层的同一个 i,而那个 i 的值在迭代完成时为 5,所以打印了五次 5。

为了得到我们预想的结果,我们可以把 i 赋值成一个局部的变量,从而摆脱外层迭代的影响。

for (var i = 0; i < 5; i++) {
  (function (idx) {
    setTimeout(function () {
      console.log(idx);
    }, 5);
  })(i);
}

4.4 this

在函数执行时,this 总是指向调用该函数的对象。要判断 this 的指向,其实就是判断 this 所在的函数属于谁。

在《javaScript语言精粹》这本书中,把 this 出现的场景分为四类,简单的说就是:

有对象就指向调用对象;
没调用对象就指向全局对象;
用new构造就指向新对象;
通过 apply 或 call 或 bind 来改变 this 的所指。

1)函数有所属对象时:指向所属对象

函数有所属对象时,通常通过 . 表达式调用,这时 this 自然指向所属对象。比如下面的例子:

var myObject = {value: 100};
myObject.getValue = function () {
  console.log(this.value);  // 输出 100

  // 输出 { value: 100, getValue: [Function] },
  // 其实就是 myObject 对象本身
  console.log(this);

  return this.value;
};

console.log(myObject.getValue()); // => 100

getValue() 属于对象 myObject,并由 myOjbect 进行 . 调用,因此 this 指向对象 myObject。

2) 函数没有所属对象:指向全局对象

var myObject = {value: 100};
myObject.getValue = function () {
  var foo = function () {
    console.log(this.value) // => undefined
    console.log(this);// 输出全局对象 global
  };

  foo();

  return this.value;
};

console.log(myObject.getValue()); // => 100

在上述代码块中,foo 函数虽然定义在 getValue 的函数体内,但实际上它既不属于 getValue 也不属于 myObject。foo 并没有被绑定在任何对象上,所以当调用时,它的 this 指针指向了全局对象 global。

据说这是个设计错误。

3)构造器中的 this:指向新对象

js 中,我们通过 new 关键词来调用构造函数,此时 this 会绑定在该新对象上。

var SomeClass = function(){
  this.value = 100;
}

var myCreate = new SomeClass();

console.log(myCreate.value); // 输出100

顺便说一句,在 js 中,构造函数、普通函数、对象方法、闭包,这四者没有明确界线。界线都在人的心中。

4) apply 和 call 调用以及 bind 绑定:指向绑定的对象

apply() 方法接受两个参数第一个是函数运行的作用域,另外一个是一个参数数组(arguments)。

call() 方法第一个参数的意义与 apply() 方法相同,只是其他的参数需要一个个列举出来。

简单来说,call 的方式更接近我们平时调用函数,而 apply 需要我们传递 Array 形式的数组给它。它们是可以互相转换的。

var myObject = {value: 100};

var foo = function(){
  console.log(this);
};

foo(); // 全局变量 global
foo.apply(myObject); // { value: 100 }
foo.call(myObject); // { value: 100 }

var newFoo = foo.bind(myObject);
newFoo(); // { value: 100 }

完。
闭包,懂不懂由你,反正我是懂了
js闭包的用途
《作用域与闭包:this,var,(function () {})》

以上是关于js闭包的主要内容,如果未能解决你的问题,请参考以下文章

JS闭包的概念

(原创)JS闭包看代码理解

关于JS闭包

js闭包的作用

js闭包

js的闭包