深刻理解JavaScript闭包之闭包的使用场景

本篇文章是上一篇 深刻理解JavaScript闭包之什么是闭包文章的下篇,闭包的使用场景。javascript

基础概念

1.函数做用域

定义在函数中的参数和变量在函数外部是不可见的。html

2.块级做用域(私有做用域)

任何一对花括号中的语句都属于一个快,在这之中的全部变量在代码块外都是不可见的,咱们称之为块级做用域。大多数类C语言都拥有块级做用域,JS却没有,好比在for循环中定义的i,出了for循环仍是有这个i变量。前端

3.私有变量

私有变量包括函数的参数,局部变量和函数内部定义的其余函数。java

4.静态私有变量

私有变量是每一个实例都是独立的,而静态私有变量是共用的。react

5.特权方法

有权访问私有变量的方法称为特权方法。面试

6.单例模式

确保一个类只有一个实例,即屡次实例化该类,也只返回第一次实例化后的实例对象。该模式不只能减小没必要要的内存开销,而且能够减小全局的函数和变量冲突。
能够来看一个简单的例子:segmentfault

let userInfo = {
    getName() {},
    getAge() {},
}

上面代码中,使用对象字面量建立的一个获取用户信息的对象。全局只暴露了一个 userInfo 对象,好比获取用户名,直接调用 userInfo.getName()。userInfo对象就是单例模式的体现。若是把 getName 和 getAge 定义在全局,很容易污染全局变量。命名空间也是单例模式的体现。平时开发网站中的登陆弹窗也是一个很典型的单例模式的应用,由于全局只有一个登陆弹窗。更多的能够看从ES6从新认识JavaScript设计模式(一): 单例模式这边文章。设计模式

7.构造函数模式

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayName = function() {
        console.log(obj.name);
    }
}

const person1 = new Person('litterstar', 18);
console.log(person1);

特色:缓存

  1. 可使用 constructor 或 instanceof识别对象实例的类型
  2. 使用 new 来建立实例

缺点:微信

  1. 每次建立实例时,每一个方法都要被建立一次

8.原型模式

function Person() {}

Person.prototype.name = 'litterstar';
Person.prototype.age = 18;
Person.prototype.sayName = function () {
    console.log(this.name);
}
const person1 = new Person();

特色:
方法不会被重复建立

缺点:

  1. 不能初始化实例参数
  2. 全部的属性和方法都被实例共享
构造函数模式 和 原型模式

闭包的应用场景

1. 模仿块级做用域

好比咱们可使用闭包能使下面的代码按照咱们预期的进行执行(每隔1s打印 0,1,2,3,4)。

for(var i = 0; i < 5; i++) {
    (function(j){
        setTimeout(() => {
            console.log(j);
        }, j * 1000);
    })(i)
}
咱们应该尽可能避免往全局做用域中添加变量和函数。经过闭包模拟的块级做用域

2. 私有变量

JavaScript中没有私有成员的概念,全部属性都是公有的。可是有私有变量的概念,任何在函数中定义的变量,均可以认为是私有变量,由于在函数的外部不能访问这些变量。私有变量包括函数的参数,局部变量和函数内部定义的其余函数。

来看下面这个例子

function add(a, b) {
    var sum = a + b;
    return sum;
}

add 函数内部,有3个私有变量,a, b, sum。只能在函数内部访问,函数外面是访问不到它们的。可是若是在函数内部建立一个闭包,闭包能够经过本身的做用域链就能够访问这些变量。因此利用闭包,咱们就能够建立用于访问私有变量的公有方法(也称为特权方法)

有两种在对象上建立特权的方法。
第一种,在构造函数中定义特权方法

function MyObject() {
    // 私有变量和私有函数
    var privateVariable = 10;
    function privateFunction() {
        return false;
    }
    // 特权方法
    this.publicMethod = function() {
        privateVariable++;
        return privateFunction;
    }
}

这个模式在构造函数内部定义了私有变量和函数,同时建立了可以访问这些私有成员的特权方法。可以在构造函数中定义特权方法,是由于特权方法做为闭包有权访问在构造函数中定义的全部变量和函数。
上面代码中,变量 privateVariable 和函数 privateFunction() 只能经过特权方法 publicMethod()来访问。在建立 MyObject 实例后,只能使用 publicMethod来访问 变量 privateVariable 和函数 privateFunction()

第二种,利用私有和特权成员,能够隐藏哪些不该该被直接修改的数据。

