JavaScript 闭包和高阶函数

作者: tww844475003 分类: 前端开发 发布时间: 2021-06-04 20:06

一、闭包

  • 变量的作用域

在函数中声明一个变量,如果变量前面没有带上关键字 var ,这个变量就会成为全局变量,用 var 关键字声明变量,变量即为局部变量,只有在函数内部可以访问

var func = function() {
  var a = 'JavaScript';

  console.log(a); // JavaScript
}

func();
console.log(a) // Uncaught ReferenceError: a is not defined

在 JavaScript 中,函数可以用来创建函数作用域,此时函数就好比一堵半透明的墙,内部可以访问外部变量,外部却不能读取函数内部的变量。

变量的搜索是从内到外,而非从外到内的。

var a = 1;

var func = function() {
  var b = 2;
  var childFn = function() {
    var c = 3;
    console.log(b); // 2
    console.log(a); // 1
  }
  childFn();
  console.log(c); // Uncaught ReferenceError: c is not defined
}

func();
  • 变量的生存周期

全局变量的生存周期是永久的,除非主动销毁了这个全局变量。

函数内部用 var 关键字声明的局部变量,在退出函数时,这些局部变量就会随着函数调用结束而被销毁。

var func = function() {
  var a = 1; // 退出函数后局部变量 a 就会被销毁
  console.log(a);
}

func();

再看下面一段代码:

var func = function() {
  var count = 1;
  return function() {
    count++;
    console.log(count);
  }
}

var f = func();
f(); // 2
f(); // 3
f(); // 4
f(); // 5
f(); // 6

运行后发现似乎与我们之前的结论刚好相反,并没有销毁,什么原因了。这是因为访问 func() return 了一个匿名函数的引用,在这里它产生了一个闭包结构,局部变量并没有销毁,这就是闭包的奇妙之处。

闭包在我们日常开发过程中,到底能帮我们做些什么了,下面我们来看一个开发过程中经常用到的示例:

<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
  <li>6</li>
</ul>

<script>
  var nodes = document.getElementsByTagName('li');

  for (var i = 0, len = nodes.length; i <len; i++) {
    nodes[i].onclick = function() {
      console.log(i);
    }
  }
</script>

运行发现每个 li 的点击并没有像我们想像的那样,打印对印节点的下标。无论点击哪个结果都是 6 。这是因为 li 节点的 onclick 事件是被异步触发的,事件触发时,for 循环早已结束,此时变量 i 的值已经是 6,故每次打印的值为 6 。

此时闭包就能派上用场了,利用闭包把每次循环的 i 值都封闭起来。当事件函数中顺着作用域链中从内到外查找变量 i 时,会优先找到封闭的闭包环境中的 i 。
<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
  <li>6</li>
</ul>

<script>
  var nodes = document.getElementsByTagName('li');

  for (var i = 0, len = nodes.length; i <len; i++) {
    (function(i) {
      nodes[i].onclick = function() {
        console.log(i);
      }
    })(i);
  }
</script>
  • 封装变量

闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”

var func = function() {
  var a = 1;
  for (var i = 0, len = arguments.length; i < len; i++) {
    a = a * arguments[i];
  }

  return a;
};

console.log(func(1, 2, 3)) // 6
console.log(func(1, 2, 3)) // 6

明显上面的函数,如果相同参数时,每次都进行计算是一种浪费,我们可以加入缓存机制来提高这个函数的性能。

var func = (function() {
  var cache = {};
  var calculate = function() {
    var a = 1;

    for (var i = 0, len = arguments.length; i < len; i++) {
      a = a * arguments[i];
    }

    return a;
  };

  return function() {
    var args = Array.prototype.join.call(arguments, ',');

    if (args in cache) {
      return cache[args];
    }

    return cache[args] = calculate.apply(null, arguments);
  }
})();

console.log(func(1, 2, 3)); // 6
console.log(func(1, 2, 3)); // 6
console.log(func(1, 2, 3, 4)); // 24
  • 延续局部变量的寿命

img 对象经常用于进行数据上报,示例:

var report = function(src) {
  var img = new Image();

  img.src = src;
}

report('https://www.ifrontend.net/')

这个代码看着逻辑并没有问题,可是在一些低版本浏览器中会存在 bug ,在这些浏览器下使用 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;
  }
})();

report('https://www.ifrontend.net/')
  • 闭包和面向对象设计
对象以方法的形式包含了过程,而闭包则是在过程中以环境的形式包含了数据。通常用面向对象思想能实现的功能,用闭包也能实现,反之亦然。

闭包实现

var func = function() {
  var count = 0;

  return {
    call: function() {
      count++;
      console.log(count);
    }
  }
};

var f = func();

f.call(); // 1
f.call(); // 2
f.call(); // 3

面向对象

var func = function() {
  this.count = 0;
}

func.prototype.call = function() {
  this.count++;

  console.log(this.count);
}

var f = new func();

f.call(); // 1
f.call(); // 2
f.call(); // 3
  • 用闭包实现命令模式
