关于闭包的一些实践与应用

115 阅读10分钟

关于闭包的一些实践与应用

闭包是什么

  • 闭包让你可以在一个内层函数中访问到其外层函数的作用域
  • 闭包出现的原因是因为作用域链的存在,使得内部的函数可以有一条作用域链指向外部函数以及外部环境
  • 主要有两个使用场景
    • 创建私有变量
    • 延长变量的生命周期 -> 一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的

闭包的的经典问题

闭包的作用域问题

let a = 'glows777';
function foo() {
    let a = 'glows';
    function fo() {
        console.log(a);
    }
    return fo;
}

function f(func) {
    let a = '我来了';
    func();
}

f(foo());
// 'glows'

  - 在函数foo()中,return了一个函数fo(),所以,fo()是闭包,那么,f(foo())传入的参数就是函数fo(),因为fo()的上级作用域就是foo(),所以输出'glows'
var n = 10
function fn(){
    var n =20
    function f() {
       n++;
       console.log(n)
     }
    f()
    return f
}

var x = fn()
x()
x()
console.log(n)
/* 输出
*  21
    22
    23
    10
/
  • 第1,2,3的输出,因为存在闭包,所以访问的都是用一个n,所以会叠加

闭包中的this

  • this对象是运行时基于函数的执行环境绑定的。全局函数中,this指向 window,当函数被作用某个对象的方法调用时,this指向这个对象,不过匿名函数的执行环境具有全局性,因此其this对象通常指向window
// 情况1
var name = 'window';

let obj = {
    name: 'obj',
    getName: function() {
        return function() {
            return this.name;
        }
    }
}

console.log(obj.getName()()); // 'window',非严格模式 ,此时,是先调用obj.getName(),返回了一个匿名函数,再执行该匿名函数,此时,作用域是window,匿名函数的this指向window


// 情况2
var name = 'window';

let obj = {
    name: 'obj',
    getName: function() {
        let that = this; // 保存到另外的变量
        return function() {
            return that.name;
        }
    }
}

console.log(obj.getName()()); // 'obj'
  • 情况1,为什么匿名函数没有取得包含作用域的this对象,?每个函数在被调用时会自动获取两个特殊的变量: this, arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数的这两个变量
  • 情况2,把外部作用域中的 this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了,此时闭包return出去后,that还是指向obj这个对象,所以返回obj
  • 永远记住,this指向作用域!!!

柯里化函数

  • 对于已经柯里化后的 _fn 函数来说,当接收的参数数量与原函数的形参数量相同时,执行原函数; 当接收的参数数量小于原函数的形参数量时,返回一个函数用于接收剩余的参数,直至接收的参数数量与形参数量一致,执行原函数
  • 一般,柯里化用于提高函数的自由度,比如在封装验证电话号码,邮箱等
function checkByRegExp(regExp, string) { 
    return regExp.test(string); 
} 
checkByRegExp(/^1\d{10}$/, '18642838455');  // 校验电话号码 
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com'); // 校验邮箱


// 如果需要验证多个邮箱或者电话,则需要多次传入一样的正则
checkByRegExp(/^1\d{10}$/, '18642838455');
checkByRegExp(/^1\d{10}$/, '13109840560'); 
checkByRegExp(/^1\d{10}$/, '13204061212'); 

// 通过柯里化函数,我们可以简化代码
let _check = currying(checkByRegExp); // currying函数会将checkByRegExp函数柯里化
let checkPhone = _check(/^1\d{10}$/);
let checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);

// 调用的话,就这样调用
checkPhone('18642838455'); // 校验电话号码 
checkPhone('13109840560'); // 校验电话号码 
checkPhone('13204061212'); // 校验电话号码 
checkEmail('test@163.com'); // 校验邮箱 
checkEmail('test@qq.com'); // 校验邮箱 
checkEmail('test@gmail.com'); // 校验邮箱

如何实现

  • 通过函数的 length 属性,获取函数的形参个数,形参的个数就是所需的参数个数
  • 在调用柯里化工具函数时,手动指定所需的参数个数
     /**
       * 柯里化函数
       * @param { Function } func 柯里化原函数
       * @param { Number } len 原函数需要的参数
       * @param { ? } holder 占位符,表示这个位置的参数由下一次调用的参数来填充
       * @return { Function } 柯里化的新函数
       */
      function currying(func, len = func.length, holder = currying) {
        return _curry.call(this, func, len, holder, [], []);
      }
    
      /**
       * 中转函数
       * @param fn            柯里化的原函数
       * @param length        原函数需要的参数个数
       * @param holder        接收的占位符
       * @param args          已接收的参数列表
       * @param holders       已接收的占位符位置(索引)列表
       * @return { function }   继续柯里化的函数 或 最终结果
       */
      function _curry(func, len, holder, args, holders) {
        return function (..._args) { // 此时的_args,代表的是柯里化后第1, 2 ,3,4....次调用时输入的参数
          let params = args.slice(); // 拷贝一份参数列表,防止污染原参数列表,传给下一次调用的参数为这个
          let _holders = holders.slice(); // 拷贝一份占位符位置列表,传给下一次调用的占位符位置参数为这个
          _args.forEach((arg) => {
            if (arg !== holder && holders.length) { // 非占位符, 上一次输入的参数中存在占位符,将占位符替换为参数
              let index = holders.shift();
              _holders.splice(_holders.indexOf(index), 1);
              params[index] = arg; 
            } else if (arg !== holder && !holders.length) { // 非占位符,且上一次输入的参数中不存在占位符, 直接添加参数
              params.push(arg);
            } else if (arg === holder && !holders.length) { // 占位符,且上一次输入的参数中没有占位符, 将占位符添加到新参数列表以及占位符列表,等待下一次调用时处理
              params.push(arg);
              _holders.push(params.length - 1);
            } else if (arg === holder && holders.length) { 
            // 占位符,且上一次输入的参数中有占位符,删除holders中该占位符的位置,表示上一次调用输入的占位符参数,由下一次调用来填充,我先删掉用于本轮调用中 记录上次调用占位符位置的变量(holders),方便我接下来的循环处理本轮调用输入的其他参数
            // 因为本轮输入的参数,也是占位符,所以本轮调用无法填充这个位置,因此删除的是用于本轮调用时 记录上次调用(爸爸)占位符位置的变量(仅删除holders,_holders不能删),然后在下一次调用时,如果输入的参数是非占位符,就要根据下一次循环传入的参数(也就是本轮调用的 _holders变量记录的位置,这也就是_holders不能删的原因)替换上一次调用时的输入的占位符
            // 可以这么理解:爸爸输入的占位符,来到我这里,本来是我来处理,结果我输入的也是占位符,那就丢给儿子去处理,爸爸先把其他的参数填充好
              holders.shift(); 
            }
          });
          if (params.length >= len && params.slice(0, len).every(i => i != holder)) return func.apply(this, params);
          else return _curry.call(this, func, len, holder, params, _holders);
        };
      }
    
      // 测试
      const myPrint = function(a, b, c, d, e) {
        console.log([a, b, c, d, e]);
      };
    
      let _ = {};
      let _myPrint = currying(myPrint, 5, _); 
      _myPrint(1, 2, 3, 4, 5); // [1, 2, 3, 4, 5]
      _myPrint(1, 2, 3, 4, _)(5); // [1, 2, 3, 4, 5]
      _myPrint(1, 2, 3, _, 5)(4); // [1, 2, 3, 4, 5]
      _myPrint(1, _, 3)(_, 4,_)(2)(5); // [1, 2, 3, 4, 5]
    

