su37josephxia/frontend-interview

Day12 - 如何利用闭包完成类库封装

su37josephxia opened this issue · 29 comments

类库封装最重要的要求就是不能让类库中的变量污染全局,而这正是闭包的精髓特征。通过即时函数将类库封装在即时函数func中,这样保证了所有的变量在一个独立的空间内,不会污染全局环境,也不会被其他的对象改变内部环境结构。
(function () {
var jQuery = window.$ = function() {
// Intialize
}
})()

首先我们要强调一下我们在封装类库的时候为什么要用到闭包和立即执行函数。类库的主要的几个特点就是复用性高,耦合性低。低耦合性就要求类库不仅脱离业务,最好是有自己单独的作用域和变量名。假设类库中使用 var 定义了一个name 的变量,那么他就很可能会影响到 window下的name。但如果采用立即之心函数和闭包就能够为类库创建单独的作用域,从而避免影响全局,在变量的命名上面也将不会收到外部作用域的影响。在ES6之前是没有class这个概念的,但是通过闭包可以起到类似 Class 的作用,
总结:避免污染全局对象,避免变量命名相互影响。所以封装类库常用的方法就是使用立即执行函数+闭包的形式最终 return 出你想封装的方法或对象。

jQuery就是一个工具类库,它就利用了闭包的**,保护了它自己私有化的特点,不给全局变量造成污染。

(function(global, factory) {
    "use strict";
    if (typeof module === "object" && typeof module.exports === "object") {
        // 当前运行JS的环境是支持CommonJS模块规范「node.js/webpack,浏览器默认不支持」
        // webpack环境下
        //      module.exports = factory(window, true)
        // node 环境下 【不支持jQuery的使用】
        //     module.exports = function(w) {...}
        //     使用 let $ = require('jquery'); -> $() ->报错了
        module.exports = global.document ? 
            factory(global, true) : 
           function (w) {
               if(!w.document) {
                     throw new Error("jQuery requires a window with a document")
              }
             return factory(w)
           }
    } else {
        // 浏览器或者webview环境
        // -> B(window)
        factory(global);
    }
})(typeof window !== 'undefined' ? window : this, function (window, noGlobal) {
   "use strict";
   // 浏览器环境下导入JQ: window -> window  noGlobal -> undefined
   //  webpack环境下运行:window-> window noGlobal -> true 把factory执行的返回值导出
})

在自己封装类库时,也要进行参照jquery**进行判断是否支持对应的规范

  • 类库封装:把相同的功能打包成一个函数,只留下相应的接口,就可以同一个功能,调用不同的参数反复使用。

  • 通过闭包封装类库的方式

    • 通过返回值

      var my_lib = (function(){
          return {
              doSomething: function(){ ... },
              doSomething_2: function(){ ... }
          };
      }());
    • 通过参数,返回类库对象

      (function(w) {
          w.my_lib = {
              doSomething: function(){ 
                  ... 
              },
              doSomething_2: function(){ 
              ... 
              }
          };
      })(window);
  • 使用全局变量

    (function ($) {
    // 这里,我们的代码就可以使用全局的jQuery对象了
    } (jQuery));
  • 暴露全局变量,保存私有变量不暴露

    var blogModule = (function () {
      var my = {}, privateName = "song";
      function privateAddTopic(data) {
      // 这里是内部处理代码
      }
      my.Name = privateName;
      my.AddTopic = function (data) {
      		privateAddTopic(data);
      };
    	return my;
    } ());
  • 用单例模式+立即执行函数封装一个类
var Parent = (function () {
    var ParantClass = function () {}
    var instance 
    return function() {
        if(instance) {
            return instance
        }
        instance = new ParantClass()
        return instance
    }
})()

var childA = Parent()
var childB = Parent()

childA === childB // true

在内部用闭包储存实例,并保证只有一个

类库封装的要求就是封闭性,对于需要开放的,则使用对应约定使其开启,需要封闭在内的,就藏在闭包之中。

Jquery库使用的就是闭包方式进行的封装,示例如下

(function () {
var jQuery = window.$ = function() {
// Intialize
}
})()

编程语言中JAVA是支持将方法声明为私有的,而JavaScript没有这种原生支持,但可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免了非核心的代码弄乱了代码的公共接口
var Counter = (function() {
var privateCounter = 0 // 私有变量
function changBy(val) {
privateCounter += val
}
return {
intcrement: function() { // 三个闭包共用一个词法语境
changBy(1)
},
delcrement: function() {
changBy(-1)
},
value: function() {
return privateCounter
}
}
})()
闭包的典型框架应该就是jQuery,闭包是JavaScript的一大特点,主要应用闭包场合是为了:设计私有的方法和变量。
这在做框架的时候体现更明显,有些方法和属性只是运算逻辑过程中的使用的,不想让外部修改这些属性,因此就可以设计一个闭包来只提供方法获取

