闭包和作用域

quote

I figure life is a gift and I dont intend on wasting it. You never know what hand youre going to get dealt next. You learn to take life as it comes at you.

对于那些有一点JavasScript使用经验但从未真正理解闭包概念的人来说,理解闭包可以看做某种意义上的重生,但是需要付出非常多的努力和牺牲才能理解这个概念。闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用他们而有意识地创建闭包。闭包的创建和使用在你的代码中随处可见。你缺少的是根据你自己的意愿来识别、拥抱和影响闭包的思维环境。

执行上下文

JavaScript引擎通过执行上下文来跟踪代码,基于单线程的执行模式,通过执行上下文栈,来确定执行顺序。

JavaScript代码有两种类型:
全局代码 在所有函数外部定义,对应全局执行上下文,只有一个;
函数代码 在函数内部定义,对应函数执行上下文,每次调用函数,就会创建一个新的。

作用域

作用域就是变量的合法使用的范围。作用域是JavaScript引擎内部用来跟踪标识符与特定变量之间的映射关系,也叫词法环境。

  • 全局作用域
  • 函数作用域:属于这个函数的全部变量都可以在整个函数的范围内使用及复用。
  • 块级作用域:将变量的使用范围缩小到一个代码块{ }
let a = 0
function fn1(){
let a1 = 100
function fn2(){
let a2 = 200
function fn3() {
let a3 = 300
return a + a1 + a3
}
fn3()
}
fn2()
}

自由变量 一个变量在当前作用域没有定义,但被使用了,就会成为自由变量。

寻找自由变量就要去父级作用域中去寻找,直到找到全局作用域,如果没有就报错xx is not defined。层层嵌套的作用域就会形成作用域链。作用域应用的特殊情况,有两种表现:

  • 函数作为参数被传递;
  •  函数作为返回值被传
// 函数作为返回值
function create() {
    const a = 100
    return function () {
        console.log(a)
    }
}
const fn = create()
const a = 200
fn()
```

                                                          

调用fn时,执行create函数,打印a,此时在crate作用域内寻找到a,输出100。

// 函数作为参数被传递
function print(fn) {
    const a = 200
    fn()
}
const a = 100
function fn() {
    console.log(a)
}
print(fn) 

当调用print是,输入fn,执行fn()打印a,这时候应该去定义fn的作用域去寻找a,此时a没有,则往父级作用域找到a,输出100。

总结:所有自由变量的查找,都是在函数定义的地方开始查找,不是在执行的地方。

闭包

闭包是有权访问另一个函数作用域中的变量的函数。当函数可以记住并访问所在的词法作用域是,就产生了闭包,即使函数是在当前词法作用域之外执行的。闭包封闭的是外部状态,当外部状态的cope消失的时候,通过闭包还能访问到原始作用域(我自己感觉闭包更像一个空间,里面包括函数和变量)。

闭包三个条件:

  • 闭包是一个函数;
  • 闭包允许函数访问并操作函数创建时所在的作用域内的变量
  • 闭包存在于定义该变量的作用域中。

    var outerValue = 'ninja'
      function outerFunction(){
        assert(outerValue === 'ninja', 'I can see the ninja')
      }
      outerFunction()

    在同一作用域中声明outerFunctionouterValueouterFunction可以看见并访问outerValue,这个闭包的例子优势不明显。 每一个通过闭包访问变量的函数都具有一个作用域链,作用域链包含闭包的所有信息。

使用闭包

使用闭包时,所有的信息都会存储在内存中,直到JavaScript引擎确保这些信息不再使用或页面卸载时,才会清理这些信息,所以不能滥用闭包,否则会影响网页性能,造成内存泄漏。

当不需要使用闭包的时候,要及时释放内存,可将内层函数中对象的变量赋值为null

使用闭包可以:

  • 读取函数中的变量;
  • 将函数中的变量存储在内存中,保护变量不被污染;

封装私有变量

原生JavaScript不支持私有变量,通过使用闭包可以实现很接近的、可接受的私有变量。

// 闭包隐藏数据,只提供 API
// 操作数据只能通过set和get来操作。
function createCache() {
    const data = {}                  // 闭包中的数据,被隐藏,不被外界访问
    return {
        set: function (key, val) {
            data[key] = val
        },
        get: function (key) {
            return data[key]
        }
    }
}

const c = createCache()
c.set('a', 100)
console.log( c.get('a') )

回调函数

回调函数是需要在将来不确定的某一时刻异步调用的函数。回调函数需要频繁地访问外部数据。 使用闭包可以减少全局变量,减少传递函数的参数量。

经典案例

for (var i = 0; i < 10; i++) {
   a = document.createElement('a')
   a.innerHTML = i + '<br>'
   a.addEventListener('click', function (e) {
   e.preventDefault()
   alert(i)
   })
 document.body.appendChild(a)
}

我们的预期目标是点击哪一个数字弹出相应的数字,现在是无论点击哪一个数字都会弹出10。

原因是因为鼠标点击事件的回调会在循环结束时才执行,此时i是10

当使用var的时候i是定义在全局作用域的,每次循环时捕获的i都是一个i。所以我们需要每次循环时都有一个闭包作用域,就把i定义到每次循环的里面。

正确的方式是:把var i改变为let i。

for (let i = 0; i < 10; i++) {
 a = document.createElement('a')
 a.innerHTML = i + '<br>'
 a.addEventListener('click', function (e) {
 e.preventDefault()
 alert(i)
    })
 document.body.appendChild(a)
}

评论没有加载,检查你的局域网

Cannot load comments. Check you network.

eat();

sleep();

code();

repeat();