闭包
闭包是 JavaScript 一个非常重要的特性,这意味着当前作用域总是能够访问外部作用域中的变量。 因为函数 是 JavaScript 中唯一拥有自身作用域的结构,因此闭包的创建依赖于函数。
闭包是一种特殊的对象,它由两部分组成:执行上下文(代号 A),以及在该执行上下文中创建的函数 (代号 B),当 B 执行时,如果访问了 A 中变量对象的值,那么闭包就会产生,且在 Chrome 中使用这个执行上下文 A 的函数名代指闭包。
JavaScript拥有自动的垃圾回收机制,关于垃圾回收机制,有一个重要的行为,那就是,当一个值,在内存中失去引用时,垃圾回收机制会根据特殊的算法找到它,并将其回收,释放内存。
而我们知道,函数的执行上下文,在执行完毕之后,生命周期结束,那么该函数的执行上下文就会失去引用。其占用的内存空间很快就会被垃圾回收器释放。可是闭包的存在,会阻止这一过程。
一个简单的列子
var fn = null;
function foo() {
var a = 2;
function innnerFoo() {
console.log(a);
}
fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}
function bar() {
fn(); // 此处的保留的innerFoo的引用
}
foo();
bar(); // 2
在上面的例子中,foo()
执行完毕之后,按照常理,其执行环境生命周期会结束,所占内存被垃圾收集器释放。但是通过fn = innerFoo
,函数innerFoo的引用被保留了下来,复制给了全局变量fn。这个行为,导致了foo的变量对象,也被保留了下来。于是,函数fn在函数bar内部执行时,依然可以访问这个被保留下来的变量对象。所以此刻仍然能够访问到变量a的值。
这样,我们就可以称foo为闭包。
模拟私有变量
function Counter(start) {
var count = start;
return {
increment: function() {
count++;
},
get: function() {
return count;
}
}
}
var foo = Counter(4);
foo.increment();
foo.get(); // 5
这里,Counter
函数返回两个闭包,函数 increment
和函数 get
。 这两个函数都维持着 对外部作用域 Counter
的引用,因此总可以访问此作用域内定义的变量 count
.
为什么不可以在外部访问私有变量
因为 JavaScript 中不可以对作用域进行引用或赋值,因此没有办法在外部访问 count
变量。 唯一的途径就是通过那两个闭包。
var foo = new Counter(4);
foo.hack = function() {
count = 1337;
};
上面的代码不会改变定义在 Counter
作用域中的 count
变量的值,因为 foo.hack
没有 定义在那个作用域内。它将会创建或者覆盖全局变量 count
。
循环中的闭包
一个常见的错误出现在循环中使用闭包,假设我们需要在每次循环中调用循环序号
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
上面的代码不会输出数字 0
到 9
,而是会输出数字 10
十次。
当 console.log
被调用的时候,匿名函数保持对外部变量 i
的引用,此时 for
循环已经结束, i
的值被修改成了 10
.
为了得到想要的结果,需要在每次循环中创建变量 i
的拷贝。
避免引用错误
为了正确的获得循环序号,最好使用 匿名包装器(其实就是我们通常说的自执行匿名函数)。
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}
外部的匿名函数会立即执行,并把 i
作为它的参数,此时函数内 e
变量就拥有了 i
的一个拷贝。
当传递给 setTimeout
的匿名函数执行时,它就拥有了对 e
的引用,而这个值是不会被循环改变的。
有另一个方法完成同样的工作,那就是从匿名包装器中返回一个函数。这和上面的代码效果一样。
for(var i = 0; i < 10; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 1000)
}
模块化
(function () {
var a = 10;
var b = 20;
function add(num1, num2) {
var num1 = !!num1 ? num1 : a;
var num2 = !!num2 ? num2 : b;
return num1 + num2;
}
window.add = add;
})();
add(10, 20);
在上面的例子中,我使用函数自执行的方式,创建了一个模块。add是模块对外暴露的一个公共方法。而变量a,b被作为私有变量。在面向对象的开发中,我们常常需要考虑是将变量作为私有变量,还是放在构造函数中的this中,因此理解闭包,以及原型链是一个非常重要的事情
防抖节流
// 节流
function throttle(fn, delay) {
var timer = null, firstTime = true;
return function () {
if (timer) { return false;}
var that = this;
var args = arguments;
fn.apply(that, args);
timer = setTimeout(function () {
clearTimeout(timer);
timer = null;
}, delay || 500);
};
}
// 防抖
function debounce(fn, delay) {
var timer = null;
return function () {
var that = this;
var args = arguments;
clearTimeout(timer);// 清除重新计时
timer = setTimeout(function () {
fn.apply(that, args);
}, delay || 500);
};
}
字节面试
var result = [];
var a = 3;
var total = 0;
function foo(a) {
for (var i = 0; i < 3; i++) {
result[i] = function () {
total += i * a;
console.log(total);
}
}
}
foo(1);
result[0](); // 3
result[1](); // 6
result[2](); // 9
result[2](); // 15
result[2](); // 18
-
循环之后,i = 3 不变
-
result[0/1/2] 都是一个方法
ƒ () { total += i * a; console.log(total); }
-
tip:这里也形成了闭包。total 被外层引用没有被销毁。每一次执行一次,total 都会增加
总结
形成: 函数中嵌套函数
作用: 函数内部调用外部变量、构造函数的私有属性、延长变量生命周期
优点: 希望一个变量长期存在内存中、模块化代码避免全局变量的污染、私有属性
缺点: 无法回收闭包中引用变量,容易造成内存泄漏
应用:
- ajax请求的成功回调
- 事件绑定的回调方法
- setTimeout的延时回调
- 函数内部返回另一个匿名函数
- 模块化
- 防抖节流
递归
递归就是函数调用函数本身的函数
-
一个简单的列子,求 N个数的累加,递归多次容易出现内存栈溢出。
function sunNme(n) { if (n === 1) return 1 return n + sunNme(n - 1) } sunNme(3) // 6 sunNme(100000) // Uncaught RangeError: Maximum call stack size exceeded
递归一定主要有出口,比如上面的
if(n===1) return 1
不然会造成内存溢出 -
递归一般用循环都可以实现
function sunNum(n) { let result = 0 for (let i = n; i !== 0; i--) { result += i } return result } let result = sunNum(10)
练习题
1、拷贝
var deepCopy = function (obj) {
if (typeof obj !== 'object') return obj;
// // 根据obj的类型判断是新建一个数组还是对象
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
}
}
return newObj;
}
2、数组扁平化
const flatten = (arr) => {
let result = [];
arr.forEach((item, i, arr) => {
// 若为数组,递归调用 faltten,并将结果与result合并
if (Array.isArray(item)) {
result = result.concat(flatten(item));
} else {
result.push(arr[i])
}
})
return result;
};
const arr = [1, [2, [3, 4, 5]]];
console.log(flatten(arr)); // [1, 2, 3, 4, 5]
console.log(arr.flat(Infinity)); // [1, 2, 3, 4, 5]
3、将原来字符串倒过来。
function reverse(str) {
if(str.length <= 1) return str;
return reverse(str.slice(1)) + str[0];
}
reverse('abc')
4、写一个函数,接受一串字符串,同时从前后开始拿一位字符对比,如果两个相等,返回 true
,如果不等,返回 false
。
function isPalindrome(str){
if(str.length === 1) return true;
if(str.length === 2) return str[0] === str[1];
if(str[0] === str.slice(-1)) return isPalindrome(str.slice(1,-1))
return false;
}
var str = 'abba'
isPalindrome(str)
5、编写一个函数,接受一个对象,这个对象值是偶数,则让它们相加,返回这个总值。
function nestedEvenSum(obj, sum=0) {
for (var key in obj) {
if (typeof obj[key] === 'object'){
sum += nestedEvenSum(obj[key]);
} else if (typeof obj[key] === 'number' && obj[key] % 2 === 0){
sum += obj[key];
}
}
return sum;
}
nestedEvenSum({c: 4,d: {a: 2, b:3}})
6、编写一个函数,接受一个对象,返回一个数组,这个数组包括对象里所有的值是字符串
function collectStrings(obj) {
let newArr = []
for (let key in obj) {
if (typeof obj[key] === 'string') {
newArr.push(obj[key])
} else if(typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
newArr = newArr.concat(collectStrings(obj[key]))
}
}
return newArr
}
var obj = {
a: '1',
b: {
c: 2,
d: 'dd'
}
}
collectStrings(obj)
闭包:
https://segmentfault.com/a/1190000012646221
http://bonsaiden.github.io/JavaScript-Garden/zh/#function.closures
递归:https://juejin.cn/post/6844903699584647176