<button id="open">点击我开始</button>
<button id="close">点击我关闭</button>
<script>
var func = {
  start: function() {
    console.log('start');
  },
  end: function() {
    console.log('end');
  }
}

var createCommand = function(operate) {
  var open = function() {
    return operate.start(); // 开始
  }

  var close = function() {
    return operate.end(); // 结束
  }

  return {
    open: open,
    close: close
  }
}

var setCommand = function(command) {
  document.getElementById('open').onclick = function() {
    command.open();
  }
  document.getElementById('close').onclick = function() {
    command.close();
  }
}

setCommand(createCommand(func))
</script>
  • 闭包与内存管理

闭包是一个非常强大的特性,但人们对其也有诸多误解。一种耸人听闻的说法是闭包会造成内存泄露,所以要尽量减少闭包的使用。

局部变量本来就应该在函数退出的时候被解除使用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量就能一直生存下去。从这个意义上看,闭包的确会使用一些数据无法被及时销毁。但是使用闭包的一部分原因是我们选择主动把一些变量封闭在闭包中,因为可能在以后还需要使用这些变量,把这些变量放在闭包中和放在全局作用域中,对内存方面的影响是一致的,这里并不能说成是内存泄露。如果在将来需要回收这些变量,我们可以手动把这些变量设置成 null 。

跟闭包和内存泄露有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包的作用域链中保存着一些 DOM 节点,这时候就有可能造成内存泄露。但这本身并非闭包问题,也并非 JavaScript 的问题。在 IE 浏览器中,由于 BOM 和 DOM 中的对象是使用 C++ 以 COM 对象的方式实现的,面 COM 对象的垃圾收集机制采用的是引用计数策略。在基于引用计数策略的垃圾回收机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收,但循环引用造成的内存泄露在本质上也不是闭包造成的。

同样,如果要解决循环引用带来的内存泄露问题,我们中需要把循环引用中的变量设为 null 即可。将变量设置为 null 意味着切断了变量与它此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。

二、高阶函数

高阶函数是指至少满足下列条件之一的函数

  • 函数可以作为参数被传递
  • 函数可以作为返回值输出
  • 回调函数
var getArticleInfo = function(id, callback) {
  $.ajax('https://www.ifrontend.net/getArticle?id=' + id, function(data) {
    if (typeof callback === 'function') {
      callback(data);
    }
  })
}

getArticleInfo(1, function(data) {
  console.log(data)
})
  • Array.prototype.sort
// 从小到大排序
var a1 = [1, 9, 6, 4].sort(function(a, b) {
  return a - b;
})
console.log(a1) // [1, 4, 6, 9]

// 从大到小排列
var a2 = [1, 9, 6, 4].sort(function(a, b) {
  return b - a;
})
console.log(a2) // [9, 6, 4, 1]
  • 判断数据的类型
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]';
}

getSingle 单例模式实现

  • 单例模式:
  1. 保证一个类仅有一个实例,并提供一个访问它的全局访问点
  2. 多次调用也仅设置一次
var getSingle = function(fn) {
  var ret;
  
  return function() {
    return ret || (ret = fn.apply(this, arguments));
  }
}

var getDiv = getSingle(function() {
  return document.createElement('div');
});
var s1 = getDiv();
var s2 = getDiv();
console.log(s1 === s2); // ture
  • 高阶函数实现AOP

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。这样可以很好的保证业务逻辑的纯净和高内附聚性,也可以很方便的复用日志统计等功能。

Function.prototype.before = function(beforeFn) {
  var self = this; // 保存原函数的引用

  return function() { // 返回包含了原函数和新函数的 “代理” 函数
    beforeFn.apply(this, arguments); // 执行新函数,修正 this
    
    return self.apply(this, arguments); // 执行原函数
  }
}

Function.prototype.after = function(afterFn) {
  var self = this; // 保存原函数的引用

  return function() { // 返回包含了原函数和新函数的 “代理” 函数
    var ret = self.apply(this, arguments); // 执行新函数,修正 this
    
    afterFn.apply(this, arguments)
    return ret
  }
}

var func = function() {
  console.log(2);
}

func = func.before(function() {
  console.log(1);
}).after(function() {
  console.log(3);
})

func(); // 1, 2, 3
  • currying 函数柯里化

currying 又称部分求值。一个 currying 的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来,待到函数真正需要求值的时候,之前传的所有参数都会被一次性用于求值。

var currying = function(fn) {
  var args = [];

  return function() {
    if (arguments.length === 0) {
      return fn.apply(this, args);
    } else {
      [].push.apply(args, arguments);

      return arguments.callee;
    }
  }
}

var finalScoring= (function() {
  var totalScore = 0;

  return function() {
    for (var i = 0, len = arguments.length; i < len; i++) {
      totalScore += arguments[i];
    }

    return totalScore;
  }
})();

var finalScoring = currying(finalScoring);

finalScoring(60);
finalScoring(70, 100, 120);
finalScoring(80);

console.log(finalScoring());
  • uncurrying

