JavaScript 闭包

为了更好地理解 JavaScript 闭包,笔者将先从 JavaScript 执行上下文以及 JavaScript 作用域开始写起,如果读者对这方面已经了解了,可以直接跳过。

1. 执行上下文

简单来说,JavaScript 有三种代码运行环境,分别是:

  1. Global Code 是 JavaScript 代码开始运行的默认环境
  2. Function Code 是 JavaScript 函数运行的环境
  3. Eval Code 是 利用 eval 函数执行的代码环境

执行上下文可以理解为上述为了执行对应的代码而创建的环境。

例如在上述某个环境执行前,我们需要考虑

  1. 该环境下的所有变量对象

    例如用 let const var 定义的变量,或者是函数声明,函数参数 arguments

  2. 该环境下的作用域链

    包括 该环境下的所用变量对象 以及父亲作用域 (我们当然可以用到父亲作用域提供的函数和变量

  3. 是谁执行了这个环境 (this)

拥有了这些东西后,我们才可以分配内存,起到一个准备的作用。

我们用下述代码加深对执行上下文的理解

let global = 1;

function getAgeByName(name) {
    let xxx = 1;
    function age() {
        console.log(this);
        const age = 10;
        if (name === "huro")
            return age;
        else
            return age * 10;
    }
    return age();
}

假设我们执行 age 函数

  1. 创建当前环境下的作用域链

    这里作用域链显然是 当前环境下的变量(还没初始化)以及父亲作用域(这里面包括了 global 变量以及 xxx 变量, name 形参)等,这些我们当然都可以在 age 中使用。

  2. 创建当前环境下的变量

    当前环境下的变量包括接收到的形参 arguments age 变量

  3. 设置 this 是谁

    由于没有明确指定是谁调用 age 方法,因此 this 在浏览器环境下设置为 window

在创建好上下文后当需要进行变量的搜索的时候

会先搜索当前环境下的变量,如果没有随着作用域链往上搜索。

另外由于 ES6 箭头函数并不创建 this ,通过上述讲解,相信你可以了解为什么箭头函数用的是上一层函数的 this 了。

上述提到了作用域,作用域也分几种

作用域

  1. 块级作用域

    在很多语言的规范里经常告诉我们,如果你需要一个变量再去定义,但是如果你使用 JavaScriptvar 定义变量,你最好别这么干。最好是都定义在头部。

    因为 var 没有块级作用域

if (true) {
    var name = "huro";
}
console.log(name); // huro

​不过当你使用 letconst 定义的话,就不存在这样的问题。

if (true) {
    let name = "huro";
}
console.log(name); // name is not defined
  1. 函数和全局作用域

    这个和大部分语言是一致的。

let a = 1;
function fn() {
    let a = 2;
    console.log(a); // 2
}

闭包

闭包实质上可以理解为"定义在一个函数内部的函数"

拥有了作用域和作用域链,内部函数可以访问定义他们的外部函数的参数和变量,这非常好。

如果我们希望一个对象不被外界更改(污染)

const myObject = () => {
    let value = 1;
    return {
        increment: (inc) => {
            value += inc;
        }
        getValue: () => {
            return value;
        }
    }
}

由于外界不可能直接访问到 value 因此就不可能修改他。

利用闭包

在构造函数中,对象的属性都是可见的,没法得到私有变量和私有函数。一些不知情的程序员接受了一种伪装私有的模式。

例如

function Person() {
    this.________name = "huro";
}

用于保护这个属性,并且希望使用代码的用户假装看不到这种奇怪的成员元素,但是其实编译器并不知情,仍会在你输入 xxx.__ 的时候提示你有 xxx.________name 属性

利用闭包可以很轻易的解决这个问题。

function Person(spec) {
    let { name } = spec;

    this.getName = () => {
        return name;
    }
    this.setName = (name) => {
        name = "huro";
    }
    return this;
}
const p = new Person({ name: "huro" });
console.log(p.name) // undefined
console.log(p.getName()) // "huro"

注意闭包带来的问题

<body>
    <div class="name">
       	huro
    </div>
    <div class="name">
        lero
    </div>
</body>
const addHandlers = (nodes) => {
    let i ;
    for (i = 0; i < nodes.length; i += 1) {
        nodes[i].addEventListener("click", () => {
            alert(i); // 总是 nodes.length
        })
    }
}
const doms = document.getElementsByClassName("name");
addHandlers(doms);

你会发现,打印出来的结果总是 2,这是作用域的原因,由于 i 是父作用域链的变量,当向上查找的时候,i 已经变成 2 了。

正确的写法应该是

const addHandlers = (nodes) => {
    for (let i = 0; i < nodes.length; i += 1) {
        nodes[i].addEventListener("click", () => {
            alert(i);
        })
    }
}
const doms = document.getElementsByClassName("name");
addHandlers(doms);
02-08 22:23