模拟私有变量

  • JS本质上是没有私有成员的概念,所有的对象属性都是公有的,不过,有私有变量的概念
  • 可以说,任何定义在函数或者块,外部无法访问的变量,都是私有变量,包括函数参数,局部变量,函数内部定义的其他函数等

ES5实现私有变量

 // 非静态私有变量
     function Person(name) {
        this.getName = function () {
          return name;
        };
        this.setName = function (newName) {
          name = newName;
        };
      }
      let person = new Person("John");
      console.log('person1 ', person.getName()); // John
      person.setName("Jane");
      console.log('person1 ' ,person.getName()); // Jane
      let person2 = new Person("James");
      console.log('person2: ' ,person2.getName()); // James
      person2.setName("Jim");
      console.log('person2: ' ,person2.getName()); // Jim
      person.setName("Jimmy");
      console.log('person1: ' ,person.getName(), 'person2: ' ,person2.getName()); // Jimmy Jim
      
 // 静态私有变量
       (function () {
       // 私有变量和函数
        let name = "";
        let age = "";
        function privateMethods() {
            return false;
        }
        
        // 构造函数 -> 这里是关键,不用关键字创造
        Person = function (value) {
          name = value;
        };
        // 公有方法
        Person.prototype.setAge = function (value) {
          age = value;
        };
        Person.prototype.getAge = function () {
          return age;
        };
        Person.prototype.getName = function () {
          return name;
        };
        Person.prototype.setName = function (newName) {
          name = newName;
        };
      })();

      let person1 = new Person("张三");
      person1.setAge(18);
      console.log(person1.getName(), person1.getAge()); // 张三,18
      person1.setName("李四");
      console.log(person1.getName()); // 李四
      
      let person2 = new Person("王五");
      console.log(person2.getName(), person2.getAge()); // 王五,18
      
      
      person2.setAge(20);
      console.log('person1: ', person1.getAge(),'person2: ',person2.getAge());  // 会发现,person1和person2的age都变化了,因为这是静态的属性,所以,修改一个,会影响全部(原型链上的)
  • 非静态的私有变量,对于每一个实例而言,都是独一无二的,每次调用构造函数都会重新创建一套变量和方法,这样每个实例都是独立的属性,会比较耗费资源、
  • 静态私有变量,是全局的,所有实例共享这个变量,修改一个会影响所有,主要的实现思路是,用匿名函数表达式创建一个包含构造函数和方法的私有作用域,,然后声明Person没有使用任何关键字,因为不是用关键字创建的变量会创建在全局的作用域,这样就会使得Person变成全局变量,可以在这个私有作用域外部被访问。(非严格模式)

ES6实现私有变量

  • 分别有约定写法,闭包写法,Symbol写法,WeakMap写法以及新提案写法

约定写法

  • 主要是约定通过下划线(_XXX)来标识为私有变量
  • 缺点:本质上,内外部都可以访问得到该变量,而且通过for in会将所有属性枚举出来
class A {
  constructor() {
    this._name = 'glows777'
  }
  getName() {
    return this._name;
  }
}
const a1 = new A();
console.log(a1.getName()); // "glows777"
console.log(a1._name); // "glows777" ,本质上,还是可以直接访问

闭包写法

  • 本质上,也是和ES5实现的方法一样,也有两种类型(静态和非静态),只不过是结合了class语法糖
非静态私有变量
  • 缺点
    • 挂载在实例上而非原型链上,浪费资源,无法通过super关键字调用
    • constructor 的逻辑变得复杂。构造函数应该只做对象初始化的事情,现在为了实现私有变量,必须包含部分方法的实现,代码组织上略不清晰
class A {
  constructor() {
    let _name = 'glows777';
    this.getName = function() {
      return _name;
    }
  }
}
const a1 = new A();
console.log(a1._name); // "undefined"
console.log(a1.getName()); // "glows777"
静态私有变量
const A = (function() {
  let _name = '';
  class helper {
    constructor(name) {
      _name = name;
    }
    getName() {
      return _name;
    }
  }
  return helper;
} ());
const a1 = new A("glows777");
console.log(a1._name); // "undefined"
console.log(a1.getName()); // "glows777"

const a2 = new A("Liam");
console.log(a2.getName()); // "Liam"
console.log(a1.getName()); // "Liam", 此时,a1,a2的name都被修改了,说明这个_name是个静态变量
/**
* 因为此时,在自执行函数闭包中,
* 始终指向的是同一个_name变量,所以所有实例始终指向的是同一个_name,修改了会影响全局!
*/

Symbol写法

  • 缺点:
    • 写法稍微复杂,但是无性能损失
    • 可能被Object.getOwnPropertySymbols()方法获取到,所以,并不完全是私有变量
const A = (function() {
  let _name = Symbol('name');
  class helper {
    constructor(name) {
      this[_name] = name;
    }
    getName() {
      return this[_name];
    }
  }
  return helper;
} ());
const a1 = new A("glows777");
console.log(a1._name); // "undefined"
console.log(a1.getName()); // "glows777"

const a2 = new A("Liam");
console.log(a2.getName()); // "Liam"
console.log(a1.getName()); // "glows777", 此时,两个实例的name属性是挂载在实例上的,是独立的,互不影响
/**
* 之所以是是非静态的,
* 因为每次指的是this,属于每个实例的属性,
* 但是,因为Symbol的唯一性,所以,外界无法拿到
*/

let b = Object.getOwnPropertySymbols(a1); // 获取Symbol类型的变量
console.log(a1[b[0]]); // "glows777", 可以获取到私有变量name

WeakMap写法

  • 缺点:
    • 写法复杂,兼容性不好,有一定的性能代价
const A = (function() {
  let _privateStorage = new WeakMap();
  class helper {
    constructor(name) {
      _privateStorage.set(this, { name }); // 一般是存储为对象(方便存储多个私有)
    }
    getName() {
      return _privateStorage.get(this).name;
    }
  }
  return helper;
} ());
const a1 = new A("glows777");

console.log(a1._name); // "undefined"
console.log(a1.getName()); // "glows777"

const a2 = new A("Liam");
console.log(a2.getName()); // "Liam"
console.log(a1.getName()); // "glows777"

新提案

  • 用#XXX(变量前面加一个#来标识)
class Point {
  #x;
  #y;

  constructor(x, y) {
    this.#x = x;
    this.#y = y;
  }

  equals(point) {
    return this.#x === point.#x && this.#y === point.#y;
  }
}

由于个人疏忽原因,学习以上内容时忘记标记学习的文章,博客出处,因此只能在文章结尾对网上各位大佬输出的博客致以感谢和歉意😭😭😭