closure

看是否对自由变量进行了捕获

变量作用域

  • 函数作用域

  • 全局作用域

作用域链:变量的搜索从内而外。函数像一层半透明的玻璃,在函数里面可以看到函数外面的变量,而函数外面则无法看到函数里面的变量

变量的生命周期

  • 全局变量:永久,除非主动销毁

  • 局部变量:退出函数时就没有了价值,即随着函数调用的结束而被销毁

闭包的作用

  • 封装变量

  • 延续局部变量的寿命

  1. // 然而闭包可以延续局部变量的生命周期
  2. var func = function() {
  3. var a = 1;
  4. return function() {
  5. a++;
  6. alert(a)
  7. }
  8. }
  9. var f = func()
  10. f() // 2
  11. f() // 3
  12. f() // 4
  13. f() // 5

当退出函数时,局部变量a并没有消失,而是似乎一直在某个地方存活着。这是因为当执行var f = func()时,f返回了一个匿名函数的引用,它可以访问到func()被调用时产生的环境,而局部变量a一直处在这个环境里。既然局部变量所在的环境还能被外界访问,这个局部变量就有了不被销毁的理由。

  1. var Type = {}
  2. for (var i = 0; type = ['String', 'Array', 'Number']; i++) {
  3. (function(type){
  4. Type[`is${type}`] = function(obj) {
  5. return Object.prototype.toString.call(obj) === '[object ${type}]'
  6. }
  7. })(type)
  8. }
  9. Type.isArray([])
  10. Type.isString('str')
  1. // 对于相同的参数来说,每次都进行计算时一种浪费,引入缓存机制提高函数性能
  2. var cache = {};
  3. function mult() {
  4. var args = Array.prototype.join.call(arguments, ',')
  5. if (cache[args]) return cache[args]
  6. var result = 1
  7. for (var i = 0; i < arguments.length; i++) {
  8. result = result * arguments[i]
  9. }
  10. return cache[args] = result
  11. }
  12. console.log(mult(1, 2, 3)) // 6
  13. console.log(mult(1, 2, 3)) // 6
  14. console.log(cache)

代码重构:1. 去掉全局变量 2. 独立可复用的代码

  1. var mult = (
  2. function(){
  3. var cache = {};
  4. return function () {
  5. var args = Array.prototype.join.call(arguments, ',')
  6. if (cache[args]) return cache[args]
  7. // return cache[args] = calculate.apply(null, arguments) // null 即 windows 对象
  8. return cache[args] = calculate(...arguments)
  9. }
  10. }
  11. )()
  12. var calculate = function () {
  13. var result = 1
  14. for (var i = 0; i < arguments.length; i++) {
  15. result = result * arguments[i]
  16. }
  17. return result
  18. }
  19. console.log(mult(1, 2, 3)) // 6
  20. console.log(mult(1, 2, 3)) // 6
  1. var report = function (src) {
  2. var imgs = []
  3. return function(src){
  4. var img = new Image()
  5. imgs.push(img)
  6. img.src = src
  7. }
  8. }

面向对象设计

过程与数据的结合是形容面向对象中的“对象”时经常使用的表达
对象以方法的形式包含了过程
而闭包则是在过程中以环境的形式包含了数据

  1. // 闭包
  2. var extent = function() {
  3. var value = 0
  4. return {
  5. call: function() {
  6. value++
  7. console.log(value)
  8. }
  9. }
  10. }
  11. var extent = extent()
  12. extent.call()
  13. extent.call()
  1. // 面向对象
  2. var extent = {
  3. value: 0,
  4. call: function() {
  5. this.value++
  6. console.log(this.value)
  7. }
  8. }
  9. extent.call() // 1
  10. extent.call() // 2
  11. extent.call() // 3
  1. // 构造函数
  2. var Extent = function() {
  3. this.value = 0
  4. }
  5. Extent.prototype.call = function() {
  6. this.value++
  7. console.log(this.value)
  8. }
  9. var extent = new Extent()
  10. extent.call() // 1
  11. extent.call() // 2
  12. extent.call() // 3

用闭包实现命令模式

