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;elsereturn 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) // undefinedconsole.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);