Skip to content

【JS】作用域链 & 闭包 #46

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
zh-rocco opened this issue Oct 31, 2018 · 0 comments
Open

【JS】作用域链 & 闭包 #46

zh-rocco opened this issue Oct 31, 2018 · 0 comments

Comments

@zh-rocco
Copy link
Owner

zh-rocco commented Oct 31, 2018

相关文章

作用域链

作用域链是一个 对象列表(list of objects),用以检索上下文代码中出现的 标识符(identifiers)

标示符[Identifiers] 可以理解为变量名称、函数声明和普通参数。

var x = 10;

function foo() {
  var y = 20;
  console.log(x + y);
}

foo(); // 30

函数 foo 如何访问到变量 x (函数能访问一个更高一层上下文的变量对象)?

这种机制是通过函数内部的 [[scope]] 属性来实现的。[[scope]] 是所有父变量对象的层级链,处于当前函数上下文之上,在函数创建时存于其中。

  • [[scope]] 是函数的一个属性,而不是一个上下文环境。
  • [[scope]] 在函数创建时被存储(静态作用域),直至函数销毁。即:函数可以永不调用,但 [[scope]] 属性已经写入,并存储在函数对象中。

通过构造函数创建的函数的 [[scope]]

通过函构造函数创建的函数的 [[scope]] 属性总是唯一的全局对象。考虑到这一点,如通过这种函数创建除全局之外的最上层的上下文闭包是不可能的。

var x = 10;

function foo() {
  var y = 20;

  function barFD() {
    // 函数声明
    console.log(x);
    console.log(y);
  }

  var barFE = function() {
    // 函数表达式
    console.log(x);
    console.log(y);
  };

  var barFn = Function('console.log(x); console.log(y);');

  barFD(); // 10, 20
  barFE(); // 10, 20
  barFn(); // 10, "y" is not defined
}

foo();

代码执行时对作用域链的影响

在 ECMAScript 中,在代码执行阶段有两个声明能修改作用域链。这就是 with 声明和 catch 语句。它们添加到作用域链的 最前端,对象须在这些声明中出现的标识符中查找。

with

var foo = { x: 10, y: 20 };

with (foo) {
  console.log(x); // 10
  console.log(y); // 20
}
var x = 10;
var y = 10;

with ({ x: 20 }) {
  var x = 30;
  var y = 30;

  console.log(x); // 30
  console.log(y); // 30
}

console.log(x); // 10
console.log(y); // 30
  1. 变量声明赋值:x = 10, y = 10;
  2. 对象 { x: 20 } 添加到作用域的最前端;
  3. with 内部,遇到了 var 声明,当然什么也没创建,因为在进入上下文时,所有变量已被解析添加;
  4. 在第二步中,仅修改变量 x,实际上对象中的 x 现在被解析,并添加到作用域链的最前端,x 为 20,变为 30;
  5. 同样也有变量对象 y 的修改,被解析后其值也相应的由 10 变为 30;
  6. 此外,在 with 声明完成后,它的特定对象从作用域链中移除(已改变的变量 x 30 也从那个对象中移除),即作用域链的结构恢复到 with 得到加强以前的状态。
  7. 在最后两个 console 中,当前变量对象的 x 保持同一,y 的值现在等于 30,在 with 声明运行中已发生改变。

catch

同样,catch 语句的异常参数变得可以访问,它创建了只有一个属性的新对象:异常参数名

try {
  // ...
} catch (ex) {
  console.log(ex);
}

作用域链修改为:

var catchObject = {
  ex: <exception object>
};

Scope = catchObject + AO|VO + [[Scope]]

catch 语句完成运行之后,作用域链恢复到以前的状态

闭包

ECMAScript 中,闭包指的是:

  • 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  • 从实践角度:以下函数才算是闭包:
    • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    • 在代码中引用了自由变量

闭包的作用

1. 私有化变量

2. 延续局部变量的寿命

var report = function(src) {
  var img = new Image();
  img.src = src;
};
report('http://xxx.com/getUserInfo');

因为一些低版本浏览器的实现存在 bug,在这些浏览器下使用 report 函数进行数据上报会丢失 30% 左右的数据,也就是说,report 函数并不是每一次 都成功发起了 HTTP 请求。丢失数据的原因是 img 是 report 函数中的局部变量,当 report 函数的调用结束后,img 局部变量随即被销毁,而此时或许还没来得及发出 HTTP 请求,所以此次请求就会丢失掉。

把 img 变量用闭包封闭起来,便能解决请求丢失的问题:

var report = (function() {
  var imgs = [];
  return function(src) {
    var img = new Image();
    imgs.push(img);
    img.src = src;
  };
})();

闭包与内存管理

  • 对于全局变量来说,全局变量的生存周期当然是永久的,除非我们主动销毁这个全局变量。
  • 而对于在函数内用 var 关键字声明的局部变量来说,当退出函数时,这些局部变量即失去了它们的价值,它们都会随着函数调用的结束而被销毁。

不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。

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

如果要解决循环引用带来的内存泄露问题,我们只需要把循环引用中的变量设为 null 即可。将变量设置为 null 意味着切断变量与它此前引用的值之间的连接。

参考

@zh-rocco zh-rocco self-assigned this Oct 31, 2018
Repository owner locked as resolved and limited conversation to collaborators Oct 31, 2018
@zh-rocco zh-rocco changed the title 【JS】闭包 【JS】作用域链 & 闭包 Mar 29, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

1 participant