从面向对象的角度理解javascript闭包

in JavaScript with 0 comment

面向对象

一般做过后端,熟悉面向对象的同学,比如java, php 这种面向对象的语言写一个类,一般是这样,以php为例

class Person {
  //公有属性name
  public $name; 
  //私有属性age
  protected $age;
  //保护属性
  private $height;
  //静态属性
  public static $static_val = 'php object';

  //构造方法
  public function __construct($name, $age){
    $this->name = $name;
    $this->age = $age;
  }
  //公有方法,这里对应javascript闭包的工厂方法
  public function getUserInfo(){
    echo 'name is: '. $this->name. ' and age is: '.$this->age . '<br/>'; 
  }
  //私有方法,通过getPrivatePropsAndPrivateMethod暴露出去, 类似javascript的模块模式
  private function getPrivateUserInfo(){
    echo 'name is: '. $this->name. ' and age is: '.$this->age . '<br/>'; 
  } 
  public function getPrivatePropsAndPrivateMethod(){
    $this->getPrivateUserInfo();
    //私有属性通过 getPrivatePropsAndPrivateMethod 函数暴露出去, 类似javascript的模块模式
    $this->height = 180;
    echo 'height is: '. $this->height;
  }
  
}
$p1 = new Person('wang', '22');
$p1->getUserInfo();

$p2 = new Person('zhang', '32');
$p2->getUserInfo();

$p3 = new Person('ma', '29');
$p3->getPrivatePropsAndPrivateMethod();

先上官网概念:

一个闭包,就是 一个函数 与其 被创建时所带有的作用域对象 obj = {} 的组合。闭包允许你保存状态——所以,它们可以用来代替对象

javascript 闭包其实就是想实现传统面向对象要实现的功能,知乎有人这样评价 javascript 到底算不算是面向对象的语言

面向对象本质上就是函数作用域的一种封装。函数的闭包和对象原型链,就可以实现类似java的面向对象,闭包和原型链语法更加底层,也就是说更加强大和难理解。

MSDN: JavaScript是一种基于原型而不是基于类的基于对象(object-based)语言。正是由于这一根本的区别,其如何创建对象的层级结构以及对象的属性与属性值是如何继承的并不是那么清晰。

这样看来,js 的闭包其实是更加底层的东西,java/php 这种面向对象其实是包装的更贴近现实,更容易让人理解。既然 java/php 这种面向对象的写法更容易理解,那如果有面向对象编程经验的同学,那么借助面向对象来理解闭包其实是很有帮助的。

想要更详细的了解javascript面向对象及与传统面向对象语言的区别看这里,MSDN的解释真是非常经典

js 闭包场景

//闭包实例1
    function init() {
      var name = 'Mozilla'

      function displayName() {
        console.log(name)
      }
      displayName()
    }
    init()
    //闭包实例2
    function makeFunc() {
      var name = "Mozilla";

      function displayName() {
        console.log(name)
      }
      return displayName;
    }

    var myFunc = makeFunc();
    myFunc();

    //工厂模式
    function makeAdder(x) {
      return function (y) {
        return x + y;
      };
    }

    var add5 = makeAdder(5);
    var add10 = makeAdder(10);

    console.log(add5(2)); // 7
    console.log(add10(2)); // 12

    //模块模式,类似对外暴露私有属性和方法

    var Counter = (function () {
      //私有属性(类似private)
      var privateCounter = 10
      //私有函数(类似private)
      function changeBy(val) {
        privateCounter += val
      }
      return {
        increment: function () {
          changeBy(1)
        },
        decrement: function () {
          changeBy(-1)
        },
        value: function () {
          return privateCounter
        }
      }
    })()
    console.log(Counter.value())
    Counter.increment()
    Counter.increment()
    Counter.decrement(-1)
    console.log(Counter.value())


    //模块模式,类似类的实例化2个对象
    var makeCounter = function(){
       //私有属性(类似private int privateCounter = 100)
       var privateCounter = 100
       //私有函数(类似private function changeBy())
       function changeBy(val) {
         privateCounter += val
       }
       return {
         increment: function() {
           changeBy(1)
         },
         decrement: function() {
           changeBy(-1)
         },
         value: function() {
           return privateCounter
         }
       }
     }
     var counter1 = makeCounter()
     var counter2 = makeCounter()
     console.log(counter1.value())
     counter1.increment()
     counter1.increment()
     counter1.decrement(-1)
     console.log(counter1.value())

     var counter2 = makeCounter()
     console.log(counter2.value())
     counter2.increment()
     counter2.increment()
     console.log(counter2.value())

分析闭包运行机制和垃圾回收机制(makeAdder函数)

微信图片_20210311141657.jpg

