一篇文章图文并茂地带你轻松学完 JavaScript 闭包
JavaScript 闭包
为了更好地理解 JavaScript
闭包,笔者将先从 JavaScript
执行上下文以及 JavaScript
作用域开始写起,如果读者对这方面已经了解了,可以直接跳过。
1. 执行上下文
简单来说,JavaScript
有三种代码运行环境,分别是:
- Global Code 是
JavaScript
代码开始运行的默认环境 - Function Code 是
JavaScript
函数运行的环境 - Eval Code 是 利用
eval
函数执行的代码环境
执行上下文可以理解为上述为了执行对应的代码而创建的环境。
例如在上述某个环境执行前,我们需要考虑
-
该环境下的所有变量对象
例如用
let
const
var
定义的变量,或者是函数声明,函数参数arguments
等 -
该环境下的作用域链
包括 该环境下的所用变量对象 以及父亲作用域 (我们当然可以用到父亲作用域提供的函数和变量
-
是谁执行了这个环境 (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
函数
-
创建当前环境下的作用域链
这里作用域链显然是 当前环境下的变量(还没初始化)以及父亲作用域(这里面包括了
global
变量以及xxx
变量,name
形参)等,这些我们当然都可以在age
中使用。 -
创建当前环境下的变量
当前环境下的变量包括接收到的形参
arguments
age
变量 -
设置
this
是谁由于没有明确指定是谁调用
age
方法,因此this
在浏览器环境下设置为window
在创建好上下文后当需要进行变量的搜索的时候
会先搜索当前环境下的变量,如果没有随着作用域链往上搜索。
另外由于 ES6
箭头函数并不创建 this
,通过上述讲解,相信你可以了解为什么箭头函数用的是上一层函数的 this
了。
上述提到了作用域,作用域也分几种
作用域
-
块级作用域
在很多语言的规范里经常告诉我们,如果你需要一个变量再去定义,但是如果你使用
JavaScript
的var
定义变量,你最好别这么干。最好是都定义在头部。因为
var
没有块级作用域
if (true) {
var name = "huro";
}
console.log(name); // huro
不过当你使用 let
或 const
定义的话,就不存在这样的问题。
if (true) {
let name = "huro";
}
console.log(name); // name is not defined
-
函数和全局作用域
这个和大部分语言是一致的。
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);