命令模式的意图是把请求封装为对象,从而分离请求的发起者和请求的接收者(执行者)之间的耦合关系。在命令被执行之前,可以预先往命令对象中植入命令的接收者。

  1. <body>
  2. <div>
  3. <button id="execute">点我执行命令</button>
  4. <button id="undo">点我执行命令</button>
  5. </div>
  6. <script type="text/javascript">
  7. var Tv = {
  8. open: function() {
  9. console.log('打开电视机')
  10. }
  11. close: function() {
  12. console.log('关上电视机')
  13. }
  14. }
  15. var OpenTvCommand = function(receiver) {
  16. this.receiver = receiver
  17. }
  18. OpenTvCommand.prototype.execute = function() {
  19. this.receiver.open() // 执行命令,打开电视机
  20. }
  21. OpenTvCommand.prototype.undo = function() {
  22. this.receiver.close() // 执行命令,关闭电视机
  23. }
  24. var setCommand = function(command) {
  25. document.getElementById('execute').onclick = function() {
  26. command.execute()
  27. }
  28. document.getElementById('undo').onclick = function() {
  29. command.undo()
  30. }
  31. }
  32. setCommand(new OpenTvCommand(Tv))
  33. </script>
  34. </body>
  1. <script type="text/javascript">
  2. var Tv = {
  3. open: function() {
  4. console.log('打开电视机')
  5. }
  6. close: function() {
  7. console.log('关上电视机')
  8. }
  9. }
  10. var createCommand = function(receiver) {
  11. var execute = function() {
  12. return receiver.open() // 执行命令,打开电视机
  13. }
  14. var undo = function() {
  15. return receiver.close() // 执行命令,关闭电视机
  16. }
  17. return {
  18. execute: execute,
  19. undo: undo
  20. }
  21. }
  22. var setCommand = function(command) {
  23. document.getElementById('execute').onclick = function() {
  24. command.execute()
  25. }
  26. document.getElementById('undo').onclick = function() {
  27. command.undo()
  28. }
  29. }
  30. setCommand(createCommand(Tv))
  31. </script>

闭包与内存管理

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

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

高阶函数

  • 函数可以作为参数被传递

  • 函数可以作为返回值输出

作为参数被传递

分离业务代码中变化和不变的部分

  1. 回调函数
  • 异步请求

  • 当一个函数不适合执行一些请求时,可以把这些请求封装成一个函数,“委托”给另外一个函数来执行

作为返回值输出

  1. var getSingle = function(fn) {
  2. var ret
  3. return function() {
  4. return ret || (ret = fn.apply(this, arguments))
  5. }
  6. }
  7. var getScript = getSingle(function() {
  8. return document.createElement('script')
  9. })
  10. var script1 = getScript()
  11. var script2 = getScript()
  12. alert(script1 === script2) // true

高阶函数实现AOP

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。

  1. Function.prototype.before = function(beforefn) {
  2. var _self = this // 保存原函数的引用,this 值是func -> {console.log(2)}
  3. return function() { // 返回包含了原函数和新函数的“代理”函数
  4. beforefn.apply(this, arguments) // 执行新函数,修正this,this是window
  5. return _self.apply(this, arguments) // 执行原函数
  6. }
  7. }
  8. Function.prototype.after = function(afterfn) {
  9. var _self = this // this 值是“代理”函数
  10. return function() {
  11. var ret = _self.apply(this, arguments)
  12. afterfn.apply(this, arguments)
  13. return ret
  14. }
  15. }
  16. var func = function() {
  17. console.log(2)
  18. return 2
  19. }
  20. func = func.before(function() {
  21. console.log(1)
  22. return 1
  23. }).after(function() {
  24. console.log(3)
  25. return 3
  26. })
  27. func()