function Foo(name){
    this.getName = function(){
        return name;
    };
};
var foo = new Foo('luckyStar');
console.log(foo.name); //  => undefined
console.log(foo.getName()); //  => 'luckyStar'

上面代码的构造函数中定义了一个特权方法 getName(),这个方法能够在构造函数外面使用,能够经过它访问内部的私有变量name。由于该方法是在构造函数内部定义的,做为闭包能够经过做用域链访问name。私有变量 nameFoo的每一个实例中都不同,所以每次调用构造函数都会从新建立该方法。

在构造函数中定义特权方法的缺点就是你必须使用构造函数模式。以前一篇文章 JavaScript的几种建立对象的方式 中提到构造函数模式会针对每一个实例建立一样一组新方法,使用静态私有变量实现特权能够避免这个问题。

3. 静态私有变量

建立特权方法也经过在私有做用域中定义私有变量或函数来实现。

(function() {
    var name = '';
    //
    Person = function(value) {
        name = value;
    }
    Person.prototype.getName = function() {
        return name;
    }
    Person.prototype.setName = function(value) {
        name = value;
    }
})()

var person1 = new Person('xiaoming');
console.log(person1.getName()); // xiaoming
person1.setName('xiaohong');
console.log(person1.getName()); // xiaohong

var person2 = new Person('luckyStar');
console.log(person1.getName()); // luckyStar
console.log(person2.getName()); // luckyStar

上面代码经过一个匿名函数实现块级做用域,在块级做用域中 变量 name 只能在该做用域中访问,一样的经过闭包(做用域链)的方式实现 getNamesetName 来访问 name, 而 getNamesetName 又是原型对象的方法,因此它们成了 Person 实例的共享方法。
这种模式下,name 就变成了一个静态的、由全部实例共享的属性。在一个实例上调用 setName() 会影响全部的实例。

4. 模块模式

模块模式是为单例建立私有变量和特权方法。单例(singleton),指的是只有一个实例的对象。

var singleton = {
    name: value,
    method: function() {},
}

上面使用对象字面量的方式来建立单例对象,这种适用于简单的应用场景。复杂点的,好比改对象须要一些私有变量和私有方法

模块模式经过单例添加私有变量和特权方法可以使其加强。

var singleton = function() {
    var privateVarible = 10;
    function privateFunction() {
        return false;
    }

    return {
        publicProperty: true,
        publicMethod: function() {
            privateVarible++;
            return privateFunction();
        }
    }
}

模块模式使用了一个返回对象的匿名函数。在这个匿名函数内部,首先定义了私有变量和函数.

加强模块模式

var singleton = function() {
    var privateVarible = 10;
    function privateFunction() {
        return false;
    }

    var object = new CustomType();
    object.publicProperty = true;
    object.publicMethod = function() {
        privateVarible++;
        return privateFunction();
    }
    // 返回这个对象
    return object;
}

在返回对象以前加入对其加强的代码。这种加强的模块模式适合单例必须是某种类型的实例。

Vue源码中的闭包

1. 数据响应式Observer中使用闭包(省略闭包以外的相关逻辑)

function defineReactive(obj, key, value) {
    return Object.defineProperty(obj, key, {
        get() {
            return value;
        },
        set(newVal) {
            value = newVal;
        }
    })
}

value 还函数中的一个形参,属于私有变量,可是为何在外部使用的时候给value赋值,仍是能达到修改变量的目的呢。

这样就造成了一个闭包的结构了。根据闭包的特性,内层函数能够引用外层函数的变量,而且当内层保持引用关系时外层函数的这个变量,不会被垃圾回收机制回收。那么,咱们在设置值的时候,把newVal保存在value变量当中,而后get的时候再经过value去获取,这样,咱们再访问 obj.name时,不管是设置值仍是获取值,实际上都是对value这个形参进行操做的。

2. 结果缓存

Vue源码中常常能看到下面这个cached函数(接收一个函数,返回一个函数)。

/**
* Create a cached version of a pure function.
*/
function cached (fn) {
var cache = Object.create(null);
return (function cachedFn (str) {
    var hit = cache[str];
    return hit || (cache[str] = fn(str))
})
}

这个函数能够读取缓存,若是缓存中没有就存一下放到缓存中再读。闭包正是能够作到这一点,由于它不会释放外部的引用,从而函数内部的值能够得以保留。

如今再看源码或者如今再看本身写的代码的时候,就会发现,不经意间其实咱们已经写过和见过不少闭包了,只是以前可能不太认识而已。好比这篇文章 记忆化技术介绍——使用闭包提高你的 React 性能也提到了闭包。

