javascript闭包总结
闭包是javascript中⼀个⼗分常见但⼜很难掌握的概念,⽆论是⾃⼰写代码还是阅读别⼈的代码都会接触到⼤量闭包。之前也没有系统学习过,最近系统地总结了⼀些闭包的使⽤⽅法和使⽤场景,这⾥做个记录,希望⼤家指正补充。
⼀、定义
《JavaScript忍者秘籍》中对于闭包的定义是这样的:
闭包是⼀个函数在创建时允许该⾃⾝函数访问并操作该⾃⾝函数之外的变量时所创建的作⽤域。换句话说,闭包可以让函数访问所有的变量和函数,只要这些变量和函数存在于该函数声明时的作⽤域内就⾏。
注意:这⾥说的是创建时,⽽不是调⽤时。
⼆、外部操作函数私有变量
正常来讲,函数可以声明⼀个块级作⽤域,作⽤域内部对外部是不可见的,如:
function P(){
var innerValue = 1
}
var p = new P()
console.log(p.innerValue) //输出undefined
但是,闭包可以让我们能够访问私有变量:
function P(){
var innerValue = 1
console.log(innerValue)
}
this.setValue = function(newValue){
innerValue = newValue
}
}
var p = new P()
console.Value()) //1
p.setValue(2)
console.Value()) //2
三、只要有回调的地⽅就有闭包
这可能是我们在⽇常开发中接触闭包最多的场景,可能有些同学还没有意识到这就是闭包,举个例⼦:
javascript全局数组function bindEvent(name, selector) {
console.log( "Activating: " + name );
} );
}
bindEvent( "Closure 1", "test1" );
bindEvent( "Closure 2", "test2" );
执⾏了两次bindEvent函数后,最后传⼊的name是Closure 2,为什么点击id为test1的按钮输出的不是Closure 2⽽是Closure 1?这当然是闭包帮我们记住了每次调⽤bindEvent时的⼊参name。
四、绑定函数上下⽂(bind⽅法的实现)
先看⼀段代码:
HTML:
<button id="test1">click1</button>
Js:
var elem = ElementById('test1')
var aHello = {
name : "hello",
showName : function(){
console.log(this.name);
}
}
当点击按钮时会有什么现象呢?会输出“hello”吗?结果是会输出something,但是输出的不是“hello”,⽽是空。为什么呢?显然
是“this.name”的this搞的⿁,原来当我们绑定事件后触发这个事件,浏览器会⾃动把函数调⽤上下⽂切换到⽬标元素(本例中是id为test1的button元素)。所以this是指向button按钮的,并不是aHello 对象,所以没有输出“hello”。
那么我们如何将代码改成我们想要的样⼦呢?
1. 最常⽤的⽅式就是⽤⼀个匿名函数将showName包装⼀下:
aHello.showName()
}
通过这样使aHello来调⽤showName,这样this就指向aHello了。
2. 使⽤bind函数来改变上下⽂
强⾏把this指向aHello对象,再点击按钮,就能正常输出“hello”了。是不是很神奇?那么如果让你来实现bind函数,怎么写呢?我简单写了⼀个:
Function.prototype.bind = function(){
var fun = this; //指向aHello.showName函数
var obj = Array.prototype.slice.call(arguments).shift(); //这⾥没有处理多个参数,假设只有⼀个参数
return function(){
fun.apply(obj)
}
}
核⼼代码是使⽤apply⽅法来改变this的指向,通过闭包来记住调⽤bind函数的函数,还有bind函数的⼊参。
五、函数柯⾥化
有同学可能会问柯⾥化是什么?先看⼀个例⼦:
假如有⼀个求和函数:
return a + b
}
console.log(add(1,2)) //3
如果是柯⾥化的写法:
function add(a){
return function(b){
return a+b
}
}
console.log(add(1)(2)) //3
来看百度百科中柯⾥化的定义:
把接受多个参数的函数变换成接受⼀个单⼀参数(最初函数的第⼀个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
通俗来讲,柯⾥化也叫部分求值(不会⽴刻求值,⽽是到了需要的时候再去求值),是⼀个延迟计算的过程。之所以能延迟,少不了闭包来记录参数。来看⼀个更有深度的例⼦,这是社区中某⼤神丢出的⼀个问题:
完成plus函数,通过全部的测试⽤例
function plus(n){}
var assert = require('assert')
var plus = require('../lib/assign-4')
describe('闭包应⽤',function(){
it('plus(0) === 0',function(){
assert.equal(0,plus(0).sum())
})
it('plus(1)(1)(2)(3)(5) === 12',function(){
assert.equal(12,plus(1)(1)(2)(3)(5).sum())
})
it('plus(1)(4)(2)(3) === 10',function(){
assert.equal(10,plus(1)(4)(2)(3).sum())
})
it('⽅法引⽤',function(){
var plus2 = plus(1)(1)
assert.equal(12,plus2(1)(4)(2)(3).sum())
})
})
整理思路时考虑到以下⼏点:
1. plus()()这种调⽤⽅式意味着plus函数的返回值⼀定是个函数,⽽且由于后⾯括号的个数并没有限制,想到plus函数是在递归调⽤⾃⼰。
2. plus所有的⼊参都应该保存起来,可以建⼀个数组来保存,⽽这个数组是要放在闭包中的。
3. plus()().sum(),sum的调⽤形式意味着sum应该是plus的⼀个属性,⽽且最终的求和计算是sum来完成的
基于这⼏点,我写了⼀个plus函数:
var arr = []//这⾥要声明个闭包变量,⽽且只能声明⼀次,所以plus函数⾥⾯多包了⼀层f函数
var f = function(){
f.sum = function(){
duce(function(total, curvalue){
return total + curvalue
}, 0)
}
Array.prototype.push.apply(arr, Array.prototype.slice.call(arguments))//这句可以写成arr.push(...arguments)
return arguments.callee
}
return f
}
var plus = plus1()
六、缓存记忆功能
有些函数的操作可能⽐较费时,⽐如做复杂计算。这时就需要⽤缓存来提⾼运⾏效率,降低运⾏环境压⼒。以前我通常的做法是直接搞个全局对象,然后以键值对的形式将函数的⼊参和结果存到这个对象中,如果函数的⼊参在该对象中能查到,那就根据键读出值返回就好,不⽤重新计算。
这种全局对象的搞法肯定不具有通⽤性,所以我们想到使⽤闭包,来看⼀个《JavaScript忍者秘籍》中的例⼦:
ized = function(key){
this._values = this._values || {} //this指向function(num){...}函数
return this._values[key] !== undefined ? this._values[key] : this._values[key] = this.apply(this, arguments);
}
ize = function(){
var fn = this; //this指向function(num){...}函数
return function(){
ized.apply(fn, arguments)
}
}
var isPrime = (function(num){
console.log("没有缓存")
var prime = num != 1;//1不是质数
for(var i = 2;i < num; i++){
if(num % i == 0){
prime = false;
break;
}
}
return prime
}).memoize()
测试执⾏:
console.log(isPrime(5))
console.log(isPrime(5))
输出:
没有缓存
true
true
该例⼦巧妙地利⽤闭包将缓存存在计算函数的⼀个属性中,⽽且实现了缓存函数与计算函数的解耦,使得缓存函数具有通⽤性。
七、即时函数IIFE
先来看代码:
var a = 0
return function(){
console.log(++a)
}
})()
p() //1
p() //2
p() //3
有了IIFE和闭包,这种功能再也不需要全局变量了。所以,IIFE的⼀个作⽤就是创建⼀个独⽴的、临时的作⽤域,这也是后⾯要说的模块化实现的基础。
再来看⼀个基本所有前端都遇到过的⾯试题:
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, 1000 );
}
⼤家都知道这段代码会在1s后打印5个6,为什么会这样呢?因为timer中每次打印的i和for循环⾥⾯的i是同⼀个变量,所以当1s后要打印时,循环早已跑完,i的值定格在6,故打印5个6。
那么,怎么输出1,2,3,4,5呢?
答案就是使⽤IIFE:
for (var j=1; j<=5; j++) {
(function(n){
setTimeout(function timer() {
console.log( n );
}, 1000 )
})(j)
}
通过在for循环中加⼊即时函数,我们可以将正确的值传给即时函数(也就是内部函数的闭包),在for循环每次迭代的作⽤域中,j变量都会被重新定义,从⽽给timer的闭包传⼊我们期望的值。
当然,在ES6的时代,⼤可不必这么⿇烦,上代码:
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, 1000 );
}
问题解决!具体原因,⼤家请⾃⾏百度…
⼋、模块机制
先看⼀个最简单的函数实现模块封装的例⼦:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论