currying

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

  1. var cost = (
  2. var args = []
  3. return function(){
  4. if (arguments.length === 0){
  5. var money = 0
  6. for(var i = 0; i < args.length; i++) {
  7. money += args[i]
  8. }
  9. return money
  10. } else {
  11. [].push.apply(args, arguments)
  12. }
  13. }
  14. )()
  15. cost(100) // 未真正求值
  16. cost(200) // 未真正求值
  17. cost(300) // 未真正求值
  18. console.log(cost())
  1. var currying = function(fn) {
  2. var args = []
  3. return function() {
  4. if(arguments.length === 0){
  5. return fn.apply(this, args)
  6. } else {
  7. [].push.apply(args, arguments)
  8. console.log('arguments', arguments, arguments.callee )
  9. return arguments.callee
  10. }
  11. }
  12. }
  13. var cost = (function(){
  14. var money = 0
  15. return function(){
  16. for (var i = 0; i < arguments.length; i++) {
  17. money += arguments[i]
  18. }
  19. return money
  20. }
  21. })()
  22. var cost = currying(cost)
  23. cost(100) // 未真正求值
  24. cost(200) // 未真正求值
  25. cost(300) // 未真正求值
  26. alert(cost()) // 求值并输出: 600
  1. Function.prototype.uncurrying = function() {
  2. var self = this
  3. console.log(this)
  4. return function() {
  5. var obj = Array.prototype.shift.call(arguments)
  6. console.log(arguments)
  7. return self.apply(obj, arguments)
  8. }
  9. }
  10. var push = Array.prototype.push.uncurrying()
  11. (function(){
  12. push(arguments, 4)
  13. console.log(arguments)
  14. })(1,2,3)
  1. Function.prototype.uncurrying = function() {
  2. var self = this
  3. return function() {
  4. return Function.prototype.call.apply(self, arguments)
  5. }
  6. }
  1. Function.prototype.uncurrying = function() {
  2. var self = this
  3. return function() {
  4. var obj = Array.prototype.shift.call(arguments)
  5. return self.apply(obj, arguments)
  6. }
  7. }
  8. var Arg = {}
  9. for (var i = 0, fn, arr = ['push', 'shift', 'forEach']; fn = arr[i++];) {
  10. console.log(fn)
  11. Arg[fn] = Array.prototype[fn].uncurrying()
  12. }
  13. var obj = {
  14. 'length': 3,
  15. '0': 1,
  16. '1': 2,
  17. '2': 3
  18. }
  19. Arg.push(obj, 4) // 向对象中添加一个元素
  20. console.log(obj.length) // 4

函数节流

有些情况下,函数的触发不是由用户直接控制的,这样当函数被非常频繁的调用时,会造成大的性能问题。

场景

  • window.onresize():给window对象绑定了resize事件,当浏览器窗口大小被拖动而改变的时候,这个事件触发的频率非常之高。如果我们在window.onresize事件函数里有一些跟DOM节点相关的操作,而跟DOM节点相关的操作往往是非常消耗性能的,这时候浏览器可能就会吃不消而造成卡顿现象。

  • mousemove():同样,给一个div节点绑定了拖拽事件(主要是mousemove),div节点被拖动的时候,也会频繁地触发该拖拽事件函数

  • 上传进度:微云的上传功能使用了一个浏览器插件。该插件在真正开始上传文件之前,会对文件进行扫描并随时通知JS函数,以便在页面中显示当前的扫描进度。但该插件通知的频率非常之高,约10s一次

原理

当函数被触发的频率太高时,需要按时间段来忽略掉一些事件请求,借助setTimeout来完成

将即将被执行的函数用setTimeout延迟一段时间执行。如果该次延迟执行还没有完成,则忽略接下来调用该函数的请求

一定要等当前执行的事件执行完了,才会让新的事件进来,否则全部作废

  1. var throttle = function(fn, interval) {
  2. var _self = fn, // 保存需要被延迟执行的函数引用
  3. timer, // 定时器
  4. firstTime = true; // 是否是第一次调用
  5. return function() {
  6. var args = arguments,
  7. _me = this;
  8. if (firstTime) { // 如果是第一次调用,不需要延迟执行
  9. _self.apply(_me, args)
  10. return firstTime = false
  11. }
  12. if (timer) { // 如果定时器还在,说明前一次延迟执行还没有完成
  13. return false
  14. }
  15. timer = setTimeout(function() { // 延迟一段时间执行
  16. clearTimeout(timer)
  17. timer = null
  18. _self.apply(_me, args)
  19. }, interval || 500)
  20. }
  21. }
  22. window.onresize = throttle(function() {
  23. console.log(1)
  24. }, 500)

函数防抖

场景

  • 搜索引擎搜索:希望用户在输入完最后一个字才调用查询接口,适用延迟执行的防抖函数,

分时函数

在短时间内大量触发请求会严重影响页面性能,解决方案之一是让这些请求分批进行,比如把1秒钟创建1000个节点,改为每隔200毫秒创建8个节点

  1. // 创建节点时需要用到的数据
  2. // 封装了创建节点逻辑的函数
  3. // 每一批创建的节点数量
  4. var timeChunk = function(ary, fn, count) {
  5. var obj, t
  6. var len = ary.length
  7. var start = function () {
  8. for (var i = 0; i < Math.min(count || 1, ary.length); i++) {
  9. var obj = ary.shift()
  10. fn(obj)
  11. }
  12. }
  13. return function() {
  14. t = setInterval(function() {
  15. if (ary.length === 0) { // 如果全部节点都已经被创建好
  16. return clearInterval(t)
  17. }
  18. start()
  19. }, 200) // 分批执行的时间间隔,也可以用参数的形式传入
  20. }
  21. }

资源

7分钟理解JS的节流、防抖及使用场景