React Hooks中闭包的坑

咱们先来看一下使用 setState 的更新机制:

ReactsetState函数实现中,会根据一个变量isBatchingUpdates 判断是直接更新this.state仍是放到 队列中回头再说。而isBatchingUpdates 默认是false,也就表示setState会同步更新this.state。可是,有一个函数 batchedUpdates, 这个函数会把isBatchingUpdates修改成true,而当React在调用事件处理函数以前就会调用这个batchedUpdates,形成的后果,就是由React控制的事件处理程序过程setState不会同步更新this.state

知道这些,咱们下面来看两个例子。

下面的代码输出什么?

class Example extends React.Component {
   constructor() {
     super();
     this.state = {
       val: 0
     };
   }
   
   componentDidMount() {
     this.setState({val: this.state.val + 1});
     console.log(this.state.val);    // 第 1 次 log
 
     this.setState({val: this.state.val + 1});
     console.log(this.state.val);    // 第 2 次 log
 
     setTimeout(() => {
       this.setState({val: this.state.val + 1});
       console.log(this.state.val);  // 第 3 次 log 1 
 
       this.setState({val: this.state.val + 1});
       console.log(this.state.val);  // 第 4 次 log 2
     }, 0);
   }
 
   render() {
     return null;
   }
 };

打印结果是: 0, 0, 2, 3。

  1. 第一次和第二次都是在react自身生命周期内,触发 isBatchingUpdates 为true, 因此并不会直接执行更新state, 而是加入了 dirtyComponents,因此打印时获取的都是更新前的状态 0
  2. 两次setState时,获取到 this.state.val 都是 0,因此执行时都是将0设置为1,在react内部会被合并掉,只执行一次。设置完成后 state.val值为1。
  3. setTimeout中的代码,触发时 isBatchingUpdates为false,因此可以直接进行更新,因此连着输出 2, 3

上面代码改用react hooks的话

import React, { useEffect, useState } from 'react';

const MyComponent = () => {
    const [val, setVal] = useState(0);

    useEffect(() => {
        setVal(val+1);
        console.log(val);

        setVal(val+1);
        console.log(val);

        setTimeout(() => {
            setVal(val+1);
            console.log(val);

            setVal(val+1);
            console.log(val);
        }, 0)
    }, []);
    return null
};

export default MyComponent;

打印输出: 0, 0, 0, 0。

更新的方式没有改变。首先是由于 useEffect 函数只运行一次,其次setTimeout是个闭包,内部获取到值val一直都是 初始化声明的那个值,因此访问到的值一直是0。以例子来看的话,并无执行更新的操做。

在这种状况下,须要使用一个容器,你能够将更新后的状态值写入其中,并在之后的 setTimeout中访问它,这是useRef的一种用例。能够将状态值与refcurrent属性同步,并在setTimeout中读取当前值。

关于这部分详细内容能够查看 React useEffect的陷阱。React Hooks 的实现也用到了闭包,具体的能够看 超性感的React Hooks(二)再谈闭包

总结

当在函数内部定义了其余函数,就建立了闭包。闭包有权访问包含函数内部的全部变量,原理以下:

  • 在后台执行环境中,闭包的做用域链包含它本身的做用域链、包含函数的做用域和全局做用域
  • 一般,函数的做用域及其全部变量都会在函数执行结束后销毁
  • 可是,当函数返回来了一个闭包,这个函数的做用域将一直在内存中保存在闭包不存在为止。

使用闭包能够在JavaScript中模仿块级做用域(JavaScript自己没有块级做用域的概念),要点以下:

  • 建立并当即调用一个函数,这样既能够执行其中的代码,又不会在内存中留下对该函数的引用
  • 结果就是函数内部的全部变量都会被销毁 -- 除非将某些变量赋值给了包含做用域(即外部做用域)中的变量

闭包还能够用于在对象中建立私有变量,相关概念和要点以下。

  • 即便JavaScript中没有正式的私有对象属性的概念,但可使用闭包来实现公有方法,而经过公有方法能够访问在包含做用域中定义的变量
  • 可使用构造函数模式,原型模式来实现自定义类型的特权方法也可使用模块模式、加强的模块模式来实现单例的特权方法

参考

其余

最近发起了一个100天前端进阶计划,主要是深挖每一个知识点背后的原理,欢迎关注 微信公众号「牧码的星星」,咱们一块儿学习,打卡100天。同时也会分享一些本身学习的一些心得和想法,欢迎你们一块儿交流。

相关文章
相关标签/搜索