① 什么是闭包
1 概念
- 闭包就是指有权访问另一函数作用域中的变量的函数
这种模式通常在函数嵌套结构中实现
1. 实现原理
- 在构造函数体内定义另外的函数作为目标对象的方法函数,而这个对象的方法函数反过来引用外层函数体中的临时变量
内部函数调用外部函数的变量,外部函数的变量不被释放
2. 作用
-
加强封装,设计实现私有变量
-
实现常驻内存的变量
闭包不能滥用,否则会导致内存泄露,影响网页的性能
闭包使用完了后,要立即释放资源,将引用变量指向 null
2 应用
1. 在函数内使用函数外的变量:函数作为返回值
- 闭包作用:避免变量被环境污染
funciton F1() {
var a = 100
return function() {
console.log(a)
}
}
var f1 = F1()
var a = 200
f1() // 100
2. 函数作为参数传递
function F1() {
var a = 100
return function() {
console.log(a)
}
}
var f1 = F1()
function F2(fn) {
var a = 200
fn()
}
F2(f1) // 100
3. 将函数与其所操作的某些数据关联起来
- 通常,你使用只有一个方法的对象的地方,都可以使用闭包
4. 用闭包模拟私有方法
5. 循环里面的闭包
- 怎样才能实现输层0-5呢?
题目
for(var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, 1000)
}
// 55555
方法一:为每一个回调创建一个新的词法环境
function makeCb(i) {
return function() {
console.log(i)
}
}
for(var i = 0; i < 5; i++) {
setTimeout(makeCb(i), 1000)
} // 01234
方法二:使用匿名闭包
for(var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i)
})
})(i)
}
// 01234
使用let声明变量
for(let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, 1000)
}
// 01234
3 知识点
-
词法作用域和动态作用域
-
js的作用域和作用域链
-
js执行上下文栈
-
堆栈溢出和内存泄漏
-
节流和防抖
-
柯里化
3.1 词法作用域和动态作用域
1. 词法作用域
- 函数的作用域在函数定义的时候决定
2. 动态作用域
- 函数的作用域在函数调用的时候决定
3. js采用词法作用域
- js函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的
var scope = 'global scope'
function checkscope() {
var scope = 'local scope'
function f() {
return scope
}
return f()
}
checkscope()
// local scope
var scope = 'global scope'
function checkscope() {
var scope = 'local scope'
return function f() {
return scope
}
}
checkscope()()
// local scope
3.2 js的作用域和作用域链
1. 作用域
全局作用域
- 全局作用域在代码中任何地方都能被访问
局部作用域
- 局部作用域一般只在固定的代码片段内可以被访问
2. 作用域访问规则
-
在作用域中访问变量时,首先会在当前作用域中查找
-
如果找到则直接使用
-
没找到就向外层作用域查找
-
如果找到则直接使用
-
没找到就继续向外层作用域查找,直到全局作用域
- 如果全局作用域也没有找到,则报错
-
-
3. 作用域链
-
当查找变量的时候,会先从当前上下文的变量对象中查找
-
如果没有找到,就会从父级执行上下文的变量对象中查找
-
一直找到全局执行上下文的变量对象,也就是全局对象
-
这样由多个执行上下文的变量对象构成的链条
3.3 js执行上下文栈
1. 执行上下文栈类型
-
全局上下文
-
函数上下文 -- 函数被调用时才创建
-
eval 函数执行上下文
2. 执行上下文组成
-
词法环境
-
变量环境
-
this值
3.4 堆栈溢出和内存泄漏
1. 堆栈溢出
-
是指内存空间已经被申请完,没有足够的内存提供了
-
程序代码运行都需要一定的计算存储空间--栈,栈遵循先进后出的原则,所以程序从栈底开始运行计算,程序内部函数的调用以及返回会不停的执行进栈和出栈的操作,栈内被所占的资源也在不断的对应变化,但是一旦你的调用即进栈操作过多,返回即出栈不够,这时候就会导致栈满了,再进栈的就会溢出来
2. 内存泄漏
- 是指申请的内存执行完后没有及时地清理或者销毁,占用空闲内存,内存泄漏过多的话,就会导致后面的程序申请不到内存。因此内存泄露会导致内存溢出
3. 解决办法
-
标记清除法
-
在一个变量进入执行环境后就给它添加一个标记:进入环境
-
进入环境的变量不会被释放,因为只要“执行流”进入响应的环境,就可能用到他们
-
当变量离开环境后,则将其标记为“离开环境”
-
4. 预防方法
-
减少不必要的全局变量
-
减少闭包的使用(因为闭包会导致内存泄漏)
-
避免死循环的发生
3.5 节流和防抖
1. 防抖(执行最后一次)
当持续触发事件时,函数是完全不执行的,等最后一次触发结束的一段时间之后,再去执行
① 应用
-
search
搜索联想,用户在不断输入值时,用防抖来节约请求资源 -
window触发
resize
时,不断地调整浏览器窗口大小会不断触发该事件,用防抖来让其只触发一次
② 分解
-
持续触发不执行
-
不触发的一段时间之后再执行
③ 实现
- 不触发的一段时间之后再执行
定时器里面调用要执行的函数,将
arguments
传入
- 封装一个函数,将目标函数(持续触发的事件)作为回调传进去,等待一段时间过后执行目标函数
function debound(func, delay) {
return function() {
setTimeout(() => {
func.apply(this, arguments)
}, delay)
}
}
- 持续触发不执行
function debound(func, delay) {
let timeout
return function() {
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(this, arguments)
}, delay)
}
}
- 用法
box.onmousemove = debound(function(e) {
box.innerHTML = `${e.clientX}, ${e.clientY}`
}, 1000)
2. 节流(执行第一次)
让函数有节制地执行,即在一段时间内,只执行一次
① 应用
-
鼠标不断点击触发,
mousedown
、mousemove
-
监听滚动事件,比如是否滑到底部自动加载更多
② 分解
-
持续触发并不会执行多次
-
到一定时间再去执行
③ 实现
持续触发,并不会执行,但是到时间了就会执行
- 关键点: 执行的时机
-
要做到控制执行的时机,可以通过一个开关,与定时器
setTimeout
结合完成 -
函数执行的前提是开关打开,持续触发时,持续关闭开关,等到
setTimeout
到时间了,再把开关打开,函数就会执行了
function throttle(func, delay) {
let run = true
return function() {
if(!run) {
return
}
run = false
setTimeout(() => {
func.apply(this, argument)
run = true
}, delay)
}
}
- 用法
box.onmousemove = throttle(function(e) {
box.innerHTML = `${e.clientX}, ${e.clientY}`
}, 1000)
-
节流还能用时间间隔去控制
如果当前事件与上次执行时间的时间差大于一个值,就执行
3.6 函数柯里化currying
1. 什么是柯里化
-
把函数完全变成 接受一个参数;返回一个值 的固定形式,这样对于讨论和优化会更加方便。
-
柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术
① 一个简单的柯里化函数
function sum (a, b) {
console.log(a + b);
}
sum(1, 2); // 3
// Currying后
function curryingAdd(x) {
return function (y) {
return x + y
}
}
curryingAdd(1)(2) // 3
2. 柯里化的目的:减少代码冗余,以及增加代码的可读性
3. 柯里化的好处
-
参数复用
-
提前确认
-
延迟执行
① 参数复用
function check(reg, txt) {
return reg.text(txt)
}
check(/\d+/g, 'test') // false
check(/[a-z]+/g, 'test') // true
function curryingCheck(reg) {
return function(txt) {
return reg.test(txt)
}
}
var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)
hasNumber('test1') // true
hasNumber('testtest') // false
hasLetter('1212') // false
② 提前确认
③ 延迟执行
4. 封装柯里化
function curry (fn, currArgs) {
return function() {
let args = [].slice.call(arguments);
// 首次调用时,若未提供最后一个参数currArgs,则不用进行args的拼接
if (currArgs !== undefined) {
args = args.concat(currArgs);
}
// 递归调用
if (args.length < fn.length) {
return curry(fn, args);
}
// 递归出口
return fn.apply(null, args);
}
}
//这样就可以直接调用curry了
-
currArgs 是调用 curry 时传入的参数列表
-
currArgs !== undefined 的判断,是为了解决递归调用时的参数拼接
测试
function sum(a, b, c) {
console.log(a + b + c);
}
const fn = curry(sum);
fn(1, 2, 3); // 6
fn(1, 2)(3); // 6
fn(1)(2, 3); // 6
fn(1)(2)(3); // 6
5. 柯里化性能
-
存取
arguments
对象通常要比存取命名参数要慢一点 -
一些老版本的浏览器在
arguments.length
的实现上是相当慢的 -
使用
fn.apply( … )
和fn.call( … )
通常比直接调用fn( … )
稍微慢点 -
创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上
6. 编程 -- 实现一个add方法,使计算结果能够满足如下预期
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
var _args = Array.prototype.slice.call(arguments);
// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
var _adder = function() {
_args.push(...arguments);
return _adder;
};
// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
}
return _adder;
}
add(1)(2)(3) // 6
console.log(add(1)(2)(3)) // f 6
add(1, 2, 3)(4) // 10
add(1)(2)(3)(4)(5) // 15
add(2, 6)(1) // 9
Object.prototype.toString()
函数的隐式转换
为什么上面输出的是f 6而不是6?
function add() {
return 20
}
console.log(add + 10) // function add 20
function add() {
return 20
}
add.toString = function() {
return 10
}
console.log(add + 10) // 20
function add() {
return 20
}
add.valueOf = function() {
return 5
}
add.toString = function() {
return 10
}
console.log(add + 10) // 15
-
当我们没有重新定义
toString
与valueOf
时,函数的隐式转换会调用默认的toString
方法,它会将函数的定义内容作为字符串返回。 -
而当我们主动定义了
toString/vauleOf
方法时,那么隐式转换的返回结果则由我们自己控制了。 -
其中
valueOf
会比toString
后执行
7. 总结
- 函数的柯里化,是 js 中函数式编程的一个重要概念。它返回的是一个函数的函数。其实现方式,需要依赖参数以及递归,通过拆分参数的方式,来调用一个多参数的函数方法,以达到减少代码冗余,增加可读性的目的。