Skip to content
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

理解 JS 中的闭包 #73

Open
yanyue404 opened this issue Sep 25, 2019 · 0 comments
Open

理解 JS 中的闭包 #73

yanyue404 opened this issue Sep 25, 2019 · 0 comments

Comments

@yanyue404
Copy link
Owner

yanyue404 commented Sep 25, 2019

概念

闭包 是指一个函数可以记住其外部变量并可以访问这些变量。闭包是函数和声明该函数的词法环境的组合。

通过函数嵌套函数的形式延长内部函数的词法环境,获取到外部环境的 [[Environment]] 属性。这一条词法链将会保留在内存中,不会被垃圾回收机制销毁。

词法环境

在 JavaScript 中,每个运行的函数,代码块 {...} 以及整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏)的关联对象。

词法环境对象由两部分组成:

环境记录(Environment Record) —— 一个存储所有局部变量作为其属性(包括一些其他信息,例如 this 的值)的对象。
对 外部词法环境 的引用,与外部代码相关联。

当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。

let value = "Surprise!";

function f() {
  let value = "the closest value";

  function g() {
    return value;
  }

  return g;
}

let g = f();
console.dir(g); // 这里的词法优先顺序是 Closure > Script > Global

// ƒ g()
//   arguments: null
//   caller: null
//   length: 0
//   name: "g"
//   prototype: {constructor: ƒ}
//   [[FunctionLocation]]: aa.html:16
//   [[Prototype]]: ƒ ()
//   [[Scopes]]: Scopes[3]
//   0: Closure (f) {value: 'the closest value'}
//   1: Script {value: 'Surprise!', g: ƒ}
//   2: Global {window: Window, self: Window, document: document, name: '', location: Location, …}

console.log(g()); // the closest value

垃圾回收

通常,函数调用完成后,会将词法环境和其中的所有变量从内存中删除。因为现在没有任何对它们的引用了。与 JavaScript 中的任何其他对象一样,词法环境仅在可达时才会被保留在内存中。

但是,如果有一个嵌套的函数在函数结束后仍可达,则它将具有引用词法环境的 [[Environment]] 属性。

在下面这个例子中,即使在(外部)函数执行完成后,它的词法环境仍然可达。因此,此词法环境仍然有效。

例如:

function f() {
  let value = 123;

  return function () {
    alert(value);
  };
}

let g = f(); // g.[[Environment]] 存储了对相应 f() 调用的词法环境的引用

请注意,如果多次调用 f(),并且返回的函数被保存,那么所有相应的词法环境对象也会保留在内存中。下面代码中有三个这样的函数:

function f() {
  let value = Math.random();

  return function () {
    alert(value);
  };
}

// 数组中的 3 个函数,每个都与来自对应的 f() 的词法环境相关联
let arr = [f(), f(), f()];

当词法环境对象变得不可达时,它就会死去(就像其他任何对象一样)。换句话说,它仅在至少有一个嵌套函数引用它时才存在。

在下面的代码中,嵌套函数被删除后,其封闭的词法环境(以及其中的 value)也会被从内存中删除:

function f() {
  let value = 123;

  return function () {
    alert(value);
  };
}

let g = f(); // 当 g 函数存在时,该值会被保留在内存中

g = null; // ……现在内存被清理了

常见场景

  • 封装私有变量

用相同的 makeCounter 函数创建了两个计数器(counters):counter 和 counter2。

构造具有独立性,闭包维护自身私有变量,相互不会产生影响

function makeCounter() {
  let count = 0;

  return function () {
    return count++;
  };
}

let counter = makeCounter();
let counter2 = makeCounter();

alert(counter()); // 0
alert(counter()); // 1

alert(counter2()); // 0
alert(counter2()); // 1
function Counter() {
  let count = 0;

  this.up = function () {
    return ++count;
  };

  this.down = function () {
    return --count;
  };
}

let counter = new Counter();

alert(counter.up()); // 1
alert(counter.up()); // 2
alert(counter.down()); // 1
  • 在循环中创建闭包
var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
    return function () {
      console.log(i);
    };
  })(i);
}

data[0]();
data[1]();
data[2]();

每隔一秒依次打印 1,2,3,4,5

for (var i = 1; i <= 5; i++) {
  (function (j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}
  • 节流函数
function throttle(fn, gapTime = 1500) {
  let _lastTime = null;
  // 返回新的函数
  return function () {
    let _nowTime = +new Date();
    if (_nowTime - _lastTime > gapTime || !_lastTime) {
      fn.apply(this, arguments); //将this和参数传给原函数
      _lastTime = _nowTime;
    }
  };
}
  • 闭包 sum

编写一个像 sum(a)(b) = a+b 这样工作的 sum 函数。

是的,就是这种通过双括号的方式(并不是错误)。

举个例子:

sum(1)(2) = 3
sum(5)(-1) = 4

为了使第二个括号有效,第一个(括号)必须返回一个函数。

就像这样:

function sum(a) {
  return function (b) {
    return a + b; // 从外部词法环境获得 "a"
  };
}

alert(sum(1)(2)); // 3
alert(sum(5)(-1)); // 4
  • 通过函数筛选

我们有一个内建的数组方法 arr.filter(f)。它通过函数 f 过滤元素。如果它返回 true,那么该元素会被返回到结果数组中。

制造一系列“即用型”过滤器:

  • inBetween(a, b) —— 在 a 和 b 之间或与它们相等(包括)。

  • inArray([...]) —— 包含在给定的数组中。
    用法如下所示:

  • arr.filter(inBetween(3,6)) —— 只挑选范围在 3 到 6 的值。

  • arr.filter(inArray([1,2,3])) —— 只挑选与 [1,2,3] 中的元素匹配的元素。

例如:

/* .. inBetween 和 inArray 的代码 */
let arr = [1, 2, 3, 4, 5, 6, 7];

alert(arr.filter(inBetween(3, 6))); // 3,4,5,6

alert(arr.filter(inArray([1, 2, 10]))); // 1,2
// inBetween 筛选器

function inBetween(a, b) {
  return function (x) {
    return x >= a && x <= b;
  };
}

let arr = [1, 2, 3, 4, 5, 6, 7];
alert(arr.filter(inBetween(3, 6))); // 3,4,5,6
// inArray 筛选器

function inArray(arr) {
  return function (x) {
    return arr.includes(x);
  };
}

let arr = [1, 2, 3, 4, 5, 6, 7];
alert(arr.filter(inArray([1, 2, 10]))); // 1,2
  • 按字段排序
    我们有一组要排序的对象:
let users = [
  { name: "John", age: 20, surname: "Johnson" },
  { name: "Pete", age: 18, surname: "Peterson" },
  { name: "Ann", age: 19, surname: "Hathaway" },
];

通常的做法应该是这样的:

// 通过 name (Ann, John, Pete)
users.sort((a, b) => (a.name > b.name ? 1 : -1));

// 通过 age (Pete, Ann, John)
users.sort((a, b) => (a.age > b.age ? 1 : -1));

我们可以让它更加简洁吗,比如这样?

users.sort(byField("name"));
users.sort(byField("age"));
function byField(fieldName) {
  return (a, b) => (a[fieldName] > b[fieldName] ? 1 : -1);
}

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant