阅读 253

作用域、执行上下文、闭包

相关内容

  • 作用域、作用域链
  • 执行栈、执行上下文
  • 闭包

作用域和作用域链

规定变量和函数的可使用范围叫做作用域。

function fn1() {
  let a = 1;
}

function fn2() {
  let b = 2;
}
复制代码

声明两个函数,分别创建量两个私有的作用域(可以理解为两个封闭容器),fn2 是不能直接访问私有作用域 fn1 的变量 a 的。同样的,在 fn1 中不能访问到 fn2 中的 b 变量的。一个函数就是一个作用域。

每个函数都会有一个作用域,查找变量和函数时,由局部作用域到全局作用域一次查找,这些作用域形的集合就成为作用域链。

let a = 1
function fn() {
  function fn1() {
    function fn2() {
      let c = 3;
      console.log(a);
    }
    // 执行 fn2
    fn2();
  }
  // 执行 fn1
  fn1();
}
// 执行函数
fn();
复制代码

虽然上边看起来嵌套有点复杂,我们前边说过,一个函数就是一个私有作用域,根据定义,在 fn2 作用域中打印 a,首先在自己所在作用域搜索,如果没有就向上级作用域搜索,直到搜索到全局作用域,a = 1,找到了打印出值。整个搜索的过程,就是基于作用域链搜索的。

执行栈和执行上下文

执行上下文

执行上下文分为三个,分别是:全局执行上下文、函数执行上下文、Eval函数执行上下文

全局执行上下文

这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局 Window 对象(浏览器的情况下),并设置 this 的值等于这个全局对象。一个程序中只能有一个全局执行上下文。

函数执行上下文

每当一个函数被调用时,都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数执行上下文可以有任意多个。

Eval函数执行上下文

执行在 eval 函数内部的代码也会有它属于自己的执行上下文。

执行上下文的特点

  • 单线程,在主线程上运行
  • 同步执行,从上往下按顺序执行
  • 全局上下文只有一个,在关闭浏览器时会被弹出栈
  • 函数执行上下文没有数目限制
  • 函数没被调用一次,都会产生一个新的执行上下文环境

执行栈

执行栈,也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。

当 JavaScript 引擎第一次遇到脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数被调用,它会该函数创建一个新的执行上下文并压入栈的顶部。

引擎会执行那些执行上下文位于栈顶的函数,当函数执行结束时,执行上下文会栈中弹出,控制流程到达当前栈中的下一个上下文。

闭包

什么是闭包

函数执行,形成一个私有作用域,保护里面的私有变量不受外界的干扰,除了保护私有变量外,还可以保存一些内容,这样的模式叫做闭包。

function fn() {
  let num = 1;
  let name = "jack";
  return function () {
    console.log(num++);
    return name;
  };
}

let f1 = fn();
f1(); // 1
f1(); // 2
f1(); // 3
f1(); // 4
console.log(f1()); // 5 jack
复制代码

闭包的作用

闭包的作用有两个:保存和保护

保护

  • 团队开发时,每个开发者把自己的代码放在一个私有的作用域中,防止相互之间的变量命名冲突;把需要提供给别人的方法,通过 returnwindow.xxx 的方式暴露在全局下;
  • jQuery 的源码中也是利用了这种保护机制;
  • 封装私有变量;

保存

  • 选项卡闭包的解决方案。我们经常在网页中使用选项卡,但是它存在一个问题,那就是索引引发的问题,其实和下边的经典面试题问题相同。

循环绑定事件引发的索引什么问题?怎么解决这种问题?

var btnBox = document.getElementById("btnBox"),
  inputs = btnBox.getElementsByTagName("input");
var len = inputs.length;
for (var i = 0; i < len; i++) {
  inputs[i].onclick = function () {
    alert(i);
  };
}
复制代码

此时运行程序,你会得出的结果都是 len 的数值。

为什么会出现这种问题,我们如何解决呢?

原因很简单,所有的事件绑定都是异步的,当触发点击事件,执行方法的时候,循环早就结束了。

  • 同步:JS 中当前这个任务没有完成,下面的任务都不会执行,只有等当前彻底执行完成,才会执行下面的任务;
  • 异步:JS 中当前任务没有完成,需要等一会在完成,此时我们可以执行下面的任务;

解决方案

当点击事件执行的时候,就会在私有作用域查找 i 的值,此时私有作用域没有 i ,就回去全局作用域查找,此时全局作用域的 i 已经被改变。所以说,要创建一个私有作用域的 i

  • 方式一:闭包的方式,用来保存私有的变量,但是闭包解决有又优点,也有缺点
    • 优点:通过创建私有作用域(闭包)方式解决,循环几次,就会创建几个私有作用域,然后每个私有作用域都会有一个私有变量 i,存的值分别是循环的值。
    • 缺点:生成多个不销毁的私有作用域(堆内存),对性能有一定的影响。
var btnBox = document.getElementById("btnBox"),
  inputs = btnBox.getElementsByTagName("input");
var len = inputs.length;
for (var i = 0; i < len; i++) {
  (function(i) {
    inputs[i].onclick = function () {
      alert(i);
    };
  })(i)
}
复制代码
  • 方式二:使用自定义属性。我们给每个对象添加一个索引属性就可以了
var btnBox = document.getElementById("btnBox"),
  inputs = btnBox.getElementsByTagName("input");
var len = inputs.length;
for (var i = 0; i < len; i++) {
  inputs[i].myIndex = i;
  inputs[i].onclick = function () {
    alert(this.myIndex);
  };
}
复制代码
  • 方式三:使用ES6
    • 终极解决方案,这是 ES6 中的知识,因为之前在 JS 中是没有块级作用域的概念的,到了 ES6 中就有了,let 声明的变量就可以更好的解决上述问题;
var btnBox = document.getElementById("btnBox"),
  inputs = btnBox.getElementsByTagName("input");
var len = inputs.length;
for (let i = 0; i < len; i++) {
  inputs[i].onclick = function () {
    alert(i);
  };
}
复制代码

闭包的内存回收机制

内存回收机制就是不在用到的内存,我们系统就自动进行回收从而清理出空间供其他程序使用。那回收的规则是什么?

内部函数引用着外部的函数的变量,外部的函数尽管执行完毕,作用域也不会销毁。从而形成了一种不销毁的私有作用域。

某一变量或者对象被引用着,因此在回收的时候不会释放它,因为被引用代表着被使用,回收器不会对正在被引用的变量或者对象回收的。

大白话说什么是闭包,那就是在一个函数里边再定义一个函数。这个内部函数一直保持有对外部函数中作用域的访问权限。

函数执行,形成一个私有的作用域,保护里面的私有变量不受外界的干扰 ,除了保护私有变量外,还可以存储一些内容,这样的模式叫做闭包。

经典面试题

var num = 10;
var obj = {
  num: 20
};

obj.fn = (function (num) {
  this.num = num * 3;
  num++;
  return function (n) {
    this.num += n;
    num++; 
    console.log(num);
  };
})(obj.num);

var fn = obj.fn;
fn(5);
obj.fn(10);

console.log(num, obj.num);
复制代码

输出结果为:

22
23
65 30
复制代码
文章分类
前端
文章标签