浅谈 js 闭包
1、前言
闭包, js 前端永远绕不过去的面试题。这个问题其实考察的相对全面, 主要考察是否理解js 的运行机制,还有对于js 的作用域 和作用域链 是否理解。 如果这两个都很清楚 , 那闭包是什么? 怎么造成的? 又要怎么解决? 有什么作用? 又会造成什么? 这些通通就不是问题了
2、什么是作用域
谈闭包前,无可避免的需要知道什么是作用域。 作用域 就是该函数声明时 函数会通过内部的属性 [scope] 来记录创建的范围, 这个范围 就是该函数的作用域。
3、什么是闭包?
闭包, 顾名思义,一个封闭的区间。在这个封闭区间内,自成一套。在该作用域里面的 私有变量变量不与外界沟通,无法在外界获取,也不自动销毁,需要手动回收。
js 作者说: 闭包是一个函数加上到创建函数的作用域连接, 闭包 关闭了函数的自由变量。
这句话前半句的意思是 闭包是设计出来的一种机制,它是一个函数 和 该函数创建者作用域到该函数的连接的一个组合体。 这句话很绕, 其实说白了 就是一个函数包裹另外一个函数, 第一个函数作用域里面的 跟第二个函数相关的部分的统一组合体就叫做闭包。
后半句阐明了闭包都干了啥, 他关闭了函数的自由变量, 导致它内部的自由变量外界无法访问, 也暂时无法自动销毁,常常需要去手动销毁。
重点:
两个 函数, 这两个函数的作用域[scope] 还需要连接上。
关闭函数的自由变量,它暂时不会销毁
4、代码诠释
下面用代码来诠释闭包
function fn(n){
return function(m) {
n+=m
return n
}
}
// 第一步
const f = fn(1); // 无输出,记录了 n=1
// 第二步
f(5) // 输出 6
如果这么写
console.log(f(5)) // 6
console.log(f(5)) //11
console.log(f(5)) // 16
可以看到n 不会消失
console.log(n) // 报错 n is not defined
这里就是 n 可以用但是 外界无法直接访问
5、作用
作者既然设计了闭包, 那就是为了用的, 那它又解决了什么问题呢?
其实作者也有说明: 无意间贡献环境
直接代码说明:
下面这段代码输出的啥?
for(var i=0;i<5;i++){
setTimeout(() => {
console.log(i)
})
}
这里就是考虑到 js 的运行机制 , 咱们都知道,js 是服务于客户端的语言,用户是不肯能多线程操作的, 所以js 语言设计初衷就是单线程的。
我们都知道 作者是花了十几天的时间设计出来的这门语言。 初衷就是为了简单易学 易上手,所以他定义的初衷就是 js 万物皆对象, 函数也是一种特殊的对象!
但是又不得不考虑继承的因素,又不想用 c\java 之类的 class 类来做继承 ,因为那样又显得太正式 太沉重。 所以作者设计了 protoType(函数),和 __proto__(对象) 来解决继承的问题。
上面讲的多了啊, 讲的原型与原型链 这里并不涉及,只做了解就好
切回 上述所说,js 是单线程的, 所以在js 的运行机制里面是 分堆和栈的概念的
基础数据类型 (在js 栈里面按顺序 后进先出,先进后出的方式存储
string\number\undifined\null\Boolean\Symbol
引用类型 Object /函数/定时器 等需要消耗时间 的操作 放在堆里面储存, 然后只是在栈里面有个 标记, 标记的是该函数或者对象的引用地址。
在js 运行机制里面, 同步的先执行, 执行查到异步操作 就放到异步队列里面 啥时候执行完 啥时候放到同步执行的队列里面去渲染
在上述的代码里面 for 循环是同步进行的操作, 而 setTimeout 是一个异步的定时器, 哪怕时间没有设置 它也是异步的。 这样的话 先执行的 就是 for循环, 五次循环 i ++ 分别为1、2、3、4、5,所以这里 先执行同步的for 循环 i自增为5, 再执行 定时器异步 所以输出 5次 5。
for(var i=0;i<5;i++){
var index= i
setTimeout(() => {
console.log(index)
})
} // 输出 5次 4
那要解决上述问题 想要依次输出 那怎么办? 这里就要用到闭包去解决
代码如下
for(var i=0;i<5;i++){
(function () {
var index = i
setTimeout(() => {
console.log(index)
})
})()
}
// 输出 0,1,2,3,4
上面就是闭包使用的经典案例, 在这个函数内部 index 成了私有变量,它临时继承了i的值 不会被销毁也不会被更改 五次循环, 依次输出 0,1,2,3,4
知识点(不相关,了解即可 不常用):
Symbol 是 ES6 新增的一种原始数据类型,它的字面意思是:符号、标记。代表独一无二的值 。
在 ES6 之前,对象的属性名只能是字符串,这样会导致一个问题,当通过 mixin 模式为对象注入新属性的时候,就可能会和原来的属性名产生冲突 。而在 ES6 中,Symbol 类型也可以作为对象属性名,凡是属性名是 Symbol 类型的,就都是独一无二的,可以保证不会与其他属性名产生冲突。
Symbol 值通过函数生成,如下所示:
let s = Symbol(); //s是独一无二的值
typeof s ; // symbol
和其他基本类型不同的是,Symbol 作为基本类型,没有对应的包装类型,也就是说 Symbol 本身不是对象,而是一个函数。因此,在生成 Symbol 类型值的时候,不能使用 new 操作符 。
Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 值的描述,当有多个 Symbol 值时,比较容易区分
var s1 = Symbol('s1');
var s2 = Symbol('s2');
console.log(s1,s2); // Symbol(s1) Symbol(s2)
注意,Symbol 函数的参数只是表示对当前 Symbol 值的描述,因此,相同参数的 Symbol 函数的返回值也是不相等的
用 Symbol 作为对象的属性名时,不能直接通过点的方式访问属性和设置属性值。因为正常情况下,引擎会把点后面的属性名解析成字符串。
var s = Symbol();
var obj = {};
obj.s = 'Jack';
console.log(obj); // {s: "Jack"}
obj[s] = 'Jack';
console.log(obj) ; //{Symbol(): "Jack"}
Symbol 作为属性名,该属性不会出现在 for...in、for...of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols() 方法,专门获取指定对象的所有 Symbol 属性名。
var obj = {};
var s1 = Symbol('s1');
var s2 = Symbol('s2');
obj[s1] = 'Jack';
obj[s2] = 'Tom';
Object.keys(obj); //[]
for(var i in obj){
console.log(i); //无输出
}
Object.getOwnPropertySymbols(obj); //[Symbol(s1), Symbol(s2)]
另一个新的API,Reflect.ownKeys 方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
var obj = {};
var s1 = Symbol('s1');
var s2 = Symbol('s2');
obj[s1] = 'Jack';
obj[s2] = 'Tom';
obj.name = 'Nick';
Reflect.ownKeys(obj); //[Symbol(s1), Symbol(s2),"name"]
有时,我们希望重新使用同一个 Symbol 值,Symbol.for 方法可以做到这一点。它接受一个字符串作为参数,然后全局搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。
var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
s1 === s2 //true
Symbol.for() 也可以生成 Symbol 值,它 和 Symbol() 的区别是:
Symbol.for() 首先会在全局环境中查找给定的 key 是否存在,如果存在就返回,否则就创建一个以 key 为标识的 Symbol 值
Symbol.for() 生成的 Symbol 会登记在全局环境中供搜索,而 Symbol() 不会。
因此,Symbol.for() 永远搜索不到 用 Symbol() 产生的值。
var s = Symbol('foo');
var s1 = Symbol.for('foo');
s === s1; // false
Symbol.keyFor() 方法返回一个已在全局环境中登记的 Symbol 类型值的 key 。
var s1 = Symbol.for('s1');
Symbol.keyFor(s1); //foo
var s2 = Symbol('s2'); //未登记的 Symbol 值
Symbol.keyFor(s2); //undefined