闭包
- 闭包的作用:保存数据
变量作用域
- 函数作用域,函数内的变量只有在函数内才能访问到
var func = function(){
var a = 1;
alert ( a ); // 输出: 1
};
func();
alert ( a ); // 输出:Uncaught ReferenceError: a is not defined
-
变量的作用域,是由函数内向往依次查找,直到查找到顶层的全局作用域
-
变量 a 会沿着作用域链依次查找,先在函数 func2 查找,然后函数 func1 作用域,最后找到全局变量 a = 1
变量的保存周期
- 函数内的变量,函数执行完毕后就销毁
- 全局变量会一直存在,除非主动销毁
var func = function(){
var a = 1; // 退出函数后局部变量 a 将被销毁
alert ( a );
};
func();
- 全局函数f 的函数作用域保存变量a,
一直未销毁过
var func = () =>{
var a = 1
return () => {
a++
alert(a)
}
}
var f = func()
f(); //2
f(); //3
- 在不使用let时,闭包的经典使用场景
for(var i = 0; i< nodes.length ; i++){
( function(){
nodes[i].onclick = function(){
console.log(i)
}
})(i)
}
- 根据经典的闭包场景,可以写出判断数据类型的变式
var Type = {}
for(var i = 0,type = [ 'String', 'Array', 'Number']; i<type.length; i++){
(function(type){
Type['is' + type] = (obj) => {
return Object.prototype.toString.call(obj) === '[object ' + type + ']'
}
})(type[i])
}
Type.isArray([]) //true
闭包的更多作用
- 函数内部一直保存对全局变量的引用实现缓存机制
- 注意:arguments 是伪数组无法使用数组的api,所以需要借用 Array.prototype.join方法,call,bind,apply可以实现
借用
const cache = {}
var multi = function (){
let a = 1
// const args = arugments.join(',') 伪数组没有join方法
const args = Array.prototype.join.call(arguments,',')
if(cache[args]){
return cache[args]
}
for(let i = 0; i < arguments.length; i++){
a = arguments[i] * a
}
cache[args] = a
return a
}
- 如果变量一直引用全局变量会导致污染,需要使用自调用函数,再返回一个函数(再封装一层)
var multi = (function(){
var cache = {}
return () => {
var a = 1
var args = Array.prototype.join.call(arguments,',')
if(cache[args]) return
for(var i = 0; i < arguments.length; i++){
a = arguments[i] * a
}
return cache[args] = a
}
})()
- 大函数拆分:大函数中有很多个单独的功能块能独立处理,如mutli 可以分为 1 求乘积 2 取缓存 这2个部分
var multi = (function(){
var cache = {}
var calculate = () => {
var a = 1
for( var i = 0 ; i < arguments.length; i++){
a = a * arguments
}
return a
}
// 此处为什么一定要返回一个函数? 因为需要函数作用域一直保持外层变量cache的引用
// 此处不能使用箭头函数,箭头函数没有arguments对象
return function(){
var args = Array.prototype.join.call(arguments,'')
if(args in cache){
return cache[args]
}
// 不能写成,cache[args] = calculate()
return cache[args] = calculate.appply(null,arguments)
}
})()
延续局部变量的寿命
var report = function(src){
var img = new Image()
img.src = src
}
- 当局部变量img被销毁,而report还未来得及发HTTP请求,此时请求就会丢失如何解决这个问题?
var report = (function(){
var imgs = []
return function(src){
var img = new Image()
imgs.push(img)
img.src = src
}
})()
- 以上通过闭包保存函数内部的数据,来
延长变量的生命周期
闭包和面向对象设计
- 通过闭包保存数据的,面向对象的写法为
var extend = function(){
var value = 0
return {
call:function(){
value++
console.log(value)
}
}
}
var extend = extend()
extend.call()
extend.call()
- 上面为通过函数的形式实现,将extend改为对象实现
var extend = {
value:0,
call:function(){
this.value++
console.log(this.value)
}
}
extend.call()
- 也可以通过构造函数的方式实现
var Extent = function(){
this.value = 0
}
Extent.prototype.call = function(){
this.value++
console.log(this.value)
}
var extend = new Extend()
extend.call()
闭包与内存管理
-
闭包和内存泄露的关系:使用闭包的同时比较容易形成循环引用,如果闭包的作用域链中保存着一些 DOM 节点,这时候就有可能造成内存泄露
-
解决内存泄漏问题:们只需要把循环引用中的变量设为 null即可。将变量设置为 null 意味着切断变量与它此前引用的值之间的连接。
高阶函数
- 函数可以作为参数被传递
- 函数可以作为返回值输出
函数作为参数
- 常见的函数最为参数,就是回调函数
var getUserInfo = function(userId,callback){
$.ajax('http://xxx.com/getUserInof?' + userId,function(data){
if(typeof callback === 'function'){
callback(data)
}
})
}
getUserInfo(13157,function(data){
alert(data.userName)
})
- 函数作为参数,常见的还有事件委托,不适合在函数内部执行一些动作,需要委托给外部方法去执行
var appendDiv = function(){
for(var i = 0; i < 100; i++){
var div = document.createElement('div')
div.innerHTML = i
document.body.appendChild(div)
div.style.display = 'none'
}
}
- 并不是所有的元素都是隐藏操作,需要将div委托给另外一个函数
var appendDiv = function(callback){
for(var i = 0; i < 100; i++){
var div = document.createElement('div')
div.innerHTML = i
document.body.appendChild(div)
if(typeof callback === 'function'){
callback(div)
}
}
}
appendDiv(function(node){
node.style.display = 'none'
})
函数作为返回值
- 判断数据类型
function isType(typing) {
return function (val) {
return Object.prototype.toString.call(val) == `[object ${typing}]`;
};
}
let util = {};
["String", "Number", "Boolean"].forEach((mehtod) => {
util["is" + mehtod] = isType(mehtod);
});
console.log(util.isString("hello"));
console.log(util.isNumber(222));
高阶函数实现APO
- 动态织入,到另外一个函数汇总,具体的实现技术有很多,Function.prototype 就是其中一个
Function.prototype.before = function(beforeFn){
var _self = this
return function a(){
beforeFn.apply(this,arguments)
return _self.apply(this,arguments)
}
}
Function.prototype.after = function(afterFn){
var _self = this
return function b(){
var ret = _self.apply(this,arguments)
afterFn.apply(this,arguments)
return rete
}
}
var func = function(){
console.log(2)
}
const result = func.before(function(){
console.log(1)
}).after(() => {
console.log(3)
})
result() // 1 2 3
- 如果工作上希望在某个别人的方法上加上某些方法,最好的办法就是在原来的基础上再包一层
- 如他人的工具 otherTool, 使用 Object.craete(otherTool)产生自己的myTool,然后再加方法
let otherTool = {
add:function(a,b){
console.log('调用别人的方法')
return a + b
}
}
let myTool = Object.create(otherTool)
myTool.add = function(a,b){
console.log('调用自己的方法')
return a + b * 2
}
console.log("使用add方法",myTool.add(1,2))
高阶函数的应用场景
柯里化
- 什么是柯里化:柯里化又称分部分求值,即柯里化完后的函数,不会直接求值,而是继续返回一个函数
function add(){
return function(a){
return function(b){
return function(c){
return function(d){
return a + b + c + d
}
}
}
}
}
// 第一次调用返回一个函数,第二次调用开始保存参数,最后一次传入参数才执行相加
add()(1)(2)(3)(4)
- 如何将一个函数柯里化
//柯里化
1 有2个参数,第一个参数是一个函数,第二个参数是每次传入的参数
2 返回值为一个函数
3 如果传入的参数个数与函数的形参的个数一致才执行该函数,否则仍然返回一个函数
function curring(fn,arr = []){
return function(...args){
let params = [...arr,...args]
if(fn.length === params.length){
fn(...params)
} else {
return curring(fn,params)
}
}
}
反柯里化
- 反柯里化不是柯里化的反向操作,不是将一个分多步调用的函数一次性调用,而是借用其他对象的方法并调用
- 如果让对象可以调用push方法?
Function.prototype.uncurring = function(){
var self = this
return function(){
// 取出形参的第一个,即为反柯里化的函数
var curFn = Array.prototype.shift.call(arguments)
// 执行当前函数,并调用当前函数,并将上下文指向当前执行的函数,并传递参数
return self.apply(curFn,arguments)
}
}
var push = Array.prototype.push.uncurring()
var obj2 = {
a:1,
b:3
}
push(obj2,2) // {0: 2, a: 1, b: 3, length: 1}
函数节流防抖
- 节流:记录每次函数执行的时间戳,类似于到点就发车的班车,每次发车的时间间隔是固定的
function throttle(fn, wait, options) {
let args, context, start = 0;
// 必须在函数的外部申明形成闭包,这样才能保存数据,不然每次都是重新赋值
let throttled = function (e) {
context = this
args = arguments;
let now = Date.now();
if ((now - start) < wait) {
// 当前时间小于事件间隔则无需触发
} else {
start = now;
fn.apply(context, args);
timeout = null;
}
}
return throttled
}
- 防抖:每次用户点击都会开启一个定时器,目标函数只会执行一次
function debounce(fn,wait,options){
let timeout,args,context;
return function() {
const context = this
const args = arguments
//每次用户点击都会清空定时器
timeout = setTimeout(() => {
fn.apply(context,args)
},wait)
}
}
- 防抖函数是在触发事件的单位时间后执行一次函数,在单位时间内多次触发不执行函数,重新计时
分时函数
- 有大的同步任务,需要分成多个小任务
function computeBigData(data,cb){
let result = []
function next() {
const chunk = data.splice(0,5)
if(data.length > 0 ){
setTimeout(() =>{
// 假设此处为大量的同步计算
result = result.concat(chunk.map => chunk *2)
// 如果数据未处理完毕,则继续调用next函数
next()
},0)
} else {
cb(result)
}
}
next()
}