Javascript 闭包

1 闭包定义

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

// 示例1
function makeFunc() {
    // name是个局部变量,在makeFunc的函数作用域中
    var name = "Mozilla";
    function displayName() {
        // displayName函数使用了自己函数作用域以外的变量
        alert(name);
    }
    return displayName;
}

var myFunc = makeFunc();
myFunc();

// 示例2
function makeAdder(x) {
  // x是makeAdder函数的入参,在makeAdder的函数作用域中,被一个匿名函数使用了
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。

2 使用场景

2.1 回调函数

// 示例1
// 在页面上添加一些可以调整字号的按钮。
function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;

2.2 封装私有变量、私有函数

var makeCounter = function() {
  // 私有变量
  var privateCounter = 0;
  // 私有函数
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }
};
/* makeCounter执行一次会生成新的局部变量privateCounter,由于有闭包,makeCounter执行完后         
 * privateCounter不会释放,每个闭包都是引用自己词法作用域内的变量privateCounter
 */
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */

2.3 防抖与节流(闭包的典型应用)

// 防抖
function debounce (fn, delay=500) {
    let timer = null;    
    return function() {
        if(timer) {
            clearTimeout(timer);
        }
        timer = setTimerout(() => {
                fn.apply(this, arguments);
                timer = null;
            }, delay);
        
    }    
}

// 节流
function throttle (fn, delay=500) {
    let timer = null;    
    return function() {
        if(timer) {
            return;
        }
        timer = setTimerout(() => {
                fn.apply(this, arguments);
                timer = null;
            }, delay);

    }    
}

3 与闭包经常一起使用的语法 IIFE(立即调用函数表达式)

3.1 IIFE定义

IIFE( 立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。

写法: (函数声明)(函数参数)

3.2 IIFE特性

当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问。

(function () {
    var name = "Barry";
})();
// 无法从外部访问变量 name
name // 抛出错误:"Uncaught ReferenceError: name is not defined"

将 IIFE 分配给一个变量,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果

var result = (function () {
    var name = "Barry";
    return name;
})();
// IIFE 执行后返回的结果:
result; // "Barry"

4 使用闭包时的常见错误

4.1循环中创建闭包

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    // var变量只有全局作用域及函数作用域,这里每次循环使用的是同一个item 
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

赋值给 onfocus 的是闭包。这些闭包是由他们的函数定义和在 setupHelp 作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item。三次循环后,变量对象item(被三个闭包所共享)已经指向了helpText的最后一项。

解决办法:

1.使用更多闭包

// 其余地方不变,这里只修改了循环部分
for (var i = 0; i < helpText.length; i++) {
    // 三次循环生成了三个匿名函数,每个onfocus的回调绑定了一个各自的词法作用域
    (function() {
       var item = helpText[i];
       document.getElementById(item.id).onfocus = function() {
         showHelp(item.help);
       }
    })(); // 马上把当前循环项的item与事件回调相关联起来

2.使用es5中的let

// 其余地方不变,这里只修改了循环部分
for (var i = 0; i < helpText.length; i++) {
    // let具有块级作用域,循环三次生成了三个
    let item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }

5 性能考量

如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。应该尽量少使用闭包。

// 这个构造函数每次调用MyObject都会生成新的getName、getMessage方法
function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

// 推荐改为
function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

参考

[MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures)

[MDN](https://developer.mozilla.org/zh-CN/docs/Glossary/IIFE)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值