# 作用域闭包

# 什么是闭包?

红宝书(p178)上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数,

MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。

由此,我们可以看出闭包共有两部分组成:

  • 是一个函数
  • 能访问另外一个函数作用域中的变量

对于闭包有下面三个特性:

1、闭包可以访问当前函数以外的变量

function getOuter(){
    var date = '815';
    function getDate(str){
        console.log(str + date);  //访问外部的date
    }
    return getDate('今天是:'); //"今天是:815"
}
getOuter();

2、即使外部函数已经返回,闭包仍能访问外部函数定义的变量

function getOuter(){
    var date = '815';
    function getDate(str){
        console.log(str + date);  //访问外部的date
    }
    return getDate;     //外部函数返回
}
var today = getOuter();
today('今天是:');   //"今天是:815"
today('明天不是:');   //"明天不是:815"

3、闭包可以更新外部变量的值

function updateCount(){
    var count = 0;
    function getCount(val){
        count = val;
        console.log(count);
    }
    return getCount;     //外部函数返回
}
var count = updateCount();
count(815); //815
count(816); //816

为什么闭包的应用都有关键词 return,引用 JavaScript 秘密花园中的一段话:

闭包是 JavaScript 一个非常重要的特性,这意味着当前作用域总是能够访问外部作用域中的变量。 因为 函数 是 JavaScript 中唯一拥有自身作用域的结构,因此闭包的创建依赖于函数。

# 应用场景

具体应用场景你知道哪些??

  • 保护函数内的变量安全:如迭代器、生成器。
  • 在内存中维持变量:如缓存数据、柯里化。

# 私有属性

var foo = (function(){
    var secret = 'secret'
    // “闭包”内的函数可以访问 secret 变量,而 secret 变量对于外部却是隐藏的
    return {
        get_secret() {
            return secret
        },
        new_secret(new_secret) {
            secret = new_secret
        }
    }
})()

foo.secret              // undefined
foo.get_secret()        // 'secret'
foo.new_secret('哈哈哈') // 修改secret值
foo.get_secret()        // '哈哈哈'

之所以可能通过这种方式在 JavaScript 种实现公有,私有,特权变量正是因为闭包,闭包是指在 JavaScript 中,内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后。

let sque = (function () {
    let _width = Symbol();

    class Squery {
        constructor(s) {
            this[_width] = s
        }
        foo() {
            console.log(this[_width])
        }
    }
    return Squery
})();

let ss = new sque(20);

ss.foo()    // 20
console.log(ss[_width]) // ReferenceError: _width is not defined

# 单例模式

class Modal {
    constructor(name) {
        this.name = name
        this.getName()
    }
    getName() {
        return this.name
    }
}

let ProxySing = (function(){
    let instance;
    return function(name) {
        if (!instance) {
            instance = new Modal(name)
        }
        return instance
    }
})()

let a = new ProxySing('问题框');
let b = new ProxySing('回答框');

console.log(a === b); // true
console.log(a.getName());  // '问题框'
console.log(b.getName());  // '问题框'

# 函数防抖

const fn = () => console.log('fn')
window.onresize = debounce(fn, 1000)
function debounce(fn, interval) {
    let timer = null;
    return function (...args) {
        if(timer) clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, args);
        }, interval);
    }
}

# 面试题

接下来,看这道刷题必刷,面试必考的加强版闭包题:

for (var i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    }, 1000);
}
console.log(i)
// 5 5 5 5 5 5

答案是都是5,6个5,让我们分析一下原因:

由于作用域链机制的影响,闭包只能取得内部函数的最后一个值,这引起的一个副作用就是如果内部函数在一个循环中,那么变量的值始终为最后一个值。

如果要强制返回预期的结果(5,0,1,2,3,4),怎么办???

加个闭包

# 方法1:立即执行函数

把值传参给一个自执行的函数,函数具有块级作用域

for (var i = 0; i < 5; i++) {
    ((num) => {
        setTimeout(() => {
            console.log(num);
        }, 1000);
    })(i);
}
console.log(i)

# 方法2:setTimeout传参

setTimeout被遗忘的第三个参数,定时器启动时候,第三个以后的参数是作为第一个func()的参数传进去。

for (var i = 0; i < 5; i++) {
    setTimeout((j) => {
        console.log(j);
    }, 1000, i);
}
console.log(i)

# 方法3:使用ES6中的let

let具有块级作用域,所以外面的会报错,未定义该变量,在这儿行不通

for (let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    }, 1000);
}
console.log(i)  // i is not defined

# 方法4:函数调用

var output = function (i) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
};
for (var i = 0; i < 5; i++) {
    output(i);  // 这里传过去的 i 值被复制了
}
console.log(i)

# 闭包面试题再升级 - Promise

想要让它按顺序输出(0,1,2,3,4,5),并且要求代码块的循环和两处console.log不变

新的需求可以精确的描述为:代码执行时,立即输出 0,之后每隔 1 秒依次输出 1,2,3,4,循环结束后在大概第 5 秒的时候输出 5

怎么做:

下面,有请大神Promise出场,掌声在哪里呢???

const tasks = [];
for (var i = 0; i < 5; i++) {   // 这里 i 的声明不能改成 let,如果要改该怎么做?
    ((j) => {
        tasks.push(new Promise((resolve) => {
            setTimeout(() => {
                console.log(new Date, j);
                resolve();  // 这里一定要 resolve,否则代码不会按预期 work
            }, 1000 * j);   // 定时器的超时时间逐步增加
        }));
    })(i);
}

Promise.all(tasks).then(() => {
    setTimeout(() => {
        console.log(new Date, i);
    }, 1000);   // 注意这里只需要把超时设置为 1 秒
});

听说这是拥有加分项的模块化

const tasks = []; // 这里存放异步操作的 Promise
const output = (i) => new Promise((resolve) => {
    setTimeout(() => {
        console.log(new Date, i);
        resolve();
    }, 1000 * i);
});

// 生成全部的异步操作
for (var i = 0; i < 5; i++) {
    tasks.push(output(i));
}

// 异步操作完成之后,输出最后的 i
Promise.all(tasks).then(() => {
    setTimeout(() => {
        console.log(new Date, i);
    }, 1000);
});

# 闭包面试题再升级 - async/await

有大神Promise能搞定的事情,那我async/await也无所畏惧

// 模拟其他语言中的 sleep,实际上可以是任何异步操作
const sleep = (time) => new Promise((resolve) => {
    setTimeout(resolve, time);
});

(async () => {  // 声明即执行的 async 函数表达式
    for (var i = 0; i < 5; i++) {
        if (i > 0) {
            await sleep(1000);
        }
        console.log(new Date, i);
    }

    await sleep(1000);
    console.log(new Date, i);
})();