闭包
闭包是一个难懂又必须征服的概念。闭包的形成与变量的作用域以及变量的生存周期密切相关。
变量的作用域
当在函数中声明一个变量的时候,如果该变量前面没有var关键字,这个变量就会成为全局变量,这当然是一种容易造成命名冲突的做法。
另一种就是用var关键字声明函数中的变量,这个时候的变量就是局部变量,只有在该函数内部才能访问到这个变量,在函数外部访问不到的
代码
var func = function () {
var a = 1
console.log(a)
}
func()
console.log(a)
输出
1
Uncaught ReferenceError: a is not defined
代码
var func = function () {
a = 1
console.log(a)
}
func()
console.log(a)
输出
1
1
变量的生命周期
除了变量的作用域之外,另外一个跟闭包有关的概念是变量的生存周期。
对于全局变量来说,全局变量的生存周期当然是永久的,除非我们主动销毁这个全局变量。
而对于在函数内用 var 关键字声明的局部变量来说,当退出函数时,这些局部变量即失去了 它们的价值,它们都会随着函数调用的结束而被销毁:
代码
var func = function () {
var a = 1
console.log(a)
}
func()
输出
1
闭包代码
var func = function () {
var a = 1;
return function(){
a++
console.log(a)
}
}
var f = func()
f()
f()
f()
f()
输出
2
3
4
5
闭包的作用
封装变量
闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”。假设有一个计算乘积的
简单函数
代码
var mult = function () {
var a = 1
for (var i = 0; i < arguments.length; i++) {
a = a * arguments[i]
}
return a
}
mult(1,2,3,4,5)
输出
120
加入缓存代码
var cache = {}
var mult = function () {
var args = Array.prototype.join.call(arguments, ',')
if (cache[args]) {
return cache[args]
}
var a = 1
for (var i = 0; i < arguments.length; i++) {
a = a * arguments[i]
}
return (cache[args] = a)
}
console.log(mult(1, 2, 3))
console.log(mult(1, 2, 3))
console.log(cache)
console.log(mult(3, 2, 1))
console.log(cache)
输出
6
6
{1,2,3: 6}
6
{1,2,3: 6, 3,2,1: 6}
上面的代码暴露在了全局作用域中
var mult = (function () {
var cache = {}
return function () {
var args = Array.prototype.join.call(arguments, ',')
if (cache[args]) {
return cache[args]
}
var a = 1
for (var i = 0; i < arguments.length; i++) {
a = a * arguments[i]
}
return (cache[args] = a)
}
})()
console.log(mult(1, 2, 3))
console.log(mult(1, 2, 3))
console.log(mult(3, 2, 1))
输出
6
6
6
延续局部变量的寿命
var report = function (src) {
var img = new Image()
img.src = src
}
report('http://xxxx')
问题
但是通过查询后台的记录我们得知,因为一些低版本浏览器的实现存在 bug,在这些浏览器 下使用 report 函数进行数据上报会丢失 30%左右的数据,也就是说,report 函数并不是每一次 都成功发起了 HTTP 请求。丢失数据的原因是 img 是 report 函数中的局部变量,当 report 函数的 调用结束后,img 局部变量随即被销毁,而此时或许还没来得及发出 HTTP 请求,所以此次请求 就会丢失掉。
现在我们把 img 变量用闭包封闭起来,便能解决请求丢失的问题:
var report = (function () {
var imgs = []
return function (src) {
var img = new Image()
imgs.push(img)
img.src = src
}
})()
闭包与内存管理
闭包是一个非常强大的特性,但人们对其也有诸多误解。一种耸人听闻的说法是闭包会造成内存泄露,所以要尽量减少闭包的使用。
局部变量本来应该在函数退出的时候被解除引用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量就能一直生存下去。从这个意义上看,闭包的确会使一些数据无法被及时 销毁。使用闭包的一部分原因是我们选择主动把一些变量封闭在闭包中,因为可能在以后还需要 使用这些变量,把这些变量放在闭包中和放在全局作用域,对内存方面的影响是一致的,这里并不能说成是内存泄露。如果在将来需要回收这些变量,我们可以手动把这些变量设为 null。
跟闭包和内存泄露有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包的作用域链中保存着一些 DOM 节点,这时候就有可能造成内存泄露。但这本身并非闭包的问题,也并非 JavaScript 的问题。在 IE 浏览器中,由于 BOM 和 DOM 中的对象是使用 C++以 COM 对象 的方式实现的,而 COM 对象的垃圾收集机制采用的是引用计数策略。在基于引用计数策略的垃圾回收机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收,但循环引用造成的内存泄露在本质上也不是闭包造成的。
同样,如果要解决循环引用带来的内存泄露问题,我们只需要把循环引用中的变量设为 null 即可。将变量设置为 null 意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运 行时,就会删除这些值并回收它们占用的内存。
高阶函数
高阶函数值指至少满足下列条件之一的函数。
- 函数可以作为参数被传递
- 函数可以作为返回值输出
函数可以作为参数被传递
回调函数
```javascript var getUserInfo = function (userId, callback) { $.ajax(‘http://xxxxxxx?userId=‘ + userId, function (data) { callback(data) }) }
getUserInfo(123, function (data) { console.log(data) })
回调函数的应用不仅只在异步请求中,当一个函数不适合执行一些请求时,我们也可以把这 些请求封装成一个函数,并把它作为参数传递给另外一个函数,“委托”给另外一个函数来执行。<br />比如,我们想在页面上创建100个div节点,然后把这些节点设置为隐藏
<a name="m5593"></a>
##### 原来代码
```javascript
var appendDiv = function () {
for (var i = 0; i < 100; i++) {
var div = document.createElement('div')
dix.innerHTML = i
document.body.appendChild('div')
div.style.display = 'none'
}
}
appendDiv()
把div.style.display = 'none'写在方法里面显得有些不合理
委托后代码
var appendDiv = function (callback) {
for (var i = 0; i < 100; i++) {
var div = document.createElement('div')
dix.innerHTML = i
document.body.appendChild('div')
if (typeof callback === 'function') {
callback(div)
}
}
}
appendDiv(function (node) {
node.style.display = 'none'
})
Array.prototype.sort
[1, 2, 3, 4, 5, 6].sort(function (a, b) {
return a - b
})
// 从大到小排序
逗号运算符
- 上面的代码我写错了
- 逗号运算符,返回最后一个
[(1, 2, 3, 4, 5, 6)].sort(function (a, b) {
return a - b
})
// 最后返回的是:6
[1, 2].reduce((t,c) => (t+=c, t), 0)
函数作为返回值输出
判断数类型
代码
```javascript var isString = function (obj) { return Object.prototype.toString.call(obj) === ‘[object String]’ }
var isArray = function (obj) { return Object.prototype.toString.call(obj) === ‘[object Array]’ }
var isNumber = function (obj) { return Object.prototype.toString.call(obj) === ‘[object Number]’ }
- 我们发现大多数的代码都是相同的
<a name="lUMXh"></a>
##### 优化后的代码
```javascript
var isType = function (type) {
return function (obj) {
return Object.prototype.toString.call(obj) === '[object ' + type + ']'
}
}
var isString = isType('String')
高阶函数的其他应用
函数节流
- throttle
分时函数
一个例子是创建 WebQQ 的 QQ 好友列表。列表中通常会有成百上千个好友,如果一个好友 用一个节点来表示,当我们在页面中渲染这个列表的时候,可能要一次性往页面中创建成百上千 个节点。
在短时间内往页面中大量添加 DOM 节点显然也会让浏览器吃不消,我们看到的结果往往就 是浏览器的卡顿甚至假死。
代码
var arr = []
for (var i = 1; i < 1000; i++) {
arr.push(i)
}
var renderFriendList = function (data) {
for (var i = 0; i < data.length; i++) {
var div = document.createElement('div')
div.innerHTML = i
document.body.appendChild(div)
}
}
renderFriendList(arr)
- 优化,每200毫秒创建8个节点
优化后代码
var timeChunk = function (arr, fn, count) {
var obj
var t
var len = arr.length
var start = function () {
for (var i = 0; i < Math.min(count || 1, arr.length); i++) {
var obj = arr.shift()
fn(obj)
}
}
return function () {
t = setInterval(function () {
if (arr.length === 0) {
return clearInterval(t)
}
start()
}, 200)
}
}
测试代码
var ary = []
for (var i = 1; i <= 1000; i++) {
ary.push(i)
}
var renderFriendList = timeChunk(
ary,
function (n) {
var div = document.createElement('div')
div.innerHTML = n
document.body.appendChild(div)
},
8
)
renderFriendList()
惰性加载函数