js 闭包常见错误解释

    //err demo,这里用var声明的变量是全局的,因为var声明的变量要么是全局要么是函数级别的
    // var a = [];
    // for (var i = 0; i < 10; i++) {
    //   a[i] = function () {
    //     //闭包由函数及函数捕获的词法作用域里的变量组成,var声明的变量会被捕获
    //     //如果使用let,由于let是块级作用域,因此这里的闭包捕获的是当前块级作用域里的变量
    //     console.log(i);//这里的i当前函数中没有定义,向上一层作用域查找,只能找到for循环中的全局作用域了
    //   };
    // }

    //solution: pro
    var a = [];
    for (var i = 0; i < 10; i++) {
      a[i] = callBackClosure(i)
    }

    function callBackClosure(i) {
      //这里形成闭包, 词法作用域是callBackClosure这个函数内,这个i是传递过来的i
      return function () {
        console.log(i)
      }
    }

    // solution: pro max
    // var a = [];
    // for (var i = 0; i < 10; i++) {
    //   (function(i){//这里的i就是刚才传递进来的
    //     //这里形成闭包,词法作用域是这个匿名函数内,这里的i是通过匿名函数传进来的,i在整个匿名函数内可引用
    //     a[i] = function() {
    //     console.log(i) //这里的i在当前函数中没有定义,向上一层作用域找即找到通过形参传递进来的变量
    //     }
    //   })(i) //立即执行函数的值是从这里传进去的
    // }

    //调用a[6]()函数时,循环已经运行完毕
    a[6](); //10

上边这个题是比较经典的闭包问题,其它严格来说用var的for循环这个例子不是闭包问题,其实是因为var声明的变量没有块(Block)级作用域的结果,以下debug截图可以很清楚的看到这一点,换成let才是闭包问题,因为let声明了个块级作用域(Block),这里闭包函数的词法作用域对象hold住了当前的i变量

注: es5 之前js只有全局作用域和函数级作用域(Local)

1 var声明

QQ图片20210417170802.png

2 let 声明

QQ图片20210417171116.png

闭包的应用场景

  1. 创建单例对象
var CreateDiv = function (html) {
        this.html = html;
        this.init();
    };
    
    CreateDiv.prototype.init = function () {
        var div = document.createElement('div');
        div.innerHTML = this.html;
        document.body.appendChild(div);
    };
    //用立即执行函数构造一个闭包,hold住instance实例
    var ProxySingletonCreateDiv = (function () {
        var instance;
        return function (html) {
            if (!instance) {
                instance = new CreateDiv(html);
            }
            return instance;
        }
    })();
    
    //var a = new ProxySingletonCreateDiv('sven1');
    var b = new ProxySingletonCreateDiv('sven2');
    var c = new ProxySingletonCreateDiv('sven3');

    console.log(c === b); //true
  1. 虚拟代理 图片loading预加载
  // 创建本体对象,生成img标签,对外提供setSrc接口
    //这样myImage就作为一个全局变量来使用了
var myImage = (function() {
  //console.log('我就运行一次')
  var imgNode = document.createElement('img');
  document.body.appendChild(imgNode);
  return {
    setSrc: function (src) {
      //console.log('每次调用我就运行')
      //这里形成闭包引用imgNode
      imgNode.src = src;
    }
  }
})();

 // 引入代理对象,图片加载之前会有个loading.gif
 var proxyImage = (function () {
   //img 在这是一个临时对象,使用它的onload方法,来分发src属性
   //'use strict'
   var img = new Image();
   //img.src = 'https://zongliwei.com/usr/uploads/2021/04/1297596746.png'
   //console.log(this.src) //undefined
   console.log('我在这')
   img.onload = function () {
     //如果生成的img不指定src属性,则不会运行img.onload方法
    console.log('我xxxxx这')
    console.log(this.src)
    setTimeout(() => {
      //这样是直接替换上边生成的img标签里的src
      myImage.setSrc(this.src);
      }, 2000)
    // 这样是直接又生成了一个img元素标签append到body上
    // setTimeout( () => {
    //   document.body.appendChild(img);
    // },2000)
   }

   return {
     setSrc: function (src) {
      myImage.setSrc('loading.gif');
      img.src= src;
     }
   }
 })();
 // 通过代理得到图片
 proxyImage.setSrc('https://zongliwei.com/usr/uploads/2021/04/1297596746.png');

3. 记下函数调用次数

function after(times, callback) {
        //这里使用了闭包达到保存times变量的目的
        return function () {
          if(--times === 0) {
            callback()
          }
        }
      }
      let fn = after(3, function(){
        console.log('我被调用3次')
      })
      fn()
      fn()
      fn()

闭包的优点

可以减少全局作用域下的函数的数量,减少使用全局变量,通过创建内部变量,使得这些变量不能被外部随意修改,同时又可以通过指定的函数接口来操作[面向对象私有属性]

闭包的问题

  1. js闭包因为会hold住词法作用域内的变量,需要依靠垃圾回收机制回收内存,老IE上容易造成内存泄露
  2. 比较占用内存等资源,不容易理解,MSDN上这样说

如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。

评论已关闭.