我们在封装类库的时候,类库的主要几个特点就是复用性,最好有单独的作用域和变量名,避免使用var定义全局变量他很有可能会影响到Window下的类库,如果使用立即执行函数闭包,就能够为类库创作单独的作用域在Es6我们可以使用class来完成类库的封装

封装类库需要做到两点:

  1. 将内部的属性和方法封装,避免造成全局作用域污染
  2. 保证第一点的同时,需要让外部能访问到内部属性和方法
    第一点,可以通过闭包,自执行函数的方式实现
(function () {

})()

第二点,可以暴露一个变量绑定到window上,如下

(function () {
  var jQuery = window.$ = function() {
    return {
      a:1
     }
  }
})()
console.log(window.$.a); //1

往一个立即执行函数传入this,然后就可以在函数的第一个参数得到全局上下文。然后在不污染全局的情况下,封装方法,用闭包保留私有变量,最后把封装的方法统一放在一个对象里,挂载到全局上下文。

  (function (_this) {
    let uid = 0
    function createId() {
      return uid++
    }
    
    _this.myLib = {
      createId
    }
  })(this)

类库封装最重要的要求就是不能让类库中的变量污染全局,而这正是闭包的精髓特征。通过即时函数将类库封装在即时函数func中,这样保证了所有的变量在一个独立的空间内,不会污染全局环境,也不会被其他的对象改变内部环境结构。
(function () {
var jQuery = window.$ = function() {
// Intialize
}
})()

什么是类库呢?就是引入这个类库通过一些约定好的方式去使用类库提供的各种方法。
而类库实现的主要方式就是return一堆的方法以供调用,这时就用到了闭包。
而且类库也要保证类库内的变量不会影响到使用者自己定义的变量,就要有自己独立的作用域,这时就可以使用立即调用函数的方式。

封装类库最重要的就是要避免污染全局作用域,所以可以利用闭包的优点来实现,具体则可以使用立即执行函数将类库的实现封闭在一个内部作用域中,对外只暴露一个对象或者函数给外界调用

通过即时函数将需要的变量传入,可以避免污染全局作用域,又可以用闭包将传入参数放置到当前作用域中方便查找

将类库打包成自调函数,在vue中global中就是这样的,代码大致如下:
(function () {
var Vue= function() {
...
}
})()

避免变量污染全局 使用闭包和即时函数
将所有的数据和功能都封装在一个函数的内部
只向外暴露一个包含有多个个方法的对象或者函数
模块使用者只需要通过模块暴露的对象调用方法来实现相对应的功能

(function (window) {
    var message="hi"

    function log(record) {
        console.log(message+record);
    }

    window.$ = {
        log: log,
    }
})(window);
//调用
$.log("partiallove")
  • 类库封装是一系列功能方法的一个封装体,它的特点就是自成空间,即拿即用,不对使用环境造成污染,这也是我们封装类库需要考虑的最重要的一点。

  • 那么就需要我们通过闭包的独立作用域将类库与外部隔离开,让类库实例可以访问闭包产生的私有变量,不会污染它使用者的作用域

  • 将类库的私有方法放入闭包中保存,将类库作用域与外部环境隔离。
  • 通过立即执行函数将调用方法抛出;外部可以通过类库实例访问闭包中的私有变量和方法
  (function (window) {
    var DEBUG = "debug"
    function log(v) {
      console.log('我会将参数v打印出来', v)
    }
    window.$ = {
      log: log,
    }
  })(window);
  //调用

  $.log("dsqwd")
  • 封装类库要求不能使内部变量污染全局,所以使用闭包将类库的私有属性和方法与外部隔离
  • 可以通过return类库名在外部接收或内部定义往window上绑定类库名

ES6之后,一个类库,它至少是一个文件吧,作为一个模块文件被import,然后在这个文件里面会有各种各样的逻辑,会有一些本地变量。执行完了这些逻辑之后,最终会有导出。

那么这个过程就是通过导出实现了我们所谓的封装性,也就是说,我们屏蔽了内部的实现而暴露出某一些接口。

如果暴露出的接口里面包括了一些函数,然后函数它大概率是会访问到模块内本地的变量的,那么这无形之中,其实就是使用了闭包。