在 JavaScript 中,调用对象方法,其实不用去关心该对象原本是否被设计为拥有这个方法,这是动态类型语言的特点。

同理一个对象未必只能使用它自身的方法,那么有什么办法可以让对象去借用一个原本不属于它的方法呢?call 和 apply 都可以简单的实现。

var a1 = {
  name: 'JavaScript'
};
var a2 = {
  getName: function() {
    return this.name;
  }
};

console.log(a2.getName.call(a1)); // JavaScript

类数组对象去借用 Array.prototype 的方法,call 和 apply

(function() {
  Array.prototype.push.call(arguments, 4);
  console.log(arguments); // 1, 2, 3, 4
})(1, 2, 3);
Function.prototype.uncurrying = function() {
  var self = this;

  return function() {
    var obj = Array.prototype.shift.call(arguments);

    return self.apply(obj, arguments);
  }
}

var push = Array.prototype.push.uncurrying();

(function() {
  push(arguments, 4);

  console.log(arguments);
})(1, 2, 3)
  • 函数节流
  1. window.onresize 事件
  2. mousemove 事件
  3. input onchange事件

这些事件在实际业务中,事件的触发频率非常之高,如果跟 Dom 节点或运算量大的业务绑在一起时,该操作往往会非常消耗性能,浏览器极有可能就会因为吃不消页造成卡顿现象。

var throttle = function(fn, interval) {
  var self = fn, timer, firstTime = true;

  return function() {
    var args = arguments, me = this;

    // 如果是第一次调用,不需要延迟执行
    if (firstTime) {
      self.apply(me, args);

      return firstTime = false;
    }

    // 如果前一次延迟执行中
    if (timer) {
      return false;
    }

    timer = setTimeout(function() {
      clearTimeout(timer);
      timer = null;

      self.apply(me, args);
    }, interval || 500)
  }
}

window.onresize = throttle(function() {
  console.log('执行');
}, 500)
  • 分时函数

而有些业务是用户主动调用的,但因为一些客观原因,这些函数会严重地影响页面的性能,这样一种限制函数被频繁调用的解决方案就出来了。防抖。

比如需要频繁创建 dom 的需求:

var timeChunk = function(ary, fn, count, interval) {
  var obj, t;
  var len = ary.length;

  var start = function() {
    for (var i = 0; i < Math.min(count || 1, ary.length); i++) {
      var obj = ary.shift();
      
      fn(obj);
    }
  }

  return function() {
    t = setInterval(function() {
      if (ary.length === 0) {
        return clearInterval(t);
      }

      start();
    }, interval || 200)
  }
}

var ary = [];

for (var i = 1; i <= 100; i++) {
  ary.push(i);
}

var renderDomLi = timeChunk(ary, function(n) {
  var li = document.createElement('li');

  li.innerHTML = n;
  document.body.appendChild(li);
}, 10);

renderDomLi();
  • 惰性加载函数

由于浏览器之间的实现差异,一些嗅探工作总是不可避免的,比如浏览器事件绑定函数,可以把嗅探浏览器的操作提前到代码加载的时候,在代码加载的时候就立刻进行一次判断,以便让 addEvent 返回一个包裹了正确逻辑的函数,而不是每次创建事件去重复去 if 判断

var addEvent = (function() {
  if (window.addEventListener) {
    return function(elem, type, handler) {
      elem.addEventListener(type, handler, false);
    }
  }
  if (window.attachEvent) {
    return function(elem, type, handler) {
      elem.attachEvent('on' + type, handler);
    }
  }
})();

var div = document.getElementById('div1');

addEvent(div, 'click', function() {
  console.log(1);
});

addEvent(div, 'click', function() {
  console.log(2);
})var addEvent = (function() {
  if (window.addEventListener) {
    return function(elem, type, handler) {
      elem.addEventListener(type, handler, false);
    }
  }
  if (window.attachEvent) {
    return function(elem, type, handler) {
      elem.attachEvent('on' + type, handler);
    }
  }
})();

// var addEvent = function(elem, type, handler) {
//   if (window.addEventListener) {
//     addEvent = function(elem, type, handler) {
//       elem.addEventListener(type, handler, false);
//     }
//   } else if (window.attachEvent) {
//     addEvent = function(elem, type, handler) {
//       elem.attachEvent('on' + type, handler);
//     }
//   }

//   addEvent(elem, type, handler);
// }


var div = document.getElementById('div1');

addEvent(div, 'click', function() {
  console.log(1);
});

addEvent(div, 'click', function() {
  console.log(2);
})var addEvent = (function() {
  if (window.addEventListener) {
    return function(elem, type, handler) {
      elem.addEventListener(type, handler, false);
    }
  }
  if (window.attachEvent) {
    return function(elem, type, handler) {
      elem.attachEvent('on' + type, handler);
    }
  }
})();

var div = document.getElementById('div1');

addEvent(div, 'click', function() {
  console.log(1);
});

addEvent(div, 'click', function() {
  console.log(2);
})
前端开发那点事
微信公众号搜索“前端开发那点事”

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注