【面试直接答系列】JS基础之作用域及闭包

951 阅读4分钟

闭包

思考几个问题

  1. JavaScript 中的作用域是什么意思?
  1. 闭包会在哪些场景中使用?
  1. 通过定时器循环输出自增的数字通过 JS 的代码如何实现?

作用域基本介绍

本质上指的是变量可以访问的范围;ES5之前作用域分为全局作用域和函数作用域两种,ES6之后又出现了块级作用域

全局作用域

在JS中,全局的变量是指挂载到window对象上的变量,所有任何情况都可以访问到

var globalName = 'global';
function getName() { 
  console.log(globalName) // global
  var name = 'inner'
  console.log(name) // inner
} 
getName();
console.log(name); // 
console.log(globalName); //global
function setName(){ 
  vName = 'setName';
}
setName();
console.log(vName); // setName
console.log(window.vName) // setName// globalName 这个变量无论在什么地方都是可以被访问到的
// getName 函数中作为局部变量的 name 变量是不具备这种能力的

优点:任何地方都能访问到

缺点:命名冲突

函数作用域

函数中定义的变量叫作函数变量,这个时候只能在函数内部才能访问到它,所以它的作用域也就是函数的内部,称为函数作用域

function getName () {
  var name = 'inner';
  console.log(name); //inner
}
getName();
console.log(name);
​
// name 这个变量是在 getName 函数中进行定义的,所以 name 是一个局部的变量,它的作用域就是在 getName 这个函数里边,也称作函数作用域

除了这个函数内部,其他地方都是不能访问到它的。同时,当这个函数被执行完之后,这个局部变量也相应会被销毁。所以你会看到在 getName 函数外面的 name 是访问不到的

块级作用域

ES6 中新增了块级作用域,最直接的表现就是新增的 let 关键词,使用 let 关键词定义的变量只能在块级作用域中被访问,有“暂时性死区”的特点,也就是说这个变量在定义之前是不能被使用的

就是在 JS 编码过程中 if 语句及 for 语句后面 {...} 这里面所包括的,就是块级作用域

console.log(a) //a is not defined
if(true){
  let a = '123';
  console.log(a); // 123
}
console.log(a) //a is not defined

什么是闭包?

闭包其实就是一个可以访问其他函数内部变量的函数

function fun1() {
  var a = 1;
  return function(){
    console.log(a);
  };
}
fun1();
var result = fun1();
result();  // 1

闭包产生的原因

其实很简单,当访问一个变量时,代码解释器会首先在当前的作用域查找,如果没找到,就去父级作用域去查找,直到找到该变量或者不存在父级作用域中,这样的链路就是作用域链。

var a = 1;
function fun1() {
  var a = 2
  function fun2() {
    var a = 3;
    console.log(a);//3
  }
}

fun1 函数的作用域指向全局作用域(window)和它自己本身;fun2 函数的作用域指向全局作用域 (window)、fun1 和它本身;而作用域是从最底层向上找,直到找到全局作用域 window 为止,如果全局还没有的话就会报错。

本质: 当前环境中存在指向父级作用域的引用。

闭包的表现形式

返回一个函数

function fun1() {
  var a = 1;
  return function(){
    console.log(a);
  };
}

在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包

// 定时器
setTimeout(function handler(){
  console.log('1');
},1000);
// 事件监听
$('#app').click(function(){
  console.log('Event Listener');
});

作为函数参数传递的形式

var a = 1;
function foo(){
  var a = 2;
  function baz(){
    console.log(a);
  }
  bar(baz);
}
function bar(fn){
  // 这就是闭包
  fn();
}
foo();  // 输出2,而不是1

IIFE(立即执行函数),创建了闭包,保存了全局作用域(window)和当前函数的作用域,因此可以输出全局的变量

var a = 2;
(function IIFE(){
  console.log(a);  // 输出2
})();

如何解决循环输出问题?

for(var i = 1; i <= 5; i ++){
  setTimeout(function() {
    console.log(i)
  }, 0)
}
// 5,5,5,5,5

原因:

  1. setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行。
  2. 因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6

那么我们再来看看如何按顺序依次输出 1、2、3、4、5 呢?

利用 IIFE

for(var i = 1;i <= 5;i++){
  (function(j){
    setTimeout(function timer(){
      console.log(j)
    }, 0)
  })(i)
}
//  1、2、3、4、5 

使用 ES6 中的 let

ES6 中新增的 let 定义变量的方式,使得 ES6 之后 JS 发生革命性的变化,让 JS 有了块级作用域,代码的作用域以块级为单位进行执行。通过改造后的代码,可以实现上面想要的结果。

for(let i = 1; i <= 5; i++){
  setTimeout(function() {
    console.log(i);
  },0)
}

定时器传入第三个参数

for(var i=1;i<=5;i++){
  setTimeout(function(j) {
    console.log(j)
  }, 0, i)
}

\