封装类库可能会和用户自定义的全局变量名产生冲突
因此,封装类库通常是使用立即执行函数 + 闭包的形式,将内部需要用到的变量都在内部创建并销毁,只将很少的,需要暴露出去的变量通过闭包的形式暴露出去,不会污染到全局的环境

封装的类库还有可能使用在不同的环境中
所以通常需要使用 umd 来兼容各种不同的环境,umd 也是通过立即执行函数 + 闭包来实现的

示例代码

const $ = (function(){
  const snail = {
    name:'snail',
    age:18,
  }
  return function(){
    snail.age++
    console.log(`my name is snail,i am ${snail.age} years old.`)
  }
})()

原因

类库封装一个很重要的点就是不能让类库中的变量污染全局。
而利用即时函数配合闭包,就保证了所有的变量在一个独立的空间内,不会污染全局环境,同时内部状态只可以通过类库暴露出去的方法进行修改。

封装类库最重要的要求就是不能让类库中的变量污染全局,也不能让外部随意修改删除内部代码。
使用即时函数和闭包,可以隔离全局作用域,创建私有空间,定义内部私有属性等,防止全局等相互影响。

首先类库通俗来说是为了完成某些特定功能的代码块,其中包含一些接口、抽象类等等,并且可能运行在多种环境中,因此其中的变量就必须与外部隔离开,要防止变量污染。而即时函数可以开辟一个独立的作用域,如果在即时函数中定义一个的变量被内部函数捕获使用,这个函数就形成了闭包。所以可以利用这一特性,将类库中的变量定义在即时函数中,并在即时函数内部定义函数方法使用这些变量,最后导出定义的方法,以达成类库的封装。

将所有的数据和功能都封装在一个函数的内部,只向外暴露一个包含有n个方法的对象或者函数,使用者只需要通过对暴露的对象调用方法来实现相对应的功能,避免了类库中的变量污染全局,也不能让外部随意修改删除内部代码,因此我们可以通过即时函数来进行封装。

我们封装类库,要特别注意的就是,尽量的减少和其他模块代码的耦合,体现类库的公共特性,而且要避免变量名的污染。

我们可以利用闭包缓存私有属性的特点,和即时函数作用域隔离的特性,去将类库自己的逻辑和变量缓存在闭包里,只返回一些和外部定义好的方法和对象即可。
这样的话,我们既可以保证代码的独立性,又可以保存私有属性,保证不会造成变量污染。

如下结构:

const lib = (function(){
  let obj = {},private = 'abc';
  obj.getPrivate = function(){
    return private;
  }
  obj.addtoPrivate = function (val){
    return private+val
  }
  return obj;
})()

也可以如下直接挂在全局

(function (global){
  let obj = {},private = 'abc';
  obj.getPrivate = function(){
    return private;
  }
  obj.addtoPrivate = function (val){
    return private+val
  }
 window.obj = obj;
})(this)

类库就是把一些特殊的功能封装到一个函数中,留出一个或多个api的接口给外部函数调用;可以通过闭包的即时函数来实现

1.类库封装最重要的要求就是不能让类库中的变量污染全局,我们可以通过构建闭包来实现这种要求。
为了实现类库中的变量私有化,我们可以使用高阶函数或者立即执行函数的形式或者两者结合都可以,具体可以根据需求来确定,这样我们不想暴露出去的变量,可以通过接口以供用户调用,而且用户不能直接访问该变量。
// IIFF
const MyLibrary = (function (global) {
const myData = '***';
function feature1(params) {
console.log(params, global);
}
function feature2(params) {
console.log(params);
}
function ReturnFunction(params) {
// do something;
return {
myData,
feature1,
};
}
ReturnFunction.myData = myData;
ReturnFunction.feature2 = feature2;
return ReturnFunction;
})(typeof window !== 'undefined' ? window : this);

  1. 其次是类库一般要求唯一性,如果有这个要求我们可以利用闭包构建一个单例模式来完成这个需求。

// 单例需求 singleton
const MyLibrary3 = (function (global) {
const myData = '***';
let instance;
function feature1(params) {
console.log(params, global);
}
function feature2(params) {
console.log(params);
}
function ReturnFunction(params) {
// do something;
return {
myData,
feature1,
};
}
ReturnFunction.myData = myData;
ReturnFunction.feature2 = feature2;
return function () {
if (instance) {
return instance;
}
instance = new ReturnFunction();
return instance;
};
})(typeof window !== 'undefined' ? window : this);