<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
    <channel>
            <title>金梦乐章</title>
            <link>https://jupiter-smile.cn:666</link>
        <generator>Halo 1.5.4</generator>
        <lastBuildDate>Mon, 14 Aug 2023 22:51:39 CST</lastBuildDate>
                <item>
                    <title>
                        <![CDATA[迪士尼乐园]]>
                    </title>
                    <link>https://jupiter-smile.cn:666/archives/di-shi-ni-le-yuan</link>
                    <description>
                            <![CDATA[<p><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E8%BF%AA%E5%A3%AB%E5%B0%BC%E7%94%9C%E5%BF%83%E5%B1%8B.png" alt="迪士尼甜心屋" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E8%BF%AA%E5%A3%AB%E5%B0%BC%E7%B3%96%E6%9E%9C%E5%B1%8B.png" alt="迪士尼糖果屋" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E8%BF%AA%E5%A3%AB%E5%B0%BC%E5%B0%8F%E9%A9%AC%E5%9C%BA.png" alt="迪士尼小马场" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E8%BF%AA%E5%A3%AB%E5%B0%BC%E6%9C%AA%E6%9D%A5%E4%B8%96%E7%95%8C.png" alt="迪士尼未来世界" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E8%BF%AA%E5%A3%AB%E5%B0%BC%E5%B0%8F%E9%A3%9E%E8%B1%A1.png" alt="迪士尼小飞象" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E8%BF%AA%E5%A3%AB%E5%B0%BC%E5%B7%A1%E6%B8%B8.png" alt="迪士尼巡游" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E8%BF%AA%E5%A3%AB%E5%B0%BC%E6%B8%B8%E8%88%B9.png" alt="迪士尼游船" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E8%BF%AA%E5%A3%AB%E5%B0%BC%E5%A4%9C%E6%99%AF.png" alt="迪士尼夜景" /></p>]]>
                    </description>
                    <pubDate>Sat, 10 Jun 2023 21:13:29 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[原神爱好者]]>
                    </title>
                    <link>https://jupiter-smile.cn:666/archives/yuan-shen-ai-hao-zhe</link>
                    <description>
                            <![CDATA[<p><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E5%8E%9F%E7%A5%9E.png" alt="原神" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E5%8E%9F%E7%A5%9E%E6%88%90%E5%B0%B1.png" alt="原神成就" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E5%8E%9F%E7%A5%9E%E8%A7%92%E8%89%B21.png" alt="原神角色1" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E5%8E%9F%E7%A5%9E%E8%A7%92%E8%89%B22.png" alt="原神角色2" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E5%8E%9F%E7%A5%9E%E8%A3%85%E5%A4%871.png" alt="原神装备1" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E5%8E%9F%E7%A5%9E%E8%A3%85%E5%A4%872.png" alt="原神装备2" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E5%8E%9F%E7%A5%9E%E5%8F%AF%E8%8E%89.png" alt="原神可莉" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E5%8E%9F%E7%A5%9E%E7%BB%AE%E8%89%AF%E8%89%AF.png" alt="原神绮良良" /></p>]]>
                    </description>
                    <pubDate>Sun, 04 Jun 2023 02:12:46 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[我的小家]]>
                    </title>
                    <link>https://jupiter-smile.cn:666/archives/wo-de-xiao-jia</link>
                    <description>
                            <![CDATA[<p><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E6%8A%B1%E6%9E%95.png" alt="抱枕" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E5%85%AC%E4%BB%94.png" alt="公仔" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E5%AE%B6.png" alt="家" /></p>]]>
                    </description>
                    <pubDate>Sun, 04 Jun 2023 01:47:15 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[我的手绘]]>
                    </title>
                    <link>https://jupiter-smile.cn:666/archives/wo-de-shou-hui</link>
                    <description>
                            <![CDATA[<p><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E4%BC%8A%E5%B8%83%E6%89%8B%E7%BB%98.png" alt="伊布手绘" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E9%98%BF%E7%8B%B8%E6%89%8B%E7%BB%98.png" alt="阿狸手绘" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E6%9D%B0%E5%B0%BC%E6%89%8B%E7%BB%98.png" alt="杰尼手绘" /><br /><img src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E5%B0%8F%E9%BB%91%E5%8F%8A%E7%B1%B3%E8%80%81%E9%BC%A0%E6%89%8B%E7%BB%98.png" alt="小黑及米老鼠手绘" /></p>]]>
                    </description>
                    <pubDate>Sun, 04 Jun 2023 01:39:17 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[海昌海洋公园表演]]>
                    </title>
                    <link>https://jupiter-smile.cn:666/archives/hai-chang-hai-yang-gong-yuan-biao-yan</link>
                    <description>
                            <![CDATA[<div style="position: relative; top:50px; padding-top: 550px;"><iframe style="position: absolute; width: 100%; height: 100%; left: 0; top: 0;"src="https://jupiter-1304905571.cos.ap-shanghai.myqcloud.com/%E6%B5%B7%E6%B4%8B%E5%85%AC%E5%9B%AD%E8%A1%97%E5%A4%B4%E8%A1%A8%E6%BC%94.mp4" nscrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe></div>]]>
                    </description>
                    <pubDate>Sun, 04 Jun 2023 01:30:19 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[设计模式（下）]]>
                    </title>
                    <link>https://jupiter-smile.cn:666/archives/she-ji-mo-shi--xia-</link>
                    <description>
                            <![CDATA[<h1 id="%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%88%E4%B8%8B%EF%BC%89" tabindex="-1">设计模式（下）</h1><p>本篇是《JavaScript 设计模式与开发实践》第二部分读书笔记，总结后 7 种设计模式：模板方法模式、享元模式、职责链模式、中介者模式、装饰者模式、状态模式、适配器模式。</p><h2 id="%E6%A8%A1%E6%9D%BF%E6%96%B9%E6%B3%95%E6%A8%A1%E5%BC%8F" tabindex="-1">模板方法模式</h2><p>模板方法模式是一种只需使用继承就可以实现的非常简单的模式。</p><p>模板方法模式由两部分结构组成，第一部分是抽象父类，第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架，包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类，也继承了整个算法结构，并且可以选择重写父类的方法。</p><h3 id="coffee-or-tea" tabindex="-1">Coffee or Tea</h3><p>以下是一个冲泡咖啡或茶的例子，这里将咖啡与茶抽象为“饮料”，冲泡和浸泡抽象为“泡”，牛奶和柠檬抽象为“调料”。</p><pre><code class="language-javascript">var Beverage = function() {}Beverage.prototype.boilWater = function() {  console.log(&#39;把水煮沸&#39;)}Beverage.prototype.brew = function() {} // 空方法，应该由子类重写Beverage.prototype.pourInCup = function() {} // 空方法，应该由子类重写Beverage.prototype.addCondiments = function() {} // 空方法，应该由子类重写Beverage.prototype.init = function() {  this.boilWater()  this.brew()  this.pourInCup()  this.addCondiments()}var Coffee = function() {}Coffee.prototype = new Beverage()Coffee.prototype.brew = function() {  console.log(&#39;用沸水冲泡咖啡&#39;)}Coffee.prototype.pourInCup = function() {  console.log(&#39;把咖啡倒进杯子&#39;)}Coffee.prototype.addCondiments = function() {  console.log(&#39;加糖和牛奶&#39;)}var Coffee = new Coffee()Coffee.init()var Tea = function() {}Tea.prototype = new Beverage()Tea.prototype.brew = function() {  console.log(&#39;用沸水浸泡茶叶&#39;)}Tea.prototype.pourInCup = function() {  console.log(&#39;把茶倒进杯子&#39;)}Tea.prototype.addCondiments = function() {  console.log(&#39;加柠檬&#39;)}var tea = new Tea()tea.init()</code></pre><p>上面例子中，真正的模板方法是 <code>Beverage.prototype.init</code>，它被称为模板方法的原因是，该方法中封装了子类的算法框架，它作为一个算法的模板，指导子类以何种顺序去执行哪些方法。</p><h3 id="%E6%8A%BD%E8%B1%A1%E7%B1%BB" tabindex="-1">抽象类</h3><p>模板方法模式是一种严重依赖抽象类的设计模式。JavaScript 在语言层面并没有提供对抽象类的支持，也很难模拟抽象类的实现，这里以 Java 中的抽象类讨论。</p><p>在 Java 中，类分为两种，一种为具体类，另一种为抽象类。具体类可以被实例化，抽象类不能被实例化。抽象类和接口一样可以用于向上转型，把对象的真正类型隐藏在抽象类或者接口之后，这些对象才可以被互相替换使用，这可以让 Java 程序尽量遵守依赖倒置原则。</p><p>除了用于向上转型，抽象类也可以表示一种契约。继承了这个抽象类的所有子类都将拥有跟抽象类一致的接口方法，抽象类的主要作用就是为它的子类定义这些公共接口。</p><p>抽象方法被声明在抽象类中，抽象方法并没有具体的实现过程，是一些“哑”方法。除了抽象方法之外，如果每个子类中都有一些同样的具体实现方法，那这些方法也可以选择放在抽象类中，这可以节省代码以达到复用的效果，这些方法叫作具体方法。</p><p>JavaScript 并没有从语法层面提供对抽象类的支持。抽象类的第一个作用是隐藏对象的具体类型，由于 JavaScript 是一门“类型模糊”的语言，所以隐藏对象的类型在 JavaScript 中并不重要。另一方面，当在 JavaScript 中使用原型继承来模拟传统的类式继承时，并没有编译器进行任何形式的检查，没有办法保证子类会重写父类中的“抽象方法”。</p><p>下面提供两种变通的解决方案。</p><ul><li>第 1 种方案是用鸭子类型来模拟接口检查，以便确保子类中确实重写了父类的方法。但模拟接口检查会带来不必要的复杂性，而且要求程序员主动进行这些接口检查，这就要求在业务代码中添加一些跟业务逻辑无关的代码。</li><li>第 2 种方案是让抽象方法方法直接抛出一个异常，如果因为粗心忘记编写子类方法，那么至少会在程序运行时得到一个错误，这种方式实现简单，缺点是得到错误信息的时间点太靠后。</li></ul><h3 id="%E9%92%A9%E5%AD%90%E6%96%B9%E6%B3%95" tabindex="-1">钩子方法</h3><p>通过模板方法模式，在父类中封装了子类的算法框架，但如果父类的算法框架并不适用全部子类，那便需要用到钩子方法，放置钩子是隔离变化的一种常见手段。</p><p>在父类中容易变化的地方放置钩子，钩子可以有一个默认的实现，究竟要不要“挂钩”，这由子类自行决定。钩子方法的返回结果决定了模板方法后面部分的执行步骤，也就是程序接下来的走向，这样一来，程序就拥有了变化的可能。</p><p>依旧以上面的 Coffee or Tea 代码为例，改写模板方法：</p><pre><code class="language-javascript">Beverage.prototype.customerWantsCondiments = function() {  return true // 默认需要调料}Beverage.prototype.init = function() {  this.boilWater()  this.brew()  this.pourInCup()  if (this.customerWantsCondiments()) {    this.addCondiments()  }}// 省略其他代码...Coffee.prototype.customerWantsCondiments = function() {  return window.confirm(&#39;请问需要调料吗？&#39;)}</code></pre><h3 id="%E5%A5%BD%E8%8E%B1%E5%9D%9E%E5%8E%9F%E5%88%99" tabindex="-1">好莱坞原则</h3><p>好莱坞原则指的是，允许底层组件将自己挂钩到高层组件中，而高层组件会决定什么时候、以何种方式去使用这些底层组件，高层组件对待底层组件的方式，跟演艺公司对待新人演员一样，都是“别调用我们，我们会调用你”。</p><p>模板方法模式是好莱坞原则的一个典型使用场景，它与好莱坞原则的联系非常明显，当我们用模板方法模式编写一个程序时，就意味着子类放弃了对自己的控制权，而是改为父类通知子类，哪些方法应该在什么时候被调用。作为子类，只负责提供一些设计上的细节。</p><p>除此之外，好莱坞原则还常常应用于其他模式和场景，例如发布-订阅模式和回调函数。</p><p>在发布—订阅模式中，发布者会把消息推送给订阅者，取代了原先不断去 fetch 消息的形式。在回调函数中，如 ajax 请求，当数据返回之后，回调函数才被执行，取代了时刻轮询判断是否返回了数据。</p><h3 id="%E7%9C%9F%E7%9A%84%E9%9C%80%E8%A6%81%E2%80%9C%E7%BB%A7%E6%89%BF%E2%80%9D%E5%90%97" tabindex="-1">真的需要“继承”吗</h3><p>模板方法模式是基于继承的一种设计模式，父类封装了子类的算法框架和方法的执行顺序，子类继承父类之后，父类通知子类执行这些方法，好莱坞原则很好地诠释了这种设计技巧，即高层组件调用底层组件。</p><p>模板方法模式是为数不多的基于继承的设计模式，但 JavaScript 语言实际上没有提供真正的类式继承，继承是通过对象与对象之间的委托来实现的。</p><p>下面改写上面的 Coffee or Tea 的例子：</p><pre><code class="language-javascript">var Beverage = function(param) {  var boilWater = function() {    console.log(&#39;把水煮沸&#39;)  }  var brew =    param.brew ||    function() {      throw new Error(&#39;必须传递 brew 方法&#39;)    }  var pourInCup =    param.pourInCup ||    function() {      throw new Error(&#39;必须传递 pourInCup 方法&#39;)    }  var addCondiments =    param.addCondiments ||    function() {      throw new Error(&#39;必须传递 addCondiments 方法&#39;)    }  var F = function() {}  F.prototype.init = function() {    boilWater()    brew()    pourInCup()    addCondiments()  }  return F}var Coffee = Beverage({  brew: function() {    console.log(&#39;用沸水冲泡咖啡&#39;)  },  pourInCup: function() {    console.log(&#39;把咖啡倒进杯子&#39;)  },  addCondiments: function() {    console.log(&#39;加糖和牛奶&#39;)  }})var coffee = new Coffee()coffee.init()</code></pre><p>模板方法模式是一种典型的通过封装变化提高系统扩展性的设计模式。但在 JavaScript 中，很多时候都不需要依样画瓢地去实现一个模版方法模式，高阶函数是更好的选择。</p><h2 id="%E4%BA%AB%E5%85%83%E6%A8%A1%E5%BC%8F" tabindex="-1">享元模式</h2><p>享元（flyweight）模式是一种用于性能优化的模式，“fly”在这里是苍蝇的意思，意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。</p><h3 id="%E5%86%85%E9%83%A8%E7%8A%B6%E6%80%81%E4%B8%8E%E5%A4%96%E9%83%A8%E7%8A%B6%E6%80%81" tabindex="-1">内部状态与外部状态</h3><p>享元模式要求将对象的属性划分为内部状态与外部状态。享元模式的目标是尽量减少共享对象的数量，关于如何划分内部状态和外部状态，下面的几条经验提供了一些指引：</p><ul><li>内部状态存储于对象内部。</li><li>内部状态可以被一些对象共享。</li><li>内部状态独立于具体的场景，通常不会改变。</li><li>外部状态取决于具体的场景，并根据场景而变化，外部状态不能被共享。</li></ul><p>这样一来，我们便可以把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态可以从对象身上剥离出来，并储存在外部。剥离了外部状态的对象成为共享对象，外部状态在必要时被传入共享对象来组装成一个完整的对象。虽然组装外部状态成一个完整对象需要时间，但可以大大减少系统中对象数量，所以说享元模式是一种时间换空间的优化模式。</p><p>通常来讲，内部状态有多少种组合，系统中便最多存在多少个对象。使用享元模式的关键是如何区别内部状态和外部状态。可以被对象共享的属性通常被划分为内部状态，而外部状态取决于具体的场景，并根据场景而变化。</p><p>这里以文件上传为例，文件上传方式为内部状态，文件名和文件大小为外部状态：</p><pre><code class="language-javascript">var Upload = function(uploadType) {  this.uploadType = uploadType}Upload.prototype.delFile = function(id) {  uploadManager.setExternalState(id, this) // 通过管理器设置外部状态  if (window.confirm(&#39;确定要删除该文件吗? &#39; + this.fileName)) {    return this.dom.parentNode.removeChild(this.dom)  }}// 上传对象创建工厂，如果内部状态对象已被创建，则直接返回var UploadFactory = (function() {  var createdFlyWeightObjs = {}  return {    create: function(uploadType) {      if (createdFlyWeightObjs[uploadType]) {        return createdFlyWeightObjs[uploadType]      }      return (createdFlyWeightObjs[uploadType] = new Upload(uploadType))    }  }})()// 管理器负责管理外部状态var uploadManager = (function() {  var uploadDatabase = {} // 保存所有 uoload 对象外部状态  return {    add: function(id, uploadType, fileName, fileSize) {      var flyWeightObj = UploadFactory.create(uploadType)      var dom = document.createElement(&#39;div&#39;)      dom.innerHTML =        &#39;&lt;span&gt;文件名称:&#39; +        fileName +        &#39;, 文件大小: &#39; +        fileSize +        &#39;&lt;/span&gt;&#39; +        &#39;&lt;button class=&quot;delFile&quot;&gt;删除&lt;/button&gt;&#39;      dom.querySelector(&#39;.delFile&#39;).onclick = function() {        flyWeightObj.delFile(id)      }      document.body.appendChild(dom)      uploadDatabase[id] = { fileName: fileName, fileSize: fileSize, dom: dom }      return flyWeightObj    },    setExternalState: function(id, flyWeightObj) {      var uploadData = uploadDatabase[id]      for (var i in uploadData) {        flyWeightObj[i] = uploadData[i]      }    }  }})()var id = 0window.startUpload = function(uploadType, files) {  for (var i = 0, file; (file = files[i++]); ) {    var uploadObj = uploadManager.add(++id, uploadType, file.fileName, file.fileSize)  }}</code></pre><p>享元模式是一种很好的性能优化方案，但它也会带来一些复杂性的问题，上面代码的比较可以看到，使用了享元模式之后，需要分别多维护一个 factory 对象和一个 manager 对象，在大部分不必要使用享元模式的环境下，这些开销是可以避免的。</p><p>享元模式带来的好处很大程度上取决于如何使用以及何时使用，一般来说，以下情况发生时便可以使用享元模式。</p><ul><li>一个程序中使用了大量的相似对象。</li><li>由于使用了大量对象，造成很大的内存开销。</li><li>对象的大多数状态都可以变为外部状态。</li><li>剥离出对象的外部状态之后，可以用相对较少的共享对象取代大量对象。</li></ul><h2 id="%E8%81%8C%E8%B4%A3%E9%93%BE%E6%A8%A1%E5%BC%8F" tabindex="-1">职责链模式</h2><p>职责链模式的定义是：使多个对象都有机会处理请求，从而避免请求的发送者和接收者之间的耦合关系，将这些对象连成一条链，并沿着这条链传递该请求，直到有一个对象处理它为止。</p><p>职责链模式的最大优点：请求发送者只需要知道链中的第一个节点，从而弱化了发送者和一组接收者之间的强联系。</p><h3 id="%E7%81%B5%E6%B4%BB%E5%8F%AF%E6%8B%86%E5%88%86%E7%9A%84%E8%81%8C%E8%B4%A3%E9%93%BE%E8%8A%82%E7%82%B9" tabindex="-1">灵活可拆分的职责链节点</h3><p>这里以购物预付订金为例，根据不同订金获取不同优惠券，如果某个节点不能处理请求，则返回一个特定的字符串 ‘nextSuccessor’ 来表示该请求需要继续往后面传递，这样一来，各个节点之间不再耦合，节点可以灵活拆分和重组。</p><pre><code class="language-javascript">var order500 = function(orderType, pay, stock) {  if (orderType === 1 &amp;&amp; pay === true) {    console.log(&#39;500元定金预购，得到100优惠券&#39;)  } else {    return &#39;nextSuccessor&#39;  }}var order200 = function(orderType, pay, stock) {  if (orderType === 2 &amp;&amp; pay === true) {    console.log(&#39;200元定金预购，得到50优惠券&#39;)  } else {    return &#39;nextSuccessor&#39;  }}var orderNormal = function(orderType, pay, stock) {  if (stock &gt; 0) {    console.log(&#39;普通购买，无优惠券&#39;)  } else {    console.log(&#39;手机库存不足&#39;)  }}var Chain = function(fn) {  this.fn = fn  this.successor = null}// Chain.prototype.setNextSuccessor  指定在链中的下一个节点Chain.prototype.setNextSuccessor = function(successor) {  return (this.successor = successor)}// Chain.prototype.passRequest  传递请求给某个节点Chain.prototype.passRequest = function() {  var ret = this.fn.apply(this, arguments)  if (ret === &#39;nextSuccessor&#39;) {    return this.successor &amp;&amp; this.successor.passRequest.apply(this.successor, arguments)  }  return ret}var chainOrder500 = new Chain(order500)var chainOrder200 = new Chain(order200)var chainOrderNormal = new Chain(orderNormal)chainOrder500.setNextSuccessor(chainOrder200)chainOrder200.setNextSuccessor(chainOrderNormal)chainOrder500.passRequest(1, true, 500) // 输出：500元定金预购，得到100优惠券chainOrder500.passRequest(1, false, 0) // 输出：手机库存不足</code></pre><h3 id="%E5%BC%82%E6%AD%A5%E7%9A%84%E8%81%8C%E8%B4%A3%E9%93%BE" tabindex="-1">异步的职责链</h3><p>上一节的职责链模式中，每个节点函数同步返回一个特定的值 “nextSuccessor” 来表示是否把请求传递给下一个节点。而在现实开发中，经常会遇到一些异步的问题，比如要在节点函数中发起一个 ajax 异步请求，异步请求返回的结果才能决定是否继续在职责链中 passRequest。</p><p>这时候让节点函数同步返回 “nextSuccessor” 已经没有意义了，所以要给 Chain 类再增加一个原型方法 Chain.prototype.next，表示手动传递请求给职责链中的下一个节点：</p><pre><code class="language-javascript">Chain.prototype.next = function() {  return this.successor &amp;&amp; this.successor.passRequest.apply(this.successor, arguments)}</code></pre><p>来看一个异步职责链的例子：</p><pre><code class="language-javascript">var fn1 = new Chain(function() {  console.log(1)  return &#39;nextSuccessor&#39;})var fn2 = new Chain(function() {  console.log(2)  var self = this  setTimeout(function() {    self.next()  }, 1000)})var fn3 = new Chain(function() {  console.log(3)})fn1.setNextSuccessor(fn2).setNextSuccessor(fn3)fn1.passRequest()</code></pre><p>现在得到了一个特殊的链条，请求在链中的节点里传递，但节点有权利决定什么时候把请求交给下一个节点。</p><h3 id="%E8%81%8C%E8%B4%A3%E9%93%BE%E6%A8%A1%E5%BC%8F%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9" tabindex="-1">职责链模式的优缺点</h3><p>职责链模式的最大优点就是解耦了请求发送者和 N 个接收者之间的复杂关系，由于不知道链中的哪个节点可以处理你发出的请求，所以你只需把请求传递给第一个节点即可。</p><p>其次，使用了职责链模式之后，链中的节点对象可以灵活地拆分重组。增加或者删除一个节点，或者改变节点在链中的位置都是轻而易举的事情。</p><p>职责链模式还有一个优点，那就是可以手动指定起始节点，请求并不是非得从链中的第一个节点开始传递。</p><p>但这种模式也并非没有弊端，首先我们不能保证某个请求一定会被链中的节点处理。</p><p>另外，职责链模式使得程序中多了一些节点对象，可能在某一次的请求传递过程中，大部分节点并没有起到实质性的作用，它们的作用仅仅是让请求传递下去，从性能方面考虑，我们要避免过长的职责链带来的性能损耗。</p><h3 id="aop-%E5%AE%9E%E7%8E%B0%E8%81%8C%E8%B4%A3%E9%93%BE" tabindex="-1">AOP 实现职责链</h3><p>在之前的职责链实现中，利用了一个 Chain 类来把普通函数包装成职责链的节点。其实利用 JavaScript 的函数式特性，有一种更加方便的方法来创建职责链。</p><p>实现一个 Function.prototype.after 函数，使得第一个函数返回 ‘nextSuccessor’ 时，将请求继续传递给下一个函数。</p><pre><code class="language-javascript">Function.prototype.after = function(fn) {  var self = this  return function() {    var ret = self.apply(this, arguments)    if (ret === &#39;nextSuccessor&#39;) {      return fn.apply(this, arguments)    }    return ret  }}var order = order500yuan.after(order200yuan).after(orderNormal)order(1, true, 500) // 输出：500元定金预购，得到100优惠券</code></pre><h2 id="%E4%B8%AD%E4%BB%8B%E8%80%85%E6%A8%A1%E5%BC%8F" tabindex="-1">中介者模式</h2><p>面向对象设计鼓励将行为分布到各个对象中，把对象划分成更小的粒度，有助于增强对象的可复用性，但由于这些细粒度对象之间的联系激增，又有可能会反过来降低它们的可复用性。</p><p>中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后，所有的相关对象都通过中介者对象来通信，而不是互相引用，所以当一个对象发生改变时，只需要通知中介者对象即可。中介者使各对象之间耦合松散，而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系。</p><p>这里以多人组队游戏为例，设置一个中介者管理状态：</p><pre><code class="language-javascript">function Player(name, teamColor) {  this.name = name // 角色名字  this.teamColor = teamColor // 队伍颜色  this.state = &#39;alive&#39; // 玩家生存状态}Player.prototype.win = function() {  console.log(this.name + &#39; won &#39;)}Player.prototype.lose = function() {  console.log(this.name + &#39; lost&#39;)}Player.prototype.die = function() {  this.state = &#39;dead&#39;  playerDirector.ReceiveMessage(&#39;playerDead&#39;, this) // 给中介者发送消息，玩家死亡}Player.prototype.remove = function() {  playerDirector.ReceiveMessage(&#39;removePlayer&#39;, this) // 给中介者发送消息，移除一个玩家}Player.prototype.changeTeam = function(color) {  playerDirector.ReceiveMessage(&#39;changeTeam&#39;, this, color) // 给中介者发送消息，玩家换队}// 玩家创建工厂var playerFactory = function(name, teamColor) {  var newPlayer = new Player(name, teamColor) // 创造一个新的玩家对象  playerDirector.ReceiveMessage(&#39;addPlayer&#39;, newPlayer) // 给中介者发送消息，新增玩家  return newPlayer}// 中介者var playerDirector = (function() {  var players = {}, // 保存所有玩家    operations = {} // 中介者可以执行的操作  /****** 新增一个玩家 *********/  operations.addPlayer = function(player) {    var teamColor = player.teamColor // 玩家的队伍颜色    players[teamColor] = players[teamColor] || [] // 如果该颜色的玩家还没有成立队伍，则新成立一个队伍    players[teamColor].push(player) // 添加玩家进队伍  }  /****** 移除一个玩家 *********/  operations.removePlayer = function(player) {    var teamColor = player.teamColor, // 玩家的队伍颜色      teamPlayers = players[teamColor] || [] //该队伍所有成员    for (var i = teamPlayers.length - 1; i &gt;= 0; i--) {      // 遍历删除      if (teamPlayers[i] === player) {        teamPlayers.splice(i, 1)      }    }  }  /******* 玩家换队 **********/  operations.changeTeam = function(player, newTeamColor) {    // 玩家换队    operations.removePlayer(player) // 从原队伍中删除    player.teamColor = newTeamColor // 改变队伍颜色    operations.addPlayer(player) // 增加到新队伍中  }  operations.playerDead = function(player) {    // 玩家死亡    var teamColor = player.teamColor,      teamPlayers = players[teamColor] // 玩家所在队伍    var all_dead = true    for (var i = 0, player; (player = teamPlayers[i++]); ) {      if (player.state !== &#39;dead&#39;) {        all_dead = false        break      }    }    if (all_dead === true) {      // 全部死亡      for (var i = 0, player; (player = teamPlayers[i++]); ) {        player.lose() // 本队所有玩家lose      }      for (var color in players) {        if (color !== teamColor) {          var teamPlayers = players[color] // 其他队伍的玩家          for (var i = 0, player; (player = teamPlayers[i++]); ) {            player.win() // 其他队伍所有玩家win          }        }      }    }  }  var ReceiveMessage = function() {    // arguments的第一个参数为消息名称    var message = Array.prototype.shift.call(arguments)    operations[message].apply(this, arguments)  }  return { ReceiveMessage: ReceiveMessage }})()</code></pre><p>中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则，是指一个对象应该尽可能少地了解另外的对象（类似不和陌生人说话）。如果对象之间的耦合性太高，一个对象发生改变之后，难免会影响到其他的对象，而在中介者模式里，对象之间几乎不知道彼此的存在，它们只能通过中介者对象来互相影响对方。</p><p>因此，中介者模式使各个对象之间得以解耦，以中介者和对象之间的一对多关系取代了对象之间的网状多对多关系。各个对象只需关注自身功能的实现，对象之间的交互关系交给了中介者对象来实现和维护。</p><p>不过，中介者模式也存在一些缺点。其中，最大的缺点是系统中会新增一个中介者对象，因为对象之间交互的复杂性，转移成了中介者对象的复杂性，使得中介者对象经常是巨大的。中介者对象自身往往就是一个难以维护的对象。</p><h2 id="%E8%A3%85%E9%A5%B0%E8%80%85%E6%A8%A1%E5%BC%8F" tabindex="-1">装饰者模式</h2><p>装饰者模式可以动态地给某个对象添加一些额外的职责，而不会影响从这个类中派生的其他对象。</p><p>在传统的面向对象语言中，给对象添加功能常常使用继承的方式，但是继承的方式并不灵活，还会带来许多问题：一方面会导致超类和子类之间存在强耦合性，当超类改变时，子类也会随之改变；另一方面，继承这种功能复用方式通常被称为“白箱复用”，“白箱”是相对可见性而言的，在继承方式中，超类的内部细节是对子类可见的，继承常常被认为破坏了封装性。</p><p>使用继承还会带来另外一个问题，在完成一些功能复用的同时，有可能创建出大量的子类，使子类的数量呈爆炸性增长。</p><p>为了解决上述问题，可以给对象动态地增加职责，这种方式称为装饰者（decorator）模式。装饰者模式能够在不改变对象自身的基础上，在程序运行期间给对象动态地添加职责。</p><h3 id="%E4%BC%A0%E7%BB%9F%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%AF%AD%E8%A8%80%E7%9A%84%E8%A3%85%E9%A5%B0%E8%80%85%E6%A8%A1%E5%BC%8F" tabindex="-1">传统面向对象语言的装饰者模式</h3><p>作为一门解释执行的语言，给 JavaScript 中的对象动态添加或者改变职责是一件再简单不过的事情，虽然这种做法改动了对象自身，跟传统定义中的装饰者模式并不一样，但这无疑更符合 JavaScript 的语言特色。</p><pre><code class="language-javascript">var Plane = function() {}Plane.prototype.fire = function() {  console.log(&#39;发射普通子弹&#39;)}var MissileDecorator = function(plane) {  this.plane = plane}MissileDecorator.prototype.fire = function() {  this.plane.fire()  console.log(&#39;发射导弹&#39;)}var plane = new Plane()plane = new MissileDecorator(plane)plane.fire() // 分别输出：发射普通子弹、发射导弹</code></pre><p>这种给对象动态增加职责的方式，并没有真正地改动对象自身，而是将对象放入另一个对象之中，这些对象以一条链的方式进行引用，形成一个聚合对象。这些对象都拥有相同的接口（fire 方法），当请求达到链中的某个对象时，这个对象会执行自身的操作，随后把请求转发给链中的下一个对象。</p><p>因为装饰者对象和它所装饰的对象拥有一致的接口，所以它们对使用该对象的客户来说是透明的，被装饰的对象也并不需要了解它曾经被装饰过，这种透明性使得我们可以递归地嵌套任意多个装饰者对象。</p><h3 id="javascript-%E7%9A%84%E8%A3%85%E9%A5%B0%E8%80%85%E6%A8%A1%E5%BC%8F" tabindex="-1">JavaScript 的装饰者模式</h3><p>JavaScript 语言动态改变对象相当容易，我们可以直接改写对象或者对象的某个方法，并不需要使用“类”来实现装饰者模式：</p><pre><code class="language-javascript">var plane = {  fire: function() {    console.log(&#39;发射普通子弹&#39;)  }}var missileDecorator = function() {  console.log(&#39;发射导弹&#39;)}var fire1 = plane.fireplane.fire = function() {  fire1()  missileDecorator()}plane.fire() // 分别输出：发射普通子弹、发射导弹</code></pre><h3 id="%E8%A3%85%E9%A5%B0%E8%80%85%E4%B9%9F%E6%98%AF%E5%8C%85%E8%A3%85%E5%99%A8" tabindex="-1">装饰者也是包装器</h3><p>在《设计模式》成书之前，GoF 原想把装饰者（decorator）模式称为包装器（wrapper）模式。从功能上而言，decorator 能很好地描述这个模式，但从结构上看，wrapper 的说法更加贴切。装饰者模式将一个对象嵌入另一个对象之中，实际上相当于这个对象被另一个对象包装起来，形成一条包装链。请求随着这条链依次传递到所有的对象，每个对象都有处理这条请求的机会。</p><h3 id="%E8%A3%85%E9%A5%B0%E5%87%BD%E6%95%B0" tabindex="-1">装饰函数</h3><p>在 JavaScript 中，几乎一切都是对象，其中函数又被称为一等对象。在 JavaScript 中可以很方便地给某个对象扩展属性和方法，但却很难在不改动某个函数源代码的情况下，给该函数添加一些额外的功能。</p><p>一般可以通过保存原引用的方式就可以改写某个函数：</p><pre><code class="language-javascript">window.onload = function() {  alert(1)}var _onload = window.onload || function() {}window.onload = function() {  _onload()  alert(2)}</code></pre><p>但是这种方式存在以下两个问题:</p><ul><li>必须维护中间变量，如果函数的装饰链较长，或者需要装饰的函数变多，这些中间变量的数量也会越来越多</li><li>this 容易被劫持的问题，如果把 window.onload 换成 document.getElementById 会报错</li></ul><p>通过使用 AOP，可以提供一种完美的方法给函数动态添加功能。</p><pre><code class="language-javascript">Function.prototype.before = function(beforefn) {  var __self = this // 保存原函数的引用  return function() {    // 返回包含了原函数和新函数的&quot;代理&quot;函数    beforefn.apply(this, arguments)    return __self.apply(this, arguments) // 执行原函数并返回原函数的执行结果，并且保证 this 不被劫持  }}Function.prototype.after = function(afterfn) {  var __self = this  return function() {    var ret = __self.apply(this, arguments)    afterfn.apply(this, arguments)    return ret  }}</code></pre><p>Function.prototype.before 接受一个函数当作参数，这个函数即为新添加的函数，它装载了新添加的功能代码。接下来把当前的 this 保存起来，这个 this 指向原函数，然后返回一个“代理”函数，这个“代理”函数只是结构上像代理而已，并不承担代理的职责（比如控制对象的访问等）。它的工作是把请求分别转发给新添加的函数和原函数，且负责保证它们的执行顺序，让新添加的函数在原函数之前执行（前置装饰），这样就实现了动态装饰的效果。通过 Function.prototype.apply 来动态传入正确的 this，保证了函数在被装饰之后，this 不会被劫持。</p><p>如此一来便可以给 document.getElementById 添加新功能并不会被劫持 this:</p><pre><code class="language-javascript">document.getElementById = document.getElementById.before(function() {  alert(1)})var button = document.getElementById(&#39;button&#39;)window.onload = function() {  alert(1)}window.onload = (window.onload || function() {})  .after(function() {    alert(2)  })  .after(function() {    alert(3)  })  .after(function() {    alert(4)  })</code></pre><p>上面的 AOP 实现是在 Function.prototype 上添加 before 和 after 方法，如果不喜欢这种污染原型的方式，那么可以做一些变通，把原函数和新函数都作为参数传入 be-fore 或者 after 方法：</p><pre><code class="language-javascript">var before = function(fn, beforefn) {  return function() {    beforefn.apply(this, arguments)    return fn.apply(this, arguments)  }}var a = before(  function() {    alert(3)  },  function() {    alert(2)  })a = before(a, function() {  alert(1)})a()</code></pre><h3 id="aop-%E5%BA%94%E7%94%A8" tabindex="-1">AOP 应用</h3><p>用 AOP 装饰函数的技巧在实际开发中非常有用。不论是业务代码的编写，还是在框架层面，都可以把行为依照职责分成粒度更细的函数，随后通过装饰把它们合并到一起，这有助于我们编写一个松耦合和高复用性的系统。</p><p>回顾上面 AOP 函数可以发现 beforefn 和原函数 _self 共用一组参数列表 arguments，当在 beforefn 的函数体内改变 arguments 的时候，原函数 _self 接收的参数列表自然也会变化。</p><pre><code class="language-javascript">var func = function(param) {  console.log(param) // 输出：{a: &quot;a&quot;, b: &quot;b&quot;}}func = func.before(function(param) {  param.b = &#39;b&#39;})func({ a: &#39;a&#39; })</code></pre><p>借此启发，可以使用此特性为网络请求动态设置 token：</p><pre><code class="language-javascript">var getToken = function() {  return &#39;Token&#39;}ajax = ajax.before(function(type, url, param) {  param.Token = getToken()})ajax(&#39;get&#39;, &#39;http:// xxx.com/userinfo&#39;, { name: &#39;sven&#39; })</code></pre><p>用 AOP 的方式给 ajax 函数动态装饰上 Token 参数，保证了 ajax 函数是一个相对纯净的函数，提高了 ajax 函数的可复用性，它在被迁往其他项目的时候，不需要做任何修改。</p><p>同理，也可以使用 AOP 在表单上传前做表单验证：</p><pre><code class="language-javascript">Function.prototype.before = function(beforefn) {  var __self = this  return function() {    // beforefn 返回 false 的情况直接 return，不再执行后面的原函数    if (beforefn.apply(this, arguments) === false) {      return    }    return __self.apply(this, arguments)  }}var validata = function() {  if (username.value === &#39;&#39;) {    alert(&#39;用户名不能为空&#39;)    return false  }  if (password.value === &#39;&#39;) {    alert(&#39;密码不能为空&#39;)    return false  }}var formSubmit = function() {  var param = {    username: username.value,    password: password.value  }  ajax(&#39;http:// xxx.com/login&#39;, param)}formSubmit = formSubmit.before(validata)submitBtn.onclick = function() {  formSubmit()}</code></pre><p>值得注意的是，因为函数通过 Function.prototype.before 或者 Function.prototype.after 被装饰之后，返回的实际上是一个新的函数，如果在原函数上保存了一些属性，那么这些属性会丢失：</p><pre><code class="language-javascript">var func = function() {  alert(1)}func.a = &#39;a&#39;func = func.after(function() {  alert(2)})alert(func.a) // 输出：undefined</code></pre><p>另外，这种装饰方式也叠加了函数的作用域，如果装饰的链条过长，性能上也会受到一些影响。</p><h3 id="%E8%A3%85%E9%A5%B0%E8%80%85%E6%A8%A1%E5%BC%8F%E5%92%8C%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F" tabindex="-1">装饰者模式和代理模式</h3><p>装饰者模式和代理模式的结构看起来非常相像，这两种模式都描述了怎样为对象提供一定程度上的间接引用，它们的实现部分都保留了对另外一个对象的引用，并且向那个对象发送请求。</p><p>代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是，当直接访问本体不方便或者不符合需要时，为这个本体提供一个替代者。本体定义了关键功能，而代理提供或拒绝对它的访问，或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对象动态加入行为。换句话说，代理模式强调一种关系（Proxy 与它的实体之间的关系），这种关系可以静态的表达，也就是说，这种关系在一开始就可以被确定。而装饰者模式用于一开始不能确定对象的全部功能时。代理模式通常只有一层代理-本体的引用，而装饰者模式经常会形成一条长长的装饰链。</p><p>在虚拟代理实现图片预加载的例子中，本体负责设置 img 节点的 src，代理则提供了预加载的功能，这看起来也是“加入行为”的一种方式，但这种加入行为的方式和装饰者模式的偏重点是不一样的。装饰者模式是实实在在的为对象增加新的职责和行为，而代理做的事情还是跟本体一样，最终都是设置 src。但代理可以加入一些“聪明”的功能，比如在图片真正加载好之前，先使用一张占位的 loading 图片反馈给客户。</p><h2 id="%E7%8A%B6%E6%80%81%E6%A8%A1%E5%BC%8F" tabindex="-1">状态模式</h2><p>状态模式的关键是区分事物内部的状态，事物内部状态的改变往往会带来事物的行为改变。</p><p>通常谈到封装，一般都会优先封装对象的行为，而不是对象的状态。但在状态模式中刚好相反，状态模式的关键是把事物的每种状态都封装成单独的类，跟此种状态有关的行为都被封装在这个类的内部。同时还可以把状态的切换规则事先分布在状态类中，这样就有效地消除了原本存在的大量条件分支语句。</p><p>以电灯开关为例，在不使用状态模式情况下：</p><pre><code class="language-javascript">Light.prototype.init = function() {  var button = document.createElement(&#39;button&#39;),    self = this  button.innerHTML = &#39;开关&#39;  this.button = document.body.appendChild(button)  this.button.onclick = function() {    self.buttonWasPressed()  }}Light.prototype.buttonWasPressed = function() {  if (this.state === &#39;off&#39;) {    console.log(&#39;弱光&#39;)    this.state = &#39;weakLight&#39;  } else if (this.state === &#39;weakLight&#39;) {    console.log(&#39;强光&#39;)    this.state = &#39;strongLight&#39;  } else if (this.state === &#39;strongLight&#39;) {    console.log(&#39;关灯&#39;)    this.state = &#39;off&#39;  }}var light = new Light()light.init()</code></pre><p>使用了状态模式之后：</p><pre><code class="language-javascript">var OffLightState = function(light) {  this.light = light}OffLightState.prototype.buttonWasPressed = function() {  console.log(&#39;弱光&#39;) // offLightState 对应的行为  this.light.setState(this.light.weakLightState) // 切换状态到 weakLightState}var WeakLightState = function(light) {  this.light = light}WeakLightState.prototype.buttonWasPressed = function() {  console.log(&#39;强光&#39;) // weakLightState 对应的行为  this.light.setState(this.light.strongLightState) // 切换状态到 strongLightState}var StrongLightState = function(light) {  this.light = light}StrongLightState.prototype.buttonWasPressed = function() {  console.log(&#39;关灯&#39;) // strongLightState 对应的行为  this.light.setState(this.light.offLightState) // 切换状态到 offLightState}var Light = function() {  this.offLightState = new OffLightState(this)  this.weakLightState = new WeakLightState(this)  this.strongLightState = new StrongLightState(this)  this.button = null}Light.prototype.init = function() {  var button = document.createElement(&#39;button&#39;),    self = this  this.button = document.body.appendChild(button)  this.button.innerHTML = &#39;开关&#39;  this.currState = this.offLightState  this.button.onclick = function() {    self.currState.buttonWasPressed()  }}Light.prototype.setState = function(newState) {  this.currState = newState}var light = new Light()light.init()</code></pre><p>通过状态模式，状态之间的切换都被分布在状态类内部，这使得我们无需编写过多的 if、else 条件分支语言来控制状态之间的转换。</p><h3 id="%E7%8A%B6%E6%80%81%E6%A8%A1%E5%BC%8F%E7%9A%84%E5%AE%9A%E4%B9%89" tabindex="-1">状态模式的定义</h3><p>状态模式定义：允许一个对象在其内部状态改变时改变它的行为。意思是将状态封装成独立的类，并将请求委托给当前的状态对象，当对象的内部状态改变时，会带来不同的行为变化。这个对象看起来是从不同的类中实例化而来的，实际上这是使用了委托的效果。</p><h3 id="%E7%8A%B6%E6%80%81%E6%A8%A1%E5%BC%8F%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9" tabindex="-1">状态模式的优缺点</h3><p>状态模式的优点如下：</p><ul><li>状态模式定义了状态与行为之间的关系，并将它们封装在一个类里。通过增加新的状态类，很容易增加新的状态和转换。</li><li>避免 Context 无限膨胀，状态切换的逻辑被分布在状态类中，也去掉了 Context 中原本过多的条件分支。</li><li>用对象代替字符串来记录当前状态，使得状态的切换更加一目了然。</li><li>Context 中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。</li></ul><p>状态模式的缺点是会在系统中定义许多状态类，而且系统中会因此而增加不少对象。另外由于逻辑分散在状态类中，虽然避开了条件分支语句，但也造成了逻辑分散的问题，无法在一个地方就看出整个状态机的逻辑。</p><h3 id="%E7%8A%B6%E6%80%81%E6%A8%A1%E5%BC%8F%E4%B8%8E%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F" tabindex="-1">状态模式与策略模式</h3><p>状态模式与策略模式类似，它们的类图看起来几乎一模一样，但在意图上有很大不同，因此它们是两种迥然不同的模式。策略模式和状态模式的相同点是，它们都有一个上下文、一些策略或者状态类，上下文把请求委托给这些类来执行。</p><p>它们之间的区别是策略模式中的各个策略类之间是平等又平行的，它们之间没有任何联系，所以客户必须熟知这些策略类的作用，以便客户可以随时主动切换算法；而在状态模式中，状态和状态对应的行为是早已被封装好的，状态之间的切换也早被规定完成，“改变行为”这件事情发生在状态模式内部。对客户来说，并不需要了解这些细节。这正是状态模式的作用所在。</p><h3 id="javascript-%E7%8A%B6%E6%80%81%E6%9C%BA" tabindex="-1">Javascript 状态机</h3><p>前面示例是模拟传统面向对象语言的状态模式实现，为每种状态都定义一个状态子类，然后在 Context 中持有这些状态对象的引用，以便把 currState 设置为当前的状态对象。</p><p>状态模式是状态机的实现之一，但在 JavaScript 这种“无类”语言中，没有规定让状态对象一定要从类中创建而来。另外一点，JavaScript 可以非常方便地使用委托技术，并不需要事先让一个对象持有另一个对象。</p><pre><code class="language-javascript">var Light = function() {  this.currState = FSM.off // 设置当前状态  this.button = null}Light.prototype.init = function() {  var button = document.createElement(&#39;button&#39;),    self = this  button.innerHTML = &#39;已关灯&#39;  this.button = document.body.appendChild(button)  this.button.onclick = function() {    self.currState.buttonWasPressed.call(self) // 把请求委托给 FSM 状态机  }}var FSM = {  off: {    buttonWasPressed: function() {      console.log(&#39;关灯&#39;)      this.button.innerHTML = &#39;下一次按我是开灯&#39;      this.currState = FSM.on    }  },  on: {    buttonWasPressed: function() {      console.log(&#39;开灯&#39;)      this.button.innerHTML = &#39;下一次按我是关灯&#39;      this.currState = FSM.off    }  }}var light = new Light()light.init()</code></pre><p>也可以尝试另一种方法，即利用下面的 delegate 函数来完成这个状态机编写。这是面向对象设计和闭包互换的一个例子，前者把变量保存为对象的属性，而后者把变量封闭在闭包形成的环境中：</p><pre><code class="language-javascript">var delegate = function(client, delegation) {  return {    buttonWasPressed: function() {      // 将客户的操作委托给delegation对象      return delegation.buttonWasPressed.apply(client, arguments)    }  }}var FSM = {  off: {    buttonWasPressed: function() {      console.log(&#39;关灯&#39;)      this.button.innerHTML = &#39;下一次按我是开灯&#39;      this.currState = this.onState    }  },  on: {    buttonWasPressed: function() {      console.log(&#39;开灯&#39;)      this.button.innerHTML = &#39;下一次按我是关灯&#39;      this.currState = this.offState    }  }}var Light = function() {  this.offState = delegate(this, FSM.off)  this.onState = delegate(this, FSM.on)  this.currState = this.offState // 设置初始状态为关闭状态  this.button = null}Light.prototype.init = function() {  var button = document.createElement(&#39;button&#39;),    self = this  button.innerHTML = &#39;已关灯&#39;  this.button = document.body.appendChild(button)  this.button.onclick = function() {    self.currState.buttonWasPressed()  }}var light = new Light()light.init()</code></pre><h2 id="%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F" tabindex="-1">适配器模式</h2><p>适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后，原本由于接口不兼容而不能工作的两个软件实体可以一起工作。</p><pre><code class="language-javascript">var googleMap = {  show: function() {    console.log(&#39;开始渲染谷歌地图&#39;)  }}var baiduMap = {  display: function() {    console.log(&#39;开始渲染百度地图&#39;)  }}var baiduMapAdapter = {  show: function() {    return baiduMap.display()  }}renderMap(googleMap) // 输出：开始渲染谷歌地图renderMap(baiduMapAdapter) // 输出：开始渲染百度地图</code></pre><p>适配器模式是一对相对简单的模式。有一些模式跟适配器模式的结构非常相似，比如装饰者模式、代理模式和外观模式。这几种模式都属于“包装模式”，都是由一个对象来包装另一个对象。区别它们的关键仍然是模式的意图。</p><ul><li>适配器模式主要用来解决两个已有接口之间不匹配的问题，它不考虑这些接口是怎样实现的，也不考虑它们将来可能会如何演化。适配器模式不需要改变已有的接口，就能够使它们协同作用。</li><li>装饰者模式和代理模式也不会改变原有对象的接口，但装饰者模式的作用是为了给对象增加功能。装饰者模式常常形成一条长的装饰链，而适配器模式通常只包装一次。代理模式是为了控制对对象的访问，通常也只包装一次。</li><li>外观模式的作用倒是和适配器比较相似，有人把外观模式看成一组对象的适配器，但外观模式最显著的特点是定义了一个新的接口。</li></ul>]]>
                    </description>
                    <pubDate>Sun, 04 Jun 2023 01:17:50 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[设计模式（上）]]>
                    </title>
                    <link>https://jupiter-smile.cn:666/archives/she-ji-mo-shi--shang-</link>
                    <description>
                            <![CDATA[<h1 id="%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%88%E4%B8%8A%EF%BC%89" tabindex="-1">设计模式（上）</h1><p>本篇是《JavaScript 设计模式与开发实践》第二部分读书笔记，总结前 7 种设计模式：单例模式、策略模式、代理模式、迭代器模式、发布-订阅模式、命令模式、组合模式。</p><h2 id="%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F" tabindex="-1">单例模式</h2><p>单例模式的定义是：保证一个类仅有一个实例，并提供一个访问它的全局访问点。单例模式常见如线程池、全局缓存、浏览器中的 window 对象等。</p><h3 id="%E4%BB%A3%E7%90%86%E5%8D%95%E4%BE%8B" tabindex="-1">代理单例</h3><p>通过引入代理类的方式来管理单例逻辑：</p><pre><code class="language-javascript">var CreateDiv = function(html) {  this.html = html  this.init()}CreateDiv.prototype.init = function() {  var div = document.createElement(&#39;div&#39;)  div.innerHTML = this.html  document.body.appendChild(div)}var ProxySingletonCreateDiv = (function() {  var instance  return function(html) {    if (!instance) {      instance = new CreateDiv(html)    }    return instance  }})()var a = new ProxySingletonCreateDiv(&#39;sven1&#39;)var b = new ProxySingletonCreateDiv(&#39;sven2&#39;)alert(a === b) // true</code></pre><h3 id="%E6%83%B0%E6%80%A7%E5%8D%95%E4%BE%8B" tabindex="-1">惰性单例</h3><p>惰性单例指的是在需要的时候才创建对象实例。可以把管理单例的逻辑从原来的代码中抽离出来，封装在 getSingle 函数内部，创建对象的方法 fn 被当成参数动态传入 getSingle 函数。</p><p>下面以创建登录悬浮窗为例：</p><pre><code class="language-javascript">var getSingle = function(fn) {  var result  return function() {    return result || (result = fn.apply(this, arguments))  }}var createLoginLayer = function() {  var div = document.createElement(&#39;div&#39;)  div.innerHTML = &#39;我是登录浮窗&#39;  div.style.display = &#39;none&#39;  document.body.appendChild(div)  return div}var createSingleLoginLayer = getSingle(createLoginLayer)document.getElementById(&#39;loginBtn&#39;).onclick = function() {  var loginLayer = createSingleLoginLayer()  loginLayer.style.display = &#39;block&#39;}</code></pre><p>比较上面的代理单例，可以发现只是将立即执行函数表达式提取出单独函数 getSingle，其余毫无二致。</p><h2 id="%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F" tabindex="-1">策略模式</h2><p>策略模式的定义是：定义一系列的算法，把它们一个个封装起来，并且使它们可以相互替换。</p><p>一个基于策略模式的程序至少由两部分组成。第一个部分是一组策略类，策略类封装了具体的算法，并负责具体的计算过程。第二个部分是环境类 Context，用来接受客户的请求，随后把请求委托给某一个策略类。要做到这点，说明 Context 中要维持对某个策略对象的引用。</p><h3 id="%E8%AE%A1%E7%AE%97%E5%A5%96%E9%87%91" tabindex="-1">计算奖金</h3><pre><code class="language-javascript">var strategies = {  S: function(salary) {    return salary * 4  },  A: function(salary) {    return salary * 3  },  B: function(salary) {    return salary * 2  }}var calculateBonus = function(level, salary) {  return strategies[level](salary)}calculateBonus(&#39;S&#39;, 20000)</code></pre><p>通过使用策略模式重构代码，消除了程序中大片的条件分支语句。通过替换 Context 中当前保存的策略对象，便能执行不同的算法来得到想要的结果，这也是多态在策略模式中的体现。</p><h3 id="%E7%BC%93%E5%8A%A8%E5%8A%A8%E7%94%BB" tabindex="-1">缓动动画</h3><pre><code class="language-javascript">var tween = {  linear: function(t, b, c, d) {    return (c * t) / d + b  },  easeIn: function(t, b, c, d) {    return c * (t /= d) * t + b  },  strongEaseIn: function(t, b, c, d) {    return c * (t /= d) * t * t * t * t + b  },  strongEaseOut: function(t, b, c, d) {    return c * ((t = t / d - 1) * t * t * t * t + 1) + b  },  sineaseIn: function(t, b, c, d) {    return c * (t /= d) * t * t + b  },  sineaseOut: function(t, b, c, d) {    return c * ((t = t / d - 1) * t * t + 1) + b  }}var Animate = function(dom) {  this.dom = dom // 进行运动的 dom 节点  this.startTime = 0 // 动画开始时间  this.startPos = 0 // 动画开始时，dom 的初始位置  this.endPos = 0 // 动画结束时，dom 的目标位置  this.propertyName = null // dom 节点需要被改变的 css 属性名  this.easing = null // 缓动算法  this.duration = null // 动画持续时间}Animate.prototype.start = function(propertyName, endPos, duration, easing) {  this.startTime = +new Date() // 动画启动时间  this.startPos = this.dom.getBoundingClientRect()[propertyName] // dom 节点初始位置  this.propertyName = propertyName // dom 节点需要被改变的CSS属性名  this.endPos = endPos // dom 节点目标位置  this.duration = duration // 动画持续时间  this.easing = tween[easing] // 缓动算法  var self = this  var timeId = setInterval(function() {    if (self.step() === false) {      // 启动定时器，开始执行动画      clearInterval(timeId) // 如果动画已结束，则清除定时器    }  }, 19)}Animate.prototype.step = function() {  var t = +new Date() // 取得当前时间  if (t &gt;= this.startTime + this.duration) {    this.update(this.endPos)    return false  }  var pos = this.easing(t - this.startTime, this.startPos, this.endPos - this.startPos, this.duration)  this.update(pos)}Animate.prototype.update = function(pos) {  this.dom.style[this.propertyName] = pos + &#39;px&#39;}var div = document.getElementById(&#39;div&#39;)var animate = new Animate(div)animate.start(&#39;left&#39;, 500, 1000, &#39;strongEaseOut&#39;)</code></pre><p>上面的缓动动画使用策略模式把算法传入动画类中，来达到各种不同的缓动效果，这些算法都可以轻易地被替换为另外一个算法，这是策略模式的经典运用之一。策略模式的实现并不复杂，关键是如何从策略模式的实现背后，找到封装变化、委托和多态性这些思想的价值。</p><h3 id="%E8%A1%A8%E5%8D%95%E9%AA%8C%E8%AF%81" tabindex="-1">表单验证</h3><pre><code class="language-javascript">var strategies = {  isNonEmpty: function(value, errorMsg) {    if (value === &#39;&#39;) {      return errorMsg    }  },  minLength: function(value, length, errorMsg) {    if (value.length &lt; length) {      return errorMsg    }  },  isMobile: function(value, errorMsg) {    if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {      return errorMsg    }  }}var Validator = function() {  this.cache = [] // 保存校验规则}Validator.prototype.add = function(dom, rules) {  var self = this  for (var i = 0, rule; (rule = rules[i++]); ) {    // @蝉時雨：没有必要用立即执行函数表达式，用 forEach 是否更合适    ;(function(rule) {      var strategyAry = rule.strategy.split(&#39;:&#39;)      var errorMsg = rule.errorMsg      self.cache.push(function() {        // 把校验的步骤用空函数包装起来，并且放入 cache        var strategy = strategyAry.shift()        strategyAry.unshift(dom.value)        strategyAry.push(errorMsg)        return strategies[strategy].apply(dom, strategyAry)      })    })(rule)  }}Validator.prototype.start = function() {  for (var i = 0, validatorFunc; (validatorFunc = this.cache[i++]); ) {    var msg = validatorFunc() // 开始校验，并取得校验后的返回信息    if (msg) {      return msg // 如果有确切的返回值，说明校验没有通过    }  }}var validataFunc = function() {  var validator = new Validator() // 创建一个 validator 对象  /*************** 添加一些校验规则 ****************/  validator.add(registerForm.userName, [    { strategy: &#39;isNonEmpty&#39;, errorMsg: &#39;用户名不能为空&#39; },    { strategy: &#39;minLength:10&#39;, errorMsg: &#39;用户名长度不能小于10位&#39; }  ])  validator.add(registerForm.password, [{ strategy: &#39;minLength:6&#39;, errorMsg: &#39;密码长度不能小于6位&#39; }])  validator.add(registerForm.phoneNumber, [{ strategy: &#39;isMobile&#39;, errorMsg: &#39;手机号码格式不正确&#39; }])  var errorMsg = validator.start()  return errorMsg // 返回校验结果}var registerForm = document.getElementById(&#39;registerForm&#39;)registerForm.onsubmit = function() {  var errorMsg = validataFunc() // 如果 errorMsg 有确切的返回值，说明未通过校验  if (errorMsg) {    return false // 阻止表单提交  }}</code></pre><h3 id="%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F%E4%BC%98%E7%BC%BA%E7%82%B9" tabindex="-1">策略模式优缺点</h3><p>通过以上三个例子，总结策略模式优点：</p><ul><li>策略模式利用组合、委托和多态等技术和思想，可以有效地避免多重条件选择语句。</li><li>策略模式提供了对开放—封闭原则的完美支持，将算法封装在独立的 strategy 中，使得它们易于切换，易于理解，易于扩展。</li><li>策略模式中的算法也可以复用在系统的其他地方，从而避免许多重复的复制粘贴工作。</li><li>在策略模式中利用组合和委托来让 Context 拥有执行算法的能力，这也是继承的一种更轻便的替代方案。</li></ul><p>当然，策略模式也有一些缺点：</p><ul><li>使用策略模式会在程序中增加许多策略类或者策略对象，但实际上这比把它们负责的逻辑堆砌在 Context 中要好。</li><li>要使用策略模式，必须了解所有的 strategy，必须了解各个 strategy 之间的不同点，这样才能选择一个合适的 strategy。此时 strategy 要向客户暴露它的所有实现，违反最少知识原则。</li></ul><h3 id="%E4%B8%80%E7%AD%89%E5%87%BD%E6%95%B0%E5%AF%B9%E8%B1%A1%E4%B8%8E%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F" tabindex="-1">一等函数对象与策略模式</h3><p>在以类为中心的传统面向对象语言中，不同的算法或者行为被封装在各个策略类中，Context 将请求委托给这些策略对象，这些策略对象会根据请求返回不同的执行结果，这样便能表现出对象的多态性。</p><blockquote><p>在函数作为一等对象的语言中，策略模式是隐形的。strategy 就是值为函数的变量。 – Peter Norvig</p></blockquote><p>在 JavaScript 中，除了使用类来封装算法和行为之外，使用函数当然也是一种选择。这些“算法”可以被封装到函数中并且四处传递，也就是常说的“高阶函数”。 实际上在 JavaScript 这种将函数作为一等对象的语言里，策略模式已经融入到了语言本身当中，我们经常用高阶函数来封装不同的行为，并且把它传递到另一个函数中。当对这些函数发出“调用”的消息时，不同的函数会返回不同的执行结果。在 JavaScript 中，“函数对象的多态性”来得更加简单。</p><h2 id="%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F" tabindex="-1">代理模式</h2><p>代理模式是为一个对象提供一个代用品或占位符，以便控制对它的访问。</p><p>代理模式的关键是，当客户不方便直接访问一个对象或者不满足需要的时候，提供一个替身对象来控制对这个对象的访问，客户实际上访问的是替身对象。替身对象对请求做出一些处理之后，再把请求转交给本体对象。</p><p>常见代理模式有：</p><ul><li>虚拟代理：把一些开销很大的对象，延迟到真正需要它的时候才去创建。</li><li>缓存代理：缓存代理可以为一些开销大的运算结果提供暂时的存储。</li><li>保护代理：用于对象应该有不同访问权限的情况，过滤请求。</li><li>防火墙代理：控制网络资源的访问，保护主题不让“坏人”接近。</li><li>远程代理：为一个对象在不同的地址空间提供局部代表，在 Java 中，远程代理可以是另一个虚拟机中的对象。</li><li>智能引用代理：取代了简单的指针，它在访问对象时执行一些附加操作，比如计算一个对象被引用的次数。</li></ul><p>在 JavaScript 开发中最常用的是虚拟代理和缓存代理。</p><h3 id="%E8%99%9A%E6%8B%9F%E4%BB%A3%E7%90%86" tabindex="-1">虚拟代理</h3><h4 id="%E5%9B%BE%E7%89%87%E9%A2%84%E5%8A%A0%E8%BD%BD" tabindex="-1">图片预加载</h4><pre><code class="language-javascript">var myImage = (function() {  var imgNode = document.createElement(&#39;img&#39;)  document.body.appendChild(imgNode)  return {    setSrc: function(src) {      imgNode.src = src    }  }})()var proxyImage = (function() {  var img = new Image()  img.onload = function() {    myImage.setSrc(this.src)  }  return {    setSrc: function(src) {      myImage.setSrc(&#39;loading.gif&#39;)      img.src = src    }  }})()proxyImage.setSrc(&#39;avatar.jpg&#39;)</code></pre><p>单一职责原则指的是，就一个类（通常也包括对象和函数等）而言，应该仅有一个引起它变化的原因。如果一个对象承担了多项职责，就意味着这个对象将变得巨大，引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中，如果一个对象承担的职责过多，等于把这些职责耦合到了一起，这种耦合会导致脆弱和低内聚的设计。当变化发生时，设计可能会遭到意外的破坏。</p><p>同时在大多数情况下，若违反其他任何原则，同时将违反开放—封闭原则。</p><p>上面的预加载代码中，给 img 节点设置 src 和图片预加载这两个功能，被隔离在两个对象里，它们可以各自变化而不影响对方。何况就算有一天不再需要预加载，那么只需要改成请求本体而不是请求代理对象即可。其中关键是代理对象和本体都对外提供了 setSrc 方法，在客户看来，代理对象和本体是一致的，代理接手请求的过程对于用户来说是透明的，用户并不清楚代理和本体的区别，这样做有两个好处：</p><ul><li>用户可以放心地请求代理，他只关心是否能得到想要的结果。</li><li>在任何使用本体的地方都可以替换成使用代理。</li></ul><h4 id="%E5%90%88%E5%B9%B6-http-%E8%AF%B7%E6%B1%82" tabindex="-1">合并 HTTP 请求</h4><p>频繁触发的 HTTP 请求可以缓存起来一并发送，能大大减轻服务器的压力，通过代理模式实现如下：</p><pre><code class="language-javascript">var synchronousFile = function(id) {  console.log(&#39;开始同步文件，id为: &#39; + id)}var proxySynchronousFile = (function() {  var cache = [], // 保存一段时间内需要同步的ID    timer // 定时器  return function(id) {    cache.push(id)    if (timer) {      // 保证不会覆盖已经启动的定时器      return    }    timer = setTimeout(function() {      synchronousFile(cache.join(&#39;,&#39;)) // 2秒后向本体发送需要同步的ID集合      clearTimeout(timer) // 清空定时器      timer = null      cache.length = 0 // 清空ID集合    }, 2000)  }})()var checkbox = document.getElementsByTagName(&#39;input&#39;)for (var i = 0, c; (c = checkbox[i++]); ) {  c.onclick = function() {    if (this.checked === true) {      proxySynchronousFile(this.id)    }  }}</code></pre><h3 id="%E7%BC%93%E5%AD%98%E4%BB%A3%E7%90%86" tabindex="-1">缓存代理</h3><p>缓存代理可以为一些开销大的运算结果提供暂时的存储，在下次运算时，如果传递进来的参数跟之前一致，则可以直接返回前面存储的运算结果。</p><pre><code class="language-javascript">var mult = function() {  var a = 1  for (var i = 0, l = arguments.length; i &lt; l; i++) {    a = a * arguments[i]  }  return a}var plus = function() {  var a = 0  for (var i = 0, l = arguments.length; i &lt; l; i++) {    a = a + arguments[i]  }  return a}// 创建缓存代理的工厂var createProxyFactory = function(fn) {  var cache = {}  return function() {    var args = Array.prototype.join.call(arguments, &#39;,&#39;)    if (args in cache) {      return cache[args]    }    return (cache[args] = fn.apply(this, arguments))  }}var proxyMult = createProxyFactory(mult),  proxyPlus = createProxyFactory(plus)alert(proxyMult(1, 2, 3, 4)) // 输出：24alert(proxyPlus(1, 2, 3, 4)) // 输出：10</code></pre><h2 id="%E8%BF%AD%E4%BB%A3%E5%99%A8%E6%A8%A1%E5%BC%8F" tabindex="-1">迭代器模式</h2><p>迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素，而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来，在使用迭代器模式之后，即使不关心对象的内部构造，也可以按顺序访问其中的每个元素。</p><p>迭代器可以分为内部迭代器和外部迭代器。</p><p>内部迭代器调用方便，外界不用关心迭代器内部的实现，跟迭代器的交互也仅仅是一次初始调用，但这也刚好是内部迭代器的缺点，内部迭代器的迭代规则已经被提前规定。</p><p>外部迭代器必须显式地请求迭代下一个元素。外部迭代器增加了一些调用的复杂度，但相对也增强了迭代器的灵活性，可以手工控制迭代的过程或者顺序。</p><p>下面这个外部迭代器的实现来自《松本行弘的程序世界》第 4 章，原例用 Ruby 写成，这里翻译成 JavaScript：</p><pre><code class="language-javascript">var Iterator = function(obj) {  var current = 0  var next = function() {    current += 1  }  var isDone = function() {    return current &gt;= obj.length  }  var getCurrItem = function() {    return obj[current]  }  return {    next: next,    isDone: isDone,    getCurrItem: getCurrItem  }}</code></pre><p>迭代器模式是一种相对简单的模式，简单到很多时候都不认为它是一种设计模式，目前的绝大部分语言都内置了迭代器。</p><h3 id="%E8%BF%AD%E4%BB%A3%E5%99%A8%E6%A8%A1%E5%BC%8F%E7%9A%84%E5%BA%94%E7%94%A8" tabindex="-1">迭代器模式的应用</h3><p>这里已文件上传为例，在不同的浏览器环境下，选择的上传方式是不一样的：</p><pre><code class="language-javascript">var getActiveUploadObj = function() {  try {    return new ActiveXObject(&#39;TXFTNActiveX.FTNUpload&#39;) // IE上传控件  } catch (e) {    return false  }}var getFlashUploadObj = function() {  if (supportFlash()) {    // supportFlash 函数未提供    var str = &#39;&lt;object type=&quot;application/x-shockwave-flash&quot;&gt;&lt;/object&gt;&#39;    return $(str).appendTo($(&#39;body&#39;))  }  return false}var getFormUpladObj = function() {  var str = &#39;&lt;input name=&quot;file&quot; type=&quot;file&quot; class=&quot;ui-file&quot;/&gt;&#39; // 表单上传  return $(str).appendTo($(&#39;body&#39;))}var iteratorUploadObj = function() {  for (var i = 0, fn; (fn = arguments[i++]); ) {    var uploadObj = fn()    if (uploadObj !== false) {      return uploadObj    }  }}var uploadObj = iteratorUploadObj(getActiveUploadObj, getFlashUploadObj, getFormUpladObj)</code></pre><p>在 getActiveUploadObj、getFlashUploadObj、getFormUpladObj 这 3 个函数中都有同一个约定：如果该函数里面的 upload 对象是可用的，则让函数返回该对象，反之返回 false，提示迭代器继续往后面进行迭代。</p><h2 id="%E5%8F%91%E5%B8%83-%E8%AE%A2%E9%98%85%E6%A8%A1%E5%BC%8F" tabindex="-1">发布-订阅模式</h2><p>发布—订阅模式又叫观察者模式，它定义对象间的一种一对多的依赖关系，当一个对象的状态发生改变时，所有依赖于它的对象都将得到通知。在 JavaScript 开发中，一般用事件模型来替代传统的发布—订阅模式。</p><h3 id="%E5%85%A8%E5%B1%80%E8%AE%A2%E9%98%85%E4%B8%8E%E9%80%9A%E4%BF%A1" tabindex="-1">全局订阅与通信</h3><p>发布—订阅模式可以用一个全局的 Event 对象来实现，订阅者不需要了解消息来自哪个发布者，发布者也不知道消息会推送给哪些订阅者，Event 作为一个类似“中介者”的角色，把订阅者和发布者联系起来。</p><p>利用基于一个全局的 Event 对象，可以在两个封装良好的模块中进行通信，这两个模块可以完全不知道对方的存在。但模块之间如果用了太多的全局发布—订阅模式来通信，那么模块与模块之间的联系就被隐藏到了背后，无法分辨消息的来源与流向。</p><pre><code class="language-javascript">var Event = (function() {  var global = this,    Event,    _default = &#39;default&#39;  Event = (function() {    var _listen,      _trigger,      _remove,      _slice = Array.prototype.slice,      _shift = Array.prototype.shift,      _unshift = Array.prototype.unshift,      namespaceCache = {},      _create,      find,      each = function(ary, fn) {        var ret        for (var i = 0, l = ary.length; i &lt; l; i++) {          var n = ary[i]          ret = fn.call(n, i, n)        }        return ret      }    _listen = function(key, fn, cache) {      if (!cache[key]) {        cache[key] = []      }      cache[key].push(fn)    }    _remove = function(key, cache, fn) {      if (cache[key]) {        if (fn) {          for (var i = cache[key].length; i &gt;= 0; i--) {            if (cache[key][i] === fn) {              cache[key].splice(i, 1)            }          }        } else {          cache[key] = []        }      }    }    _trigger = function() {      var cache = _shift.call(arguments),        key = _shift.call(arguments),        args = arguments,        _self = this,        ret,        stack = cache[key]      if (!stack || !stack.length) {        return      }      return each(stack, function() {        return this.apply(_self, args)      })    }    _create = function(namespace) {      var namespace = namespace || _default      var cache = {},        offlineStack = [], // 离线事件        ret = {          listen: function(key, fn, last) {            _listen(key, fn, cache)            if (offlineStack === null) {              return            }            if (last === &#39;last&#39;) {              offlineStack.length &amp;&amp; offlineStack.pop()()            } else {              each(offlineStack, function() {                this()              })            }            offlineStack = null          },          one: function(key, fn, last) {            _remove(key, cache)            this.listen(key, fn, last)          },          remove: function(key, fn) {            _remove(key, cache, fn)          },          trigger: function() {            var fn,              args,              _self = this            _unshift.call(arguments, cache)            args = arguments            fn = function() {              return _trigger.apply(_self, args)            }            if (offlineStack) {              return offlineStack.push(fn)            }            return fn()          }        }      return namespace        ? namespaceCache[namespace]          ? namespaceCache[namespace]          : (namespaceCache[namespace] = ret)        : ret    }    return {      create: _create,      one: function(key, fn, last) {        var event = this.create()        event.one(key, fn, last)      },      remove: function(key, fn) {        var event = this.create()        event.remove(key, fn)      },      listen: function(key, fn, last) {        var event = this.create()        event.listen(key, fn, last)      },      trigger: function() {        var event = this.create()        event.trigger.apply(this, arguments)      }    }  })()  return Event})()/************** 先发布后订阅 ********************/Event.trigger(&#39;click&#39;, 1)Event.listen(&#39;click&#39;, function(a) {  console.log(a) // 输出：1})/************** 使用命名空间 ********************/Event.create(&#39;namespace1&#39;).listen(&#39;click&#39;, function(a) {  console.log(a) // 输出：1})Event.create(&#39;namespace1&#39;).trigger(&#39;click&#39;, 1)Event.create(&#39;namespace2&#39;).listen(&#39;click&#39;, function(a) {  console.log(a) // 输出：2})</code></pre><p>JavaScript 中的发布—订阅模式，跟一些别的语言（比如 Java）中的实现还是有区别的。在 Java 中通常会把订阅者对象自身当成引用传入发布者对象中，同时订阅者对象还需提供一个名为诸如 update 的方法，供发布者对象在适合的时候调用。而在 JavaScript 中，用注册回调函数的形式来代替传统的发布—订阅模式，显得更加优雅和简单。</p><p>另外，在 JavaScript 中，无需去选择使用推模型还是拉模型。推模型是指在事件发生时，发布者一次性把所有更改的状态和数据都推送给订阅者。拉模型不同的地方是，发布者仅仅通知订阅者事件已经发生了，此外发布者要提供一些公开的接口供订阅者来主动拉取数据。</p><p>拉模型的好处是可以让订阅者“按需获取”，但同时有可能让发布者变成一个“门户大开”的对象，同时增加了代码量和复杂度。刚好在 JavaScript 中，arguments 可以很方便地表示参数列表，所以一般都会选择推模型，使用 Function.prototype.apply 方法把所有参数都推送给订阅者。</p><p>发布—订阅模式的优点非常明显，一为时间上的解耦，二为对象之间的解耦。发布—订阅模式还可以用来帮助实现一些别的设计模式，比如中介者模式。从架构上来看，无论是 MVC 还是 MVVM，都少不了发布—订阅模式的参与，而且 <strong>JavaScript 本身也是一门基于事件驱动的语言</strong>。</p><p>当然，发布—订阅模式也不是完全没有缺点。创建订阅者本身要消耗一定的时间和内存，而且订阅一个消息后，也许此消息最后都未发生，但这个订阅者会始终存在于内存中。另外发布—订阅模式虽然可以弱化对象之间的联系，但如果过度使用的话，对象和对象之间的必要联系也将被深埋在背后，会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一起的时候，要跟踪一个 bug 不是件轻松的事情。</p><h2 id="%E5%91%BD%E4%BB%A4%E6%A8%A1%E5%BC%8F" tabindex="-1">命令模式</h2><p>命令模式是最简单和优雅的模式之一，命令模式中的命令（command）指的是一个执行某些特定事情的指令。</p><p>命令模式最常见的应用场景是：有时候需要向某些对象发送请求，但是并不知道请求的接收者是谁，也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序，使得请求发送者和请求接收者能够消除彼此之间的耦合关系。</p><p>另外，相对于过程化的请求调用，command 对象拥有更长的生命周期。对象的生命周期是跟初始请求无关的，因为这个请求已经被封装在了 command 对象的方法中，成为了这个对象的行为，可以随时调用。</p><p>除了这两点之外，命令模式还支持撤销、排队等操作。</p><h3 id="javascript-%E4%B8%AD%E7%9A%84%E5%91%BD%E4%BB%A4%E6%A8%A1%E5%BC%8F" tabindex="-1">JavaScript 中的命令模式</h3><p>一个最简单的命令模式代码如下：</p><pre><code class="language-javascript">var MenuBar = {  refresh: function() {    console.log(&#39;刷新菜单目录&#39;)  }}// ① 面向对象实现var RefreshMenuBarCommand = function(receiver) {  this.receiver = receiver}RefreshMenuBarCommand.prototype.execute = function() {  this.receiver.refresh()}var refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar)var button = document.getElementById(&#39;button&#39;)var setCommand = function(button, command) {  button.onclick = function() {    command.execute()  }}setCommand(button, refreshMenuBarCommand)</code></pre><p>上面示例代码是模拟传统面向对象语言的命令模式实现。命令模式将过程式的请求调用封装在 command 对象的 execute 方法里，通过封装方法调用，可以把运算块包装成形。command 对象可以被四处传递，所以在调用命令的时候，不需要关心事情是如何进行的。</p><p><strong>命令模式的由来，其实是回调（callback）函数的一个面向对象的替代品</strong>。</p><p>JavaScript 作为将函数作为一等对象的语言，跟策略模式一样，命令模式也早已融入到了 JavaScript 语言之中。运算块不一定要封装在 command.execute 方法中，也可以封装在普通函数中。函数作为一等对象，本身就可以被四处传递。即使依然需要请求“接收者”，那也未必使用面向对象的方式，闭包可以完成同样的功能。</p><p>在面向对象设计中，命令模式的接收者被当成 command 对象的属性保存起来，同时约定执行命令的操作调用 command.execute 方法。在使用闭包的命令模式实现中，接收者被封闭在闭包产生的环境中，执行命令的操作可以更加简单，仅仅执行回调函数即可。无论接收者被保存为对象的属性，还是被封闭在闭包产生的环境中，在将来执行命令的时候，接收者都能被顺利访问。</p><p>使用闭包方式重写上面命令封装：</p><pre><code class="language-javascript">// ② 闭包实现var RefreshMenuBarCommand = function(receiver) {  return {    execute: function() {      receiver.refresh()    }  }}</code></pre><h3 id="%E5%AE%8F%E5%91%BD%E4%BB%A4" tabindex="-1">宏命令</h3><p>宏命令是一组命令的集合，通过执行宏命令的方式，可以一次执行一批命令。宏命令是命令模式与组合模式的联用产物。</p><pre><code class="language-javascript">var openPcCommand = {  execute: function() {    console.log(&#39;开电脑&#39;)  }}var openQQCommand = {  execute: function() {    console.log(&#39;登录QQ&#39;)  }}var MacroCommand = function() {  return {    commandsList: [],    add: function(command) {      this.commandsList.push(command)    },    execute: function() {      for (var i = 0, command; (command = this.commandsList[i++]); ) {        command.execute()      }    }  }}var macroCommand = MacroCommand()macroCommand.add(openPcCommand)macroCommand.add(openQQCommand)macroCommand.execute()</code></pre><h3 id="%E6%99%BA%E8%83%BD%E5%91%BD%E4%BB%A4%E5%92%8C%E5%82%BB%E7%93%9C%E5%91%BD%E4%BB%A4" tabindex="-1">智能命令和傻瓜命令</h3><p>回顾上面的命令代码：</p><pre><code class="language-javascript">var openPcCommand = {  execute: function() {    console.log(&#39;开电脑&#39;)  }}</code></pre><p>命令中没有接收者 receiver，本身就包揽了执行请求的行为。一般来说，命令模式都会在 command 对象中保存一个接收者来负责真正执行客户的请求，这种情况下命令对象是“傻瓜式”的，它只负责把客户的请求转交给接收者来执行，这种模式的好处是请求发起者和请求接收者之间尽可能地得到了解耦。</p><p>但是也可以定义一些更“聪明”的命令对象叫做智能命令，智能命令可以直接实现请求，不再需要接收者的存在。没有接收者的智能命令和策略模式非常相近，从代码结构上已经无法分辨它们，它们只有意图的不同。策略模式指向的问题域更小，所有策略对象的目标总是一致的，它们只是达到这个目标的不同手段，它们的内部实现是针对“算法”而言的。而智能命令模式指向的问题域更广，command 对象解决的目标更具发散性。</p><h2 id="%E7%BB%84%E5%90%88%E6%A8%A1%E5%BC%8F" tabindex="-1">组合模式</h2><p>组合模式将对象组合成树形结构，以表示“部分-整体”的层次结构。除了用来表示树形结构之外，组合模式的另一个好处是通过对象的多态性表现，使得用户对单个对象和组合对象的使用具有一致性。</p><p>以宏命令为例，请求从树最顶端的对象往下传递，如果子节点是叶对象，叶对象自身会处理这个请求，而如果子节点还是组合对象，请求会继续往下传递。</p><p>组合模式最大的优点在于可以一致地对待组合对象和基本对象。客户不需要知道当前处理的是宏命令还是普通命令，只要它是一个命令，并且有 execute 方法，这个命令就可以被添加到树中。这种透明性带来的便利，在静态类型语言中体现得尤为明显。比如在 Java 中，实现组合模式的关键是 Composite 类和 Leaf 类都必须继承自一个 Compenent 抽象类。这个 Compenent 抽象类既代表组合对象，又代表叶对象，它也能够保证组合对象和叶对象拥有同样名字的方法，从而可以对同一消息都做出反馈。组合对象和叶对象的具体类型被隐藏在 Compenent 抽象类身后。</p><p>然而在 JavaScript 这种动态类型语言中，对象的多态性是与生俱来的，JavaScript 中实现组合模式的难点在于要保证组合对象和叶对象对象拥有同样的方法，这通常需要用鸭子类型的思想对它们进行接口检查。</p><h3 id="%E6%89%AB%E6%8F%8F%E6%96%87%E4%BB%B6%E5%A4%B9" tabindex="-1">扫描文件夹</h3><p>以下以文件复制和文件夹扫描为例:</p><pre><code class="language-javascript">/******** Folder **********/var Folder = function(name) {  this.name = name  this.files = []  this.parent = null // 增加 this.parent 属性}Folder.prototype.add = function(file) {  file.parent = this // 设置父对象  this.files.push(file)}Folder.prototype.scan = function() {  for (var i = 0, file, files = this.files; (file = files[i++]); ) {    file.scan()  }}Folder.prototype.remove = function() {  if (!this.parent) {    // 根节点或者树外的游离节点    return  }  for (var files = this.parent.files, l = files.length - 1; l &gt;= 0; l--) {    var file = files[l]    if (file === this) {      files.splice(l, 1)    }  }}/************ File **************/var File = function(name) {  this.name = name  this.parent = null}File.prototype.add = function() {  throw new Error(&#39;文件下面不能再添加文件&#39;)}File.prototype.scan = function() {  console.log(&#39;开始扫描文件: &#39; + this.name)}File.prototype.remove = function() {  if (!this.parent) {    // 根节点或者树外的游离节点    return  }  for (var files = this.parent.files, l = files.length - 1; l &gt;= 0; l--) {    var file = files[l]    if (file === this) {      files.splice(l, 1)    }  }}var folder = new Folder(&#39;学习资料&#39;)var folder1 = new Folder(&#39;JavaScript&#39;)var file1 = new File(&#39;JavaScript设计模式与开发实践&#39;)var file2 = new File(&#39;重构与模式&#39;)folder1.add(file1)folder.add(folder1)folder.add(file2)folder1.remove()folder.scan()</code></pre><p>组合模式适用的两大场景：</p><ul><li>表示对象的部分-整体层次结构</li><li>客户希望统一对待树中的所有对象</li></ul><p>然而组合模式并不是完美的，它可能会产生一个这样的系统：系统中的每个对象看起来都与其他对象差不多。它们的区别只有在运行的时候会才会显现出来，这会使代码难以理解。此外，如果通过组合模式创建了太多的对象，那么这些对象可能会让系统负担不起。</p>]]>
                    </description>
                    <pubDate>Sun, 04 Jun 2023 01:17:25 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Flex 弹性布局]]>
                    </title>
                    <link>https://jupiter-smile.cn:666/archives/flex-dan-xing-bu-ju</link>
                    <description>
                            <![CDATA[<h1 id="flex-%E5%BC%B9%E6%80%A7%E5%B8%83%E5%B1%80" tabindex="-1">Flex 弹性布局</h1><p>Flex（Flexible Box）即为“弹性布局” ，它是一种一维的布局模型。采用 Flex 布局的元素，称为 Flex 容器（flex container）。它的所有子元素自动成为容器成员，称为 Flex 项目（flex item）。</p><p>Flex 容器有两根轴线：水平的主轴（main axis）和垂直的交叉轴（cross axis）。主轴的起终点分别为 <code>main start</code>和<code>main end</code>；交叉轴的起终点分别为<code>cross start</code>和<code>cross end</code>。项目沿主轴排列，单个项目占据的主轴空间为<code>main size</code>，占据的交叉轴空间为<code>cross size</code>。</p><p>一张图说明 Flex Box 模型：</p><p><img src="https://raw.githubusercontent.com/chanshiyucx/yoi/master/2019/Flex-%E5%BC%B9%E6%80%A7%E5%B8%83%E5%B1%80/flex_box.png" alt="Flex Box" /></p><p>当前主流浏览器对 Flex 布局都能良好兼容，可以放心使用：</p><p><img src="https://raw.githubusercontent.com/chanshiyucx/yoi/master/2019/Flex-%E5%BC%B9%E6%80%A7%E5%B8%83%E5%B1%80/Can_I_Use_Flex.png" alt="Can I Use Flex" /></p><h2 id="%E5%AE%B9%E5%99%A8%E7%9A%84%E5%B1%9E%E6%80%A7" tabindex="-1">容器的属性</h2><p>Flex 容器可设置 6 个属性:</p><ul><li>flex-direction：决定主轴的方向</li><li>flex-wrap：子项目是否可换行</li><li>flex-flow：属于 flex-direction 属性和 flex-wrap 属性的简写形式</li><li>justify-content：定义了项目在主轴上的对齐方式</li><li>align-items：定义项目在交叉轴上如何对齐</li><li>align-content：定义了多根轴线的对齐方式</li></ul><h3 id="flex-direction" tabindex="-1">flex-direction</h3><p>flex-direction 属性决定主轴的方向，可选值：<code>row | row-reverse | column | column-reverse</code>，各值表现如下：</p><pre><code class="language-css">.box {  flex-direction: row | row-reverse | column | column-reverse;}</code></pre><p><img src="https://raw.githubusercontent.com/chanshiyucx/yoi/master/2019/Flex-%E5%BC%B9%E6%80%A7%E5%B8%83%E5%B1%80/flex-direction.png" alt="flex-direction" /></p><h3 id="flex-wrap" tabindex="-1">flex-wrap</h3><p>flex-wrap 属性定义子项目是否可换行，可选值：<code>nowrap | wrap | wrap-reverse</code>，各值表现如下：</p><pre><code class="language-css">.box {  flex-wrap: nowrap | wrap | wrap-reverse;}</code></pre><p><img src="https://raw.githubusercontent.com/chanshiyucx/yoi/master/2019/Flex-%E5%BC%B9%E6%80%A7%E5%B8%83%E5%B1%80/flex-wrap.png" alt="flex-wrap" /></p><h3 id="flex-flow" tabindex="-1">flex-flow</h3><p>flex-flow 属性属于 flex-direction 属性和 flex-wrap 属性的简写形式，默认值 <code>row nowrap</code>。</p><h3 id="justify-content" tabindex="-1">justify-content</h3><p>justify-content：属性定义了项目在主轴上的对齐方式，可选值：<code>flex-start | flex-end | center | space-between | space-around</code>，各值表现如下：</p><pre><code class="language-css">.box {  justify-content: flex-start | flex-end | center | space-between | space-around;}</code></pre><p><img src="https://raw.githubusercontent.com/chanshiyucx/yoi/master/2019/Flex-%E5%BC%B9%E6%80%A7%E5%B8%83%E5%B1%80/justify-content.png" alt="justify-content" /></p><h3 id="align-items" tabindex="-1">align-items</h3><p>align-items 属性定义项目在交叉轴上如何对齐，可选值 <code>flex-start | flex-end | center | baseline | stretch</code>，各值表现如下：</p><pre><code class="language-css">.box {  align-items: flex-start | flex-end | center | baseline | stretch;}</code></pre><p><img src="https://raw.githubusercontent.com/chanshiyucx/yoi/master/2019/Flex-%E5%BC%B9%E6%80%A7%E5%B8%83%E5%B1%80/align-items.png" alt="align-items" /></p><h3 id="align-content" tabindex="-1">align-content</h3><p>align-content 属性定义多根轴线的对齐方式，如果项目只有一根轴线，该属性不起作用，可选值<code>flex-start | flex-end | center | space-between | space-around | stretch</code>，各值表现如下：</p><pre><code class="language-css">.box {  align-content: flex-start | flex-end | center | space-between | space-around | stretch;}</code></pre><p><img src="https://raw.githubusercontent.com/chanshiyucx/yoi/master/2019/Flex-%E5%BC%B9%E6%80%A7%E5%B8%83%E5%B1%80/align-content.png" alt="align-content" /></p><h2 id="%E9%A1%B9%E7%9B%AE%E7%9A%84%E5%B1%9E%E6%80%A7" tabindex="-1">项目的属性</h2><p>Flex 项目可设置 6 个属性:</p><ul><li>order：定义项目的排列顺序</li><li>flex-grow：定义项目的放大比例</li><li>flex-shrink：定义了项目的缩小比例</li><li>flex-basis：定义了在分配多余空间之前，项目占据的主轴空间</li><li>flex：属于 flex-grow, flex-shrink 和 flex-basis 的简写形式</li><li>align-self：允许单个项目有与其他项目不一样的对齐方式，可覆盖 align-items 属性</li></ul><h3 id="order" tabindex="-1">order</h3><p>order 属性定义项目的排列顺序，数值越小，排列越靠前，默认为 0，具体表现如下：</p><pre><code class="language-css">.item {  order: 0;}</code></pre><p><img src="https://raw.githubusercontent.com/chanshiyucx/yoi/master/2019/Flex-%E5%BC%B9%E6%80%A7%E5%B8%83%E5%B1%80/order.png" alt="order" /></p><h3 id="flex-grow" tabindex="-1">flex-grow</h3><p>flex-grow 属性定义项目的放大比例，默认为 0，即如果存在剩余空间，也不放大，具体表现如下：</p><pre><code class="language-css">.item {  flex-grow: 0;}</code></pre><p><img src="https://raw.githubusercontent.com/chanshiyucx/yoi/master/2019/Flex-%E5%BC%B9%E6%80%A7%E5%B8%83%E5%B1%80/flex-grow.png" alt="flex-grow" /></p><h3 id="flex-shrink" tabindex="-1">flex-shrink</h3><p>flex-shrink 属性定义了项目的缩小比例，默认为 1，即如果空间不足，该项目将缩小，具体表现如下：</p><pre><code class="language-css">.item {  flex-shrink: 1;}</code></pre><p><img src="https://raw.githubusercontent.com/chanshiyucx/yoi/master/2019/Flex-%E5%BC%B9%E6%80%A7%E5%B8%83%E5%B1%80/flex-shrink.png" alt="flex-shrink" /></p><h3 id="flex-basis" tabindex="-1">flex-basis</h3><p>flex-basis 属性定义了在分配多余空间之前，项目占据的主轴空间（main size），默认值为 auto，即项目的本来大小，浏览器根据这个属性计算主轴是否有多余空间。</p><pre><code class="language-css">.item {  flex-basis: auto;}</code></pre><h3 id="flex" tabindex="-1">flex</h3><p>flex 属性是 flex-grow, flex-shrink 和 flex-basis 的简写，默认值为 <code>0 1 auto</code>，后两个值可选，不设置则为默认值。该属性有两个快捷值：<code>auto (1 1 auto)</code> 和 <code>none (0 0 auto)</code>。</p><pre><code class="language-css">.item {  flex: none;}</code></pre><h3 id="align-self" tabindex="-1">align-self</h3><p>align-self 属性允许单个项目有与其他项目不一样的对齐方式，可覆盖 align-items 属性。默认值为 auto，表示继承父元素的 align-items 属性，如果没有父元素，则等同于 stretch。</p><p><img src="https://raw.githubusercontent.com/chanshiyucx/yoi/master/2019/Flex-%E5%BC%B9%E6%80%A7%E5%B8%83%E5%B1%80/align-self.png" alt="align-self" /></p>]]>
                    </description>
                    <pubDate>Sun, 04 Jun 2023 01:16:19 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[ 洗牌算法实现数组乱序]]>
                    </title>
                    <link>https://jupiter-smile.cn:666/archives/xi-pai-suan-fa-shi-xian-shu-zu-luan-xu</link>
                    <description>
                            <![CDATA[<h1 id="%E6%B4%97%E7%89%8C%E7%AE%97%E6%B3%95%E5%AE%9E%E7%8E%B0%E6%95%B0%E7%BB%84%E4%B9%B1%E5%BA%8F" tabindex="-1">洗牌算法实现数组乱序</h1><p>关于 JavaScript 数组乱序的方法有多种实现方式，或者借助一些第三方开源工具库如 loadsh 也可以轻松实现，然而要做到数组足够的无规律乱序也非易予，还是有一些要点需要考虑。</p><h2 id="sort-%E6%96%B9%E6%B3%95" tabindex="-1">sort 方法</h2><p>最简单的便是使用 sort 函数，代码如下：</p><pre><code class="language-javascript">const shuffle = arr =&gt; {  arr.sort(() =&gt; Math.random() &gt; 0.5)  return arr}</code></pre><p>在一般场景中以上代码实现便可满足功能需求，但仅是使用 sort 函数的乱序方式并不完美，出于 v8 引擎的底层原因，它对长短数组采用不同的排序方式，并不能真正随机打乱数组排序，简而言之就是最后得到的数组不能足够乱。</p><blockquote><p>由于 v8 引擎出于对性能的考虑，sort 函数对短数组（长度小于 10）使用的是插入排序，对长数组则使用了快速排序。其实不管用什么排序方法，大多数排序算法的时间复杂度介于 O(n) 到 O(n2) 之间，元素之间的比较次数通常情况下要远小于 n(n-1)/2，也就意味着有一些元素之间根本就没机会相比较（也就没有了随机交换的可能），这使 sort 随机排序的算法自然也不能真正随机。通俗的说，其实我们使用 array.sort 进行乱序，理想的方案或者说纯乱序的方案是：数组中每两个元素都要进行比较，这个比较有 50% 的交换位置概率。如此一来，总共比较次数一定为 n(n-1)。而在 sort 排序算法中，大多数情况都不会满足这样的条件。因而当然不是完全随机的结果了。</p></blockquote><h2 id="fisher%E2%80%93yates-shuffle-%E6%B4%97%E7%89%8C%E7%AE%97%E6%B3%95" tabindex="-1">Fisher–Yates Shuffle 洗牌算法</h2><p>Fisher–Yates Shuffle 洗牌算法是目前业界最著名的数组乱序算法之一，并且能够使数组足够乱，实现如下：</p><pre><code class="language-javascript">const shuffle = arr =&gt; {  let i = arr.length  let j  while (i) {    j = Math.floor(Math.random() * i--)    ;[arr[i], arr[j]] = [arr[j], arr[i]]  }  return arr}</code></pre><p>关于更多洗牌算法的细节详见 <a href="https://bost.ocks.org/mike/shuffle/" target="_blank">Fisher–Yates Shuffle</a>，里面通过动画生动地介绍了洗牌算法地高效乱序实现方式。</p>]]>
                    </description>
                    <pubDate>Sun, 04 Jun 2023 01:15:43 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[函数节流与函数防抖]]>
                    </title>
                    <link>https://jupiter-smile.cn:666/archives/han-shu-jie-liu-yu-han-shu-fang-dou</link>
                    <description>
                            <![CDATA[<h1 id="%E5%87%BD%E6%95%B0%E8%8A%82%E6%B5%81%E4%B8%8E%E5%87%BD%E6%95%B0%E9%98%B2%E6%8A%96" tabindex="-1">函数节流与函数防抖</h1><p>函数节流（throttle）与函数防抖（debounce）核心思想都是通过限制函数调用来实现性能优化，但两者概念却有不同：</p><ul><li>函数节流：函数按指定间隔调用，限制函数调用频率</li><li>函数防抖：一定时间段连续的函数调用，只让其执行一次</li></ul><p>两者的使用场景也有不同：</p><ul><li>函数节流：页面滚动事件监听（scroll）、DOM 元素拖拽（mousemove）、键盘事件（keydown）</li><li>函数防抖：文本输入验证发送请求、窗口缩放（resize）</li></ul><h2 id="%E5%87%BD%E6%95%B0%E8%8A%82%E6%B5%81" tabindex="-1">函数节流</h2><p>函数节流可以通过设置两个时间戳来实现，通过比较两个时间戳，让其在一定时间间隔内只执行一次，然后将时间戳重置为这次执行的时间，实现代码如下：</p><pre><code class="language-javascript">function throttle(func, delay) {  let lastCall = new Date()  return function(...args) {    const now = new Date()    if (now - lastCall &lt; delay) {      return    }    lastCall = now    return func.apply(this, args)  }}</code></pre><p>loadsh.js 里的函数节流功能实现更为复杂，可以实现首调用和尾调用。这里不再细说，详情可以参考源码。</p><h2 id="%E5%87%BD%E6%95%B0%E9%98%B2%E6%8A%96" tabindex="-1">函数防抖</h2><p>函数防抖可以通过定时器来实现，还可以设置第一次触发时是否立即执行：</p><pre><code class="language-javascript">function debounce(func, wait, immediate = false) {  let timeout  return function(...args) {    clearTimeout(timeout)    timeout = setTimeout(() =&gt; {      func.apply(this, args)    }, wait)    // 是否立即执行一次任务    if (immediate) {      immediate = false      func.apply(this, args)    }  }}</code></pre>]]>
                    </description>
                    <pubDate>Sun, 04 Jun 2023 01:14:42 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[从浏览器解析 JS 运行机制]]>
                    </title>
                    <link>https://jupiter-smile.cn:666/archives/cong-liu-lan-qi-jie-xi-js-yun-xing-ji-zhi</link>
                    <description>
                            <![CDATA[<h1 id="%E4%BB%8E%E6%B5%8F%E8%A7%88%E5%99%A8%E8%A7%A3%E6%9E%90-js-%E8%BF%90%E8%A1%8C%E6%9C%BA%E5%88%B6" tabindex="-1">从浏览器解析 JS 运行机制</h1><h2 id="%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B" tabindex="-1">进程与线程</h2><p>对于进程和线程，可以比喻为工厂和工人</p><ul><li>进程是一个工厂，工厂有它的独立资源（系统分配的独立一块内存）</li><li>工厂之间相互独立（进程之间相互独立）</li><li>线程是工厂中的工人，多个工人协作完成任务（多个线程在进程中协作完成任务）</li><li>工厂内有一个或多个工人（一个进程由一个或多个线程组成）</li><li>工人之间共享空间（同一进程下的各个线程之间共享程序的内存空间）</li></ul><p>可以理解为进程是能拥有资源和独立运行的最小单位，线程是建立在进程的基础上的一次程序运行单位，一个进程中可以有多个线程，官方术语：</p><ul><li><strong>进程是 cpu 资源分配的最小单位</strong></li><li><strong>线程是 cpu 调度的最小单位</strong></li></ul><p>不同进程之间也可以通信，不过代价较大。现在一般通用说法里的单线程与多线程，都是指在一个进程内的单和多。</p><h2 id="%E6%B5%8F%E8%A7%88%E5%99%A8%E5%A4%9A%E8%BF%9B%E7%A8%8B" tabindex="-1">浏览器多进程</h2><p>需要理解浏览器的三个概念：</p><ul><li>浏览器是多进程的</li><li>浏览器之所以能够运行，是因为系统给它的进程分配了资源（cpu、内存）</li><li>每打开一个 Tab 页，就相当于创建了一个独立的浏览器进程。</li></ul><h3 id="%E6%B5%8F%E8%A7%88%E5%99%A8%E5%A4%9A%E8%BF%9B%E7%A8%8B%E4%BB%8B%E7%BB%8D" tabindex="-1">浏览器多进程介绍</h3><p>浏览器包含的进程：</p><ol><li><strong>Browser 进程</strong>：浏览器的主进程（负责协调、主控），数量只有一个，作用有：<ul><li>负责浏览器界面显示，与用户交互，如前进，后退等</li><li>负责各个页面的管理，创建和销毁其他进程</li><li>将 Renderer 进程得到的内存中的 Bitmap，绘制到用户界面上</li><li>网络资源的管理，下载等</li></ul></li><li><strong>第三方插件进程</strong>：每种类型的插件对应一个进程，仅当使用该插件时才创建</li><li><strong>GPU 进程</strong>：最多只有一个，用于 3D 绘制等</li><li><strong>Renderer 进程（浏览器渲染进程，内部是多线程的）</strong>：Renderer 进程又指浏览器内核，默认每个 Tab 页面一个进程，互不影响。主要作用为页面渲染，脚本执行，事件处理等</li></ol><p>浏览器多进程的优势：</p><ul><li>避免单个 page crash 影响整个浏览器</li><li>避免第三方插件 crash 影响整个浏览器</li><li>多进程充分利用多核优势</li><li>方便使用沙盒模型隔离插件等进程，提高浏览器稳定性</li></ul><h3 id="renderer-%E8%BF%9B%E7%A8%8B" tabindex="-1">Renderer 进程</h3><p>我们需要重点讨论的是 Renderer 进程，即浏览器渲染进程，该进程包含的主要线程有：</p><ol><li><strong>GUI 渲染线程</strong><ul><li>负责渲染浏览器界面，解析 HTML，CSS，构建 DOM 树和 RenderObject 树，布局和绘制等</li><li>当界面需要重绘（Repaint）或由于某种操作引发回流（reflow）时，该线程就会执行</li><li>注意，<strong>GUI 渲染线程与 JS 引擎线程是互斥的</strong>，当 JS 引擎执行时 GUI 线程会被挂起，GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行</li></ul></li><li><strong>JS 引擎线程</strong><ul><li>也称为 JS 内核（例如 V8 引擎），负责解析 Javascript 脚本，运行代码</li><li>JS 引擎一直等待着任务队列中任务的到来，然后加以处理，一个 Tab 页（renderer 进程）中无论什么时候都只有一个 JS 线程在运行 JS 程序</li><li>同样注意，GUI 渲染线程与 JS 引擎线程是互斥的，所以如果 JS 执行的时间过长，这样就会造成页面的渲染不连贯，导致页面渲染加载阻塞</li></ul></li><li><strong>事件触发线程</strong><ul><li><strong>归属于浏览器而不是 JS 引擎</strong>，用来控制事件循环（可以理解，JS 引擎自己都忙不过来，需要浏览器另开线程协助）</li><li>当 JS 引擎执行代码块如 setTimeout 时（也可来自浏览器内核的其他线程，如鼠标点击、AJAX 异步请求等），会将对应任务添加到事件线程中</li><li>当对应的事件符合触发条件被触发时，该线程会把事件添加到待处理队列的队尾，等待 JS 引擎的处理</li><li>注意，由于 JS 的单线程关系，所以这些待处理队列中的事件都得排队等待 JS 引擎空闲时处理</li></ul></li><li><strong>定时触发器线程</strong><ul><li>setInterval 与 setTimeout 所在线程</li><li><strong>浏览器定时计数器并不是由 JavaScript 引擎计数的</strong>，因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确，因此通过单独线程来计时并触发定时，计时完毕后，添加到事件队列中，等待 JS 引擎空闲后执行</li><li>注意，W3C 在 HTML 标准中规定，规定要求 setTimeout 中低于 4ms 的时间间隔算为 4ms。</li></ul></li><li><strong>异步 http 请求线程</strong><ul><li>在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求</li><li>将检测到状态变更时，如果设置有回调函数，异步线程就产生状态变更事件，将这个回调再放入事件队列中，再由 JavaScript 引擎执行</li></ul></li></ol><h3 id="browser-%E8%BF%9B%E7%A8%8B%E5%92%8C-renderer-%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1" tabindex="-1">Browser 进程和 Renderer 进程通信</h3><p>然后分析 Browser 进程和 Renderer 进程的通信方式：</p><ul><li>Browser 进程收到用户请求，首先需要获取页面内容（譬如通过网络下载资源），随后将该任务通过 RendererHost 接口传递给 Renderer 进程</li><li>Renderer 进程的 Renderer 接口收到消息，解释后交给渲染线程，然后开始渲染<ul><li>渲染线程接收请求，加载网页并渲染网页，这其中可能需要 Browser 进程获取资源和需要 GPU 进程来帮助渲染</li><li>可能会有 JS 线程操作 DOM（这样可能会造成回流并重绘）</li><li>最后 Renderer 进程将结果传递给 Browser 进程</li></ul></li><li>Browser 进程接收到结果并将结果绘制出来</li></ul><p><img src="https://raw.githubusercontent.com/chanshiyucx/yoi/master/2019/%E6%B5%8F%E8%A7%88%E5%99%A8%E8%A7%A3%E6%9E%90/%E6%B5%8F%E8%A7%88%E5%99%A8%E8%A7%A3%E6%9E%90-%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1.png" alt="Browser 进程和 Renderer 进程通信" /></p><h2 id="renderer-%E8%BF%9B%E7%A8%8B%E7%9A%84%E5%A4%9A%E7%BA%BF%E7%A8%8B" tabindex="-1">Renderer 进程的多线程</h2><p>从上面分析我们知道 Renderer 进程是多线程的，主要包括：<strong>GUI 渲染线程、JS 引擎线程、事件触发线程、定时触发器线程、异步 http 请求线程</strong>。对于这些线程之间，我们需要理解一些概念。</p><h3 id="gui-%E6%B8%B2%E6%9F%93%E7%BA%BF%E7%A8%8B-%E4%B8%8E-js-%E5%BC%95%E6%93%8E%E7%BA%BF%E7%A8%8B%E4%BA%92%E6%96%A5" tabindex="-1">GUI 渲染线程 与 JS 引擎线程互斥</h3><p>上面已经提到，<strong>GUI 渲染线程与 JS 引擎线程是互斥的</strong>。由于 JavaScript 是可操纵 DOM 的，如果在修改这些元素属性同时渲染界面（即 JS 线程和 UI 线程同时运行），那么渲染线程前后获得的元素数据就可能不一致了。</p><p>因此为了防止渲染出现不可预期的结果，浏览器设置 GUI 渲染线程与 JS 引擎线程为互斥的关系，当 JS 引擎执行时 GUI 线程会被挂起， GUI 更新则会被保存在一个队列中等到 JS 引擎线程空闲时立即被执行。</p><p>从上述的互斥关系，可以推导出，JS 如果执行时间过长就会阻塞页面。譬如，假设 JS 引擎正在进行巨量的计算，此时就算 GUI 有更新，也会被保存到队列中，等待 JS 引擎空闲后执行。然后，由于巨量计算，所以 JS 引擎很可能很久很久后才能空闲，自然会感觉到巨卡无比。所以，要尽量避免 JS 执行时间过长，这样就会造成页面的渲染不连贯，导致页面渲染加载阻塞的感觉。</p><h3 id="web-worker" tabindex="-1">Web Worker</h3><p>关于 Web Worker 引用 MDN 介绍如下：</p><blockquote><p>Web Worker 为 Web 内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外，他们可以使用 XMLHttpRequest 执行 I/O (尽管 responseXML 和 channel 属性总是为空)。一旦创建， 一个 worker 可以将消息发送到创建它的 JavaScript 代码, 通过将消息发布到该代码指定的事件处理程序（反之亦然）。</p></blockquote><p>Web Worker 的作用，就是为 JavaScript 创造多线程环境，允许主线程创建 Worker 线程，将一些任务分配给后者运行。在主线程运行的同时，Worker 线程在后台运行，两者互不干扰。等到 Worker 线程完成计算任务，再把结果返回给主线程。这样的好处是，一些计算密集型或高延迟的任务，被 Worker 线程负担了，主线程（通常负责 UI 交互）就会很流畅，不会被阻塞或拖慢。</p><p>Worker 线程一旦新建成功，就会始终运行，不会被主线程上的活动（比如用户点击按钮、提交表单）打断。这样有利于随时响应主线程的通信。但是，这也造成了 Worker 比较耗费资源，不应该过度使用，而且一旦使用完毕，就应该关闭。</p><h2 id="%E6%B5%8F%E8%A7%88%E5%99%A8%E6%B8%B2%E6%9F%93%E6%B5%81%E7%A8%8B" tabindex="-1">浏览器渲染流程</h2><p>浏览器内容渲染大概可以划分成以下几个步骤：</p><ol><li>解析 html 建立 dom 树</li><li>解析 css 构建 render 树（将 CSS 代码解析成树形的数据结构，然后结合 DOM 合并成 render 树）</li><li>布局 render 树（Layout/reflow），负责各元素尺寸、位置的计算</li><li>绘制 render 树（paint），绘制页面像素信息</li><li>浏览器会将各层的信息发送给 GPU，GPU 会将各层合成（composite），显示在屏幕上。</li></ol><p>渲染完毕后执行 load 事件，其流程图如下所示：</p><p><img src="https://raw.githubusercontent.com/chanshiyucx/yoi/master/2019/%E6%B5%8F%E8%A7%88%E5%99%A8%E8%A7%A3%E6%9E%90/%E6%B5%8F%E8%A7%88%E5%99%A8%E8%A7%A3%E6%9E%90-%E6%B8%B2%E6%9F%93%E6%B5%81%E7%A8%8B.png" alt="浏览器渲染流程" /></p><h3 id="load-%E4%BA%8B%E4%BB%B6%E4%B8%8E-domcontentloaded-%E4%BA%8B%E4%BB%B6" tabindex="-1">load 事件与 DOMContentLoaded 事件</h3><p>在比较 load 事件与 DOMContentLoaded 事件执行顺序之前，先了解它们各自的触发时机：</p><ul><li>当 DOMContentLoaded 事件触发时，仅当 DOM 加载完成，不包括样式表，图片（譬如如果有 async 加载的脚本就不一定完成）</li><li>当 onload 事件触发时，页面上所有的 DOM，样式表，脚本，图片都已经加载完成了</li></ul><p>综上可以看出执行顺序为 <code>DOMContentLoaded -&gt; load</code>。</p><h3 id="css-%E5%8A%A0%E8%BD%BD%E6%98%AF%E5%90%A6%E4%BC%9A%E9%98%BB%E5%A1%9E-dom-%E6%A0%91%E6%B8%B2%E6%9F%93%EF%BC%9F" tabindex="-1">css 加载是否会阻塞 dom 树渲染？</h3><p>在解答这个问题之前需要知道一个重要概念：<strong>css 是由单独的下载线程异步下载的</strong>。</p><p>然后我们可以得到答案：<strong>css 加载不会阻塞 DOM 树解析（异步加载时 DOM 照常构建），但会阻塞 render 树渲染（渲染时需等 css 加载完毕，因为 render 树需要 css 信息）。</strong></p><p>这是浏览器的一种优化机制，因为加载 css 的时候，可能会修改下面 DOM 节点的样式， 如果 css 加载不阻塞 render 树渲染的话，那么当 css 加载完之后，render 树可能又得重新重绘或者回流了，这就造成了一些没有必要的损耗。所以干脆就先把 DOM 树的结构先解析完，把可以做的工作做完，然后 css 加载完之后，再根据最终的样式来渲染 render 树，这种做法性能方面确实会比较好一点。</p><h3 id="%E6%99%AE%E9%80%9A%E5%9B%BE%E5%B1%82%E5%92%8C%E5%A4%8D%E5%90%88%E5%9B%BE%E5%B1%82" tabindex="-1">普通图层和复合图层</h3><p>我们在浏览器渲染流程第 5 步中提到：浏览器会将各层的信息发送给 GPU，GPU 会将各层合成（composite），显示在屏幕上。这里涉及到 <code>composite</code> 的概念，浏览器渲染的图层一般包含两大类：<strong>普通图层以及复合图层</strong>。</p><p>首先，普通文档流内可以理解为一个复合图层（这里称为默认复合层，里面不管添加多少元素，其实都是在同一个复合图层中）。absolute 与 fixed 布局虽然可以脱离普通文档流，但它仍然属于默认复合层。</p><p>然后，可以通过硬件加速的方式，声明一个新的复合图层，它会单独分配资源，当然也会脱离普通文档流，这样一来，不管这个复合图层中怎么变化，也不会影响默认复合层里的回流重绘。</p><p>可以这么理解：<strong>GPU 中，各个复合图层是单独绘制的，所以互不影响</strong>。这也是为什么某些场景硬件加速效果一级棒。</p><p>将 DOM 元素变成复合图层（硬件加速）的方式有：</p><ul><li><code>translate3d、translateZ</code></li><li><code>opacity</code> 属性/过渡动画（需要动画执行的过程中才会创建合成层，动画没有开始或结束后元素还会回到之前的状态）</li><li><code>will-chang</code> 属性，提前告诉浏览器要变化，这样浏览器会开始做一些优化工作（这个最好用完后就释放）</li><li><code>&lt;video&gt;&lt;iframe&gt;&lt;canvas&gt;&lt;webgl&gt;</code> 等元素</li></ul><p>通过上面分析，可以知道 absolute 与硬件加速的区别：absolute 虽然可以脱离普通文档流，但是无法脱离默认复合层。所以，就算 absolute 中信息改变时不会改变普通文档流中 render 树，但是，浏览器最终绘制时是整个复合层绘制的，所以 absolute 中信息的改变，仍然会影响整个复合层的绘制。浏览器会重绘它，如果复合层中内容多，absolute 带来的绘制信息变化过大，资源消耗是非常严重的。</p><p>而硬件加速直接就是在另一个复合层了，所以它的信息改变不会影响默认复合层，只影响属于自己的复合层，仅仅是引发最后的合成（输出视图）。</p><p>虽然硬件加速看起来那么美妙，但是仍需要谨慎使用。尽量不要大量使用复合图层，否则由于资源消耗过度，页面反而会变的更卡。</p><p>使用硬件加速时，尽可能的使用 index，防止浏览器默认给后续的元素创建复合层渲染。原因是在 webkit CSS3 中，如果元素添加了硬件加速，并且 index 层级比较低，那么在这个元素的后面其它元素会默认变为复合层渲染，如果处理不当会极大的影响性能。</p><h2 id="event-loop" tabindex="-1">Event Loop</h2><p>从这里终于讲到了本文最核心的部分：JS 的运行机制。先回顾下 Renderer 进程的五大线程：<strong>GUI 渲染线程、JS 引擎线程、事件触发线程、定时触发器线程、异步 http 请求线程</strong>。</p><p>再理解一个概念：</p><ul><li>JS 分为同步任务和异步任务</li><li>同步任务都在主线程（JS 引擎线程）上执行，形成一个<code>执行栈</code></li><li>主线程之外，事件触发线程管理着一个<code>任务队列</code>，只要异步任务有了运行结果，就在任务队列之中放置一个事件</li><li>一旦执行栈中的所有同步任务执行完毕，系统就会读取任务队列，将可运行的异步任务添加到可执行栈中，开始执行</li></ul><p><img src="https://raw.githubusercontent.com/chanshiyucx/yoi/master/2019/%E6%B5%8F%E8%A7%88%E5%99%A8%E8%A7%A3%E6%9E%90/%E6%B5%8F%E8%A7%88%E5%99%A8%E8%A7%A3%E6%9E%90-%E6%89%A7%E8%A1%8C%E6%A0%88%E4%B8%8E%E4%BB%BB%E5%8A%A1%E9%98%9F%E5%88%97.png" alt="执行栈与任务队列" /></p><p>可以解释如下：</p><ul><li>主线程运行执行栈，栈中代码执行时调用某些 API（如 ajax 请求）产生事件并添加到任务队列</li><li>执行栈中代码执行完毕，读取任务队列中的代码，如此循环</li></ul><h3 id="macrotask-%E4%B8%8E-microtask" tabindex="-1">macrotask 与 microtask</h3><p>JS 中分为两种任务类型：<code>macrotask</code>和<code>microtask</code>，即宏任务和微任务，在 ECMAScript 中，macrotask 称为 <code>task</code>，microtask 称为 <code>jobs</code>。</p><ul><li>macrotask：可以理解是每次执行栈执行的代码就是一个宏任务（包括每次从事件队列中获取一个事件回调并放到执行栈中执行），由<strong>事件触发线程</strong>维护<ul><li>每一个 task 会从头到尾将这个任务执行完毕，不会执行其它</li><li>浏览器为了能够使得 JS 内部 task 与 DOM 任务能够有序的执行，会在一个 task 执行结束后，在下一个 task 执行开始前，对页面进行重新渲染（<code>task-&gt;渲染-&gt;task-&gt;...</code>）</li></ul></li><li>microtask：可以理解是在当前 task 执行结束后立即执行的任务，在当前 task 任务后，下一个 task 之前，也在渲染之前，由<strong>JS 引擎线程</strong>维护<ul><li><strong>所以它的响应速度相比 setTimeout（setTimeout 是 task）会更快</strong>，因为无需等渲染</li><li>在某一个 macrotask 执行完后，就会将在它执行期间产生的所有 microtask 都执行完毕（在渲染前）</li></ul></li></ul><p>macrotask 与 microtask 各有哪些？</p><ul><li>macrotask：主代码块，setTimeout，setInterva</li><li>microtask：Promise，process.nextTick（在 node 环境下，process.nextTick 的优先级高于 Promise）</li></ul><p>最后总结下 macrotask 与 microtask 的运行机制：</p><p><img src="https://raw.githubusercontent.com/chanshiyucx/yoi/master/2019/%E6%B5%8F%E8%A7%88%E5%99%A8%E8%A7%A3%E6%9E%90/%E6%B5%8F%E8%A7%88%E5%99%A8%E8%A7%A3%E6%9E%90-%E8%BF%90%E8%A1%8C%E6%9C%BA%E5%88%B6.png" alt="macrotask 与 microtask 的运行机制" /></p><ul><li>执行一个宏任务（栈中没有就从事件队列中获取）</li><li>执行过程中如果遇到微任务，就将它添加到微任务的任务队列中</li><li>宏任务执行完毕后，立即执行当前微任务队列中的所有微任务（依次执行）</li><li>当前宏任务执行完毕，开始检查渲染，然后 GUI 线程接管渲染</li><li>渲染完毕后，JS 线程继续接管，开始下一个宏任务（从事件队列中获取）</li></ul><p>参考文章：<br /><a href="https://juejin.im/post/5a6547d0f265da3e283a1df7" target="_blank">从浏览器多进程到 JS 单线程，JS 运行机制最全面的一次梳理</a></p>]]>
                    </description>
                    <pubDate>Sun, 04 Jun 2023 01:13:40 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[变量声明与解构赋值]]>
                    </title>
                    <link>https://jupiter-smile.cn:666/archives/bian-liang-sheng-ming-yu-jie-gou-fu-zhi</link>
                    <description>
                            <![CDATA[<h1 id="%E5%8F%98%E9%87%8F%E5%A3%B0%E6%98%8E%E4%B8%8E%E8%A7%A3%E6%9E%84%E8%B5%8B%E5%80%BC" tabindex="-1">变量声明与解构赋值</h1><h2 id="let-%E5%92%8C-const" tabindex="-1">let 和 const</h2><h3 id="%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95" tabindex="-1">基本用法</h3><p>let 和 const 声明变量的三大特性：<strong>不存在变量提升、暂时性死区、不允许重复声明</strong>。</p><h4 id="%E4%B8%8D%E5%AD%98%E5%9C%A8%E5%8F%98%E9%87%8F%E6%8F%90%E5%8D%87" tabindex="-1">不存在变量提升</h4><p>let 声明的变量一定要在声明后使用，否则便会报错。</p><pre><code class="language-javascript">// var的情况console.log(foo) // 输出 undefinedvar foo = 2// let的情况console.log(bar) // 报错 ReferenceErrorlet bar = 2</code></pre><h4 id="%E6%9A%82%E6%97%B6%E6%80%A7%E6%AD%BB%E5%8C%BA" tabindex="-1">暂时性死区</h4><p>ES6 规定，如果区块中存在 let 和 const 命令，则这个区块对这些命令声明的变量从一开始就形成封闭作用域。这在语法上称为“暂时性死区”（temporal dead zone，简称 TDZ）。</p><pre><code class="language-javascript">var tmp = 123if (true) {  // TDZ 开始  tmp = &#39;abc&#39; // ReferenceError  let tmp  // TDZ 结束  console.log(tmp) // undefined}</code></pre><p><strong>“暂时性死区”也意味着 typeof 不再是一个百分之百安全的操作</strong>，变量用 let 声明的话，那么在声明之前引用会报错。作为比较，如果一个变量根本没有被声明，使用 typeof 反而不会报错。</p><pre><code class="language-javascript">typeof x // ReferenceErrorlet xtypeof undeclared_variable // &quot;undefined&quot;</code></pre><p>总之，暂时性死区的本质就是，<strong>只要进入当前作用域，所要使用的变量就已经存在，但是不可获取</strong>，只有等到声明变量的那一行代码出现，才可以获取和使用该变量。</p><h4 id="%E4%B8%8D%E5%85%81%E8%AE%B8%E9%87%8D%E5%A4%8D%E5%A3%B0%E6%98%8E" tabindex="-1">不允许重复声明</h4><p>let 和 const 命令不允许在相同作用域内重复声明同一个变量。</p><pre><code class="language-javascript">function foo(arg) {  let arg // 报错}function bar(arg) {  {    let arg // 不报错  }}</code></pre><h4 id="%E6%89%A9%E5%B1%95" tabindex="-1">扩展</h4><p>扩展 1：const 实际上保证的是变量指向的那个内存地址不得改动。如果真的想将对象冻结，应该使用 <code>Object.freeze</code> 方法。</p><pre><code class="language-javascript">var constantize = obj =&gt; {  Object.freeze(obj)  Object.keys(obj).forEach((key, i) =&gt; {    if (typeof obj[key] === &#39;object&#39;) {      constantize(obj[key])    }  })}</code></pre><p>扩展 2：ES5 只有两种声明变量的方法：使用 var 命令和 function 命令。ES6 除了添加了 let 和 const 命令，还有另外两种声明变量的方法：import 命令和 class 命令。所以，<strong>ES6 一共有 6 种声明变量的方法</strong>。</p><h3 id="%E5%9D%97%E7%BA%A7%E4%BD%9C%E7%94%A8%E5%9F%9F" tabindex="-1">块级作用域</h3><p>ES5 只有全局作用域和函数作用域，没有块级作用域，容易出现变量覆盖和变量泄露的问题。</p><p>ES6 引入了块级作用域，明确允许在块级作用域之中声明函数。ES6 规定：<strong>在块级作用域之中，函数声明语句的行为类似于 let，在块级作用域之外不可引用</strong>。</p><p>块级作用域的出现，实际上使得获得广泛应用的立即执行匿名函数（IIFE）不再必要了。</p><p>但是由于这条规则会对旧代码产生很大影响。为了减轻因此产生的不兼容问题，浏览器可以不遵守这条规则，所以尽量避免在块级作用域内声明函数。</p><h3 id="%E9%A1%B6%E5%B1%82%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%B1%9E%E6%80%A7" tabindex="-1">顶层对象的属性</h3><p>顶层对象在浏览器环境中指的是 window 对象，在 Node 环境中指的是 global 对象。在 ES5 中，顶层对象的属性与全局变量等价。</p><p>ES6 为了改变这一点，一方面规定，为了保持兼容性，var 命令和 function 命令声明的全局变量依旧是顶层对象的属性；另一方面规定，let 命令、const 命令、class 命令声明的全局变量不属于顶层对象的属性。</p><pre><code class="language-javascript">var a = 1window.a // 1let b = 1window.b // undefined</code></pre><h3 id="global-%E5%AF%B9%E8%B1%A1" tabindex="-1">global 对象</h3><p>ES5 的顶层对象在各种实现中是不统一的。</p><ul><li>在浏览器中，顶层对象是 window，但 Node 和 Web Worker 没有 window。</li><li>在浏览器和 Web Worker 中，self 也指向顶层对象，但是 Node 没有 self。</li><li>在 Node 中，顶层对象是 global，但其他环境都不支持。</li></ul><p>同一段代码为了能够在各种环境中都取到顶层对象，目前一般是使用 this 变量，但是也有局限性。</p><ul><li>在全局环境中，this 会返回顶层对象。但是在 Node 模块和 ES6 模块中，this 返回的是当前模块。</li><li>对于函数中的 this，如果函数不是作为对象的方法运行，而是单纯作为函数运行，this 会指向顶层对象。但是严格模式下，this 会返回 undefined。</li><li>不管是严格模式，还是普通模式，<code>new Function('returnthis')()</code> 总会返回全局对象。但是，如果浏览器用了 CSP（Content Security Policy，内容安全政策），那么 eval、new Function 这些方法都可能无法使用。</li></ul><p>综上所述，很难找到一种方法可以在所有情况下都取到顶层对象。以下是两种勉强可以使用的方法：</p><pre><code class="language-javascript">// 方法一typeof window !== &#39;undefined&#39;  ? window  : typeof process === &#39;object&#39; &amp;&amp; typeof require === &#39;function&#39; &amp;&amp; typeof global === &#39;object&#39;  ? global  : this// 方法二const getGlobal = function() {  if (typeof self !== &#39;undefined&#39;) {    return self  }  if (typeof window !== &#39;undefined&#39;) {    return window  }  if (typeof global !== &#39;undefined&#39;) {    return global  }  throw new Error(&#39;unable to locate global object&#39;)}</code></pre><h2 id="%E5%8F%98%E9%87%8F%E7%9A%84%E8%A7%A3%E6%9E%84%E8%B5%8B%E5%80%BC" tabindex="-1">变量的解构赋值</h2><h3 id="%E6%95%B0%E7%BB%84%E7%9A%84%E8%A7%A3%E6%9E%84%E8%B5%8B%E5%80%BC" tabindex="-1">数组的解构赋值</h3><p>解构（Destructuring）属于“模式匹配”，只要等号两边的模式相同，左边的变量就会被赋予对应的值。</p><pre><code class="language-javascript">let [x, y, ...z] = [&#39;a&#39;]x // &quot;a&quot;y // undefinedz // []// 不完全解构let [a, b] = [1, 2, 3]a // 1b // 2</code></pre><p>对于 Set 结构，也可以使用数组的解构赋值。</p><pre><code class="language-javascript">let [x, y, z] = new Set([&#39;a&#39;, &#39;b&#39;, &#39;c&#39;])x // &quot;a&quot;</code></pre><p>事实上，只要某种数据结构具有 Iterator 接口，都可以采用数组形式的解构赋值。</p><pre><code class="language-javascript">function* fibs() {  let a = 0  let b = 1  while (true) {    yield a    ;[a, b] = [b, a + b]  }}let [first, second, third, fourth, fifth, sixth] = fibs()sixth // 5</code></pre><p>解构赋值允许指定默认值。ES6 内部使用严格相等运算符（===）判断一个位置是否有值。所以，<strong>如果一个数组成员不严格等于 undefined，默认值是不会生效的</strong>。</p><pre><code class="language-javascript">let [x = 1] = [undefined]x // 1let [y = 1] = [null]y // null</code></pre><p>如果默认值是一个表达式，那么这个表达式是惰性求值的，即只有在用到时才会求值。</p><pre><code class="language-javascript">// x 可以取值，函数不会执行function f() {  console.log(&#39;aaa&#39;)}let [x = f()] = [1]</code></pre><p>默认值可以引用解构赋值的其他变量，但该变量必须已经声明：</p><pre><code class="language-javascript">let [x = 1, y = x] = [] // x=1; y=1</code></pre><h3 id="%E5%AF%B9%E8%B1%A1%E7%9A%84%E8%A7%A3%E6%9E%84%E8%B5%8B%E5%80%BC" tabindex="-1">对象的解构赋值</h3><p>对象的解构赋值的内部机制是先找到同名属性，然后再赋值给对应的变量。真正被赋值的是后者，而不是前者。</p><pre><code class="language-javascript">let { foo: baz } = { foo: &#39;aaa&#39;, bar: &#39;bbb&#39; }baz // &#39;aaa&#39;</code></pre><p>上面的代码中，foo 是匹配的模式，baz 才是变量。真正被赋值的是变量 baz，而不是模式 foo。</p><p>与数组一样，解构也可以用于嵌套结构的对象：</p><pre><code class="language-javascript">let obj = { p: [&#39;Hello&#39;, { y: &#39;World&#39; }] }let {  p: [x, { y }]} = obj</code></pre><p>注意，这时 p 是模式，不是变量，因此不会被赋值。如果 p 也要作为变量赋值，可以写成下面这样。</p><pre><code class="language-javascript">let {  p,  p: [x, { y }]} = obj</code></pre><p>同数组解构一样，对象解构也可以使用默认值，默认值生效的条件是，对象的属性值严格等于 undefined。</p><p>注意：如果要将一个已经声明的变量用于解构赋值，必须非常小心。</p><pre><code class="language-javascript">// 错误的写法let x;{ x } = { x: 1 } // SyntaxError: syntax error</code></pre><p>上面代码的写法会报错，因为 JavaScript 引擎会将 { x } 理解成一个代码块，从而发生语法错误。只有不将大括号写在行首，避免将其解释为代码块，才能解决这个问题。</p><pre><code class="language-javascript">// 正确的写法let x;({ x } = { x: 1 })</code></pre><p>由于数组本质是特殊的对象，因此可以对数组进行对象属性的解构。</p><pre><code class="language-javascript">let arr = [1, 2, 3]let { 0: first, [arr.length - 1]: last } = arrfirst // 1last // 3</code></pre><h3 id="%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E8%A7%A3%E6%9E%84%E8%B5%8B%E5%80%BC" tabindex="-1">字符串的解构赋值</h3><p>对字符串解构时，字符串会被转换为类数组对象。</p><pre><code class="language-javascript">const [a, b, c, d, e] = &#39;hello&#39;a // &#39;h&#39;b // &#39;e&#39;</code></pre><h3 id="%E6%95%B0%E5%80%BC%E5%92%8C%E5%B8%83%E5%B0%94%E5%80%BC%E7%9A%84%E8%A7%A3%E6%9E%84%E8%B5%8B%E5%80%BC" tabindex="-1">数值和布尔值的解构赋值</h3><p>解构赋值时，如果等号右边是数值和布尔值，则会先转为对象。</p><pre><code class="language-javascript">let { toString: s } = 123s === Number.prototype.toString // truelet { toString: s } = trues === Boolean.prototype.toString // true</code></pre><p>解构赋值的规则是，只要等号右边的值不是对象或数组，就先将其转为对象。由于 undefined 和 null 无法转为对象，所以对它们进行解构赋值时都会报错。</p><pre><code class="language-javascript">let { prop: x } = undefined // TypeErrorlet { prop: y } = null // TypeError</code></pre><h3 id="%E5%87%BD%E6%95%B0%E5%8F%82%E6%95%B0%E7%9A%84%E8%A7%A3%E6%9E%84%E8%B5%8B%E5%80%BC" tabindex="-1">函数参数的解构赋值</h3><pre><code class="language-javascript">function move({ x, y } = { x: 0, y: 0 }) {  return [x, y]}move({ x: 3, y: 8 }) // [3, 8]move({ x: 3 }) // [3, undefined]move({}) // [undefined, undefined]move() // [0, 0]</code></pre><h3 id="%E5%9C%86%E6%8B%AC%E5%8F%B7%E9%97%AE%E9%A2%98" tabindex="-1">圆括号问题</h3><p>解构赋值时，对于编译器来说，一个式子到底是模式还是表达式，没有办法从一开始就知道，必须解析到（或解析不到）等号才能知道。</p><p>ES6 规定：<strong>只要有可能导致解构的歧义，就不得使用圆括号</strong>。</p><h4 id="%E4%B8%8D%E8%83%BD%E4%BD%BF%E7%94%A8%E5%9C%86%E6%8B%AC%E5%8F%B7%E7%9A%84%E6%83%85%E5%86%B5" tabindex="-1">不能使用圆括号的情况</h4><ol><li>变量声明语句</li></ol><pre><code class="language-javascript">// 全部报错let [(a)] = [1]let {x: (c)} = {}let ({x: c}) = {}let {(x: c)} = {}let {(x): c} = {}let { o: ({ p: p }) } = { o: { p: 2 } }</code></pre><p>上面 6 个语句都会报错，因为它们都是变量声明语句，模式不能使用圆括号。</p><ol><li>函数参数</li></ol><p>函数参数也属于变量声明，因此不能使用圆括号。</p><pre><code class="language-javascript">// 报错function foo([(z)]) { return z }</code></pre><ol><li>赋值语句的模式</li></ol><pre><code class="language-javascript">// 全部报错;({ p: a }) = { p: 42 };([a]) = [5]</code></pre><p>上面的代码将整个模式放在圆括号之中，导致报错。</p><pre><code class="language-javascript">// 报错;[{ p: a }, { x: c }] = [{}, {}]</code></pre><p>上面的代码将一部分模式放在圆括号之中，导致报错。</p><h4 id="%E5%8F%AF%E4%BB%A5%E4%BD%BF%E7%94%A8%E5%9C%86%E6%8B%AC%E5%8F%B7%E7%9A%84%E6%83%85%E5%86%B5" tabindex="-1">可以使用圆括号的情况</h4><p>可以使用圆括号的情况只有一种：赋值语句的非模式部分可以使用圆括号。</p><pre><code class="language-javascript">;[b] = [3] // 正确;({ p: d } = {}) // 正确;[parseInt.prop] = [3] // 正确</code></pre><p>上面 3 行语句都可以正确执行，因为它们都是赋值语句，而不是声明语句，另外它们的圆括号都不属于模式的一部分：</p><ul><li>第 1 行语句中，模式是取数组的第 1 个成员，跟圆括号无关；</li><li>第 2 行语句中，模式是 p 而不是 d；</li><li>第 3 行语句与第 1 行语句的性质一致。</li></ul><h3 id="%E6%89%A9%E5%B1%95-1" tabindex="-1">扩展</h3><p>任何部署了 Iterator 接口的对象都可以用 for…of 循环遍历。Map 结构原生支持 Iterator 接口，配合变量的解构赋值获取键名和键值就非常方便。</p><pre><code class="language-javascript">// 获取键名for (let [key] of map) {  // ...}// 获取键值for (let [, value] of map) {  // ...}</code></pre>]]>
                    </description>
                    <pubDate>Sun, 04 Jun 2023 01:01:03 CST</pubDate>
                </item>
    </channel>
</rss>