黑马程序员技术交流社区

标题: 【上海校区】从作用域开始理解闭包 [打印本页]

作者: 没名字i    时间: 2020-2-20 19:34
标题: 【上海校区】从作用域开始理解闭包
执行上下文环境&上下文栈执行上下文什么是执行上下文环境?我们先来看一段代码:
[AppleScript] 纯文本查看 复制代码
console.log(a);  // undefined
console.log(this);//window
b(); // b() is not a function
c(); // "c"
var a = "a";
var b = function () {
    var b = "b"
    console.log(b);
}

function c () {
    var c = "c"
    console.log(c);
}

使用var声明变量存在变量提升;函数表达式和函数声明定义的函数也存在变量提升,但是前者不会把整个函数体一起提升,后者会将函数体一起提升,这就是为什么执行b()时会报错而c()不会报错的原因;“提升”发生在一段代码的执行之前,是一段代码执行之前的准备工作,一段代码执行需要准备如下工作:变量、函数表达式声明并赋值,默认值为undefined给this赋值函数声明的函数赋值我们把执行一段代码之前的准备情况称之为执行上下文或者执行上下文环境。上下文栈上下文栈就是一个存储执行上下文的栈,栈中只有一个上下文是处于活动状态的,执行全局代码时就会有一个全局上下文被压入栈,调用一个函数时就会产生一个函数的上下文环境,然后将其压入栈,目前它处于活动状态,当函数执行完后其执行上下文环境出栈,它的上下文环境和里面的数据被销毁,内存被回收,如下图是上例中代码执行时上下文栈的变化:一开始执行栈只有全局上下文,这里会进行一系列的变量提升,此时a、b为undefined,c为一个函数,this指向window,抛开执行b()报错,正常会执行c(),执行c就会生成c的执行上下文,并压入执行栈,此时c的执行上下文是激活态,在c的执行上下文中会提成一个局部变量c并赋值,当c执行完后c的执行上下文出栈,等待被北村回收,然后就接着执行全局的上下文;作用域&作用域链词法作用域常见的作用域有词法(静态)作用域和动态作用域,js是词法作用域,实际上大部分语言都是词法作用域,我们先看下面这个例子:
[AppleScript] 纯文本查看 复制代码
var a = 10;
function logA () {
  console.log(a);
}

function b () {
  var a = 20;
  logA();
}

b(); // 10

执行b()最后输出的是10,因为js是词法作用域,词法作用域关注的是函数在哪里声明,而动态作用域关注的是函数在哪里调用,上面logA是在全局作用域中声明的,它内部要访问的变量a,先在自己的作用域中找,找不到再去声明它的作用域中找;寻找变量a就是按照作用域链来找的。作用域链上面讲的寻找变量a的这个“跨”作用域的路线就是我们说的作用域链,在一个函数被创建时它的一个内部属性[[scope]]会保存一个预先包含全局作用域中变量对象的作用域链,当调用该函数时会创建一个该函数的执行上下文,然后赋值函数内[[scope]]对象构建函数执行上下文环境的作用域链,再把执行上下文中的变量对象放到作用域链前端;作用域链本质上是一个指向变量对象的指针。闭包定义什么是闭包?《JavaScript高级程序设计》是这样定义的:闭包是指有权访问另一个函数作用域中变量的函数。我们来看下下面这段代码:
[AppleScript] 纯文本查看 复制代码
function a () {
  var x = 12;
  return function () {
    var y = 10;
    return x + y;
  }
}

var b = a();
b(); // 22
上面代码中,在全局执行上下文中定义了一个函
数a和变量b,函数a内部返回一个匿名函数,所以此刻匿名函数的作用域链初始化为包含了全局变量对象和a中的变量对象,当执行var b = a()时,把函数a的执行上下文压栈,当a执行完后按理a的执行上下文应该出栈,但是因为a内部的匿名函数作用域链还引用这a的变量x,所以a的执行上下文得不到释放,这样就形成了闭包,当执行到b()时把a中的匿名函数的执行上下文入栈,此时栈中有三个执行上下文。作用域链指向的变量对象上面讲过作用域链就是指向变量对象的指针,所以闭包只能取得包含函数中任何变量的最后一个值,如下代码:
[AppleScript] 纯文本查看 复制代码
function a () {
  var arr = [];
  
  for (var i = 0; i <= 10; i++) {
    arr = function () {
      return i;
    }
  }
  
  return arr;  // [10,10,...,10]
}

上面代码返回10个10,因为里面的每个匿名函数的作用域链上的变量对象i是指向同一个变量i,所以它们的值都相等,我们可以修改代码如下:
[AppleScript] 纯文本查看 复制代码
function a () {
  var arr = [];
  
  for (var i = 0; i < 12; i++) {
    arr = function (num) {
      return num;
    }(i);
  }
  
  return arr;
}

这种方法把当前i的值传递给匿名函数的参数num,然后立即执行(IIFE)返回给数组的相应项;模块模式
[AppleScript] 纯文本查看 复制代码
function hello () {
  var textE = 'hello!';
  var textC = '你好!';

  helloInChinese () {
    console.log(textC);
  };

  helloInEnglish () {
    console.log(textE);
  };

  return {
    helloInChinese: helloInChinese,
    helloInEnglish: helloInEnglish
  }
}

现在这个函数执行后将返回一个对象,对象包含的是hello内部的两个私有函数,这是我们调用hello后就会形成闭包,如下:
[AppleScript] 纯文本查看 复制代码
var sayHi = hello();
sayHi.helloInChinese(); // "你好!"
sayHi.helloInEnglish(); // "hello!"

因为这时hello函数返回了一个对象,而sayHi引用着这个对象,而对象里面是两个方法,这两个方法又引用着hello里的两个变量,所以hello的执行上下文不会出栈,若要释放,就必须手动接触sayHi对这个对象的引用,即sayHi = null;这种做法就称之为“模块”,即把内部的方法向外部暴露出来,让外部作用域能访问内部的变量;这样做的好处是内部使用的变量通过闭包不会污染全局。总结执行上下文是执行一段代码之前的准备工作,准备工作包括以下三点:变量、函数表达式声明并赋值,默认值为undefined给this赋值函数声明的函数赋值上下文栈即用来保存执行上下文,当某一段代码执行时就把它的执行上下文入栈,执行完后就出栈并销毁,释放内存,一个上下文栈只有一个执行上下文处于活动状态;什么是 作用域 和 作用域链 ?作用域:简单说就是作用的范围,初始化时就存在全局作用域,当声明函数就会创建一个函数的作用域,当要访问某个变量时函数现在自己的作用域中找,找不到就向声明该函数的作用域中找,一直到全局作用域;作用域链:作用域链也是在函数声明时创建的,一开始包含声明它的作用域和该作用域的变量对象,保存在函数的[[scope]]属性中,当调用该函数时就赋值[[scope]]中的作用域链给它的执行上下文并往作用域链前端加上其执行上下文中声明的变量对象;闭包的形成及优缺点:缺点:形成闭包即要把一个函数当成值传递,而且该函数还引用这另一个函数的作用域链使得被引用的函数不能被回收,容易造成内存泄漏;优点:闭包里的变量不会污染全局,因为变量被封在闭包里;所有变量都在闭包里保证了隐私性和私有性;
链接:https://juejin.im/post/5e4e3aeb6fb9a07ce01a23e7





欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2