<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Benjamin's website]]></title><description><![CDATA[- Inner monologue]]></description><link>https://benlifec.com</link><image><url>https://benlifec.com/innei.svg</url><title>Benjamin&apos;s website</title><link>https://benlifec.com</link></image><generator>Shiro (https://github.com/Innei/Shiro)</generator><lastBuildDate>Tue, 08 Sep 2026 11:09:07 GMT</lastBuildDate><atom:link href="https://benlifec.com/feed" rel="self" type="application/rss+xml"/><pubDate>Tue, 08 Sep 2026 11:09:07 GMT</pubDate><language><![CDATA[zh-CN]]></language><item><title><![CDATA[一年程序员的认知成长：看问题的三个层次]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://benlifec.com/posts/life/cognize">https://benlifec.com/posts/life/cognize</a></blockquote><div><p>毕业一年后，我越来越发现，职场和生活中的问题，表面上看是一些“突发的小事”，但背后往往有更深层的规律在运作。刚工作那会儿，我总是被眼前的事件浪花吸引，看到裁员新闻就慌，看到市场利好就兴奋，直到后来经历了几轮项目的起落，才慢慢体会到：浪花只是表象，真正决定海面形态的是潮汐，甚至是更深层的气候和地理。</p><p>这一认知转变，来自我最近接触到的一个分析框架——<strong>“看问题的三个层次”</strong>：
<strong>事件（浪花） → 局势（潮汐） → 结构（气候/地理/生产关系）</strong>。
它用自然现象做隐喻，把复杂系统拆解为三个时间和空间维度，让人既能看清当下，又能看见更远。</p><hr/><h2 id="">一、三个层次：从表象到本质</h2><h3 id="1-">1. 事件（浪花）：短促、孤立、容易被看到</h3><p>事件是短时间内发生的具体现象，可以用“5W1H”清楚描述。比如：</p><ul><li>某互联网公司突然宣布裁员10%</li><li>某城市暴雨引发内涝</li></ul><p>它们像海面上的浪花，变化快、随机性强、孤立存在——很容易吸引注意，但单看一个浪花，很难判断整个海的走向。</p><h3 id="2-">2. 局势（潮汐）：关联、周期、可预测</h3><p>局势是事件之间的组合与趋势，是时间维度上延展的因果链。</p><ul><li>同一行业连续 3 年收缩</li><li>全球变暖背景下极端天气频率逐年增加</li></ul><p>它像潮汐，不是偶然的浪花，而是方向性的波动。通过局势，我们能提前看到事件的“批量到来”。</p><h3 id="3-">3. 结构（气候/地理/生产关系）：稳定、底层、长期约束</h3><p>结构是最底层的规则——地理、制度、技术——决定了局势可能出现的形态，也限制了事件的发生方式。</p><ul><li>地理结构：沿海平原适合农业 → 人口密集 → 经济形态特殊</li><li>社会结构：资本逐利性 → 周期性经济危机</li><li>技术结构：互联网普及 → 信息传播成本骤降 → 商业格局重塑</li></ul><p>它像气候与地理，短期不变，却长期塑造海洋的面貌。</p><hr/><h2 id="">二、事件，局势，结构的相互作用</h2><p>三个层次并非孤立存在，而是嵌套、动态关联的。</p><ol start="1"><li><p><strong>结构决定局势，局势塑造事件</strong>
比如：
新航路开辟（结构变化）→ 欧洲殖民扩张（局势）→ 英国纺织业爆发（事件）。</p></li><li><p><strong>事件积累反作用于局势，甚至改变结构</strong>
工业革命早期工人罢工（事件）→ 工会制度化（局势）→ 八小时工作制普及（结构调整）。</p></li><li><p><strong>稳定结构与动态局势的张力</strong>
传统燃油车产业链（结构）稳定多年 → 电动车兴起（事件）→ 全球能源转型（局势）→ 汽车产业结构可能被重构。</p></li></ol><hr/><h2 id="">三、如何训练“三层思维”</h2><h3 id="1-">1. 观察：用提问清单强制分层</h3><ul><li>事件：它是孤立还是同类事件的重复？直接原因是什么？</li><li>局势：过去几年是否出现过类似趋势？影响范围有多大？</li><li>结构：是否有长期不变的基础条件？改变它会怎样？</li></ul><p>以“2021年全球芯片短缺”为例：</p><ul><li>事件：某车企因缺芯停产</li><li>局势：半导体产能不足，多个行业受冲击</li><li>结构：ASML 技术垄断、产业链区域分布不均、地缘政治博弈</li></ul><h3 id="2-">2. 学习：跨学科积累结构敏感度</h3><ul><li>地理学看自然结构</li><li>经济学、社会学看社会结构</li><li>技术史、技术哲学看技术结构</li></ul><p>例如分析“城市化”：</p><ul><li>事件：地铁开通</li><li>局势：人口向沿海城市聚集</li><li>结构：土地财政、户籍制度、港口优势</li></ul><h3 id="3-">3. 实践：用模拟推演验证</h3><p>假设结构 → 推演局势 → 用事件验证。
比如短视频对传统媒体的冲击：</p><ul><li>结构假设：移动互联网普及降低内容门槛</li><li>局势预测：广告收入下降、受众流失</li><li>事件验证：电视台收视率下滑、报社转型做短视频</li></ul><hr/><h2 id="">四、为什么程序员也该学“三层思维”</h2><p>作为程序员，我发现这个框架和调试系统很像：</p><ul><li><strong>事件</strong>：一次 Bug 报错（表象）</li><li><strong>局势</strong>：同一模块频繁出 Bug（趋势）</li><li><strong>结构</strong>：架构设计缺陷或业务流程问题（底层）</li></ul><p>只盯着报错修 Bug，永远陷在浪花里；看清潮汐，才能做中期优化；触动结构，才可能从根上解决问题。</p><hr/><p><strong>总结</strong>
事件、局势、结构，本质是“点-线-网”的三维透视法：</p><ul><li>改事件，看短期</li><li>改局势，看中期</li><li>改结构，看长期</li></ul><p>一年职场的体会是：
<strong>不要被浪花迷住眼，要学会读潮汐，看懂气候，这样才能在风浪中掌舵，而不是被海浪推着走。</strong></p><hr/></div><p style="text-align:right"><a href="https://benlifec.com/posts/life/cognize#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://benlifec.com/posts/life/cognize</link><guid isPermaLink="true">https://benlifec.com/posts/life/cognize</guid><dc:creator><![CDATA[benjamin]]></dc:creator><pubDate>Sat, 09 Aug 2025 04:37:08 GMT</pubDate></item><item><title><![CDATA[[JS] 我所理解的 JavaScript （下）]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://benlifec.com/posts/frontend/js_next">https://benlifec.com/posts/frontend/js_next</a></blockquote><div><blockquote><p>接触 Javascript 这门编程语言，满打满算应该有将近 4 年的时间，我觉得不能说对这门语言理解的有多深刻，只是在看了众多的经典书籍和博客之后，想把自己的理解和总结也分享出来。</p></blockquote>
<h2 id="this">This</h2><ul><li><code>《你不知道的 JavaScript 下 》</code>这本书当中关于 this 内容我觉得很好的阐述了 <code>this</code> 的绑定规则。
<ul><li>默认绑定: 独立函数调用, 直接调用一个函数, 可以将这条规则看作是无法引用与其他规则的默认规则。(全局函数当中的 this 在非严格模式下等于 <code>window</code>, 在严格模式下等于 <code>undefined</code>)。</li><li>显示绑定: 通过 <code>apply</code> 和<code>call</code> 来显示绑定<code>this</code>, 还有一种显示绑定的变种是硬绑定 <code>bind()</code>。</li><li>隐式绑定:比如调用对象的方法那么方法当中的 <code>this</code> 会指向该对象。 隐式绑定有 <code>this</code> 丢失问题, 函数别名<code>var bar = obj.foo; bar();</code> 传入函数参数, 传入语言内置的函数(<code>setTimeout</code> 函数，点击事件)都会导致 <code>this</code> 丢失。</li><li><code>new</code> 绑定: .绑定构造函数当中的 <code>this</code> 到实例对象.
<ul><li><code>new</code> 一个构造函数发生了什么： 1.创建一个空对象(实例对象)。 2.将实例对象的<code>__proto__</code>指定到构造函数的<code>prototype</code>上。 3.绑定构造函数当中的 <code>this</code> 到实例对象, 执行构造函数当中的代码，给实例对象赋值属性和方法。 4.构造函数当中是否有返回值 ,没有就返回这个实例对象, 有就看返回的是否是对象类型, 是就返回这个对象, 不是就返回实例对象。</li></ul></li></ul></li></ul><h3 id="this-"><strong>this 的四项绑定规则案例</strong></h3><ul><li>默认绑定</li></ul><pre class=""><code class="">// 默认绑定: 独立函数调用
// 1.案例一:
function foo() {
  console.log(this);
}
foo();

// 2.案例二:
function foo1() {
  console.log(this);
}

function foo2() {
  console.log(this);
  foo1();
}

function foo3() {
  console.log(this);
  foo2();
}

foo3();

// 3.案例三:
function foo() {
  function bar() {
    console.log(this);
  }
  return bar;
}

var fn = foo();
fn(); // window</code></pre><ul><li>隐式绑定</li></ul><pre class=""><code class="">// 案例一
var obj = {
  name: &#x27;why&#x27;,
  foo: function () {
    console.log(this);
  },
};

var bar = obj.foo; // 函数别名 this丢失
bar(); // window
// 案例二
function foo() {
  console.log(this);
}
var obj = {
  name: &#x27;why&#x27;,
  foo: foo,
};

var bar = obj.foo; // 函数别名 this丢失
bar(); // window
// 案例三
var a = &#x27;global&#x27;;
function foo(fn) {
  fn();
}
var obj = {
  a: &#x27;local&#x27;,
  eating() {
    console.log(this.a, &#x27;正在吃饭&#x27;); // 函数参数 this丢失
  },
};
foo(obj.eating); // global 正在吃饭
// 注意在node环境下调试的时候，全局的this指向空对象
// 案例四
setTimeout(() =&gt; {
  console.log(this); // window 传入系统内置的函数
}, 1000);
// 案例五
div.addEventListener(&#x27;click&#x27;, function () {
  console.log(this); // Dom div 传入系统内置的函数
});
// 案例6 map filter
arr.map((item) =&gt; {
  console.log(this); //默认是window第二个参数可以指定绑定 this 传入系统内置的函数
}, obj);</code></pre><ul><li>显示绑定</li></ul><pre class=""><code class="">function foo() {
  console.log(&#x27;函数被调用了&#x27;, this);
}
var obj = {
  name: &#x27;obj&#x27;,
};

// call / apply是可以指定this的绑定对象;
foo.call(obj, 参数);
foo.apply(obj, 参数列表);
foo.apply(&#x27;aaaa&#x27;);</code></pre><ul><li>new 绑定</li></ul><pre class=""><code class="">function Person(name, age) {
  this.name = name;
  this.age = age;
}

var p1 = new Person(&#x27;why&#x27;, 18);
console.log(p1.name, p1.age);</code></pre><h3 id="this-"><strong>this 绑定的优先级</strong></h3><h3 id="new-------"><strong>new 绑定 &gt; 显示绑定 &gt; 隐式绑定 &gt; 默认绑定。</strong></h3><ul><li>显示绑定优先级高于隐式代码</li></ul><pre class=""><code class="">const obj = {
  name: &#x27;ryan&#x27;,
  getName() {
    return this.name;
  },
};
console.log(obj.getName.call(&#x27;string&#x27;)); // this -&gt; string;
//更明显的比较
function foo() {
  console.log(this);
}

var obj = {
  foo: foo.bind(&#x27;aaa&#x27;),
};
obj.foo(); // this -&gt; &#x27;aaa&#x27;;</code></pre><ul><li>new 绑定大于隐式绑定代码</li></ul><pre class=""><code class="">var obj = {
  name: &#x27;ryan&#x27;,
  getName() {
    return this.name;
  },
};
let fn = new obj.getName(); this -&gt; 实例对象fn</code></pre><ul><li>new 绑定大于显示绑定代码</li></ul><pre class=""><code class="">// 由于new 和 call以及apply都是调用函数，自然就不可以一起使用。
function foo() {
  console.log(this);
}
const fn = foo.bind(&#x27;a&#x27;);
const nFn = new fn();
console.log(nFn); this -&gt; 实例对象nFn</code></pre><h3 id=""><strong>特殊绑定</strong></h3><ul><li>忽略显示绑定</li></ul><pre class=""><code class="">// apply/call/bind: 当传入 null/undefined 时, 自动将 this 绑定成全局对象
foo.apply(null);
foo.apply(undefined);</code></pre><ul><li>规范绑定</li></ul><pre class=""><code class="">var obj1 = {
  name: &#x27;obj1&#x27;,
  foo: function () {
    console.log(this);
  },
};

var obj2 = {
  name: &#x27;obj2&#x27;,
};
(obj2.bar = obj1.foo)();</code></pre><h3 id="-this"><strong>箭头函数当中的 this</strong></h3><ul><li>箭头函数不绑定 this，没有 arguments，也不可以作为构造函数 使用 new 来调用。</li><li>箭头函数当中的 this 指向外层全局或者函数作用域当中的 this。</li></ul><pre class=""><code class="">var obj = {
  data: [],
  getData: function () {
    setTimeout(() =&gt; {
      var result = [&#x27;abc&#x27;, &#x27;cba&#x27;, &#x27;nba&#x27;];
      this.data = result;
    }, 2000);
  },
};</code></pre><h3 id="this-"><strong>this 面试题</strong></h3><blockquote><p>面试题误区</p></blockquote><pre class=""><code class="">var obj = {
  name: &#x27;ryan&#x27;,
  getName: () =&gt; {
    return this.name;
  },
};

// &quot;&quot;</code></pre><ul><li>如果真的弄懂了以上规则，刷题如切菜般容易。</li><li>getName 的外层作用域是全局作用域，而不是 obj 对象大括号作用域。因为在 js 当中只有全局作用域和函数作用域。</li><li>给箭头函数绑定 this 是徒劳的<code>((() =&gt; {}).call(&#x27;string&#x27;))</code>。</li></ul><hr/><ul><li>面试题 1</li></ul><pre class=""><code class="">var name = &#x27;window&#x27;;

var person = {
  name: &#x27;person&#x27;,
  sayName: function () {
    console.log(this.name);
  },
};

function sayName() {
  var sss = person.sayName;
  sss(); // window: 独立函数调用
  person.sayName(); // person: 隐式调用
  `(person.sayName())`; // person: 隐式调用
  (b = person.sayName)(); // window: 赋值表达式(独立函数调用)

sayName();</code></pre><ul><li>面试题 2</li></ul><pre class=""><code class="">var name = &#x27;window&#x27;;

var person1 = {
  name: &#x27;person1&#x27;,
  foo1: function () {
    console.log(this.name);
  },
  foo2: () =&gt; console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name);
    };
  },
  foo4: function () {
    return () =&gt; {
      console.log(this.name);
    };
  },
};

var person2 = { name: &#x27;person2&#x27; };

person1.foo1(); // person1(隐式绑定)
person1.foo1.call(person2); // person2(显示绑定优先级大于隐式绑定)

person1.foo2(); // window(不绑定作用域,上层作用域是全局)
person1.foo2.call(person2); // window

person1.foo3()(); // window(独立函数调用)
person1.foo3.call(person2)(); // window(独立函数调用)
person1.foo3().call(person2); // person2(最终调用返回函数式, 使用的是显示绑定)

person1.foo4()(); // person1(箭头函数不绑定this, 上层作用域this是person1)
person1.foo4.call(person2)(); // person2(上层作用域被显示的绑定了一个person2)
person1.foo4().call(person2); // person1(上层找到person1)</code></pre><ul><li>面试题三</li></ul><pre class=""><code class="">var name = &#x27;window&#x27;;

function Person(name) {
  this.name = name;
  (this.foo1 = function () {
    console.log(this.name);
  }),
    (this.foo2 = () =&gt; console.log(this.name)),
    (this.foo3 = function () {
      return function () {
        console.log(this.name);
      };
    }),
    (this.foo4 = function () {
      return () =&gt; {
        console.log(this.name);
      };
    });
}

var person1 = new Person(&#x27;person1&#x27;);
var person2 = new Person(&#x27;person2&#x27;);

person1.foo1(); // person1
person1.foo1.call(person2); // person2(显示高于隐式绑定)

person1.foo2(); // person1 (上层作用域中的 this 是 person1)
person1.foo2.call(person2); // person1 (上层作用域中的 this 是 person1)

person1.foo3()(); // window(独立函数调用)
person1.foo3.call(person2)(); // window
person1.foo3().call(person2); // person2

person1.foo4()(); // person1
person1.foo4.call(person2)(); // person2
person1.foo4().call(person2); // person1

var obj = {
  name: &#x27;obj&#x27;,
  foo: function () {},
};</code></pre><ul><li>面试题四</li></ul><pre class=""><code class="">var name = &#x27;window&#x27;;

function Person(name) {
  this.name = name;
  this.obj = {
    name: &#x27;obj&#x27;,
    foo1: function () {
      return function () {
        console.log(this.name);
      };
    },
    foo2: function () {
      return () =&gt; {
        console.log(this.name);
      };
    },
  };
}

var person1 = new Person(&#x27;person1&#x27;);
var person2 = new Person(&#x27;person2&#x27;);

person1.obj.foo1()(); // window 独立函数调用
person1.obj.foo1.call(person2)(); // window
person1.obj.foo1().call(person2); // person2

person1.obj.foo2()(); // obj
person1.obj.foo2.call(person2)(); // person2
person1.obj.foo2().call(person2); // obj</code></pre><h2 id="">异步</h2><h3 id=""><strong>异步概述</strong></h3><p>异步是计算机科学的一个基本概念，其中一种含义是计算机多线程的异步处理 ，也可以说异步行为就是在等待其他操作完成的同时，也可以运行其他指令，这样可以优化计算量大，时间长的操作。<code>Java</code>/go 语言当中可以通过线程池 runtime /goroutine 来实现异步，但是对于我们 <code>JavaScript</code> 这种单线程语言（一次只能完成一个任务）。我们是通过 <code>JavaScript</code> 的事件循环模型，结合浏览器多线程管理回调函数作为异步解决方案。也就是将 <code>DOM</code>操作，<code>HTTP</code> 请求，定时器等的回调函数都交给浏览器的对应线程来管理，有了操作结果之后就将回调函数加入 <code>JavaScript</code> 事件循环模型的消息队列当中，当 <code>JavaScript</code>主线程空闲时，就会轮询消息队列，将异步任务读取到调用栈当中等待主线程的顺序执行。</p><h3 id=""><strong>异步解决方案</strong></h3><p>在早期 <code>JavaScript</code> 中，只支持回调函数来处理异步操作返回的结果。之后出现的 <code>Promise</code> 和 <code>Async</code> 其实本质上仍然是在处理和表达回调函数。只不过从我们作为开发者的角度来说，我们可以用一些优雅的，安全的，可以信任的语法来换一种形式来处理异步回调函数。</p><h3 id=""><strong>早期回调函数</strong></h3><p><strong>早期回调函数处理异步回调的缺陷十分明显：</strong></p><ul><li>第一个是线性理解能力的缺失，我们期望的异步方式是有计划的，线性的。比如：我通过 HTTP 请求，获取数据之后在操作数据，但是回调在表达异步的时候是非顺序的，非线性的。比如：我先定义好拿到数据之后的操作，再去 HTTP 请求。</li><li>第二个是嵌套回调异步，如果异步返回值又依赖另一个异步返回值，回调的情况还会进一步复杂，最致命的是如果试图向其中添加新特性，他就立马会变得很难以拓展。</li><li>第三个是信任缺失，因为没有一种统一的。我们交给第三方库的回调函数，控制权在第三方库手里。 所以我们需要一种更好的方案来优雅的处理异步回调函数。</li></ul><h3 id="promise-"><strong>Promise 处理异步回调</strong></h3><p>通过手写<code>Promise</code>源码过后，我发现<code>Promise</code>在处理异步回调的核心是每次<code>resovle</code>的时候，<code>resovle</code>会挂起一个异步回调任务，然后<code>then</code>同步传递的回调函数放到这个异步任务当中去，之后这个异步回调任务调用<code>then</code>函数。如果是链式调用<code>then</code>之后又继续<code>resovle</code></p><p>之后从三个方面简单介绍一下 <strong>Promise</strong>。<strong>Promise 特性</strong>，<strong>Promise 静态方法</strong>，<strong>Promise 实例方法</strong>，<strong>Promise 缺陷</strong>，<strong><code>Promise</code>解决了什么样的问题</strong>。</p><ul><li>Promise 是一个有三种状态的对象，<code>pending（待定）</code>, <code>fufilled（兑现）</code>, <code>rejected（拒绝）</code>。无论落定为哪种状态都是不可逆的。Promise 的状态转换为兑现，就会有一个兑现的值 value，转换为拒绝，就会有拒绝的原因 reason。</li><li>Promise 静态方法 :<code>Promise.resove()</code>, <code>Promise.reject()</code>, <code>Promise.all()</code>, <code>Promise.race()</code>,<code>Promise.allSettled()</code>, <code>Promise.any()</code>。</li><li>Promise 实例方法 : <code>Promise.prototype.then()</code>, <code>Promise.prototype.catch()</code>, <code>Promise.prototype.finally()</code>。</li></ul><p><strong>Promise 嵌套</strong></p><pre class=""><code class="">// bad
loadSomething().then(function(something) {
    loadAnotherthing().then(function(another) {
        DoSomethingOnThem(something, another);
    });
});

复制代码
// good
Promise.all([loadSomething(), loadAnotherthing()])
.then(function ([something, another]) {
    DoSomethingOnThem(...[something, another]);
});</code></pre><p><strong>2.断开的 Promise 链</strong></p><pre class=""><code class="">// bad
function anAsyncCall() {
    var promise = doSomethingAsync();
    promise.then(function() {
        somethingComplicated();
    });

    return promise;
}

// good
function anAsyncCall() {
    var promise = doSomethingAsync();
    return promise.then(function() {
        somethingComplicated()
    });
}</code></pre><p><strong>3.混乱的集合</strong></p><pre class=""><code class="">// bad
function workMyCollection(arr) {
    var resultArr = [];
    function _recursive(idx) {
        if (idx &gt;= resultArr.length) return resultArr;

        return doSomethingAsync(arr[idx]).then(function(res) {
            resultArr.push(res);
            return _recursive(idx + 1);
        });
    }

    return _recursive(0);
}</code></pre><p>你可以写成：</p><pre class=""><code class="">function workMyCollection(arr) {
    return Promise.all(arr.map(function(item) {
        return doSomethingAsync(item);
    }));
}</code></pre><h3 id="promise-"><strong>Promise 解决了什么样的问题</strong></h3><p><strong>回调嵌套</strong></p><pre class=""><code class="">request(url, function(err, res, body) {
    if (err) handleError(err);
    fs.writeFile(&#x27;1.txt&#x27;, body, function(err) {
        request(url2, function(err, res, body) {
            if (err) handleError(err)
        })
    })
});</code></pre><p><strong>使用 Promise 后：</strong></p><pre class=""><code class="">request(url)
.then(function(result) {
    return writeFileAsynv(&#x27;1.txt&#x27;, result)
})
.then(function(result) {
    return request(url2)
})
.catch(function(e){
    handleError(e)
});</code></pre><p><strong>控制反转再反转</strong></p><p>使用第三方回调<code>API</code> 的时候，可能会遇到如下问题：</p><ol start="1"><li>回调函数执行多次 --这个<code>Promise</code>每次只可以<code>resovle</code>一次，剩下的调用会被忽略。</li></ol><h3 id="promise-"><strong>Promise 缺陷</strong></h3><ol start="1"><li>无法中途取消</li><li>无法得知 pending 状态，不知道目前进展到哪一个方面</li><li>单一值（每次只能有一个完成值或者拒绝原因，我们只能构造对象和数组去解构）</li><li>吃掉错误，这块项目当中会很常见：一般会和<code>async</code> <code>await</code> 以及<code>try catch</code>相互配合</li></ol><p>如果我们在<code>catch</code>块捕获到了错误，如果你只是做了简单的返回,或者打印，最上层的<code>try catch</code>块是捕获不到的，只有<code>throw</code>抛出错误或者<code>return Promise.reject()</code>才可以被外面的<code>try catch</code>同步捕获到来进行处理。而<code>try catch</code>块同样有这样的局限，也会在 catch 块中吃掉错误，如果想要被上层处理，还得继续抛出。我在项目当中的实践就是如果<code>try catch</code>太多，太冗余，我会使用<code>catch</code>返回错误，去用<code>if</code>判断，可以大大减少冗余。</p><h3 id="asyncawait-"><strong>async，await 处理异步回调</strong></h3><p><code>async</code>函数就是<code>Generator函数 + Promise</code>的语法糖，为什么这么说呢？因为我们发现<code>async</code>函数的本质，就是将<code>Generator</code>函数和 co | thunk 执行器包装再了一个<code>async</code>函数里。</p><p><strong>Generator 处理异步回调</strong></p><pre class=""><code class="">function* getData(url) {
  const re1 = yield requestData(url);
  const re2 = yield requestData(res1 + url);
  const re3 = yield requestData(res2 + url);
}</code></pre><p><strong>手动执行</strong></p><pre class=""><code class="">getData(&quot;user_1&quot;)
  .next(&quot;name&quot;)
  .value.then((res) =&gt; {
    getData(res)
      .next(&quot;id&quot;)
      .value.then((res) =&gt; {
        getData(res)
          .next()
          .value.then((res) =&gt; {
            console.log(res);
          });
      });
  });</code></pre><p><strong>co 模块自动执行器</strong></p><pre class=""><code class="">function generator(generatorFun) {
  const generator = generatorFun();
  function execu(res) {
    const generatorObj = generator.next(res);
    if (!generatorObj.done) {
      generatorObj.value.then((res) =&gt; {
        execu(res);
      });
    } else {
      return generatorObj.value;
    }
  }
  execu(res);
}</code></pre><p>我们用<code>Generator</code>实现异步回调处理的时候，通过生成器函数，<code>yield</code>请求函数<code>a</code>,请求函数<code>b</code>,请求函数<code>c</code>来表达异步处理，通过手动调用<code>.next.then</code>再继续嵌套调用<code>.next.then</code>来执行，之后有了<code>co</code>或者<code>thunk</code>执行器模块，可以帮我们去自动执行，回过头来再看<code>async</code>的写法，发现<code>async</code>函数的本质，就是将<code>Generator</code>函数和执行器包装再了一个<code>async</code>函数里。</p><p><strong>async await 处理异步</strong></p><pre class=""><code class="">// async await
async function getData(url) {
  const re1 = await requestData(url);
  const re2 = await requestData(res1 + url);
  const re3 = await requestData(res2 + url);
}</code></pre><h3 id="async--promise"><strong><code>async</code> 比 <code>Promise</code>的优势？</strong></h3><ul><li>代码更加简洁。--这个显而易见，我们不用每次把一大把逻辑都放在<code>then</code>处理函数当中。</li><li>错误处理。 --由于<code>try catch</code>只能捕获同步错误，所以不能或者说难以捕获<code>Promise</code> 异步回调的错误，<code>async/await</code>的出现使得<code>try/catch</code> 就可以同步捕获异步错误。</li><li>调试。 --- 对于<code>await</code> 我们可以按照顺序调试，而<code>Promise</code>并不会顺序执行。</li></ul><h3 id="async-"><strong>async 陷阱</strong></h3><p><strong>指开发者贪图语法上的简洁而让原本可以并行执行的内容变成了顺序执行，从而影响了性能</strong></p><pre class=""><code class="">(async () =&gt; {
  const getList = await getList();
  const getAnotherList = await getAnotherList();
})();</code></pre><p><code>getList()</code>和<code>getAnotherList()</code> 其实并没有依赖关系，但是现在的这种写法，虽然简洁，却导致了 <code>getAnotherList()</code> 只能在 <code>getList()</code> 返回后才会执行，从而导致了多一倍的请求时间。</p><pre class=""><code class="">(async () =&gt; {
  const listPromise = getList();
  const anotherListPromise = getAnotherList();
  await listPromise;
  await anotherListPromise;
})();</code></pre><p>也可以使用 <code>Promise.all()</code>：</p><pre class=""><code class="">(async () =&gt; {
  Promise.all([getList(), getAnotherList()]).then(...);
})();</code></pre><h3 id="async-"><strong>async 继发和并发</strong></h3><pre class=""><code class="">// 继发一
async function loadData() {
  var res1 = await fetch(url1);
  var res2 = await fetch(url2);
  var res3 = await fetch(url3);
  return &quot;whew all done&quot;;
}

// 继发二
async function loadData(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}</code></pre><pre class=""><code class="">// 并发一
async function loadData() {
  var res = await Promise.all([fetch(url1), fetch(url2), fetch(url3)]);
  return &quot;whew all done&quot;;
}

// 并发二（for循环里面加计时器的案例）
async function loadData(urls) {
  // 并发读取 url
  const textPromises = urls.map(async url =&gt; {
    const response = await fetch(url);
    return response.text();
  });

  // 按次序输出
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}</code></pre><h3 id="async-"><strong>async 错误捕获</strong></h3><pre class=""><code class="">// to.js
export default function to(promise) {
   return promise.then(data =&gt; {
      return [null, data];
   })
   .catch(err =&gt; [err]);
}</code></pre><pre class=""><code class="">import to from &#x27;./to.js&#x27;;

async function asyncTask() {
     let err, user, savedTask;
     [err, user] = await to(UserModel.findById(1));
     if(!user) throw new CustomerError(&#x27;No user found&#x27;);

     [err, savedTask] = await to(TaskModel({userId: user.id, name: &#x27;Demo Task&#x27;}));
     if(err) throw new CustomError(&#x27;Error occurred while saving task&#x27;);

    if(user.notificationsEnabled) {
       const [err] = await to(NotificationService.sendNotification(user.id, &#x27;Task Created&#x27;));
       if (err) console.error(&#x27;Just log the error and continue flow&#x27;);
    }
}</code></pre><h3 id="async-"><strong>async 的一些讨论</strong></h3><p><strong>async 会取代 Generator 吗？</strong></p><p><code>Generator</code> 本来是用作生成器，使用 <code>Generator</code> 处理异步请求只是一个比较 <code>hack</code> 的用法，在异步方面，<code>async</code> 可以取代 <code>Generator</code>，但是 <code>async</code> 和 <code>Generator</code> 两个语法本身是用来解决不同的问题的。</p><p><strong>async 会取代 Promise 吗？</strong></p><ol start="1"><li><code>async</code> 函数的就是返回一个 <code>Promise</code> 对象。他们两个相互成就，我觉得可以这样说。</li><li>面对复杂的异步流程，<code>Promise</code> 提供的<code>all</code>和 <code>race</code> 会更加好用。会避免掉进<code>await</code>的陷阱里</li><li><code>Promise</code> 本身是一个对象，所以可以在代码中任意传递，缓存。</li></ol><h3 id="co-"><strong>co 模块自动执行器</strong></h3><p><code>co 模块</code> 函数通过回调函数 / <code>Promise</code>对象来接收和交还程序的执行权。从而自动化执行生成器函数。<code>co 模块</code>交还和接收执行权的本质就是等待异步操作的协程有了结果之后，去递归调用传入的回调函数来归还异步函数协程的执行权。</p><p><strong>手写 co 函数</strong></p><pre class=""><code class="">const fs = require(&quot;fs&quot;);

const readFileFn = (filename) =&gt; {
  /* Promise交还执行权 */
  return new Promise((resolve, reject) =&gt; {
    fs.readFile(filename, (err, data) =&gt; {
      if (err) reject(err);
      resolve(data);
    });
  });

  /* 回调函数交还执行权 */
  return (cb) =&gt; {
    fs.readFile(filename, (err, data) =&gt; {
      if (err) throw err;
      setTimeout(() =&gt; {
        cb(data);
      });
    });
  };
};

function* generatorFn() {
  const r1 = yield readFileFn(&quot;./a.txt&quot;);
  const r2 = yield readFileFn(&quot;./b.txt&quot;);
  console.log(r1.toString(), r2.toString(), &quot;两个文件读取完毕！！！&quot;);
}
const co = (geFn) =&gt; {
  const gen = geFn();
  const coRe = (data) =&gt; {
    const result = gen.next(data);
    if (result.done) return result.value;
    if (result.value instanceof Promise) {
      result.value.then(coRe);
    } else {
      result.value(coRe);
    }
  };
  coRe();
};
co(generatorFn);</code></pre><pre class=""><code class="">// 第二版
function run(gen) {
    var gen = gen();

    return new Promise(function(resolve, reject) {

        function next(data) {
            try {
                var result = gen.next(data);
            } catch (e) {
                return reject(e);
            }

            if (result.done) {
                return resolve(result.value)
            };
            //统一回调函数和Promise
            var value = toPromise(result.value);

            value.then(function(data) {
                next(data);
            }, function(e) {
                reject(e)
            });
        }

        next()
    })

}

function isPromise(obj) {
    return &#x27;function&#x27; == typeof obj.then;
}

function toPromise(obj) {
    if (isPromise(obj)) return obj;
    if (&#x27;function&#x27; == typeof obj) return thunkToPromise(obj);
    return obj;
}

function thunkToPromise(fn) {
    return new Promise(function(resolve, reject) {
        fn(function(err, res) {
            if (err) return reject(err);
            resolve(res);
        });
    });
}

module.exports = run;</code></pre><h3 id="">协程概念</h3><blockquote><p>协程的概念是多个线程互相协作，完成异步任务。</p></blockquote>
<ul><li>协程（Coroutines）是一种比线程更加轻量级的存在 , 协程是一个特殊的函数，这个函数可以暂停，保留状态后暂时推出，之后可以重新在暂停处恢复运行。</li></ul><blockquote><p>第一步，协程 A 开始执行。第二步，协程 A 执行到一半，进入暂停，执行权转移到协程 B。第三步，（一段时间后）协程 B 交还执行权。第四步，协程 A 恢复执行。
在 ES6 当中可以通过生成器函数可以交出函数的执行权的特性来实现协程。</p></blockquote>
<h2 id="">生成器</h2><h2 id=""><strong>生成器</strong></h2><ul><li>生成器是 ES6 的一种对函数执行进行控制的方案。它让我们更加灵活的控制函数什么时候继续执行，什么时候暂停执行。</li></ul><h3 id="">生成器函数</h3><ul><li>生成器函数通过  <code>yield</code>  关键字来控制函数执行流程。</li><li>生成器函数返回生成器。</li><li><code>yield</code>  之后的返回值是，<code>next</code>方法返回对象的  <code>value</code>  和  <code>done</code>  属性。
<ul><li><code>{value: , done: false}</code></li></ul></li><li><code>para2</code>  为 第二段函数代码的参数。<code>para3</code>  为 第三段函数代码的参数。</li></ul><pre class=""><code class="">function* bar() {
  const val_1 = 1;
  console.log(&quot;第一段代码执行&quot;, val_1);
  const para2 = yield &quot;第一段代码的返回值&quot;;
  const val_2 = 2;
  console.log(&quot;第二段代码执行&quot;, val_2 * value_2);
  const para3 = yield &quot;第二段代码的返回值&quot;;
  const val_3 = 3;
  console.log(&quot;第三段代码执行&quot;, val_3 * value_3);
  yield &quot;第三段代码的返回值&quot;;
  console.log(&quot;函数执行结束&quot;);
}
const generator = bar();
console.log(generator.next());
console.log(generator.next(10));
console.log(generator.next(10));
console.log(generator.next(10));
console.log(generator.next(10));
console.log(generator.next(10));
// 第一段代码执行 1
// { value: &#x27;第一段代码的返回值&#x27;, done: false }
// 第二段代码执行 20
// { value: &#x27;第二段代码的返回值&#x27;, done: false }
// 第三段代码执行 30
// { value: &#x27;第三段代码的返回值&#x27;, done: false }
// 函数执行结束
// { value: undefined, done: true }
// { value: undefined, done: true }
// { value: undefined, done: true }</code></pre><h3 id="---return-">生成器提前结束 - return 函数</h3><ul><li>return 传值后这个生成器函数就会结束，之后调用 next 不会继续生成值了；</li></ul><pre class=""><code class="">function* bar() {
  const val_1 = 1;
  console.log(&quot;第一段代码执行&quot;, val_1);
  const value_2 = yield &quot;第一段代码的返回值&quot;;
  const val_2 = 2;
  console.log(&quot;第二段代码执行&quot;, val_2 * value_2);
  const value_3 = yield &quot;第二段代码的返回值&quot;;
  const val_3 = 3;
  console.log(&quot;第三段代码执行&quot;, val_3 * value_3);
  yield &quot;第三段代码的返回值&quot;;
  console.log(&quot;函数执行结束&quot;);
}
const generator = bar();
console.log(generator.next());
console.log(generator.return(10));
console.log(generator.next(10));
// 第一段代码执行 1
// { value: &#x27;第一段代码的返回值&#x27;, done: false }
// { value: 10, done: true }
// { value: undefined, done: true }</code></pre><h3 id="---throw-">生成器抛出异常 - throw 函数</h3><ul><li>给生成器函数内部抛出异常</li></ul><pre class=""><code class="">function* bar() {
  const val_1 = 1;
  console.log(&quot;第一段代码执行&quot;, val_1);
  const value_2 = yield &quot;第一段代码的返回值&quot;;
  const val_2 = 2;
  console.log(&quot;第二段代码执行&quot;, val_2 * value_2);
  const value_3 = yield &quot;第二段代码的返回值&quot;;
  const val_3 = 3;
  console.log(&quot;第三段代码执行&quot;, val_3 * value_3);
  yield &quot;第三段代码的返回值&quot;;
  console.log(&quot;函数执行结束&quot;);
}
const generator = bar();
console.log(generator.next());
console.log(generator.throw(&quot;err message&quot;));
console.log(generator.next(10));
//   const value_2 = yield &quot;第一段代码的返回值&quot;;
//                   ^
// err message
// (Use `node --trace-uncaught ...` to show where the exception was thrown)</code></pre><h3 id="">抛出异常后我们可以在生成器函数中捕获异常</h3><pre class=""><code class="">function* bar() {
  const val_1 = 1;
  console.log(&quot;第一段代码执行&quot;, val_1);
  try {
    const value_2 = yield &quot;第一段代码的返回值&quot;;
  } catch (err) {
    console.log(&quot;捕获了异常&quot; + err);
  }
  const val_2 = 2;
  console.log(&quot;第二段代码执行&quot;, val_2);
  const value_3 = yield &quot;第二段代码的返回值&quot;;
  const val_3 = 3;
  console.log(&quot;第三段代码执行&quot;, val_3 * value_3);
  yield &quot;第三段代码的返回值&quot;;
  console.log(&quot;函数执行结束&quot;);
}
const generator = bar();
console.log(generator.next());
console.log(generator.throw(&quot;err message&quot;));
console.log(generator.next(10));
// 第一段代码执行 1
// { value: &#x27;第一段代码的返回值&#x27;, done: false }
// 捕获了异常err message
// 第二段代码执行 2
// { value: &#x27;第二段代码的返回值&#x27;, done: false }
// 第三段代码执行 30
// { value: &#x27;第三段代码的返回值&#x27;, done: false }</code></pre><h3 id="">生成器代替迭代器</h3><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">// 生成器
function* createGenerator(arr) {
  for (const item of arr) {
    yield item;
  }
}
const generator = createGenerator([1, 3]);

// 迭代器
function createIteratot(arr) {
  let index = 0;
  return {
    next: function () {
      if (index &lt; arr.length) {
        return { value: arr[index++], done: false };
      } else {
        return { value: undefined, done: true };
      }
    },
  };
}</code></pre><h3 id="">生成器原理</h3><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">**function* helloWorldGenerator() {
  yield &#x27;hello&#x27;;
  yield &#x27;world&#x27;;
  return &#x27;ending&#x27;;
}**</code></pre><p>babel 当中有一个非常复杂的原型解构，实现了 next, return, throw 方法。</p><p><code>babel</code> 当中的核心逻辑是，是有一个 <code>while</code> 无限循环，无限循环中有 <code>switch case</code> 语句，分为对应着 <code>0 → hello，1 → world ， 2 → ending 3 → .stop()</code> 每一次 yield 都会匹配 <code>switch</code> 语句，然后去改变 <code>next,</code> <code>pre</code> 属性的值，执行下一个 <code>yield</code> 匹配，最后一个 yield 匹配后 最后一个状态会变为 <code>complete</code> , 会 <code>return {value: undefined, done: true}</code></p><p>async 和 await 被 babel 编译后的结果就是， 通过 generator + co 自动化的递归的执行 <code>_asyncToGenerator</code>函数。 来实现 async/await</p><h2 id="let--const--var">let | const | var</h2><p><strong>块级作用域：</strong>块作用域由 <code>{ }</code>包括，let 和 const 是块级作用域的块级声明，var 不存在块级作用域。存在变量提升的问题。</p><p><strong>变量提升：</strong>var 存在变量提升，let 和 const 不存在变量提升。var 的变量提升就是在初始化全局上下文对象的 VO 变量的时候，收集全局函数和全局变量。</p><p><strong>暂时性死区：</strong>在使用 let、const 命令声明变量之前，该变量都是不可用的。这在语法上，称为<strong>暂时性死区</strong>。使用 var 声明的变量不存在暂时性死区。</p><p><strong>全局作用域：</strong>var 声明的变量为全局变量，并且会将该变量添加为全局对象的属性（浏览器环境下是 window,node 环境是 global），但是 let 和 const 不会，他们被添加到一个特殊的变量环境当中来记录。</p><p><strong>重复声明：</strong>var 声明变量时，可以重复声明变量，后声明的同名变量会覆盖之前声明的变量，const 和 let 不允许重复声明变量。</p><h2 id="set-weakset--map-weakmap">Set WeakSet | Map Weakmap</h2><aside><p>💡 WeakSet 和 WeakMap 都是弱引用。强引用例子：一个数组 a, 一个对map b 当中引用了这个数组 a, 即使我们手动将a 置为 null,  也只是去掉了 a 对数组的强引用，并没有去掉 b 对 数组的强应用。所以根据 javascript 垃圾回收机制 数组还是不会被回收掉。同理 map 也是类似。如果是弱引用，只要将其他引用都清除，垃圾回收机制就会释放该对象占用的内存。weakMap 里面的键名和属性值都会自动消失，不用手动删除。</p>
</aside><ul><li>Set 和 WeakSet 都是集合数据结构。结构中的每个元素都是唯一的。</li><li>通过 add 增加值，has 查询，size 取得元素数量，delete 和 clear 删除元素。</li><li>WeakSet 只能使用对象作为值，不接受其他类型的值。</li></ul><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">// set 数据结构的基本使用
const set = new Set();
set.add(10); // 返回set对象
set.add(10);
set.add({ name: &#x27;ryan&#x27; });
console.log(set);
set.has(10); // 返回布尔值
set.delete(10); // 返回布尔值
console.log(set);
set.clear();
console.log(set);
console.log(set.size);
const set1 = new Set([1, 1, 3, 3, 0]); // 传入可迭代对象

// set的遍历
set.forEach((item) =&gt; {
  console.log(item);
});
for (const val of set) {
  console.log(val);
}</code></pre><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">const set2 = new WeakSet();
// 注意这样添加的是不同对象。相同的对象 -&gt; const obj = {name: &#x27;ryan&#x27;} set.add(obj)
set.add({ name: &#x27;ryan&#x27; }); // 返回set对象
set.add({ name: &#x27;ryan&#x27; });
set.add({ name: &#x27;ryan&#x27; });
console.log(set2);
set.has({ name: &#x27;ryan&#x27; }); // 返回布尔值
set.delete({ name: &#x27;ryan&#x27; }); // 返回布尔值
// console.log(set2);
// set.clear();
console.log(set2);
console.log(set.size);
const set3 = new WeakSet([{ name: &#x27;ryan&#x27; }]); // 传入可迭代对象
console.log(set3);
// set -&gt; []
const arr = Array.from(set2);
const arr2 = [...set2];</code></pre><hr/><ul><li>Map 和 WeakMap 都是 K V 表，映射数据结果。</li><li>之前存储键值对，只能通过 Object，并且有个局限就是属性名只能是字符串类型 ,Symbol 数字类型。Map 的属性可以是任何 JS 类型。</li><li>通过 set 添加属性， 通过 has 查询，get 获取属性，通过 delete, clear 删除属性。</li><li>WeakMap 只能使用对象作为键，不接受其他类型的值。</li><li>WeakMap 不可以遍历。也正是因为弱引用这样的特性，WeakMap 内部有多少个成员，取决于垃圾回收机制有没有运行，运行前后很可能成员个数是不一样的，而垃圾回收机制何时运行是不可预测的，因此 ES6 规定 WeakMap 不可遍历。</li></ul><pre class="language-tsx lang-tsx"><code class="language-tsx lang-tsx">const map = new Map();
const obj1 = { name: &#x27;why&#x27; };
const obj2 = { name: &#x27;kobe&#x27; };
map.set(obj1, &#x27;aaa&#x27;);
map.set(obj2, &#x27;bbb&#x27;);
map.set(1, &#x27;ccc&#x27;);

map2.set(&#x27;why&#x27;, &#x27;eee&#x27;);
console.log(map2);
console.log(map2.get(&#x27;why&#x27;));
console.log(map2.has(&#x27;why&#x27;));

map2.delete(&#x27;why&#x27;);
console.log(map2);

map2.clear();
console.log(map2);
// 传入entry
const map2 = new Map([
  [obj1, &#x27;aaa&#x27;],
  [obj2, &#x27;bbb&#x27;],
  [2, &#x27;ddd&#x27;],
]);</code></pre><h2 id="dom--bom">Dom | Bom</h2><h3 id="bom">Bom</h3><p><code>BOM</code>(Browser Object Model) <code>浏览器对象模型看成是连接</code>JavaScript 脚本与浏览器窗口的桥梁。</p><ul><li>BOM 主要包括一下的对象模型：
<ul><li>window：包括全局属性、方法。</li><li>location：操作 和 访问 URL 信息的对象。</li><li>history：操作浏览器的会话历史记录的对象 📝。</li><li>document：操作文档对象。</li></ul></li></ul><h4 id="window">window</h4><h4 id="window-">window 作为全局对象</h4><pre class=""><code class="">window.setTimeout(() =&gt; {
  console.log(&quot;setTimeout&quot;);
}, 2000);

const obj = new window.Date();</code></pre><h4 id="window-">window 常见的属性，方法和事件</h4><pre class=""><code class="">// 1.常见的属性
console.log(window.screenX);
console.log(window.screenY);

window.addEventListener(&quot;scroll&quot;, () =&gt; {
  console.log(window.scrollX, window.scrollY);
});

console.log(window.outerHeight);
console.log(window.innerHeight);

// 2.常见的方法
const scrollBtn = document.querySelector(&quot;#scroll&quot;);
scrollBtn.onclick = function () {
  // 1.scrollTo
  window.scrollTo({ top: 2000 });

  // 2.close
  window.close();

  // 3.open
  window.open(&quot;http://www.baidu.com&quot;, &quot;_self&quot;);
};

// 3.常见的事件
window.onload = function () {
  console.log(&quot;window窗口加载完毕~&quot;);
};

window.onfocus = function () {
  console.log(&quot;window窗口获取焦点~&quot;);
};

window.onblur = function () {
  console.log(&quot;window窗口失去焦点~&quot;);
};

const hashChangeBtn = document.querySelector(&quot;#hashchange&quot;);
hashChangeBtn.onclick = function () {
  location.hash = &quot;aaaa&quot;;
};
window.onhashchange = function () {
  console.log(&quot;has发生了h改变&quot;);
};</code></pre><h4 id="window--eventtarget">window 继承自 EventTarget</h4><pre class=""><code class="">const clickHandler = () =&gt; {
  console.log(&quot;window发生了点击&quot;);
};

window.addEventListener(&quot;click&quot;, clickHandler);
window.removeEventListener(&quot;click&quot;, clickHandler);

window.addEventListener(&quot;a&quot;, () =&gt; {
  console.log(&quot;a&quot;);
});
// 派发事件
window.dispatchEvent(new Event(&quot;a&quot;));</code></pre><h4 id="location">location</h4><ul><li><code>Location</code>  对象用于表示  <code>window</code>  上当前链接到的 URL 信息。</li></ul><h4 id="location-">location 方法</h4><ul><li><code>Location.assign()</code>  方法会触发窗口加载并显示指定的 URL 的内容。</li><li><code>Location.replace()</code>  方法以给定的 URL 来替换当前的资源。 与  <code>assign()</code>方法 不同的是，调用  <code>replace()</code>  方法后，当前页面不会保存到会话历史中（session History），这样，用户点击回退按钮时，将不会再跳转到该页面。</li><li><code>Location.reload()</code>  方法用来刷新当前页面。该方法只有一个参数，当值为 true 时，将强制浏览器从服务器加载页面资源，当值为 false 或者未传参时，浏览器则可能从缓存中读取页面。</li></ul><h4 id="location-">location 属性</h4><ul><li><code>打开https://developer.mozilla.org/zh-CN/docs/Web/API</code></li><li><code>console.log(location)</code></li><li><code>hash: &quot;&quot;</code></li><li><code>host: &quot;developer.mozilla.org&quot;</code></li><li><code>hostname: &quot;developer.mozilla.org&quot;</code></li><li><code>href: &quot;https://developer.mozilla.org/zh-CN/docs/Web/API&quot;</code></li><li><code>origin: &quot;https://developer.mozilla.org&quot;</code></li><li><code>pathname: &quot;/zh-CN/docs/Web/API&quot;</code></li><li><code>port: &quot;&quot;</code></li><li><code>protocol: &quot;https:&quot;</code></li></ul><h4 id="history">history</h4><ul><li><code>history</code>  对象允许我们访问浏览器曾经的会话历史记录。</li></ul><h4 id="history-">history 属性</h4><ul><li><code>length</code>：会话中的记录条数。</li><li><code>state</code>：当前保留的状态值。</li></ul><h3 id="history-">history 方法</h3><ul><li><code>back()</code>返回上一页，等价于  <code>history.go(-1)</code>。</li><li><code>forward()</code>前进下一页，等价于  <code>history.go(1)</code>。</li><li><code>go()</code>加载历史中的某一页。</li><li><code>pushState()</code>；只是改变跳转路径不刷新网页。</li><li><code>replaceState()</code>打开一个新的地址，并且使用  <code>replace</code>没有会话记录。</li></ul><h3 id="dom">Dom</h3><ul><li><code>Node</code>接口 继承了  <code>EventTarget</code>，而像  <code>Document</code>,<code>Element</code>  都继承自  <code>Node</code>  接口。</li><li><strong>继承 自 EventTarget 可以使用</strong> <code>addEventListener</code>, <code>removeEventListener</code>, <code>dispatchEvent</code>。</li><li>继承 Node 可以使用 nodeValue, nodeType, childNodes</li><li>element → classLIst className style, clientHeight, clientHeight</li><li>document → document.titile document.body, document.querySelector()</li></ul><h4 id="--eventtarget">继承 自 EventTarget</h4><ul><li><code>addEventListener</code>, <code>removeEventListener</code>, <code>dispatchEvent</code>。</li></ul><pre class=""><code class="">// document
document.addEventListener(&quot;click&quot;, () =&gt; {
  console.log(&quot;document被点击&quot;);
});

const divEl = document.querySelector(&quot;#box&quot;);
const spanEl = document.querySelector(&quot;.content&quot;);
// element -&gt; HTMLElement
divEl.addEventListener(&quot;click&quot;, () =&gt; {
  console.log(&quot;div元素被点击&quot;);
});

spanEl.addEventListener(&quot;click&quot;, () =&gt; {
  console.log(&quot;span元素被点击&quot;);
});</code></pre><h4 id="-node">继承 Node</h4><pre class=""><code class="">const divEl = document.querySelector(&quot;#box&quot;);
const spanEl = document.querySelector(&quot;.content&quot;);

// 常见的属性
console.log(divEl.nodeName, spanEl.nodeName);
console.log(divEl.nodeType, spanEl.nodeType);
console.log(divEl.nodeValue, spanEl.nodeValue);

// childNodes
const spanChildNodes = spanEl.childNodes;
const textNode = spanChildNodes[0];
console.log(textNode.nodeValue);

// 常见的方法
const strongEl = document.createElement(&quot;strong&quot;);
strongEl.textContent = &quot;我是strong元素&quot;;
divEl.appendChild(strongEl);
document.body.appendChild(strongEl);</code></pre><h4 id="document">Document</h4><pre class=""><code class="">// 常见的属性
console.log(document.body);
console.log(document.title);
document.title = &quot;Hello World&quot;;

console.log(document.head);
console.log(document.children[0]);

console.log(window.location);
console.log(document.location);
console.log(window.location === document.location);
// true
const imageEl = document.createElement(&quot;img&quot;);
const imageEl2 = new HTMLImageElement();

// 获取元素
const divEl1 = document.getElementById(&quot;box&quot;);
const divEl2 = document.getElementsByTagName(&quot;div&quot;);
const divEl3 = document.getElementsByName(&quot;title&quot;);
const divEl4 = document.querySelector(&quot;.content&quot;);
const divEl5 = document.querySelectorAll(&quot;.content&quot;);</code></pre><h4 id="element">Element</h4><pre class=""><code class="">const divEl = document.querySelector(&quot;#box&quot;);

// 常见的属性
console.log(divEl.id);
console.log(divEl.tagName);
console.log(divEl.children);
console.log(divEl.className);
console.log(divEl.classList);
console.log(divEl.clientWidth);
console.log(divEl.clientHeight);
console.log(divEl.offsetLeft);
console.log(divEl.offsetTop);

// 常见的方法
const value = divEl.getAttribute(&quot;age&quot;);
console.log(value);
divEl.setAttribute(&quot;height&quot;, 1.88);</code></pre><h4 id="">事件监听（事件流）</h4><p>事件流包含三个阶段:</p><ol start="1"><li>事件捕获阶段</li><li>目标阶段</li><li>事件冒泡阶段</li></ol><p>首先发生的是事件捕获，沿着  <code>DOM</code>  树一路向下，在经过的每个 dom 节点依次触发监听的事件。直到   目标 dom 节点  <code>document -&gt; html -&gt; body -&gt; div</code>。</p><p>然后是实际的目标 dom 节点接收到事件，触发目标节点绑定事件/触发代理事件</p><p>最后阶段是事件冒泡阶段，沿着  <code>DOM</code>  树一路向上，在经过的每个 dom 节点依次触发监听的事件。直到  <code>document</code>  对象  <code>div -&gt; body -&gt; html -&gt; document</code>。</p><pre class=""><code class="">&lt;html&gt;
    &lt;body&gt;
        &lt;div&gt;
            &lt;button&gt;&lt;/button&gt;
        &lt;/div&gt;
    &lt;/body&gt;
&lt;/html&gt;tytyt</code></pre><h4 id="">事件冒泡</h4><ul><li>沿着  <code>DOM</code>  树一路向上，在经过的每个节点依次触发。直到  <code>document</code>  对象  <code>div -&gt; body -&gt; html -&gt; document</code>。</li></ul><pre class=""><code class="">spanEl.addEventListener(&quot;click&quot;, () =&gt; {
  console.log(&quot;事件冒泡:span元素被点击了&quot;);
});

divEl.addEventListener(&quot;click&quot;, () =&gt; {
  console.log(&quot;事件冒泡:div元素被点击了&quot;);
});

document.body.addEventListener(&quot;click&quot;, () =&gt; {
  console.log(&quot;事件冒泡:body元素被点击了&quot;);
});

document.addEventListener(&quot;click&quot;, () =&gt; {
  console.log(&quot;事件冒泡:document元素被点击了&quot;);
});</code></pre><h4 id="">事件捕获</h4><ul><li>沿着  <code>DOM</code>  树一路向下，在经过的每个节点依次触发。直到   目标对象  <code>document -&gt; html -&gt; body -&gt; div</code>。</li></ul><pre class=""><code class="">spanEl.addEventListener(
  &quot;click&quot;,
  (event) =&gt; {
    console.log(&quot;事件捕获:span元素被点击了&quot;);
    event.stopPropagation();
  },
  true
);

divEl.addEventListener(
  &quot;click&quot;,
  () =&gt; {
    console.log(&quot;事件捕获:div元素被点击了&quot;);
  },
  true
);

document.body.addEventListener(
  &quot;click&quot;,
  (event) =&gt; {
    console.log(&quot;事件捕获:body元素被点击了&quot;);
  },
  true
);</code></pre><h4 id="">事件代理</h4><p>事件代理又称之为事件委托， 事件代理是把原本需要绑定在<code>子元素</code>的事件委托给<code>父元素</code>，让父元素负责事件监听和处理。</p><p><strong>事件代理的好处是有两点：</strong></p><p><strong>第一点</strong>：可以大量节省内存占用，减少事件注册事件。</p><p><strong>第二点</strong>：当新增子对象时无需再次对其绑定</p><p><strong>为什么父元素能做到事件代理呢？ 笔者认为有两点：</strong></p><p><strong>第一点</strong> ：事件冒泡到父元素，父元素可以订阅到冒泡事件。</p><p><strong>第二点</strong>：可以通过 <code>event.target</code> 得到目标节点。不然, 父元素怎么针对不同的子节点，进行定制化事件代理。</p><h4 id="">事件对象的属性和方法</h4><ul><li><code>eventTarget</code>  和·<code>currentTarget</code>  的区别：
<ul><li>点击父元素中的子元素。</li><li><code>eventTarget | target</code>  指向子元素，<code>currentTarget</code>  指向父元素。</li></ul></li><li>事件委托，向父元素上监听事件来操作子元素。</li></ul><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">divEl.addEventListener(&#x27;click&#x27;, (event) =&gt; {
  console.log(&#x27;span元素被点击:&#x27;, event);
  console.log(&#x27;事件的类型:&#x27;, event.type);
  console.log(&#x27;事件的元素:&#x27;, event.target, event.currentTarget);
  console.log(&#x27;事件发生的位置:&#x27;, event.offsetX, event.offsetY);
});</code></pre></div><p style="text-align:right"><a href="https://benlifec.com/posts/frontend/js_next#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://benlifec.com/posts/frontend/js_next</link><guid isPermaLink="true">https://benlifec.com/posts/frontend/js_next</guid><dc:creator><![CDATA[benjamin]]></dc:creator><pubDate>Mon, 24 Mar 2025 08:08:00 GMT</pubDate></item><item><title><![CDATA[[JS] 我所理解的 JavaScript （上）]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://benlifec.com/posts/frontend/js_pre">https://benlifec.com/posts/frontend/js_pre</a></blockquote><div><blockquote><p>接触 Javascript 这门编程语言，满打满算应该有将近 4 年的时间，我觉得不能说对这门语言理解的有多深刻，只是在看了众多的经典书籍和博客之后，也想把自己的理解和总结也分享出来。</p></blockquote>
<h2 id="">闭包</h2><p>很多编程语言(Golang, Python, Javascript)都有闭包，闭包是语言运行时当中的一个自然的结果，拿 Javascript 举例来说，下面是一个经典的闭包案例。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">function out() {
  const name = &quot;cyan&quot;;
  return function in() {
    console.log(name);
  }
}
const fn = out();
fn();
fn = null;</code></pre><h3 id=""><strong>为什么会产生闭包？</strong></h3><p>拿 JS 来说，就是外层函数执行上下文的变量环境对象在内存中被内层函数的作用域链 <code>[[scope]]</code> 引用，而且根据 JS 垃圾回收机制并不会销毁这一引用，所以内层函数作用域才可以通过作用域链引用外层函数作用域的属性，所以才有了闭包这种自然结果。</p><h3 id=""><strong>闭包有什么意义？</strong></h3><p>给 JavaScript 函数增加高级特性，像函数式编程中 柯里化函数 | 组合函数 | pipe | partial</p><h3 id=""><strong>闭包的负面价值：内存泄露</strong></h3><p>内存泄露，内存泄露的本质就是代码执行完毕之后，内存当中的内层函数对象和外层函数执行上下文的变量对象存在引用，但是根据 JavaScript 垃圾回收机制，并不会删除所以占据内存。造成了内存泄露。我们可以利用 JavaScript 垃圾回收机制去解决闭包的内存泄露问题。我们可以手动销毁内层函数，外层的变量对象没有了引用也会被销毁。如下 fn = null;</p><h2 id="">原型</h2><p>JavaScript 是基于原型的语言，JavaScript 实现继承，实例化的时候是将多个对象（实例对象，原型对象，普通对象）进行使用原型链进行关联或者说链接来实现的。</p><p>具体来说，JavaScript 当中的所有函数在初始化的时候都有一个 prototype 属性，该属性指向的是原型对象，也就是通过调用构造函数创建的实例对象的原型，好处是我们可以预定义属性，方法。这些属性和方法会被实例对象所共享。</p><h3 id=""><strong>原型链本质和设计</strong></h3><p>原型最核心的部分就是整个 JavaScript 整个语言的原型设计。通过 github 上的一张神图，我们可以按照显式原型和隐式原型两条线来进行全面梳理 JavaScript 语言的 原型设计。</p><blockquote><p>显式原型路线是：无论是基本类型构造函数（Number, String, BigInt, Boolean, Symbol）还是除了 Object 引用类型构造函数(Array, Date, Function, RegExp，除了 Object)，还是其他自定义构造函数，都遵守：</p><ul><li><strong><code>构造函数.prototype.__proto__</code> 等于<code>Object.prototype</code></strong></li><li><strong><code>Object.prototype.__proto__</code>等于顶层原型 <code>null</code> 。</strong></li></ul></blockquote><blockquote>
<p>隐式原型路线是：无论是基本类型构造函数（Number, String, BigInt, Boolean, Symbol），还是引用类型构造函数(Array, Date, Function, RegExp，包括 Object)，还是其他自定义构造函数，都遵守：</p><ul><li><strong><code>构造函数.__proto__ 等于 Function.prototype</code></strong></li><li><strong><code>构造函数.__proto__.__proto__ 等于 Object.prototype</code></strong></li></ul></blockquote>
<h2 id="">作用域</h2><p>作用域决定这些变量在程序的哪个区域可以使用。作用域也同时决定了变量在程序中的可见性。</p><p>在 JavaScript 中，有函数作用域/全局作用域/eval 作用域/块级作用域, 这些作用域的本质就是执行上下文环境或者说执行上下文对象。 举例来说，就是执行上下文环境中的 变量对象 VO/活动对象 AO（函数上下文使用 AO 活动对象来收集变量，函数和形参的）。</p><h2 id="">作用域链</h2><p>作用域链本质上就是上下文对象中的作用域链一个属性，具体来说，初始化一个函数执行上下文对象的作用域链属性时，会将函数执行上下文对象的 AO，压入到已保存作用域链属性的最顶端。这样以来，就形成了作用域链，那查找属性的时候，先从自己的 AO 活动对象找，一直沿着作用域链找到下一个 AO，直到找到 GO 为止。这也就是作用域链的由来和本质。</p><h2 id="">执行上下文</h2><p>我们知道<code>JavaScript</code> 是解释性语言，代码是一段一段执行的，再执行到一段可执行代码的时候，就会创建<strong>执行上下文/执行上下文环境/执行上下文对象</strong>，<code>JavaScript</code>可执行代码有三种：全局代码，函数代码，<code>eval</code>代码，所以对应的执行上下文对象也就有三种，分别是全局执行上下文，函数执行上下文，<code>eval</code>执行上下文。这三种执行上下文对象都有三个属性，第一个是变量环境对象<code>VO</code>，第二个是作用域链<code>Scope</code>，第三个是<code>this</code>。</p><p>可以用一段代码的执行来更好的解释执行上下文:</p><pre class=""><code class="">var a = 1;
function out () {
    var name = &quot;ryan&quot;;
    function in () {
        console.log(name);
    }
    return in()
}
out();</code></pre><p>有这样的一段代码，<code>var a = 1</code>，有一个外层函数<code>out</code>，<code>out</code>函数当中定义 <code>var name = 2</code>，<code>out</code>外层函数当中有一个内层函数<code>in</code>，内层函数，打印外层函数作用域的变量<code>name</code>，返回内层函数的调用。外层先调用<code>out</code>，然后再调用<code>in</code>。</p><p>首先，<code>JavaScript</code>引擎会先创建<code>GO</code>就是<code>global object</code>全局对象，这个全局对象上，我们可以使用一些预定义的对象或者函数，比如：<code>日期对象Date，数学对象 Math ……</code> 开始执行后<code>JavaScript</code>引擎会拿到他的第一段可执行代码，全局代码，所以会先创建全局执行上下文对象，并且将执行上下文 push 到调用栈当中，这时开始初始化全局上下文对象。初始化这个对象就是初始化三个属性：作用域链属性，<code>VO</code>变量对象，绑定<code>this</code>。</p><p>初始化作用域链：将<code>JavaScript</code>刚刚创建的<code>GO</code>保存为作用域链属性。</p><p>初始化<code>VO</code>：这个<code>VO</code>就是来收集定义的变量和函数的定义。 这里就是全局定义的<code>a</code>属性 和定义的<code>out</code>函数。</p><p>绑定<code>this</code>:这个就是<code>this</code>的绑定。</p><p>这时<code>out</code>函数被创建，保存全局上下文对象的作用链作为<code>out</code>函数的<code>[[scope]]</code>属性。调用<code>out</code>函数，然后创建<code>out</code>函数上下文对象，入栈，开始初始化<code>out</code>函数初始化上下文对象。</p><p>初始化<code>AO</code>活动对象：函数上下文当中叫<code>AO</code>（既可以收集定义的变量和函数，还能够收集函数的形参），全局当中我们是叫做<code>VO</code>。这里就是 <code>name变量</code> 和 <code>in</code>内部函数。</p><p>初始化作用域链：将<code>out</code>函数的<code>[[scope]]</code>属性赋值过来作为作用域链属性。再将<code>out</code>函数的 AO 对象压入<code>out</code>函数作用域链的最顶端。这样做的结果和目的就是，<code>out</code>函数开始执行查找属性和函数的时候，先在<code>out</code>函数的作用域的<code>AO</code>对象上找属性和方法，然后去<code>GO</code>上找属性和方法。</p><p>绑定<code>this</code>:还是函数的<code>this</code>绑定。</p><p>这时<code>in</code>函数被创建，保存<code>out</code>函数的作用链到<code>in</code>函数的<code>[[scope]]</code>属性，调用<code>in</code>函数，然后创建<code>in</code>函数上下文，入栈，开始初始化<code>in</code>函数初始化上下文对象。</p><p>初始化<code>AO</code>活动对象：这里没有初始化代码。</p><p>初始化作用域链：将<code>in</code>函数的<code>[[scope]]</code>属性赋值过来作为作用域链属性。再将<code>in</code>函数的<code>AO</code>对象压入<code>in</code>函数作用域链的最顶端。这样做的结果就很明确，<code>in</code>函数开始执行查找属性和函数的时候，先是自己的<code>AO</code>，然后是<code>out</code>函数的<code>AO</code>，最后是全局上下文的<code>GO</code>。</p><p>绑定<code>this</code>:还是函数的<code>this</code>绑定。</p><p>执行完<code>in</code>之后出栈，<code>out</code>出栈，全局上下文出栈，执行完毕。</p><h2 id="">垃圾回收</h2><ul><li>JavaScript 不像 C/C++, 让程序员自己去调用函数来释放内存，go javascript Java 拥有自己的一套垃圾回收算法（GC）进行自动的内存管理。</li></ul><blockquote><p>垃圾回收有两种方法：<strong>标记清除、引用计数,</strong> 引用计数不太常用，标记清除较为常用。</p></blockquote>
<h3 id=""><strong>引用计数算法</strong></h3><p>第一个就是早先的引用计数法，它的策略是跟踪记录每个变量值被使用的次数，当这个值的引用次数变为 0 的时候，说明没有其他对象引用他了，垃圾回收器会在运行的时候清理掉引用次数为 0 的垃圾对象。</p><h3 id=""><strong>缺点</strong></h3><p>循环引用的两个对象无法回收的问题，因为循环引用的两个对象的引用次数都不为 0。这样就会发生内存泄露</p><h3 id=""><strong>标记清除算法</strong></h3><p>第二个就是目前大多数浏览器的 JavaScript 引擎 都在采用标记清除算法，就像它的名字一样，此算法分为 标记 和 清除 两个阶段。</p><ul><li>垃圾收集器在运行时会给内存中的所有变量都加上一个标记，假设标记策略时全标记为 0。</li><li>然后从根对象开始遍历，把可达的对象也就是不是垃圾的节点改成 1。</li><li>清理所有标记为 0 的垃圾对象，销毁并回收它们所占用的内存空间</li><li>最后，把内存中对象的标记都修改为 0，等待下一轮垃圾回收。</li></ul><h3 id=""><strong>优化垃圾清除算法的原因</strong></h3><p>如果单单是这样的算法 会造成很多问题</p><ul><li>JS 是单线程运行的，垃圾回收机制是微任务，这意味着一旦进入到垃圾回收，那么其它的运行的业务逻辑都要暂停; 另一方面垃圾回收其实是非常耗时间的操作</li><li>未进行优化的算法之前存在很多问题，比如内存碎片化的问题</li></ul><h3 id=""><strong>标记清除算法优化</strong></h3><h4 id="-1mark-compact"><strong>优化 1：标记整理（Mark-Compact）算法</strong></h4><ul><li>垃圾清理之后，会存在大量不连续的内存碎片，所以在标记结束后，标记整理算法会将不需要清理的对象对向内存的一端移动。从而使得空闲内存块是连续的。</li></ul><h4 id="-2"><strong>优化 2：分代式垃圾回收</strong></h4><ul><li>V8 的垃圾回收策略主要基于分代式垃圾回收机制，V8 中将堆内存分为新生代和老生代两区域，两个区域采用不同的垃圾回收器也就是不同的策略管理垃圾回收 。</li><li>新生代的对象为存活时间较短的对象，简单来说就是新产生的对象，通常只支持 1 ～ 8M 的容量，而老生代的对象为存活时间较长或常驻内存的对象，简单来说就是经历过新生代垃圾回收后还存活下来的对象，容量通常比较大。</li></ul><p><strong>新生代垃圾回收</strong></p><ul><li>新生代垃圾回收所依赖的算法是<code>Scavenge算法</code>。</li><li>首先将新生代内存空间一分为二:分为使用区和空闲区，当进行垃圾回收时，V8 将 使用区 部分的对象检查一遍，如果是存活对象那么按照内存顺序排列并复制到 空闲区 内存中，如果是非存活对象直接回收即可。当所有的 使用区 中的存活对象按照顺序进入到 空闲区 内存之后，空闲区 和 使用区 两者的角色对调，空闲区 现在被闲置，使用区 为正在使用，如此循环。</li></ul><p><strong>老生代垃圾回收</strong></p><ul><li>老生代垃圾回收所依赖的算法是<code>标记清除算法</code>。</li><li>新生代中的变量如果经过多次回收之后依然存在，会采用对象晋升策略，放入到老生代内存中。</li><li>老生代当中就是先使用标记清除算法来进行垃圾回收，然后再通过标记整理来解决内存碎片化问题。</li></ul><h4 id="-3"><strong>优化 3：增量标记</strong></h4><ul><li>将垃圾回收任务分为很多小的部分完成，每做完一个小的部分 就让 JavaScript 应用逻辑执行一会儿，然后再做下面的部分，如此循环，直到任务完成。</li></ul><h2 id="">事件循环</h2><p>所有的任务都会被放到调用栈等待主线程执行，同步任务会在调用栈中按照顺序等待主线程依次执行，异步任务（将 DOM 操作，HTTP 请求，定时器等的回调函数）会在异步任务有了结果后，将注册的回调函数放入任务队列中等待主线程空闲的时候（调用栈被清空），通过轮询来读取异步任务到调用栈内等待主线程的执行。</p><p><strong>从宏任务和微任务的角度来聊 JavaScript 事件循环？</strong></p><ul><li>MacroTask（宏任务）
<ul><li>script 全部代码(普通任务)、setTimeout、setInterval、setImmediate、I/O、UI Rendering。</li></ul></li><li>MicroTask（微任务）
<ul><li>Process.nextTick（Node 独有）、Promise、MutationObserver、垃圾回收</li></ul></li></ul><p>在每一个宏任务中会定义一个微任务队列，当该宏任务执行完成，会检查其中的微任务队列，如果为空则直接执行下一个宏任务，如果不为空，则<code>依次执行微任务</code>，执行完成才去执行下一个宏任务。</p><p><strong>node 事件循环和浏览器事件循环的区别是什么？</strong></p><p>两者最主要的区别在于浏览器中的微任务是在<code>每个相应的宏任务</code>中执行的，（在执行下一个宏任务的时候会清空微任务队列）而 nodejs 中的微任务是在<code>不同阶段之间</code>执行的。</p><ul><li>node 的初始化
<ul><li>初始化 node 环境。</li><li>执行输入代码。</li><li>执行 <strong>process.nextTick</strong> 回调。</li><li><strong>process.nextTick 是一个独立于 eventLoop 的任务队列。在每一个 eventLoop 阶段完成后会去检查这个队列，如果里面有任务，会让这部分任务<code>优先于微任务</code>执行。所以在 nodejs 事件循环的每一个子阶段退出之前都会按顺序执行如下过程：</strong>
<ol start="1"><li><strong>检查是否有 process.nextTick 回调，如果有，全部执行。</strong></li><li><strong>检查是否有 microtaks，如果有，全部执行。</strong></li><li><strong>退出当前阶段。</strong></li></ol></li><li><strong>执行 microtasks</strong>。</li></ul></li><li>进入 event-loop
<ul><li>进入 <strong>timers</strong> 阶段
<ul><li>检查 timer 队列是否有到期的 timer 回调，如果有，将到期的 timer 回调按照 timerId 升序执行。这些定时器就是 setTimeout、setInterval</li><li>检查是否有 process.nextTick 任务，如果有，全部执行。</li><li>检查是否有 microtask，如果有，全部执行。</li><li>退出该阶段。</li></ul></li><li>进入<strong>IO callbacks</strong>阶段。
<ul><li>检查是否有 pending 的 I/O 回调。如果有，执行回调。如果没有，退出该阶段。</li><li>检查是否有 process.nextTick 任务，如果有，全部执行。</li><li>检查是否有 microtask，如果有，全部执行。</li><li>退出该阶段。</li></ul></li><li>进入 <strong>poll</strong> /轮询 阶段
<ul><li>首先检查是否存在尚未完成的回调，如果存在，那么分两种情况。
<ul><li>第一种情况：
<ul><li>如果有可用回调（可用回调包含到期的定时器还有一些 IO 事件等），执行所有可用回调。</li><li>检查是否有 process.nextTick 回调，如果有，全部执行。</li><li>检查是否有 microtaks，如果有，全部执行。</li><li>退出该阶段。</li></ul></li><li>第二种情况：
<ul><li>如果没有可用回调。</li><li>检查是否有 immediate 回调，如果有，退出 poll 阶段，进入 check 阶段。</li></ul></li><li>所以 <strong>immediate 的回调的调用顺序是不一定的。</strong></li></ul></li><li>如果不存在尚未完成的回调，退出 poll 阶段。</li></ul></li><li>进入 <strong>check</strong> 阶段。
<ul><li>如果有 immediate 回调，则执行所有 immediate 回调。</li><li>检查是否有 process.nextTick 回调，如果有，全部执行。</li><li>检查是否有 microtaks，如果有，全部执行。</li><li>退出 <strong>check</strong> 阶段</li></ul></li><li>进入 <strong>closing</strong> 阶段 / 关闭事件的回调阶段。
&gt; 如果一个 socket 或句柄（handle 文件/设备/套接字）被突然关闭，例如 socket.destroy()， close 事件的回调就会在这个阶段执行。
<ul><li>如果有 immediate 回调，则执行所有 immediate 回调。</li><li>检查是否有 process.nextTick 回调，如果有，全部执行。</li><li>检查是否有 microtaks，如果有，全部执行。</li><li>退出 <strong>closing</strong> 阶段</li></ul></li><li>进行下一轮事件循环</li></ul></li></ul><h2 id="">模块化</h2><p><strong>JavaScript 模块化历史</strong></p><h3 id="1-"><strong>1. 刀耕火种</strong></h3><blockquote><p>defer 与 async 的区别是：defer 要等到整个页面在内存中正常渲染结束（DOM 结构完全生成，以及其他脚本执行完成），才会执行；async 一旦下载完，排版引擎就会中断渲染，执行这个脚本以后，再继续渲染。一句话，defer 是“渲染完再执行”，async 是“下载完就执行”。另外，如果有多个 defer 脚本，会按照它们在页面出现的顺序加载，而多个 async 脚本是不能保证加载顺序的。</p></blockquote>
<p>在原始 <code>JavaScript</code> 中，出现模块化规范之前，<code>js</code> 文件之间的通信基本上靠的是 <code>window</code> 对象。我们按照 <code>js</code> 文件之间依赖关系来按顺序引入 <code>js</code>。一旦顺序没有按照依赖关系来引入，就会报错。</p><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">// 引入顺序颠倒就会报错
&lt;body&gt;
  &lt;script src=&quot;./a.js&quot;&gt;&lt;/script&gt;
  &lt;scrip src=&quot;./b.js&quot;&gt;&lt;/script&gt;
&lt;/body&gt;

// a.js
var nameP = &quot;ryan&quot;;
// b.js
console.log(nameP);</code></pre><p>所以当业务变得复杂时,就会产生很多问题：</p><ul><li>第一个问题：多而复杂的 <code>js</code> 文件不好开发和维护，需要考虑 <code>js</code> 文件之间的依赖关系。</li><li>第二个问题：<code>window</code>上挂载的全局变量避免不了存在命名冲突、占用内存无法被回收、代码可读性低等诸多问题。</li><li>如果按照这么来开发前端，开发体验会非常糟糕。</li></ul><p><strong>所以尽管当时没有模块化的时候，但也涌现出了一些解决方案。</strong></p><p><strong>解决方案 1：函数</strong></p><pre class=""><code class="">function m1() {
  //...
}

function m2() {
  //...
}</code></pre><p>将不同的功能函数当成一个个模块，这样做虽然起到了一定的效果，但是还是避免不了存在命名冲突，最重要的是，这些函数被当作模块，我们看不出来模块之间的关系。</p><p><strong>解决方案 2：对象</strong></p><pre class=""><code class="">var module1 = new Object({
  _count: 0,

  m1: function () {
    //...
  },

  m2: function () {
    //...
  },
});</code></pre><p>将不同的功能对象当成一个个模块，这样做虽然起到了一定的效果，但是还是避免不了存在命名冲突，最重要的是，这样的写法会暴露所有的模块成员。模块成员可以被随意修改。</p><p><strong>解决方案 3：IIFE（匿名立即执行函数）</strong></p><pre class=""><code class="">// a.js
var moduleA = (function () {
  return {
    name: &quot;ryan&quot;,
  };
})();
//console.log(moduleA.name); ryan</code></pre><p><strong>随着前端业务增重，代码越来越复杂，前端急需一种清晰有效方案来处理功能模块之间的依赖关系。</strong></p><h3 id="2-amd--cmd"><strong>2. AMD 和 CMD</strong></h3><p><code>node</code> 服务器端编程出来的时候，模块系统就是参照 <code>CommonJs</code> 规范实现的，所以有了服务器端模块化规范，大家也想要客户端模块化开发，而且大家也希望两者可以相互兼容，但是 <code>CommonJs</code> 对客户端模块化来说，一个非常大的局限就是：同步加载模块的方式不适合浏览器环境，因为同步加载模块会导致浏览器卡死，阻塞渲染，所以在这样的背景下 <code>AMD</code> 这种异步加载模块化的方式出现了。</p><p><code>CMD</code> 和 <code>AMD</code> 一样，都是 <code>JS</code> 的社区模块化规范，主要应用于浏览器端，可以异步加载模块。</p><p>我们只要按照规范的方式去书写，就可以被 <code>require.js</code>, <code>sea.js</code> 正确解析。从而实现异步模块化。</p><ul><li>比如 <code>require.js</code> 中的 <code>require</code> 函数，<code>define</code> 函数</li><li><code>sea.js</code>的<code>require</code>函数</li></ul><p><strong>AMD</strong></p><blockquote><p>例子来自于 yayu</p></blockquote>
<ul><li>require.js</li></ul><pre class=""><code class="">// main.js
require([&quot;./add&quot;, &quot;./square&quot;], function (addModule, squareModule) {
  console.log(addModule.add(1, 1));
  console.log(squareModule.square(3));
});

// add.js
define(function() {
    console.log(&#x27;加载了 add 模块&#x27;);
    var add = function(x, y) {&amp;emsp;
        return x + y;
    };

    return {&amp;emsp;&amp;emsp;&amp;emsp;&amp;emsp;&amp;emsp;&amp;emsp;
        add: add
    };
});
// multiply.js
define(function() {
    console.log(&#x27;加载了 multiply 模块&#x27;)
    var multiply = function(x, y) {&amp;emsp;
        return x * y;
    };

    return {&amp;emsp;&amp;emsp;&amp;emsp;&amp;emsp;&amp;emsp;&amp;emsp;
        multiply: multiply
    };
});
// square.js
define([&#x27;./multiply&#x27;], function(multiplyModule) {
    console.log(&#x27;加载了 square 模块&#x27;)
    return {&amp;emsp;&amp;emsp;&amp;emsp;&amp;emsp;&amp;emsp;&amp;emsp;
        square: function(num) {
            return multiplyModule.multiply(num, num)
        }
    };
});
// 加载了 add 模块
// 加载了 multiply 模块
// 加载了 square 模块
// 2
// 9</code></pre><p><strong>CMD</strong></p><ul><li>sea.js</li></ul><pre class=""><code class="">// main.js
define(function(require, exports, module) {
    var addModule = require(&#x27;./add&#x27;);
    console.log(addModule.add(1, 1))

    var squareModule = require(&#x27;./square&#x27;);
    console.log(squareModule.square(3))
});

// add.js
define(function(require, exports, module) {
    console.log(&#x27;加载了 add 模块&#x27;)
    var add = function(x, y) {&amp;emsp;
        return x + y;
    };
    module.exports = {&amp;emsp;&amp;emsp;&amp;emsp;&amp;emsp;&amp;emsp;&amp;emsp;
        add: add
    };
});
// square.js
define(function(require, exports, module) {
    console.log(&#x27;加载了 square 模块&#x27;)
    var multiplyModule = require(&#x27;./multiply&#x27;);
    module.exports = {&amp;emsp;&amp;emsp;&amp;emsp;&amp;emsp;&amp;emsp;&amp;emsp;
        square: function(num) {
            return multiplyModule.multiply(num, num)
        }
    };

});

// multiply.js
define(function(require, exports, module) {
    console.log(&#x27;加载了 multiply 模块&#x27;)
    var multiply = function(x, y) {&amp;emsp;
        return x * y;
    };
    module.exports = {&amp;emsp;&amp;emsp;&amp;emsp;&amp;emsp;&amp;emsp;&amp;emsp;
        multiply: multiply
    };
});

// 加载了 add 模块
// 2
// 加载了 square 模块
// 加载了 multiply 模块
// 9</code></pre><p><strong>AMD 与 CMD 的区别</strong></p><blockquote><p>yayu 总结的区别：</p></blockquote>
<p>根据上面代码的打印结果，我们也可以更好的理解他们之间的区别</p><ul><li><code>CMD</code> 推崇依赖就近，<code>AMD</code> 推崇依赖前置。</li><li><code>AMD</code> 是将需要使用的模块全部加载完再执行代码，而<code>CMD</code> 是在 <code>require</code> 的时候才去加载模块文件，加载完再接着执行。</li></ul><h3 id="commonjscjs"><strong>CommonJS(CJS)</strong></h3><p><strong>CommonJS 基本说明</strong></p><p>随着 <code>node</code> 诞生，服务器端的模块规范 <code>CommonJS</code> 被创建出来。在 <code>commonjs</code> 中每一个 js 文件都是一个单独的模块，我们在控制台也可以打印出这个 <code>module</code> 对象。所有代码都运行在模块作用域，不会污染全局作用域。同时，<code>CommonJS</code> 是运行时同步加载模块，比较适合服务器端的模块加载，因为服务器的模块文件都在硬盘，即使是同步也非常快。</p><p><strong>CJS 基本用法</strong></p><p><strong>导入</strong></p><pre class=""><code class="">const a = require(&quot;./a.js&quot;);
// a是module.exports导出的对象。</code></pre><p><strong>导出</strong></p><pre class=""><code class="">// 为module.exports对象上添加属性。
exports.name = &quot;kobe&quot;;
exports.age = 18;
// 重置了module.exports对象引用。
module.exports = {};
// 导出module.exports对象</code></pre><p><strong>cjs 模块导入导出的基本原理</strong></p><pre class=""><code class="">module.exports = {};
exports = module.exports;</code></pre><p><strong>require 引入细节</strong></p><ul><li>情况一：X 是一个 <code>Node</code> 核心模块，比如<code>path</code>、<code>http</code>。
<ul><li>直接返回核心模块，并且停止查找。</li></ul></li><li>情况二：X 是以 ./ 或 ../ 或 /（根目录）开头的。
<ul><li>第一步：将 X 当做一个文件在对应的目录下查找。
<ul><li>如果有后缀名，按照后缀名的格式查找对应的文件</li><li>如果没有后缀名，会按照如下顺序：按照 <code>js</code> -&gt; <code>json</code> -&gt; <code>node</code>的顺序。</li></ul></li><li>第二步：没有找到对应的文件，将 X 作为一个目录。
<ul><li>作为目录下查找 <code>index.js</code> -&gt; <code>.json</code> -&gt; <code>.node</code>。</li></ul></li></ul></li></ul><h3 id="es6moduleesm"><strong>ES6module(ESM)</strong></h3><ul><li><code>AMD</code> 、 <code>CMD</code> 等都是在原有 <code>JS</code> 语法的基础上二次封装的一些社区规范，<code>ES6 module</code>是 JavaScript 语言层面的规范，<code>ES6 module</code>编译时静态加载。编译时就能确定模块的依赖关系，以及输入和输出的变量。从而我们可以在编译时进行静态优化，而像 AMD, CMD 运行时加载，导致完全没办法在编译时做“静态优化”。</li></ul><p><strong>ES6module 与 CommonJS 的区别</strong></p><ul><li><code>CommonJS 模块</code>输出的是一个值的拷贝对于引用类型而言就是引用，<code>ES6</code> 模块输出的无论是什么类型都是值的引用。</li><li><code>CommonJS /模块</code>是运行时加载，<code>ES6module</code> 是编译时就能确定模块的依赖关系和输入输出的接口。</li></ul><p><strong>ES6module 与 CommonJS 导出简单值</strong></p><pre class=""><code class="">// 输出模块 counter.js -&gt; 简单值
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
// 引入模块 main.js
var mod = require(&quot;./counter&quot;);

console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3</code></pre><blockquote><p>ES6 module 模块输出的无论是什么类型都是值的引用。</p></blockquote><pre class=""><code class="">// counter.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.js
import { counter, incCounter } from &quot;./counter&quot;;
console.log(counter); // 3
incCounter();
console.log(counter); // 4</code></pre><p><strong>CommonJS 导出引用值</strong></p><pre class=""><code class="">// 输出模块 counter.js -&gt; 引用值
var counter = {
  value: 3,
};

function incCounter() {
  counter.value++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
// 引入模块 main.js
var mod = require(&quot;./counter.js&quot;);

console.log(mod.counter.value); // 3
mod.incCounter();
console.log(mod.counter.value); // 4</code></pre><p><strong>ES Module 的解析流程</strong></p><p><strong>阶段一：</strong> 构建（Construction），根据所有依赖关系去查找 js 文件，并且下载，将其解析成模块记录（Module Record）</p><p><strong>阶段二：</strong> 实例化（Instantiation），对模块记录进行实例化为模块环境记录，这时 import 会自动提升到代码的顶层。开始解析模块的导入和导出语句。并且为 bindings 的值（也就是导出的变量）分配内存空间。</p><p><strong>阶段三：</strong> 运行（Evaluation），运行代码，计算值，并且赋值到导出变量对应的内存地址中，以供导入消费。</p><blockquote><p>导入的模块不可以修改值，因为在模块环境记录当中用的是 const，修改就会报错。</p></blockquote>
<p><strong>export 用法</strong></p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">export const name = &#x27;why&#x27;;
export const age = 18;
export { name, age, foo };
export { name as fName, age as fAge, foo as fFoo };

// 直接导出,无需先引入。
export { add, sub } from &#x27;./math.js&#x27;;
export { timeFormat, priceFormat } from &#x27;./format.js&#x27;;
export * from &#x27;./math.js&#x27;;
export * from &#x27;./format.js&#x27;;

// 默认导出
export default foo;</code></pre><p><strong>import 用法</strong></p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">import { name, age } from &#x27;./foo.js&#x27;;
import { name as fName, age as fAge, foo as fFoo } from &#x27;./foo.js&#x27;;
import * as foo from &#x27;./foo.js&#x27;;

// 默认导出的引入
import why from &#x27;./foo.js&#x27;;

// 动态引入
import(&#x27;./foo.js&#x27;).then((res) =&gt; {
  console.log(&#x27;res:&#x27;, res.name);
});</code></pre><h3 id="commonjs-"><strong>CommonJS 模块的循环加载</strong></h3><aside><p>💡 一旦出现某个模块被&quot;循环加载&quot;，就只输出已经执行的部分，还未执行的部分不会输出，从而解决 cjs 模块的循环加载。</p>
</aside><p>CommonJS 模块的重要特性是加载时执行，即脚本代码在<code>require</code>的时候，就会全部执行。一旦出现某个模块被&quot;循环加载&quot;，就只输出已经执行的部分，还未执行的部分不会输出，从而解决 cjs 模块的循环加载。</p><p>让我们来看，Node <a href="https://nodejs.org/api/modules.html#modules_cycles">官方文档</a>里面的例子。脚本文件<code>a.js</code>代码如下。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">exports.done = false;
var b = require(&#x27;./b.js&#x27;);
console.log(&#x27;在 a.js 之中，b.done = %j&#x27;, b.done);
exports.done = true;
console.log(&#x27;a.js 执行完毕&#x27;);</code></pre><p>上面代码之中，<code>a.js</code>脚本先输出一个<code>done</code>变量，然后加载另一个脚本文件<code>b.js</code>。注意，此时<code>a.js</code>代码就停在这里，等待<code>b.js</code>执行完毕，再往下执行。</p><p>再看<code>b.js</code>的代码。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">exports.done = false;
var a = require(&#x27;./a.js&#x27;);
console.log(&#x27;在 b.js 之中，a.done = %j&#x27;, a.done);
exports.done = true;
console.log(&#x27;b.js 执行完毕&#x27;);</code></pre><p>上面代码之中，<code>b.js</code>执行到第二行，就会去加载<code>a.js</code>，这时，就发生了“循环加载”。系统会去<code>a.js</code>模块对应对象的<code>exports</code>属性取值，可是因为<code>a.js</code>还没有执行完，从<code>exports</code>属性只能取回已经执行的部分，而不是最后的值。</p><p><code>a.js</code>已经执行的部分，只有一行。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">exports.done = false;</code></pre><p>因此，对于<code>b.js</code>来说，它从<code>a.js</code>只输入一个变量<code>done</code>，值为<code>false</code>。</p><p>然后，<code>b.js</code>接着往下执行，等到全部执行完毕，再把执行权交还给<code>a.js</code>。于是，<code>a.js</code>接着往下执行，直到执行完毕。我们写一个脚本<code>main.js</code>，验证这个过程。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">var a = require(&#x27;./a.js&#x27;);
var b = require(&#x27;./b.js&#x27;);
console.log(&#x27;在 main.js 之中, a.done=%j, b.done=%j&#x27;, a.done, b.done);</code></pre><p>执行<code>main.js</code>，运行结果如下。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">$ node main.js

在 b.js 之中，a.done = false
b.js 执行完毕
在 a.js 之中，b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true</code></pre><p>上面的代码证明了两件事。一是，在<code>b.js</code>之中，<code>a.js</code>没有执行完毕，只执行了第一行。二是，<code>main.js</code>执行到第二行时，不会再次执行<code>b.js</code>，而是输出缓存的<code>b.js</code>的执行结果，即它的第四行。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">exports.done = true;</code></pre><p>总之，CommonJS 输入的是被输出值的拷贝，不是引用。</p><p>另外，由于 CommonJS 模块遇到循环加载时，返回的是当前已经执行的部分的值，而不是代码全部执行后的值，两者可能会有差异。所以，输入变量的时候，必须非常小心。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">var a = require(&#x27;a&#x27;); // 安全的写法
var foo = require(&#x27;a&#x27;).foo; // 危险的写法

exports.good = function (arg) {
  return a.foo(&#x27;good&#x27;, arg); // 使用的是 a.foo 的最新值
};

exports.bad = function (arg) {
  return foo(&#x27;bad&#x27;, arg); // 使用的是一个部分加载时的值
};</code></pre><p>上面代码中，如果发生循环加载，<code>require(&#x27;a&#x27;).foo</code>的值很可能后面会被改写，改用<code>require(&#x27;a&#x27;)</code>会更保险一点。</p><h3 id="es6-"><strong>ES6 模块的循环加载</strong></h3><aside><p>💡 需要开发者自己保证，真正取值的时候能够取到值,当 ESM 发现一个模块发生循环加载之后，不会去加载该模块，而是认为该模块导出的接口以及存在了，所以他会继续向下执行，从而去解决 ES6模块 的循环加载。</p>
</aside><p>ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用，如果使用<code>import</code>从一个模块加载变量（即<code>import foo from &#x27;foo&#x27;</code>），那些变量不会被缓存，而是成为一个指向被加载模块的引用，需要开发者自己保证，真正取值的时候能够取到值，</p><p>请看下面这个例子。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">// a.mjs
import { bar } from &#x27;./b&#x27;;
console.log(&#x27;a.mjs&#x27;);
console.log(bar);
export let foo = &#x27;foo&#x27;;

// b.mjs
import { foo } from &#x27;./a&#x27;;
console.log(&#x27;b.mjs&#x27;);
console.log(foo);
export let bar = &#x27;bar&#x27;;</code></pre><p>上面代码中，<code>a.mjs</code>加载<code>b.mjs</code>，<code>b.mjs</code>又加载<code>a.mjs</code>，构成循环加载。执行<code>a.mjs</code>，结果如下。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">$ node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined</code></pre><p>上面代码中，执行<code>a.mjs</code>以后会报错，<code>foo</code>变量未定义，这是为什么？</p><p>让我们一行行来看，ES6 循环加载是怎么处理的。首先，执行<code>a.mjs</code>以后，引擎发现它加载了<code>b.mjs</code>，因此会优先执行<code>b.mjs</code>，然后再执行<code>a.mjs</code>。接着，执行<code>b.mjs</code>的时候，已知它从<code>a.mjs</code>输入了<code>foo</code>接口，这时不会去执行<code>a.mjs</code>，而是认为这个接口已经存在了，继续往下执行。执行到第三行<code>console.log(foo)</code>的时候，才发现这个接口根本没定义，因此报错。</p><p>解决这个问题的方法，就是让<code>b.mjs</code>运行的时候，<code>foo</code>已经有定义了。这可以通过将<code>foo</code>写成函数来解决。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">// a.mjs
import { bar } from &#x27;./b&#x27;;
console.log(&#x27;a.mjs&#x27;);
console.log(bar());
function foo() {
  return &#x27;foo&#x27;;
}
export { foo };

// b.mjs
import { foo } from &#x27;./a&#x27;;
console.log(&#x27;b.mjs&#x27;);
console.log(foo());
function bar() {
  return &#x27;bar&#x27;;
}
export { bar };</code></pre><p>这时再执行<code>a.mjs</code>就可以得到预期结果。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">$ node --experimental-modules a.mjs
b.mjs
foo
a.mjs
bar</code></pre><p>这是因为函数具有提升作用，在执行<code>import {bar} from &#x27;./b&#x27;</code>时，函数<code>foo</code>就已经有定义了，所以<code>b.mjs</code>加载的时候不会报错。这也意味着，如果把函数<code>foo</code>改写成函数表达式，也会报错。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">// a.mjs
import { bar } from &#x27;./b&#x27;;
console.log(&#x27;a.mjs&#x27;);
console.log(bar());
const foo = () =&gt; &#x27;foo&#x27;;
export { foo };</code></pre><p>上面代码的第四行，改成了函数表达式，就不具有提升作用，执行就会报错。</p><p>我们再来看 ES6 模块加载器<a href="https://github.com/ModuleLoader/es6-module-loader/blob/master/docs/circular-references-bindings.md">SystemJS</a>给出的一个例子。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">// even.js
import { odd } from &#x27;./odd&#x27;;
export var counter = 0;
export function even(n) {
  counter++;
  return n === 0 || odd(n - 1);
}

// odd.js
import { even } from &#x27;./even&#x27;;
export function odd(n) {
  return n !== 0 &amp;&amp; even(n - 1);
}</code></pre><p>上面代码中，<code>even.js</code>里面的函数<code>even</code>有一个参数<code>n</code>，只要不等于 0，就会减去 1，传入加载的<code>odd()</code>。<code>odd.js</code>也会做类似操作。</p><p>运行上面这段代码，结果如下。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">$ babel-node
&gt; import * as m from &#x27;./even.js&#x27;;
&gt; m.even(10);
true
&gt; m.counter
6
&gt; m.even(20)
true
&gt; m.counter
17</code></pre><p>上面代码中，参数<code>n</code>从 10 变为 0 的过程中，<code>even()</code>一共会执行 6 次，所以变量<code>counter</code>等于 6。第二次调用<code>even()</code>时，参数<code>n</code>从 20 变为 0，<code>even()</code>一共会执行 11 次，加上前面的 6 次，所以变量<code>counter</code>等于 17。</p><p>这个例子要是改写成 CommonJS，就根本无法执行，会报错。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">// even.js
var odd = require(&#x27;./odd&#x27;);
var counter = 0;
exports.counter = counter;
exports.even = function (n) {
  counter++;
  return n == 0 || odd(n - 1);
};

// odd.js
var even = require(&#x27;./even&#x27;).even;
module.exports = function (n) {
  return n != 0 &amp;&amp; even(n - 1);
};</code></pre><p>上面代码中，<code>even.js</code>加载<code>odd.js</code>，而<code>odd.js</code>又去加载<code>even.js</code>，形成“循环加载”。这时，执行引擎就会输出<code>even.js</code>已经执行的部分（不存在任何结果），所以在<code>odd.js</code>之中，变量<code>even</code>等于<code>undefined</code>，等到后面调用<code>even(n - 1)</code>就会报错。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">$ node
&gt; var m = require(&#x27;./even&#x27;);
&gt; m.even(10)
TypeError: even is not a function</code></pre><h2 id="">类型</h2><h3 id=""><strong>类型检测</strong></h3><h3 id="typeof"><strong>typeof</strong></h3><p><code>typeof</code> 一般检测简单数据类型， 返回值是简单数据类型的字符串如; <code>&quot;number&quot;</code>， <code>&quot;boolean&quot;</code>，</p><p> <code>&quot;string&quot;</code> 检测引用数据类型的时候除了函数类型正常返回<code>&quot;function&quot;</code>， 其余都返回<code>&quot;object&quot;</code>。</p><ul><li>typeof 可以检测返回 8 种数据类型也就是<code>typeof</code> 可以返回 8 种数据类型的字符串：<code>undefined</code>， <code>boolean</code>， <code>string</code>， <code>number</code>， <code>object</code>， <code>function</code>，<code>symbol</code>， <code>bigint</code>。</li></ul><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">console.log(typeof 1); // &#x27;number&#x27;
console.log(typeof &quot;1&quot;); // &#x27;string&#x27;
console.log(typeof function () {}); // &#x27;function&#x27;
console.log(typeof true); // &#x27;boolean&#x27;
console.log(typeof null); // &#x27;object&#x27;
console.log(typeof undefined); // &#x27;undefined&#x27;
console.log(typeof { name: &quot;ryan&quot; }); // &#x27;object&#x27;
console.log(typeof new String(&quot;hello&quot;)); // &#x27;object&#x27;
console.log(typeof new String(1)); // &#x27;object&#x27;
console.log(typeof 120n); // &#x27;bigint&#x27;
console.log(typeof Symbol()); // &#x27;symbol&#x27;
// 由此可见typeof对于基本数据类型的检测是okay的。</code></pre><ul><li><code>typeof</code>检测<code>null</code>的时候，也会返回<code>&#x27;object&#x27;</code>, <em>出现这种情况的原因其实是<code>JavaScript</code>最初是通过 32 位 bit 来存储值，值的低三位或者低一位被当作类型标签来识别数据类型<code>（000：被识别为对象，1：被识别为 31 位的有符号位整数，110：被 识别位布尔值，110：被识别为字符串，010：被识别为双精度的浮点数）</code>，而恰巧对象和 <code>null</code> 的低三位都是 <code>000</code>，且<code>000</code>的类型标签是对象，所以 null 也被处理为对象。从技术层面来将，我认为还是挺合理的，因为特殊值<code>null</code></em>一直被我们认为是一个空对象的引用。</li></ul><h3 id="instanceof"><strong>instanceof</strong></h3><p><code>instanceof</code> 一般检测引用数据类型，返回值是布尔值，用来判断构造函数的原型对象 <code>prototype</code>
 是否存在于实例对象的原型链上。</p><blockquote><p>对于基本数据类型不能准确判断:</p></blockquote><blockquote>
<p>111 instanceof Number -&gt; false 同样的还有&quot;xx&quot;和true, 因为字面量值不是实例, 通过new构造函数构造出来的才是，new Number()才是</p></blockquote><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">console.log(&#x27;instanceof 系列&#x27;);
console.log(&#x27;instanceof 系列&#x27; instanceof String); // false
console.log([2， 3] instanceof Array); // true
console.log(1 instanceof Number); // false
console.log({ name: &#x27;ryan&#x27; } instanceof Object); // true
console.log(function () {} instanceof Function); // true
console.log(true instanceof Boolean); // false
console.log(new String(&#x27;hello&#x27;) instanceof String); // true
// 由此可以看出instanceof检测简单数据类型都时候都返回false， 除非你将简单值进行类包装如: new String(&#x27;&#x27;)</code></pre><h3 id="objectprototypetostringcall"><strong>Object.prototype.toString.call()</strong></h3><blockquote><p>可以检测 14 个数据类型，检测数据类型的神器</p></blockquote><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">function.call(thisArg， arg1， arg2， ...) // 第一个是可选参数。
console.log(Object.prototype.toString.call(new Number(1)));
console.log(Object.prototype.toString.call(&#x27;hello&#x27;));
console.log(Object.prototype.toString.call(new String(&#x27;hello&#x27;)));
console.log(Object.prototype.toString.call(console.log));
console.log(Object.prototype.toString({}));
console.log(Object.prototype.toString.call([]));
console.log(Object.prototype.toString.call(new Date()));
console.log(Object.prototype.toString.call(Math));
console.log(Object.prototype.toString.call(JSON));
console.log(Object.prototype.toString.call(undefined))
console.log(Object.prototype.toString.call(null))
console.log(Object.prototype.toString(null))
console.log(Object.prototype.toString(undefined))
//[object Number]
//[object String]
//[object String]
//[object Function]
//[object Object]
//[object Array]
//[object Date]
//[object Math]
//[object JSON]
//[object Undefined]
//[object Null]
// [object Object]
//[object Object]
//如果非要挑一挑毛病，那就是方法返回的字符串可能没有像typeof一样
// 值得注意的是，Object.prototype.toString(null | undefined) 返回的是 [object Object]</code></pre><h3 id="constructor"><strong>constructor</strong></h3><p><code>constructor</code>有两个作用，一是<code>判断数据的类型</code>，二是<code>对象实例通过 constrcutor 对象</code>访问它的构造函数。</p><ul><li>需要注意。如果创建一个对象来改变它的原型，<code>constructor</code>就不能用来判断数据类型了。（<strong>也非常好理解，因为实例上没有constructor属性，实例找该属性是通过实例的原型对象来找的，所以当原型对象改变了，找到的构造函数就改变了</strong>）</li></ul><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log((&#x27;str&#x27;).constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true</code></pre><h3 id=""><strong>类型转换</strong></h3><blockquote><p>分为显示类型转换和隐式类型转换</p></blockquote>
<h3 id=""><strong>原始值转换为布尔值</strong></h3><ul><li>原始值转换为布尔值: 只有六种值会被转换为 false, 其他都是 true.
  <ul><li><code>0</code>, <code>NaN</code>, <code>null</code>, <code>&#x27;&#x27;</code>, <code>undefined</code>, <code>false</code></li></ul></li></ul><h3 id=""><strong>原始值转换为数字</strong></h3><ul><li>原始值转换为数字: Number()如果不传值默认返回 0, 如果传了值调用调用 <code>ToNumber(value)</code>, ToNumber 是底层规范实现。
<br/><br/>  | 参数类型 | 结果 |
  | --- | --- |
  | undefined | NaN |
  | Null | +0 |
  | Boolean | 如果参数是 true，返回 1。参数为 false，返回 +0 |
  | Number | 返回与之相等的值 |
  | String | 如下段解释 |</li><li><strong>对于 <code>Number(string)</code> 会试着将包含数值字符, 加号,减号的字符串转换成浮点数或者是整数, 也可以识别 8 进制的 0 和 16 进制的 0x,会忽略前导的 0 进行转换, 如果有一个字符不是数字直接返回 <code>NaN</code>, 也就是不符合上述的情况直接返回 <code>NaN</code>.</strong></li></ul><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">console.log(Number(undefined)); // NaN
console.log(Number(null)); // 0
console.log(Number(1)); // 1
console.log(Number(true)); // 1
console.log(Number(NaN)); // NaN
console.log(Number(&quot;123j&quot;)); // NaN</code></pre><ul><li>鉴于这种严格的转换数字规则, 我们一般会使用更加灵活的 <code>parseInt()</code> 和 <code>parseFloat()</code>进行转换,他们更专注于字符串当中是否包含数值模式</li><li><code>parseInt(string, [radix])</code>; 只解析整数, <code>parseFloat(string, [radix])</code>既可以解析整数也可以解析浮点数, <code>parseInt</code> 和 <code>parseFloat</code> 都会跳过任意数量的前导空格，尽可能解析更多数值字符，并忽略后面的内容。如果第一个非空格字符是非法的数字直接量，将最终返回 <code>NaN</code>：<code>parseFloat</code>会专注于浮点数的第一次出现, 第二次就会忽略浮点数</li></ul><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">console.log(parseInt(&quot;1a2&quot;));
console.log(parseInt(&quot;0001a2&quot;));
console.log(parseInt(&quot;-0001a2&quot;));
console.log(parseInt(&quot;-z0001a2&quot;));
console.log(parseInt(&quot;+0.001a2&quot;));
console.log(parseInt(&quot;1.001a2&quot;));
console.log(parseFloat(&quot;1.001a2.30&quot;));
console.log(parseInt(070));
// 1
// 1
// -1
// NaN
// 0
// 1
// 1.001
// 56</code></pre><h3 id=""><strong>原始值转换为字符串</strong></h3><p>如果 <code>String</code> 函数不传参数，返回空字符串，如果有参数，调用 <code>ToString(value)</code>，而 <code>ToString</code> 也给了一个对应的结果表。</p><table><thead><tr><th>参数类型</th><th>结果</th></tr></thead><tbody><tr><td>Undefined</td><td>&quot;undefined&quot;</td></tr><tr><td>Null</td><td>&quot;null&quot;</td></tr><tr><td>Boolean</td><td>如果参数是 true，返回 &quot;true&quot;。参数为 false，返回 &quot;false&quot;</td></tr><tr><td>Number</td><td>num.toString() 括号当中可以选择进制, 默认是 10 进制</td></tr><tr><td>String</td><td>返回与之相等的值</td></tr></tbody></table><p>null 和 undefined 没有 toString()方法, 所有我们会使用 String()方法, 如果有参数会调用 toString(), 如果参数是 null 和 undefined 返回字符串, 相当于是 toString()的增强版 -- 注意这里的 toString()和<code>ToString(value)</code> 不一样, 一个是对外暴露的, 一个是不对外暴露的顶层规范</p><ul><li>如果 <code>String(对象类型)</code>那么会调用 <code>toPrimitive(input, String)</code>方法,返回一个基本类型的值, 然后在通过<code>ToString</code>对基本类型进行转换</li></ul><h3 id=""><strong>原始值转换为对象</strong></h3><p>可以通过构造函数 String(), Boolean(), 来将原始值转换为包装对象</p><h3 id=""><strong>对象转化为字符串</strong></h3><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">console.log(Array.prototype.toString === [1, 1].toString, &quot;布尔值&quot;); // true</code></pre><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">({ a: 1 }.toString === Object.prototype.toString); // true</code></pre><p>每个类型在调用自己身上的 <code>toString</code> 方法时的一些规则</p><h3 id="tostring"><strong>toString()</strong></h3><p>从上述代码我们可以看出来,当调用对象的 <code>toString</code> 方法的时候, 他会调用对应对象上的 <code>toString</code> 方法。</p><ul><li>数组的 <code>toString</code> 方法将每个数组元素转换成一个字符串，并在元素之间添加逗号后合并成结果字符串。</li><li>函数的 <code>toString</code> 方法返回源代码字符串。</li><li>日期的 <code>toString</code> 方法返回一个可读的日期和时间字符串。</li><li>RegExp 的 <code>toString</code> 方法返回一个表示正则表达式直接量的字符串。</li><li>如果 <code>String(对象类型)</code>那么会调用 <code>toPrimitive(input, String)</code>方法,返回一个基本类型的值, 然后在通过<code>ToString</code>对基本类型进行转换</li></ul><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">console.log(function a() {}.toString());
console.log({ a: &quot;ryan&quot; }.toString());
console.log([1, 3, 34].toString());
console.log(new Date().toString());
console.log(/^a$/.toString());
//function a() {}
//[object Object]
//&#x27;1,3,34&#x27;
//Sat Jan 08 2022 12:52:09 GMT+0800 (中国标准时间)
//^a$/</code></pre><h3 id="valueof"><strong>valueOf</strong></h3><ul><li>另一个转换对象的函数是 valueOf，表示对象的原始值。默认的 valueOf 方法返回这个对象本身，数组、函数、正则简单的继承了这个默认方法，也会返回对象本身。日期是一个例外，它会返回它的一个内容表示: 1970 年 1 月 1 日以来的毫秒数。</li></ul><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">console.log(function a() {}.valueOf());
console.log({ a: &quot;ryan&quot; }.valueOf());
console.log([1, 3, 34].valueOf());
console.log(new Date().valueOf());
console.log(/^a$/.valueOf());
// [Function: a]
// { a: &#x27;ryan&#x27; }
// [ 1, 3, 34 ]
// 1641620212097
// /^a$/</code></pre><h3 id=""><strong>对象转换为数字</strong></h3><ul><li>如果 <code>Number(对象类型)</code>那么会调用 <code>toPrimitive(input, Number)</code>方法,返回一个基本类型的值, 然后在通过<code>ToNumber</code>对基本类型进行转换</li></ul><h3 id="toprimitive"><strong>ToPrimitive()</strong></h3><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">ToPrimitive(input[, PreferredType]) []表示可选参数</code></pre><ul><li>第一个参数是 <code>input</code>，表示要处理的输入值。</li><li>第二个参数是 <code>PreferredType</code>，非必填，表示希望转换成的类型，有两个值可以选，<code>Number</code> 或者 <code>String</code>。</li><li>当不传入 <code>PreferredType</code> 时，如果 <code>input</code>是日期类型，相当于传入 <code>String</code>，否则，都相当于传入 <code>Number</code>。</li><li><strong>如果传入的 <code>input</code> 是 <code>Undefined</code>、<code>Null</code>、<code>Boolean</code>、<code>Number</code>、<code>String</code> 类型，直接返回该值。// 如果是原始值直接返回</strong></li></ul><h3 id="toprimitiveobj-number"><strong>ToPrimitive(obj, Number)</strong></h3><ul><li>如果是 ToPrimitive(obj, Number)，处理步骤如下：
<ol start="1"><li>如果 obj 为 基本类型，直接返回</li><li>否则，调用 valueOf 方法，如果返回一个原始值，则 JavaScript 将其返回。</li><li>否则，调用 toString 方法，如果返回一个原始值，则 JavaScript 将其返回。</li><li>否则，JavaScript 抛出一个类型错误异常。</li></ol></li></ul><h3 id="toprimitiveobj-string"><strong>ToPrimitive(obj, String)</strong></h3><ul><li>如果是 ToPrimitive(obj, String)，处理步骤如下：
<ol start="1"><li>如果 obj 为 基本类型，直接返回</li><li>否则，调用 toString 方法，如果返回一个原始值，则 JavaScript 将其返回。</li><li>否则，调用 valueOf 方法，如果返回一个原始值，则 JavaScript 将其返回。</li><li>否则，JavaScript 抛出一个类型错误异常。</li></ol></li></ul><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">console.log(Number({})); // NaN
console.log(Number({ a: 1 })); // NaN

console.log(Number([])); // 0
console.log(Number([0])); // 0
console.log(Number([1, 2, 3])); // NaN
console.log(
  Number(function () {
    var a = 1;
  })
); // NaN
console.log(Number(/\d+/g)); // NaN
console.log(Number(new Date(2010, 0, 1))); // 1262275200000
console.log(Number(new Error(&quot;a&quot;))); // NaN</code></pre><h3 id="jsontostringify"><strong>JSON.toStringify()</strong></h3><ul><li>处理基本类型时，与使用 <code>toString</code> 基本相同，结果都是字符串，除了 <code>undefined</code></li><li>布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。</li><li><code>undefined</code>、任意的函数以及 <code>symbol</code> 值，在序列化过程中会被忽略（出现在非数组对象的属性值中时）或者被转换成 null（出现在数组中时）。</li></ul><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">// 在对象当中和在数组当中的不同表现
JSON.stringify({ x: undefined, y: Object, z: Symbol(&quot;&quot;) });
// &quot;{}&quot;
JSON.stringify([undefined, Object, Symbol(&quot;&quot;)]);
// &quot;[null,null,null]&quot;</code></pre><ul><li><code>JSON.stringify</code> 有第二个参数 <code>replacer</code>，它可以是数组或者函数，用来指定对象序列化过程中哪些属性应该被处理，哪些应该被排除。</li></ul><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">//数组和函数当中的不同体现
function replacer(key, value) {
  if (key == &quot;name&quot;) {
    return undefined;
  }
  return value;
}
JSON.toStringify({ name: &quot;ryan&quot;, age: 18 }, replacer);
var foo = { foundation: &quot;Mozilla&quot;, model: &quot;box&quot;, week: 45, transport: &quot;car&quot;, month: 7 };
console.log(JSON.stringify(foo, [&quot;week&quot;, &quot;month&quot;]));
// {&quot;week&quot;:45,&quot;month&quot;:7}</code></pre><ul><li>如果一个被序列化的对象拥有 <code>toJSON 方法</code>，那么该 <code>toJSON</code> 方法就会覆盖该对象默认的序列化行为：不是那个对象被序列化，而是调用 <code>toJSON</code> 方法后的返回值会被序列化，例如：</li></ul><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">// toJSON返回值被序列化
var obj = {
  foo: &quot;foo&quot;,
  toJSON: function () {
    return &quot;bar&quot;;
  },
};
JSON.stringify(obj); // &#x27;&quot;bar&quot;&#x27;
JSON.stringify({ x: obj }); // &#x27;{&quot;x&quot;:&quot;bar&quot;}&#x27;</code></pre><h3 id=""><strong>隐式转换</strong></h3><h3 id=""><strong>一元操作符</strong></h3><ul><li>当一元操作符 + 操作的是原始值 <mark class="rounded-md"><span class="px-1"></span></mark> Number(原始值)</li><li>当一元操作符 + 操作的是引用类型 →  ToPrimitive(引用值, Number) -&gt; 根据 ToNumber 规范求值</li></ul><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">console.log(+[&quot;1&quot;]); // 1
console.log(+[&quot;1&quot;, &quot;2&quot;, &quot;3&quot;]); // NaN
console.log(+{}); // NaN
console.log(+&quot;1&quot;); // 1</code></pre><h3 id=""><strong>二元操作符</strong></h3><p>当计算 value1 + value2 时：</p><ol start="1"><li>0lprim = ToPrimitive(value1)</li><li>rprim = ToPrimitive(value2)</li><li>如果 lprim 是字符串或者 rprim 是字符串或者都是字符串，那么返回 ToString(lprim) 和 ToString(rprim)的拼接结果</li><li>要是不满足那么就返回 ToNumber(lprim) 和 ToNumber(rprim)的运算结果</li></ol><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">console.log(null + 1); // 1
console.log([] + []); // &#x27;&#x27;
console.log([] + {}); // &#x27;[object Object]&#x27;
console.log(1 + true); // 2
console.log({} + {}); // &#x27;[object Object][object Object]&#x27;
console.log(new Date(2017, 04, 21) + 1); // 这个知道是数字还是字符串类型就行</code></pre><h3 id="x--y-"><strong>x == y 相等</strong></h3><ul><li>当 x 和 y 是同一类型的时候:
  <ul><li>x 和 y 是 null 和 undefined 时返回 true</li><li>x 和 y 是字符串的时候, 只有字符串完全相同的时候返回 true, 否则返回 false</li><li>x 和 y 是数字的时候, 两个 NaN 在比较的时候, 返回 false, 数字相等的时候返回 true, +0 0 -0 之间的比较是 true 其他是 false</li><li>x 和 y 是对象的时候, 指向同一对象的时候返回 false</li><li>x 和 y 是布尔值的时候, 都是 false 或者都是 true 的时候返回 true 其他返回 false</li></ul></li><li>当 x 和 y 是不同的类型的时候
  <ul><li>x 和 y 一方是 null 另一方是 undefined 的时候返回 true</li><li>x 和 y 有一方是字符串另一方是数字的时候, 会则会 ToNumber(字符串)来进行比较</li><li>x 和 y 有一方是布尔值的时候, 将布尔值转换为数字进行比较</li><li>x 和 y 有一方是数字或者是字符串的时候, 另一方是对象的时候. 会 Toprimitive(对象)进行比较</li><li>其他返回 false</li></ul></li></ul><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">console.log(false == &quot;0&quot;);
console.log(false == 0);
console.log(false == &quot;&quot;);

console.log(&quot;&quot; == 0);
console.log(&quot;&quot; == []);

console.log([] == 0);

console.log(&quot;&quot; == [null]);
console.log(0 == &quot;\n&quot;);
console.log([] == 0);
// 都是false要值得注意的是 [null | undefined].toString() 结果是 &#x27;&#x27;[null, null, null, undefined].toString() j结果是 &#x27;,,,&#x27;</code></pre><h3 id=""><strong>对于<code>&lt;</code>和<code>&gt;</code>比较符</strong></h3><p><strong>如果两边都是字符串，则比较字母表顺序：</strong></p><p><code>&#x27;ca&#x27; &lt; &#x27;bd&#x27; // false</code></p><p><code>&#x27;a&#x27; &lt; &#x27;b&#x27; // true</code></p><p><strong>其他情况下，转换为数字再比较：</strong></p><p><code>&#x27;12&#x27; &lt; 13 // true</code></p><p><code>false &gt; -1 // true</code></p><p>而引用类型会被<code>ToPrimitive(obj, Number)</code>转换为基本类型再进行转换：</p><p><code>var a = {}</code></p><p><code>a &gt; 2 // false</code></p><p><code>NaN 和数字比，比不出来结果</code></p><h3 id=""><strong>类型相关面试题</strong></h3><h3 id="-0102---03"><strong>为什么 0.1+0.2 ! == 0.3？如何解决？</strong></h3><p>计算机运算器在做加和的时候，是将 10 进制的 0.1 + 0.2 转换为 2 进制的 0.1 + 0.2 进行加和。由于 0.1 和 0.2 在转换为二进制的时候会无限循环 (0.1转换为二进制，乘以二发现永远都乘不到整数)，且 <code>JavaScript Number</code>类型的实现遵循 <code>IEEE 754</code>标准：1 位存符号位 <code>sign</code> ，11 位存指数位 <code>exponent</code> ，52 位使用原码来存储尾数位<code>Fraction</code>。所以 二进制的 0.1 和 0.2 在 52 位之后二进制位会被截掉，即在保存的时候就已经出现了精度的丢失。最后，精度丢失的两个二进制数经过对阶、尾数运算、规格化、舍入处理相加之后再转变为 10 进制也就出现了<strong>0.1 + 0.2 = 0.30000000000000004440892098500626</strong>的结果。</p><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">let n1 = 0.1, n2 = 0.2
console.log(n1 + n2)  // 0.30000000000000004
(n1 + n2).toFixed(2) // 注意，toFixed为四舍五入</code></pre><pre class="language-jsx lang-jsx"><code class="language-jsx lang-jsx">//利用误差属性
function numberepsilon(arg1,arg2){
  return Math.abs(arg1 - arg2) &lt; Number.EPSILON;
}

console.log(numberepsilon(0.1 + 0.2, 0.3)); // true</code></pre><h3 id="-----"><strong>|| 和 &amp;&amp; 和 ？？ 操作符的返回值？</strong></h3><p><strong>|| 和 &amp;&amp; 首先会对第一个操作数执行条件判断，如果其不是布尔值就先强制转换为布尔类型，然后再执行条件判断。</strong></p><ul><li>对于 || 来说，如果条件判断结果为 true 就返回第一个操作数的值，如果为 false 就返回第二个操作数的值。做变量赋值。</li><li>&amp;&amp; 则相反，如果条件判断结果为 true 就返回第二个操作数的值，如果为 false 就返回第一个操作数的值。做变量渲染。</li><li>?? 控制合并是一个逻辑运算符，当左侧的操作数为 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/null"><code>null</code></a> 或者 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/undefined"><code>undefined</code></a> 时，返回其右侧操作数，否则返回左侧操作数。</li></ul><p><strong>|| 和 &amp;&amp; 返回它们其中一个操作数的值，而非条件判断的结果</strong></p><h3 id="----objectis"><strong><mark class="rounded-md"><span class="px-1">= 和 </span></mark> 与 Object.is()的区别</strong></h3><ul><li>对于 <code>==</code>（双等号） 来说，在隐式转换当中的例子当中我们也可以看到，如果 <code>==</code> 两边的类型不一致会先进行类型转换之后再比较，比如有布尔值就将布尔值转换为数字再进行比较，有字符串和数字就将字符串转换为数字再进行比较，</li><li>对于<code>===</code> (三等号) 来说，如果出现两边类型不一致的情况直接返回 false.</li><li><code>Object.is（）</code>是再三等号判断的基础上进行增强和完善，处理了特殊情况，比如在<code>Object.is()</code>中<code>0</code> 和 <code>+0</code> 不相等，两个 <code>NaN</code> 是相等的。这在三等号和双等号中 <code>0</code> 和 <code>+0</code> 相等，两个 <code>NaN</code> 不相等。</li></ul></div><p style="text-align:right"><a href="https://benlifec.com/posts/frontend/js_pre#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://benlifec.com/posts/frontend/js_pre</link><guid isPermaLink="true">https://benlifec.com/posts/frontend/js_pre</guid><dc:creator><![CDATA[benjamin]]></dc:creator><pubDate>Mon, 24 Mar 2025 02:10:45 GMT</pubDate></item><item><title><![CDATA[[Go] 我在工作中总结的 Golang 高性能编程（下）]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://benlifec.com/posts/golang/gohp_n">https://benlifec.com/posts/golang/gohp_n</a></blockquote><div><h2 id="">退出协程避免协程泄漏</h2><p><strong>协程泄漏原因一：超时控制</strong></p><p>超时控制在网络编程中是非常常见的，利用 <code>context.WithTimeout</code> 和 <code>time.After</code> 都能够很轻易地实现。下面代码是一个典型的实现超时的例子。</p><p>下面代码的问题是，<code>done</code> 是一个无缓冲 <code>channel</code> , 当走到超时逻辑的时候，<code>timeout</code> 函数退出，<code>done</code> 没有了接收方，导致 <code>doBadthing</code> 函数在发送的时候会一直阻塞。无法退出 <code>goroutine</code>。</p><pre class="language-go lang-go"><code class="language-go lang-go">func doBadthing(done chan bool) {
    time.Sleep(time.Second)
    done &lt;- true
}

func timeout(f func(chan bool)) error {
    done := make(chan bool)
    go f(done)
    select {
    case &lt;-done:
        fmt.Println(&quot;done&quot;)
        return nil
    case &lt;-time.After(time.Millisecond):
        return fmt.Errorf(&quot;timeout&quot;)
    }
}</code></pre><p>怎么解决呢？</p><p>第一种方法，创建有缓冲区的 channel, 缓冲区设置为 1，即使没有接收方，发送方也不会发生阻塞。</p><p>第二种方法，使用 select + defalt 尝试发送，如果发送失败，则说明缺少接收者(receiver)，即超时了，那么走 default 逻辑直接退出即可。</p><pre class="language-go lang-go"><code class="language-go lang-go">func doGoodthing(done chan bool) {
    time.Sleep(time.Second)
    select {
    case done &lt;- true:
    default:
        return
    }
}</code></pre><p>实际的业务中更为复杂，我们会将任务拆分为好几段，这种情况下，就只能够使用 select，而不能够设置缓冲区的方式了。</p>
<p><strong>协程泄漏原因二：channel 未关闭</strong></p><pre class="language-go lang-go"><code class="language-go lang-go">func do(taskCh chan int) {
    for {
        select {
        case t := &lt;-taskCh:
            time.Sleep(time.Millisecond)
            fmt.Printf(&quot;task %d is done\n&quot;, t)
        }
    }
}

func sendTasks() {
    taskCh := make(chan int, 10)
    go do(taskCh)
    for i := 0; i &lt; 1000; i++ {
        taskCh &lt;- i
    }
}</code></pre><p>sendTasks 中启动了一个子协程 go do(taskCh)，因为这个协程一直处于阻塞状态，等待接收任务，因此直到程序结束, 协程也没有释放</p><p>解决办法：</p><ul><li>任务发送结束之后，使用 close(taskCh) 将 channel taskCh 关闭</li><li>t, beforeClosed := &lt;-taskCh 判断 channel 是否已经关闭, 如果关闭，退出协程</li></ul><pre class="language-go lang-go"><code class="language-go lang-go">func doCheckClose(taskCh chan int) {
    for {
        select {
        case t, beforeClosed := &lt;-taskCh:
            if !beforeClosed {
                fmt.Println(&quot;taskCh has been closed&quot;)
                return
            }
            time.Sleep(time.Millisecond)
            fmt.Printf(&quot;task %d is done\n&quot;, t)
        }
    }
}

func sendTasksCheckClose() {
    taskCh := make(chan int, 10)
    go doCheckClose(taskCh)
    for i := 0; i &lt; 1000; i++ {
        taskCh &lt;- i
    }
    close(taskCh)
}</code></pre><p>这里提一嘴，关闭 chanel 的时候，可以使用 <code>sync.Once</code> 或互斥锁(<code>sync.Mutex</code>)确保 <code>channel</code> 只被关闭一次</p><pre class="language-go lang-go"><code class="language-go lang-go">type MyChannel struct {
    C    chan T
    once sync.Once
}

func NewMyChannel() *MyChannel {
    return &amp;MyChannel{C: make(chan T)}
}

func (mc *MyChannel) SafeClose() {
    mc.once.Do(func() {
        close(mc.C)
    })
}</code></pre><p>还有一种粗暴，但不推荐的关闭方式，可以无限关闭，但是有通过 recover 使程序恢复正常。</p><pre class="language-go lang-go"><code class="language-go lang-go">func SafeClose(ch chan T) (justClosed bool) {
    defer func() {
        if recover() != nil {
            // 一个函数的返回结果可以在defer调用中修改。
            justClosed = false
        }
    }()

    // 假设ch != nil。
    close(ch)   // 如果 ch 已关闭，将 panic
    return true // &lt;=&gt; justClosed = true; return
}</code></pre><h2 id="">控制协程的并发数量</h2><p>为什么要控制协程的并发数量，简单来说就是系统资源有限，每个协程至少需要消耗 2KB 的空间，那么假设计算机的内存是 2GB，那么至多允许 2GB/2KB = 1M 个协程同时存在。当然不止是内存有限，文件句柄，栈资源也是有限的，系统资源奔溃的时候，会出现以下但不限于的下面的错误信息。</p><pre class="language-go lang-go"><code class="language-go lang-go">// too many open files
// out of memory
// panic: too many concurrent operations on a single file or socket (max 1048575)</code></pre><p>怎么控制：</p><p>方式一：利用缓冲区 <code>channel</code> 实现</p><pre class="language-go lang-go"><code class="language-go lang-go">// main_chan.go
func main() {
    var wg sync.WaitGroup
    ch := make(chan struct{}, 3)
    for i := 0; i &lt; 10; i++ {
        ch &lt;- struct{}{}
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            log.Println(i)
            time.Sleep(time.Second)
            &lt;-ch
        }(i)
    }
    wg.Wait()
}</code></pre><p>方式二： <code>利用第三方库</code></p><ul><li>ants</li><li>tunny</li></ul><p>方式三：如果控制了，但还是资源不足，可以使用 ulimit 来调整系统参数，比如： <code>ulimit -n 999999</code>，将同时打开的文件句柄数量调整为 999999 来解决这个问题，其他的参数也可以按需调整</p><h2 id="syncpool">sync.Pool</h2><p>为什需要 <code>Pool</code>, 考虑下面的场景，<code>json</code> 的反序列化在文本解析和网络通信过程中非常常见，当程序并发度非常高的情况下，短时间内需要创建大量的临时对象。而这些对象是都是分配在堆上的，会给 <code>GC</code> 造成很大压力，严重影响程序的性能</p><p><code>Pool</code> 可以保存和复用临时对象，减少内存分配，降低 GC 压力</p><p>Put 归还操作的时候，记得将值进行重置，不然下一次的 Get 操作，还会得到未进行重置的值</p><pre class="language-go lang-go"><code class="language-go lang-go">var studentPool = sync.Pool{
    New: func() interface{} { 
        return new(Student) 
    },
}

stu := studentPool.Get().(*Student)
json.Unmarshal(buf, stu)
studentPool.Put(stu)</code></pre><h2 id="synconce">sync.Once</h2><p><code>sync.Once</code> 是 <code>Go</code> 标准库提供的使函数只执行一次的实现，常应用于单例模式，和 <code>init</code> 有什么区别呢？</p><p><code>sync.Once</code> 可以在代码的任意位置初始化和调用，因此可以延迟到使用时再执行，并发场景下是线程安全的, 而 init 函数是当所在的 package 首次被加载时执行，若迟迟未被使用，则既浪费了内存，又延长了程序加载时间。</p>
<h2 id="synccond">sync.Cond</h2><p><code>sync.Cond</code> 经常用在多个 <code>goroutine</code> 等待，一个 <code>goroutine</code> 通知（事件发生）的场景。如果是一个通知，一个等待，使用互斥锁或 <code>channel</code>就能搞定了。</p><p>下面是一个简单的例子，三个协程调用 <code>Wait()</code> 等待，另一个协程调用 <code>Broadcast()</code> 唤醒所有等待的协程</p><pre class="language-go lang-go"><code class="language-go lang-go">var done = false

func read(name string, c *sync.Cond) {
    c.L.Lock()
    for !done {
        c.Wait()
    }
    log.Println(name, &quot;starts reading&quot;)
    c.L.Unlock()
}

func write(name string, c *sync.Cond) {
    log.Println(name, &quot;starts writing&quot;)
    time.Sleep(time.Second)
    c.L.Lock()
    done = true
    c.L.Unlock()
    log.Println(name, &quot;wakes all&quot;)
    c.Broadcast()
}

func main() {
    cond := sync.NewCond(&amp;sync.Mutex{})

    go read(&quot;reader1&quot;, cond)
    go read(&quot;reader2&quot;, cond)
    go read(&quot;reader3&quot;, cond)
    write(&quot;writer&quot;, cond)

    time.Sleep(time.Second * 3)
}</code></pre><h2 id="">编译优化</h2><p>如何减少 <code>go</code> 程序编译后的体积, 观察下面的 build 代码。</p><pre class="language-go lang-go"><code class="language-go lang-go">go build -ldflags=&quot;-s -w&quot; -o server main.go</code></pre><p><code>-s</code> 忽略符号表和调试信息</p><p><code>-w</code> 忽略DWARFv3调试信息，使用该选项后将无法使用gdb进行调试</p>
<h2 id="">逃逸分析</h2><p>在 C 语言中，可以使用 malloc 和 free 手动在堆上分配和回收内存。Go 语言中，堆内存是通过垃圾回收机制自动管理的，无需开发者指定。那么，Go 编译器怎么知道某个变量需要分配在栈上，还是堆上呢？编译器决定内存分配位置的方式，就称之为逃逸分析(escape analysis)。逃逸分析由编译器完成，作用于编译阶段。</p><p>什么情况会逃逸？</p><p>情况一：指针逃逸</p><p>指针逃逸应该是最容易理解的一种情况了，即在函数中创建了一个对象，返回了这个对象的指针。这种情况下，函数虽然退出了，但是因为指针的存在，对象的内存不能随着函数结束而回收，因此只能分配在堆上。</p><pre class="language-go lang-go"><code class="language-go lang-go">// main_pointer.go
package main

import &quot;fmt&quot;

type Demo struct {
    name string
}

func createDemo(name string) *Demo {
    d := new(Demo) // 局部变量 d 逃逸到堆
    d.name = name
    return d
}

func main() {
    demo := createDemo(&quot;demo&quot;)
    fmt.Println(demo)
}</code></pre><p>情况二：interface{} 动态类型逃逸</p><p>在 Go 语言中，空接口即 interface{} 可以表示任意的类型，如果函数参数为 interface{}，编译期间很难确定其参数的具体类型，也会发生逃逸。</p><p>情况二：栈空间不足</p><p>操作系统对内核线程使用的栈空间是有大小限制的，64 位系统上通常是 8 MB。可以使用 ulimit -a 命令查看机器上栈允许占用的内存的大小。</p><p>对 Go 编译器而言，超过一定大小的局部变量将逃逸到堆上, 比如切片占用内存超过一定大小，或无法确定当前切片长度时，对象占用内存将在堆上分配</p>
<p>情况三：闭包</p><p>Increase() 返回值是一个闭包函数，该闭包函数访问了外部变量 n，那变量 n 将会一直存在，直到 in 被销毁。很显然，变量 n 占用的内存不能随着函数 Increase() 的退出而回收，因此将会逃逸到堆上。</p><pre class="language-go lang-go"><code class="language-go lang-go">func Increase() func() int {
    n := 0
    return func() int {
        n++
        return n
    }
}

func main() {
    in := Increase()
    fmt.Println(in()) // 1
    fmt.Println(in()) // 2
}</code></pre><p>那么如何利用逃逸分析提升性能呢？</p><p>一般情况下，对于需要修改原对象值，或占用内存比较大的结构体，选择传指针。对于只读的占用内存较小的结构体，直接传值能够获得更好的性能。虽然传指针可以减少值的拷贝，但是会导致内存分配逃逸到堆中，增加垃圾回收(GC)的负担。在对象频繁创建和删除的场景下，传递指针导致的 GC 开销可能会严重影响性能。</p><p>总结源自：<a href="https://geektutu.com/">极客兔兔</a>， 以及结合了自己在工作当中的实战经验。</p></div><p style="text-align:right"><a href="https://benlifec.com/posts/golang/gohp_n#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://benlifec.com/posts/golang/gohp_n</link><guid isPermaLink="true">https://benlifec.com/posts/golang/gohp_n</guid><dc:creator><![CDATA[benjamin]]></dc:creator><pubDate>Tue, 04 Mar 2025 09:47:33 GMT</pubDate></item><item><title><![CDATA[[Go] 我在工作中总结的 Golang 高性能编程（上）]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://benlifec.com/posts/golang/gohp_p">https://benlifec.com/posts/golang/gohp_p</a></blockquote><div><h2 id="struct">struct{}</h2><p>空结构体 <code>struct{}</code> 实例不占据任何的内存空间, 可以使用 <code>unsafe.Sizeof</code> 计算出一个数据类型实例需要占用的字节数，因为空结构体不占据内存空间，因此被广泛作为各种场景下的占位符使用。</p><p>有时候使用 channel 不需要发送任何的数据，只用来通知子协程执行/退出任务，或只用来控制协程并发度，这种情况下，声明为空结构体类型的 <code>channel</code> 是合适的。</p><p>第一种 <code>struct{}</code> 场景：通知子协程执行任务：</p><pre class="language-go lang-go"><code class="language-go lang-go">func worker(ch chan struct{}) {
    &lt;-ch
    fmt.Println(&quot;do something&quot;)
    close(ch)
}

func main() {
    ch := make(chan struct{})
    go worker(ch)
    ch &lt;- struct{}{}
}</code></pre><p>第二种 <code>struct{}</code> 场景：结构体只包含方法，不包含任何的字段。例如上面例子中的 Door，在这种情况下，声明为空结构体是最合适的。</p><pre class="language-go lang-go"><code class="language-go lang-go">type Door struct{}

func (d Door) Open() {
    fmt.Println(&quot;Open the door&quot;)
}

func (d Door) Close() {
    fmt.Println(&quot;Close the door&quot;)
}</code></pre><p>第三种 <code>struct{}</code> 场景：map 作为集合(Set)使用时，可以将值类型定义为空结构体，仅作为占位符使用即可, 这样做可以节省内存。</p><pre class="language-go lang-go"><code class="language-go lang-go">type Set map[string]struct{}

func (s Set) Has(key string) bool {
    _, ok := s[key]
    return ok
}

func (s Set) Add(key string) {
    s[key] = struct{}{}
}

func (s Set) Delete(key string) {
    delete(s, key)
}

func main() {
    s := make(Set)
    s.Add(&quot;Tom&quot;)
    s.Add(&quot;Sam&quot;)
    fmt.Println(s.Has(&quot;Tom&quot;))
    fmt.Println(s.Has(&quot;Jack&quot;))
}</code></pre><h2 id="stringsbuilder">strings.Builder</h2><p>在 Go 语言中，拼接字符串事实上是创建了一个新的字符串对象。如果代码中存在大量的字符串拼接，对性能会产生严重的影响。</p><p>结论：不建议直接使用 <code>+</code> 和 <code>fmt.Sprintf</code> 做字符串拼接，在基准测试中，生成了一个长度为 10 的字符串，并拼接 1w 次，使用 + 和 fmt.Sprintf 的效率是最低的，和其余的方式(<code>strings.Builder</code>,  <code>[]byte</code> ,  <code>bytes.Buffer</code> )相比，性能相差约 1000 倍，而且消耗了超过 1000 倍的内存。</p><p>建议直接使用 <code>strings.Builder</code> 拼接字符串</p><pre class="language-go lang-go"><code class="language-go lang-go">func builderConcat (n int, str string) string {
  var builder strings.Builder

  builder.Grow(n * len(str))

  for i := n; i &lt; n; i++ {
    builder.WriteString(str)
  }

  return builder.String()
}</code></pre><p>对于 <code>[]byte</code>  和 <code>bytes.Buffer</code> 来说 ,  <code>strings.Builder</code> 省去了 []byte 和字符串(string) 之间的转换,  直接将底层的 []byte 转换成了字符串类型返回了回来。</p><pre class="language-go lang-go"><code class="language-go lang-go">// 直接将底层的 []byte 转换成了字符串类型返回
func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&amp;b.buf))
}</code></pre><p><code>[] bytes</code> 拼接</p><pre class="language-go lang-go"><code class="language-go lang-go">func byteConcat(n int, str string) string {
    buf := make([]byte, 0)
    for i := 0; i &lt; n; i++ {
        buf = append(buf, str...)
    }
    return string(buf)
}</code></pre><p><code>bytes.Buffer</code> 拼接</p><pre class="language-go lang-go"><code class="language-go lang-go">func bufferConcat(n int, s string) string {
    buf := new(bytes.Buffer)
    for i := 0; i &lt; n; i++ {
        buf.WriteString(s)
    }
    return buf.String()
}</code></pre><h2 id="slice">slice</h2><p>先聊聊数组，在 C 语言中，数组变量是指向第一个元素的指针，但是 Go 语言中并不是。Go 语言中，数组变量属于值类型(value type)，因此当一个数组变量被赋值或者传递时，实际上会复制整个数组。例如，将 a 赋值给 b，修改 a 中的元素并不会改变 b 中的元素,  通常为了避免复制数组，一般会传递指向数组的指针。</p><pre class="language-go lang-go"><code class="language-go lang-go">a := [...]int{1, 2, 3} // ... 会自动计算数组长度
b := a
a[0] = 100
fmt.Println(a, b) // [100 2 3] [1 2 3]</code></pre><p><strong><code>slice 的性能陷阱</code></strong></p><p>大量内存得不到释放， 由于在已有切片的基础上进行切片，不会创建新的底层数组。因为原来的底层数组没有发生变化，内存会一直占用，直到没有变量引用该数组。因此很可能出现这么一种情况，原切片由大量的元素构成，但是我们在原切片的基础上切片，虽然只使用了很小一段，但底层数组在内存中仍然占据了大量空间，得不到释放。比较推荐的做法，使用 copy 替代 re-slice。</p><pre class="language-go lang-go"><code class="language-go lang-go">// good
// 通过 copy，指向了一个新的底层数组，当 origin 不再被引用后，内存会被垃圾回收(garbage collector, GC)。
func lastNumsByCopy(origin []int) []int {
    result := make([]int, 2)
    copy(result, origin[len(origin)-2:])
    return result
}

// bad
func lastNumsBySlice(origin []int) []int {
    return origin[len(origin)-2:]
}</code></pre><p>下面是常用的 <code>slice CURD</code> 操作:</p><p><strong><code>slice copy</code></strong></p><p>将 a slice 完整的 copy 到 b slice 下面三种方法都可以：</p><pre class="language-go lang-go"><code class="language-go lang-go">b = make([]T, len(a))
copy(b, a)</code></pre><pre class="language-go lang-go"><code class="language-go lang-go">b = append([]T(nil), a...)</code></pre><pre class="language-go lang-go"><code class="language-go lang-go">b = append(a[:0:0], a...)</code></pre><p><strong><code>slice append</code></strong></p><p>切片有三个属性，指针(ptr)、长度(len) 和容量(cap)。append 时有两种场景：</p><ul><li>当 append 之后的长度小于等于 cap，将会直接利用原底层数组剩余的空间</li><li>当 append 后的长度大于 cap 时，则会分配一块更大的区域来容纳新的底层数组</li></ul><p>因此，为了避免内存发生拷贝，如果能够知道最终的切片的大小，预先设置 cap 的值能够获得最好的性能</p><pre class="language-go lang-go"><code class="language-go lang-go">a = append(a, b...)</code></pre><p><strong><code>slice delete</code></strong></p><p>切片的底层是数组，因此删除意味着后面的元素需要逐个向前移位。每次删除的复杂度为 O(N)，因此切片不合适大量随机删除的场景，这种场景下适合使用链表。</p><pre class="language-go lang-go"><code class="language-go lang-go">a = append(a[:i], a[i+1:]...)</code></pre><pre class="language-go lang-go"><code class="language-go lang-go">a = [:i + copy(a[i:], a[i+1:])]</code></pre><pre class="language-go lang-go"><code class="language-go lang-go">下面这种方式,删除指定元素后，将空余的位置置空，有助于垃圾回收(GC)
copy(a[i:], a[i+1:])

a[len(a) - 1] = nil
a = a[:len(a) -1]</code></pre><p><strong><code>slice insert</code></strong></p><p>insert 和 append 类似。即在某个位置添加一个元素后，将该位置后面的元素再 append 回去。复杂度为 O(N)。因此，不适合大量随机插入的场景，这种场景下适合使用链表。</p><pre class="language-go lang-go"><code class="language-go lang-go">a = append(a[:i], append([]T{x}, a[i:]....)...)</code></pre><p><strong><code>slice filter</code></strong></p><p>当原切片不会再被使用时，就地 filter 方式是比较推荐的，可以节省内存空间</p><pre class="language-go lang-go"><code class="language-go lang-go">n := 0

for _, x := range a {
  if keep(a) {
    a[n] = x
    n++
  }
}

a = a[:n]</code></pre><p><strong><code>slice push</code></strong></p><ul><li>在末尾追加元素，不考虑内存拷贝的情况，复杂度为 O(1)</li><li><p>在头部追加元素，时间和空间复杂度均为 O(N)，不推荐</p><pre class="language-go lang-go"><code class="language-go lang-go">// 尾部追加
a = append(a, x)</code></pre></li></ul><pre class="language-go lang-go"><code class="language-go lang-go">// 头部追加
a = append([]T{x}, a...)</code></pre><p><strong><code>slice pop</code></strong></p><ul><li>尾部删除元素，复杂度 O(1)</li><li>头部删除元素，如果使用切片方式，复杂度为O(1)。但是需要注意的是，底层数组没有发生改变，第 0 个位置的内存仍旧没有释放。如果有大量这样的操作，头部的内存会一直被占用</li></ul><pre class="language-go lang-go"><code class="language-go lang-go">// 尾部删除
x, a := a[len(a) - 1], a[:len(a) - 1]</code></pre><pre class="language-go lang-go"><code class="language-go lang-go">// 头部删除
x, a := a[0], a[1:]</code></pre><h2 id="for--for-range">for | for range</h2><ul><li>for range 在迭代过程中返回的是迭代值的拷贝</li><li>如果每次迭代的元素的内存占用很低(例如：[]int)，那么 for 和 range 的性能几乎是一样</li><li>如果 for range 迭代的元素是一个包含很多属性的 struct 结构体，那么 for 的性能将显著地高于 range，有时候甚至会有上千倍的性能差异</li><li>如果想使用 range 同时迭代下标和值，则需要将切片/数组的元素改为指针，才能不影响性能</li></ul><h2 id="reflect">reflect</h2><ul><li>尽量避免使用 reflect, 如果有替代方案情况下，比如我们要对结构体进行序列化和反序列化 可以替换 go 标准库的 json 为 easyjson</li><li>FieldByName 相比于 Field 有一个数量级的性能劣化，在实际的应用中，就要避免直接调用 FieldByName。我们可以利用字典将 Name 和 Index 的映射缓存起来(map)，通过 Field 和 Index 来进行查找</li><li>Field 按照下标访问查询效率为 O(1)， FieldByName 按照 Name 访问，则需要遍历所有字段，查询效率为 O(N)。结构体所包含的字段(包括方法)越多，那么两者之间的效率差距则越大。</li></ul><blockquote><p>总结源自：<a href="https://geektutu.com/">极客兔兔</a>， 以及结合了自己在工作当中的实战经验</p></blockquote></div><p style="text-align:right"><a href="https://benlifec.com/posts/golang/gohp_p#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://benlifec.com/posts/golang/gohp_p</link><guid isPermaLink="true">https://benlifec.com/posts/golang/gohp_p</guid><dc:creator><![CDATA[benjamin]]></dc:creator><pubDate>Tue, 04 Mar 2025 09:44:23 GMT</pubDate></item><item><title><![CDATA[[Go] Golang(1.23.2) 源码走读 - Mutex | RWMutex]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://benlifec.com/posts/golang/gosource_code_mutex">https://benlifec.com/posts/golang/gosource_code_mutex</a></blockquote><div><h1 id="mutex">Mutex</h1><p>下面是Mutex 结构体和位运算常量定义：</p><ul><li>state: 32 位整数， 高 29 位存储阻塞的队列（最高 2^29-1）, 最后一位（32为）存储 mutexLocked  （是否上锁），倒数第二位存储 mutexWoken （协调唤醒操作，防止重复唤醒），倒数第三位存储 mutexStarving （是否处于饥饿模式）</li><li>sema: 通过 runtime 方法 runtime_SemacquireMutex 来阻塞/唤醒 goroutine 的信号量</li></ul><pre class="language-go lang-go"><code class="language-go lang-go">const (
    mutexLocked = 1 &lt;&lt; iota // mutex is locked // 1
    mutexWoken  // 2   
    mutexStarving  // 4 
    mutexWaiterShift = iota // 3  位运算计算队列等待数，要右移、左移的位数
    starvationThresholdNs = 1e6   // 饥饿模式等待阈值为 1ms
)

type Mutex struct {
    state int32
    sema  uint32
}</code></pre><h2 id="lock">Lock</h2><p>下面是 Lock 流程：</p><ul><li>CAS 抢锁成功返回</li><li>抢锁失败，则进入 lockSlow 流程</li></ul><pre class="language-go lang-go"><code class="language-go lang-go">func (m *Mutex) Lock() {
    // Fast path: grab unlocked mutex.
    if atomic.CompareAndSwapInt32(&amp;m.state, 0, mutexLocked) {
        return
    }
    // Slow path (outlined so that the fast path can be inlined)
    m.lockSlow()
}</code></pre><p>下面是 lockSlow 流程：</p><ul><li>如果 锁已被占用 并且 锁不为饥饿模式 并且满足自旋条件（runtime_canSpin 方法），进入自旋处理环节</li><li>在自旋处理环节中，如果当前锁有尚未唤醒的阻塞协程，则通过 CAS 操作将 state 的 mutexWoken 标识置为 1，将局部变量 awoke 置为 true</li><li>调用 runtime_doSpin 告知调度器 P 当前处于自旋模式</li><li>更新自旋次数 iter 和锁状态值 old</li><li>continue</li></ul><pre class="language-go lang-go"><code class="language-go lang-go">func (m *Mutex) lockSlow() {
    var waitStartTime int64 // 标识当前 goroutine 在抢锁过程中的等待时长，单位：ns；
    starving := false // 标识当前是否处于饥饿模式
    awoke := false  // 标记当前 goroutine 是否从阻塞中被唤醒
    iter := 0 //标识当前 goroutine 参与自旋的次数 runtime_canSpin 判断是否已经达到 go 运行时最大的自旋次数
    old := m.state // 当前状态
    for {
        // Don&#x27;t spin in starvation mode, ownership is handed off to waiters
        // so we won&#x27;t be able to acquire the mutex anyway.
        if old&amp;(mutexLocked|mutexStarving) == mutexLocked &amp;&amp; runtime_canSpin(iter) {
            // Active spinning makes sense.
            // Try to set mutexWoken flag to inform Unlock
            // to not wake other blocked goroutines.
            if !awoke &amp;&amp; old&amp;mutexWoken == 0 &amp;&amp; old&gt;&gt;mutexWaiterShift != 0 &amp;&amp;
                atomic.CompareAndSwapInt32(&amp;m.state, old, old|mutexWoken) {
                awoke = true
            }
            runtime_doSpin()
            iter++
            old = m.state
            continue
        }</code></pre><ul><li>新值 new 中置为已加锁，即尝试抢锁</li><li>旧值为已被其他 goroutine 加锁或者处于锁已经饥饿模式，可以直接令新值的阻塞协程数加1</li><li>当前为饥饿模式且旧值已加锁，则将新值置为饥饿模式</li><li>如果是唤醒后的 goroutine，将该 mutexWoken 标识更新置 0</li></ul><pre class="language-go lang-go"><code class="language-go lang-go">        new := old
        // Don&#x27;t try to acquire starving mutex, new arriving goroutines must queue.
        if old&amp;mutexStarving == 0 {
            new |= mutexLocked
        }
        if old&amp;(mutexLocked|mutexStarving) != 0 {
            new += 1 &lt;&lt; mutexWaiterShift
        }
        // The current goroutine switches mutex to starvation mode.
        // But if the mutex is currently unlocked, don&#x27;t do the switch.
        // Unlock expects that starving mutex has waiters, which will not
        // be true in this case.
        if starving &amp;&amp; old&amp;mutexLocked != 0 {
            new |= mutexStarving
        }
        if awoke {
            // The goroutine has been woken from sleep,
            // so we need to reset the flag in either case.
            if new&amp;mutexWoken == 0 {
                throw(&quot;sync: inconsistent mutex state&quot;)
            }
            new &amp;^= mutexWoken
        }</code></pre><ul><li>旧值是未加锁状态且为不为饥饿模式，说明加锁成功，返回即可</li><li>旧值中锁未释放或者处于饥饿模式，则当前 goroutine 需要进入阻塞队列挂起</li><li>queueLifo 标识当前 goroutine 是从阻塞队列被唤起的老 goroutine 还是新进流程的 goroutine</li></ul><pre class="language-go lang-go"><code class="language-go lang-go">        if atomic.CompareAndSwapInt32(&amp;m.state, old, new) {
            if old&amp;(mutexLocked|mutexStarving) == 0 {
                break // locked the mutex with CAS
            }
            // If we were already waiting before, queue at the front of the queue.
            queueLifo := waitStartTime != 0
            if waitStartTime == 0 {
                waitStartTime = runtime_nanotime()
            }
            runtime_SemacquireMutex(&amp;m.sema, queueLifo, 1</code></pre><p>下面是从阻塞态被唤醒的 goroutine 流程</p><ul><li>当前 goroutine 进入阻塞队列时间长达 1 ms，则置为饥饿模式</li><li>此时锁是饥饿模式，则当前 goroutine 无需竞争可以直接获得锁(Unlock 流程，发现是饥饿模式，会直接唤醒队头的 goroutine)</li></ul><pre class="language-go lang-go"><code class="language-go lang-go">            starving = starving || runtime_nanotime()-waitStartTime &gt; starvationThresholdNs
            old = m.state
            if old&amp;mutexStarving != 0 {
                // If this goroutine was woken and mutex is in starvation mode,
                // ownership was handed off to us but mutex is in somewhat
                // inconsistent state: mutexLocked is not set and we are still
                // accounted as waiter. Fix that.
                if old&amp;(mutexLocked|mutexWoken) != 0 || old&gt;&gt;mutexWaiterShift == 0 {
                    throw(&quot;sync: inconsistent mutex state&quot;)
                }
                delta := int32(mutexLocked - 1&lt;&lt;mutexWaiterShift)
                if !starving || old&gt;&gt;mutexWaiterShift == 1 {
                    // Exit starvation mode.
                    // Critical to do it here and consider wait time.
                    // Starvation mode is so inefficient, that two goroutines
                    // can go lock-step infinitely once they switch mutex
                    // to starvation mode.
                    delta -= mutexStarving
                }
                atomic.AddInt32(&amp;m.state, delta)
                break
            }
            awoke = true
            iter = 0
        } else {
            old = m.state
        }</code></pre><h2 id="unlock">Unlock</h2><p>下面是 <code>Unlock</code> 流程:</p><ul><li>通过原子操作解锁 ；</li><li>倘若解锁时发现，目前参与竞争的仅有自身一个 goroutine，则直接返回即可；</li><li>倘若发现锁中还有阻塞协程，则走入 unlockSlow 分支.</li></ul><pre class="language-go lang-go"><code class="language-go lang-go">func (m *Mutex) Unlock() {
    new := atomic.AddInt32(&amp;m.state, -mutexLocked)
    if new != 0 {
        m.unlockSlow(new)
    }
}</code></pre><p>解锁时，如果是非饥饿模式</p><ul><li>倘若阻塞队列内无 goroutine 或者 mutexLocked、mutexStarving、mutexWoken 标识位任一不为零，退出流程</li><li>基于 CAS 操作将 Mutex.state 中的阻塞协程数减 1，倘若成功，则唤起阻塞队列头部的 goroutine，并退出</li><li>倘若减少阻塞协程数的 CAS 操作失败，则更新此时的 Mutex.state 为新的 old 值，开启下一轮循环</li></ul><pre class="language-go lang-go"><code class="language-go lang-go">func (m *Mutex) unlockSlow(new int32) {
    if (new+mutexLocked)&amp;mutexLocked == 0 {
        fatal(&quot;sync: unlock of unlocked mutex&quot;)
    }
    if new&amp;mutexStarving == 0 {
        old := new
        for {
            // If there are no waiters or a goroutine has already
            // been woken or grabbed the lock, no need to wake anyone.
            // In starvation mode ownership is directly handed off from unlocking
            // goroutine to the next waiter. We are not part of this chain,
            // since we did not observe mutexStarving when we unlocked the mutex above.
            // So get off the way.
            if old&gt;&gt;mutexWaiterShift == 0 || old&amp;(mutexLocked|mutexWoken|mutexStarving) != 0 {
                return
            }
            // Grab the right to wake someone.
            new = (old - 1&lt;&lt;mutexWaiterShift) | mutexWoken
            if atomic.CompareAndSwapInt32(&amp;m.state, old, new) {
                runtime_Semrelease(&amp;m.sema, false, 1)
                return
            }
            old = m.state
        }
    }</code></pre><p>如果是饥饿模式解锁</p><ul><li>直接唤醒阻塞队列头部的 goroutine 即可, 其余进来抢锁的 goroutine，发现是饥饿模式，都会被排到队列的队尾。</li></ul><pre class="language-go lang-go"><code class="language-go lang-go">func (m *Mutex) unlockSlow(new int32) {
    if new&amp;mutexStarving == 0 {
    } else {
        runtime_Semrelease(&amp;m.sema, true, 1)
    }
}</code></pre><h1 id="rwmutex">RWMutex</h1><p>下面是 <code>RWMutex</code> 结构体</p><ul><li>rwmutexMaxReaders：共享读锁的 goroutine 数量上限，值为 2^29</li><li>w：RWMutex 内置的互斥锁 sync.Mutex</li><li>writerSem：写锁阻塞队列的信号量</li><li>readerSem：读锁阻塞队列的信号量</li><li>readerCount：无写锁情况下等于占用或等待读锁的 goroutine 数量，存在写锁流程时，该值一般为负数，实际值为读锁流程的 goroutine 数量减 rwmutexMaxReaders（2^29）.</li><li>readerWait：当前 goroutine 获取写锁前，还需要等待多少个 goroutine 释放读锁</li></ul><pre class="language-go lang-go"><code class="language-go lang-go">type RWMutex struct {
    w           Mutex        // held if there are pending writers
    writerSem   uint32       // semaphore for writers to wait for completing readers
    readerSem   uint32       // semaphore for readers to wait for completing writers
    readerCount atomic.Int32 // number of pending readers
    readerWait  atomic.Int32 // number of departing readers
}

const rwmutexMaxReaders = 1 &lt;&lt; 30</code></pre><h2 id="rlock">RLock</h2><p>下面是 <code>RLock</code> 流程:</p><ul><li>基于原子操作，将 RWMutex 的 readCount 变量加一，表示占用或等待读锁的 goroutine 数加一</li><li>若 RWMutex.readCount 的新值仍小于 0，说明有 goroutine 未释放写锁，因此将当前 goroutine 添加到读锁的阻塞队列中并阻塞挂起</li></ul><pre class="language-go lang-go"><code class="language-go lang-go">func (rw *RWMutex) RLock() {
    if rw.readerCount.Add(1) &lt; 0 {
        // A writer is pending, wait for it.
        runtime_SemacquireRWMutexR(&amp;rw.readerSem, false, 0)
    }
}</code></pre><h2 id="runlock">RUnlock</h2><p>下面是 <code>RUnLock</code> 流程:</p><ul><li><p>基于原子操作，将 RWMutex 的 readCount 变量加一，表示占用或等待读锁的 goroutine 数减一</p></li><li><p>readCount 的新值小于 0，说明有 goroutine 在等待获取写锁，则走入 RWMutex.rUnlockSlow 的流程中.</p></li></ul><pre class="language-go lang-go"><code class="language-go lang-go">func (rw *RWMutex) RUnlock() {
    if r := atomic.AddInt32(&amp;rw.readerCount, -1); r &lt; 0 {
        rw.rUnlockSlow(r)
    }
}</code></pre><h2 id="lock">Lock</h2><p>下面是 <code>Lock</code> 流程:</p><ul><li>对 RWMutex 内置的互斥锁进行加锁操作；</li><li>基于原子操作，对 RWMutex.readerCount 进行减少 -rwmutexMaxReaders 的操作, 目的是阻塞即将获取写锁的 goroutine</li><li>倘若此时存在未释放读锁的 gouroutine（r != 0），将当前 goroutine 添加到写锁的阻塞队列中挂起</li></ul><pre class="language-go lang-go"><code class="language-go lang-go">func (rw *RWMutex) Lock() {
    rw.w.Lock()
    r := atomic.AddInt32(&amp;rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
    if r != 0 &amp;&amp; atomic.AddInt32(&amp;rw.readerWait, r) != 0 {
        runtime_SemacquireMutex(&amp;rw.writerSem, false, 0)
    }
}</code></pre><h2 id="unlock">UnLock</h2><p>下面是 <code>UnLock</code> 流程:
<code>go
func (rw *RWMutex) Unlock() {
    r := atomic.AddInt32(&amp;rw.readerCount, rwmutexMaxReaders)
    if r &gt;= rwmutexMaxReaders {
        fatal(&quot;sync: Unlock of unlocked RWMutex&quot;)
    }
    for i := 0; i &lt; int(r); i++ {
        runtime_Semrelease(&amp;rw.readerSem, false, 0)
    }
    rw.w.Unlock()
}</code></p><ul><li>基于原子操作，将 RWMutex.readerCount 的值加上 rwmutexMaxReaders</li><li>唤醒读锁阻塞队列中的所有 goroutine</li><li>解开 RWMutex 内置的互斥锁</li></ul></div><p style="text-align:right"><a href="https://benlifec.com/posts/golang/gosource_code_mutex#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://benlifec.com/posts/golang/gosource_code_mutex</link><guid isPermaLink="true">https://benlifec.com/posts/golang/gosource_code_mutex</guid><dc:creator><![CDATA[benjamin]]></dc:creator><pubDate>Fri, 28 Feb 2025 12:15:06 GMT</pubDate></item><item><title><![CDATA[[Go] Golang(1.23.2) 源码走读 - Context]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://benlifec.com/posts/golang/gosource_context">https://benlifec.com/posts/golang/gosource_context</a></blockquote><div><h1 id="context">Context</h1><p><code>context</code> 数据结构</p><pre class="language-go lang-go"><code class="language-go lang-go">type Context interface {
    Deadline() (deadline time.Time, ok bool) // 返回 context 的过期时间
    Done() &lt;-chan struct{}                   // 返回 context 中的 channel
    Err() error                              // 返回错误
    Value(key any) any                       // 返回 context 中的对应 key 的值
}</code></pre><h2 id="contextbackground--contexttodo">context.Background | context.TODO</h2><p>我们常用的 <code>context.Background()</code> 和 <code>context.TODO()</code> 方法返回的均是 <code>emptyCtx</code> 类型的一个实例</p><pre class="language-go lang-go"><code class="language-go lang-go">var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

func Background() Context {
    return background
}

func TODO() Context {
    return todo
}</code></pre><h2 id="emptyctx">emptyCtx</h2><p><code>emptyCtx</code> 对 <code>Context</code> 接口实现如下，<code>emptyCtx</code> 本质上类型为一个整型。我们观察其实现，发现 <code>emptyCtx</code> 没有 实现 <code>Context</code> 的高级功能，像 <code>cancel | timeout</code></p><ul><li>Done 方法返回一个 nil channel，写入或者读取数据，均会陷入阻塞</li><li>Err 方法返回的错误永远为 nil</li><li>Value 方法返回的 value 同样永远为 nil</li><li>Deadline 标识当前 context 不存在过期时间</li></ul><pre class="language-go lang-go"><code class="language-go lang-go">type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
    return
}

func (*emptyCtx) Done() &lt;-chan struct{} {
    return nil
}

func (*emptyCtx) Err() error {
    return nil
}

func (*emptyCtx) Value(key any) any {
    return
}</code></pre><h2 id="contextwithcancel">context.WithCancel</h2><p>下面是 WithCancel 源码：</p><pre class="language-go lang-go"><code class="language-go lang-go">func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := withCancel(parent)
    return c, func() { c.cancel(true, Canceled, nil) }
}

func withCancel(parent Context) *cancelCtx {
    if parent == nil {
        panic(&quot;cannot create context from nil parent&quot;)
    }
    c := &amp;cancelCtx{}
    c.propagateCancel(parent, c)
    return c
}</code></pre><ul><li>校验父 context 非空</li><li>在 propagateCancel 方法 中构建父子 ctx, 保证父 context 终止时，该 cancelCtx 也会被终止</li><li>将 cancelCtx 返回，返回用以终止该 cancelCtx 的闭包函数</li></ul><p>下面是 cancelCtx 结构体：</p><pre class="language-go lang-go"><code class="language-go lang-go">type cancelCtx struct {
    Context   
  mu       sync.Mutex
  done     atomic.Value             
  children map[canceler]struct{}
  err      error
  cause    error
}

// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
    cancel(removeFromParent bool, err, cause error)
    Done() &lt;-chan struct{}
}</code></pre><p>下面是 propagateCancel 方法：</p><pre class="language-go lang-go"><code class="language-go lang-go">// propagateCancel arranges for child to be canceled when parent is.
// It sets the parent context of cancelCtx.
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
    c.Context = parent

    done := parent.Done()
    if done == nil {
        return // parent is never canceled
    }

    select {
    case &lt;-done:
        // parent is already canceled
        child.cancel(false, parent.Err(), Cause(parent))
        return
    default:
    }

    if p, ok := parentCancelCtx(parent); ok {
        // parent is a *cancelCtx, or derives from one.
        p.mu.Lock()
        if p.err != nil {
            // parent has already been canceled
            child.cancel(false, p.err, p.cause)
        } else {
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
        return
    }

    if a, ok := parent.(afterFuncer); ok {
        // parent implements an AfterFunc method.
        c.mu.Lock()
        stop := a.AfterFunc(func() {
            child.cancel(false, parent.Err(), Cause(parent))
        })
        c.Context = stopCtx{
            Context: parent,
            stop:    stop,
        }
        c.mu.Unlock()
        return
    }

    goroutines.Add(1)
    go func() {
        select {
        case &lt;-parent.Done():
            child.cancel(false, parent.Err(), Cause(parent))
        case &lt;-child.Done():
        }
    }()
}</code></pre><p>propagateCancel 方法中：</p><ul><li>parent 是不会被 cancel 的类型（如 emptyCtx），则直接返回</li><li>parent 已经被 cancel，则直接终止子 context，并以 parent 的 err 作为子 context 的 err， parent 的 Cause 作为子 context 的 cause</li><li>parent 是 cancelCtx 的类型，加锁，并将子 context 添加到 parent 的 children map 当中，函数终止</li><li>parent 不是 cancelCtx 类型但实现了 afterFuncer 接口，加锁，以 child cancel 为参数传入父 context 的 AfterFunc 函数中，构建父子 context, 此时父 context 为 stopCtx</li><li>假如 parent 不是 cancelCtx 类型，但存在 cancel 的能力（用户自定义实现的 context），则启动一个协程，通过多路复用的方式监控 parent 状态，倘若其终止，则同时终止子 context，并透传 parent 的 err</li></ul><p>下面是 <code>propagateCancel</code> 函数中的 <code>parentCancelCtx</code> 源码, 用来校验 <code>parent</code> 是否为 <code>cancelCtx</code> 的类型</p><pre class="language-go lang-go"><code class="language-go lang-go">func parentCancelCtx(parent Context) (*cancelCtx, bool) {
    done := parent.Done()
    if done == closedchan || done == nil {
        return nil, false
    }
    p, ok := parent.Value(&amp;cancelCtxKey).(*cancelCtx)
    if !ok {
        return nil, false
    }
    pdone, _ := p.done.Load().(chan struct{})
    if pdone != done {
        return nil, false
    }
    return p, true
}</code></pre><ul><li>parent 的 channel 已关闭或者是不会被 cancel 的类型，则返回 false</li><li>以特定的 cancelCtxKey 从 parent 中取值，取得的 value 是 parent 本身，则返回 true（基于 cancelCtxKey 为 key 取值时返回 cancelCtx 自身，是 cancelCtx 特有的协议）</li></ul><h2 id="cancel">cancel</h2><p>下面是 cancel 源码，当我们执行  withCancel 返回的 cancelFunc 时，会调用  <code>func() { c.cancel(true, Canceled, nil) }</code></p><pre class="language-go lang-go"><code class="language-go lang-go">// cancel closes c.done, cancels each of c&#x27;s children, and, if
// removeFromParent is true, removes c from its parent&#x27;s children.
// cancel sets c.cause to cause if this is the first time c is canceled.
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
    if err == nil {
        panic(&quot;context: internal error: missing cancel error&quot;)
    }
    if cause == nil {
        cause = err
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    c.err = err
    c.cause = cause
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
        c.done.Store(closedchan)
    } else {
        close(d)
    }
    for child := range c.children {
        // NOTE: acquiring the child&#x27;s lock while holding parent&#x27;s lock.
        child.cancel(false, err, cause)
    }
    c.children = nil
    c.mu.Unlock()

    if removeFromParent {
        removeChild(c.Context, c)
    }
}</code></pre><ul><li>进入方法主体，首先校验传入的 err 是否为空，若为空则 panic</li><li>加锁</li><li>校验 cancelCtx 自带的 err 是否已经非空，若非空说明已被 cancel，则解锁返回</li><li>将传入的 err 赋给 cancelCtx.err</li><li>处理 cancelCtx 的 channel，若 channel 此前未初始化，则直接注入一个 closedChan，否则关闭该 channel</li><li>遍历当前 cancelCtx 的 children set，依次将 children context 都进行 cancel</li><li>解锁</li><li>根据传入的 removeFromParent flag 判断是否需要手动把 cancelCtx 从 parent 的 children set 中移除</li></ul><h2 id="contextwithtimout--contextwithdeadline">context.WithTimout | context.WithDeadline</h2><pre class="language-go lang-go"><code class="language-go lang-go">func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}</code></pre><p><code>WithTimeout</code> 本质上会调用 <code>context.WithDeadline</code> 方法</p><pre class="language-go lang-go"><code class="language-go lang-go">func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
    if parent == nil {
        panic(&quot;cannot create context from nil parent&quot;)
    }
    if cur, ok := parent.Deadline(); ok &amp;&amp; cur.Before(d) {
        // The current deadline is already sooner than the new one.
        return WithCancel(parent)
    }
    c := &amp;timerCtx{
        deadline: d,
    }
    c.cancelCtx.propagateCancel(parent, c)
    dur := time.Until(d)
    if dur &lt;= 0 {
        c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
        return c, func() { c.cancel(false, Canceled, nil) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded, cause)
        })
    }
    return c, func() { c.cancel(true, Canceled, nil) }
}</code></pre><ul><li>校验 parent context 非空</li><li>校验 parent 的过期时间是否早于自己，若是，则构造一个 cancelCtx 返回即可（此时在给子 ctx 包装 timeCtx, 没意义，因为一旦父 context 退出，子 context 也会被cancel）</li><li>构造出一个新的 timerCtx</li><li>propagateCancel 构建父子 ctx</li><li>判断过期时间是否已到，若是，直接 cancel timerCtx，并返回 DeadlineExceeded (&quot;context deadline exceeded&quot;) 错误</li><li>加锁</li><li>启动 time.Timer，设定一个延时时间，即达到过期时间后会终止该 timerCtx，并返回 DeadlineExceeded 的错误</li><li>解锁</li><li>返回 timerCtx，和一个封装了 cancel 逻辑的函数</li></ul><p>下面是 timerCtx 源码：</p><pre class="language-go lang-go"><code class="language-go lang-go">// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}</code></pre><p>复用了 cancelCtx 的能力,  并且加入了定时器，可以等时间到了再去 cancel</p>
<h2 id="contextwithvalue">context.WithValue</h2><pre class="language-go lang-go"><code class="language-go lang-go">func WithValue(parent Context, key, val any) Context {
    if parent == nil {
        panic(&quot;cannot create context from nil parent&quot;)
    }
    if key == nil {
        panic(&quot;nil key&quot;)
    }
    if !reflectlite.TypeOf(key).Comparable() {
        panic(&quot;key is not comparable&quot;)
    }
    return &amp;valueCtx{parent, key, val}
}</code></pre><ul><li>parent context 为空，panic</li><li>key 为空 panic</li><li>key 的类型不可比较，panic</li><li>parent context 以及 kv对，返回一个新的 valueCtx</li></ul><h2 id="valuectxvalue">valueCtx.Value</h2><pre class="language-go lang-go"><code class="language-go lang-go">func (c *valueCtx) Value(key any) any {
    if c.key == key {
        return c.val
    }
    return value(c.Context, key)
}

func value(c Context, key any) any {
    for {
        switch ctx := c.(type) {
        case *valueCtx:
            if key == ctx.key {
                return ctx.val
            }
            c = ctx.Context
        case *cancelCtx:
            if key == &amp;cancelCtxKey {
                return c
            }
            c = ctx.Context
        case withoutCancelCtx:
            if key == &amp;cancelCtxKey {
                // This implements Cause(ctx) == nil
                // when ctx is created using WithoutCancel.
                return nil
            }
            c = ctx.c
        case *timerCtx:
            if key == &amp;cancelCtxKey {
                return &amp;ctx.cancelCtx
            }
            c = ctx.Context
        case backgroundCtx, todoCtx:
            return nil
        default:
            return c.Value(key)
        }
    }
}</code></pre><ul><li>Value 方法中，判断当前 valueCtx 的 key 是否等于用户传入的 key，是，则直接返回其 value。不是，调用 value 方法，从 parent context 中依次向上寻找</li></ul><p>阅读源码可以看出，valueCtx 不适合视为存储介质，存放大量的 kv 数据：</p><ul><li><p>一个 valueCtx 实例只能存一个 kv 对，因此 n 个 kv 对会嵌套 n 个 valueCtx，造成空间浪费；</p></li><li><p>基于 k 寻找 v 的过程是线性的，时间复杂度 O(N)；</p></li></ul></div><p style="text-align:right"><a href="https://benlifec.com/posts/golang/gosource_context#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://benlifec.com/posts/golang/gosource_context</link><guid isPermaLink="true">https://benlifec.com/posts/golang/gosource_context</guid><dc:creator><![CDATA[benjamin]]></dc:creator><pubDate>Tue, 25 Feb 2025 01:32:17 GMT</pubDate></item><item><title><![CDATA[情绪的微光-2025版]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://benlifec.com/notes/3">https://benlifec.com/notes/3</a></blockquote><div><p>当心情低谷，来一碗能疗愈自己的“心灵鸡汤“ --（2025 版）</p>
<h3 id="">陈丹青为贾樟柯作序</h3><p>陈丹青为贾樟柯作序写到的一段话，特意翻找到原文：</p><p>“ 我记得贾樟柯在一部电影的花絮中接受采访，他说，他在荒败的小县城混时，有很多机会沦落，变成坏孩子，毁了自己。这是诚实的自白。我在知青岁月中也有太多机会沦丧，破罐子破摔。刚才有年轻人问：“谁能救救我们？”我的回答可能会让年轻人不舒服：这是奴才的思维。永远不要等着谁来救我们。每个人应该自己救自己，从小救起来。什么叫做救自己呢？以我的理解，就是忠实自己的感觉，认真做每一件事，不要烦，不要放弃，不要敷衍。哪怕写文章时标点符号弄清楚，不要有错别字——这就是我所谓的自己救自己。我们都得一步一步救自己， 我靠的是一笔一笔地画画，贾樟柯靠的是一寸一寸的胶片。 ”</p>
<h3 id="">史铁生</h3><p>“我四肢健全时，常抱怨周围环境糟糕，瘫痪后，怀念当初可以行走奔跑的日子。几年后长了褥疮，怀念起前两年安稳坐在轮椅上的时光；后来得了尿毒症，怀念当初长褥疮；又过了一些年，要透析，清醒的时间很少，怀念尿毒症的时候。”</p>
<h3 id="">查理·卓别林：当我真正开始爱自己</h3><p>As I Began to Love Myself</p><p>当我真正开始爱自己</p><p>As I began to love myself I found that anguish and emotional suffering are only warning signs that I was living against my own truth. Today, I know, this is AUTHENTICITY.</p><p>当我真正开始爱自己，我才意识到，所有的痛苦和情感的折磨，仅仅是在提醒，我的生活背离了真实的自己。今日，我明白，这叫做「真实」。</p><p>As I began to love myself I understood how much it can offend somebody as I try to force my desires on this person, even though I knew the time was not right and the person was not ready for it, and even though this person was me. Today I call it RESPECT.</p><p>当我真正开始爱自己，我才懂得，当我将自己的意愿强加于人时，是多么的冒犯无礼，即使那时我知晓时机尚未成熟，那个人也并未做好准备，即便那个人就是我自己。今日我明白这叫做「尊重」。</p><p>As I began to love myself I stopped craving for a different life, and I could see that everything that surrounded me was inviting me to grow. Today I call it MATURITY.</p><p>当我开始真正爱自己，我不再苛求不一样的人生，我明白任何发生在我身边的事，都是对我成长的邀约。而今，我称之为「成熟」。</p><p>As I began to love myself I understood that at any circumstance, I am in the right place at the right time, and everything happens at the exactly right moment, so I could be calm. Today I call it SELF-CONFIDENCE.</p><p>当我开始真正爱自己，我才明白在任何情况下，我其实一直处于恰好的时机、恰好的地方，而每件事的发生也都恰如其分。由此，我得以平静。今日，我称其为「自信」。</p><p>As I began to love myself I quit stealing my own time, and I stopped designing huge projects for the future. Today, I only do what brings me joy and happiness, things I love to do and that make my heart cheer, and I do them in my own way and in my own rhythm. Today I call it SIMPLICITY.</p><p>当我开始真正爱自己，我不再挥霍自己生命和时间，不再去擘画波澜壮阔的未来。今天的我，只做能为我带来喜悦和幸福的事，做那些我所热爱，且能鼓舞我心的事情，用我特有的方式，按照我自己的节奏。今日我知晓这叫做「简单」。</p><p>As I began to love myself I freed myself of anything that is no good for my health – food, people, things, situations, and everything that drew me down and away from myself. At first I called this attitude a healthy egoism. Today I know it is LOVE OF ONESELF.</p><p>当我开始真正爱自己，我开始远离所有不健康的东西，不论是饮食、人物、事情亦或环境，远离拖累我、让我背离了真实自我的一切。从前的我称呼这为“有益健康的利己主义”。今天我知道，这其实是「自爱」。</p><p>As I began to love myself I quit trying to always be right, and ever since I was wrong less of the time. Today I discovered that is MODESTY.</p><p>当我开始真正爱自己，我不再总想做到永远正确，从那时起，我犯错的时候反而更少了。现在我发现那就是「谦逊」。</p><p>As I began to love myself I refused to go on living in the past and worry about the future. Now, I only live for the moment, where EVERYTHING is happening. Today I live each day, day by day, and I call it FULFILLMENT.</p><p>当我开始真正爱自己，我不再继续沉溺于过去，也不再为明天而忧虑。如今，我只活在这一切正在发生的当下。今天，我活在此时此地，如此日复一日，而我称这为「圆满」。</p><p>As I began to love myself I recognized that my mind can disturb me and it can make me sick. But As I connected it to my heart, my mind became a valuable ally. Today I call this connection WISDOM OF THE HEART.</p><p>当我真正开始爱自己，我明白，我的思虑扰乱了自我，使我变得了无生趣。但当我与本心相连，心灵的力量就成为了我坚实的后盾。今日我称这种连结为「心灵的智慧」。</p><p>We no longer need to fear arguments, confrontations or any kind of problems with ourselves or others. Even stars collide, and out of their crashing new worlds are born. Today I know THAT IS LIFE!</p><p>我们无需再害怕生命中的争论、冲突，或是任何出现在与我们自己或与其他人之间的问题。因为即使是星星也会有碰撞在一起的时候，而从它们的撞击中，新的世界将会诞生。今天我知道，这，就是「生命」！</p></div><p style="text-align:right"><a href="https://benlifec.com/notes/3#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://benlifec.com/notes/3</link><guid isPermaLink="true">https://benlifec.com/notes/3</guid><dc:creator><![CDATA[benjamin]]></dc:creator><pubDate>Thu, 20 Feb 2025 09:23:36 GMT</pubDate></item><item><title><![CDATA[[Go] 我在工作中常用的 Golang 设计模式]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://benlifec.com/posts/golang/godesignpattern">https://benlifec.com/posts/golang/godesignpattern</a></blockquote><div><h2 id="">责任链模式</h2><p><strong>含义：</strong>  指定顺序串行化地执行一系列的任务，前置的任务节点根据执行情况可以选择提前熔断流程或者正常向下执行.</p><p><strong>场景：</strong>  grpc 拦截器 和 gin 的中间件都是责任链模式</p><p><strong>实现：</strong></p><p>grpc 拦截器，责任链模式实现</p><p>原理：Interceptor 中的 handler 参数就指代的是下一个 Interceptor, 如果是最后一个 Interceptor， 那 handle 就是定义的 enhance hanlder。</p><pre class="language-go lang-go"><code class="language-go lang-go">type Handler func(ctx context.Context, req []string) ([]string, error)

type Interceptor func(ctx context.Context, req []string, handler Handler) ([]string, error)

func ChainInterceptors(interceptors []Interceptor) Interceptor {
    if len(interceptors) == 0 {
        return nil
    }

    return func(ctx context.Context, req []string, handler Handler) ([]string, error) {
        return interceptors[0](ctx, req, GetNextChainHandler(interceptors, 0, handler))
    }
}

func GetNextChainHandler(interceptors []Interceptor, index int, nextHandler Handler) Handler {

    if index == len(interceptors)-1 {
        return nextHandler
    }

    return func(ctx context.Context, req []string) ([]string, error) {
        return interceptors[index+1](ctx, req, nextHandler)
    }
}</code></pre><p>下面是使用 grpc 拦截器的代码</p><pre class="language-go lang-go"><code class="language-go lang-go">// 用法
// enhanced final handler
var handler Handler = func(ctx context.Context, req []string) ([]string, error) {
    req = append(req, &quot;enhance final handler&quot;)
    return req, nil
}

var interceptor1 Interceptor = func(ctx context.Context, req []string, handler Handler) ([]string, error) {
    fmt.Println(&quot;interceptor1 preprocess...&quot;)
    req = append(req, &quot;interceptor1_preprocess&quot;)
    resp, err := handler(ctx, req)
    fmt.Println(&quot;interceptor1 postprocess&quot;)
    resp = append(resp, &quot;interceptor1_postprocess&quot;)
    return resp, err
}

var interceptor2 Interceptor = func(ctx context.Context, req []string, handler Handler) ([]string, error) {
    fmt.Println(&quot;interceptor2 preprocess...&quot;)
    req = append(req, &quot;interceptor2_preprocess&quot;)
    resp, err := handler(ctx, req)
    fmt.Println(&quot;interceptor2 postprocess&quot;)
    resp = append(resp, &quot;interceptor2_postprocess&quot;)
    return resp, err
}

func main() {
    chainedInterceptor := ChainInterceptors([]Interceptor{
        interceptor1, interceptor2,
    })

    resp, _ := chainedInterceptor(context.Background(), nil, handler)
    fmt.Printf(&quot;resp: %+v&quot;, resp)
}</code></pre><p>gin 中间件 责任链模式实现,  与 grpc 拦截器层层传递不同的是，gin 的中间件是由 gin 本身管理的，需要 <code>.Next</code>,  <code>.Abort</code> 去熔断或者终止。</p><pre class="language-go lang-go"><code class="language-go lang-go">package main

const MaxHandlersCnt = 100

type Handler func(ctx *Context, req map[string]interface{}) (map[string]interface{}, error)

type HandlerManager struct {
    index    int
    handlers []Handler
}

func NewHandlerManager(handlers ...Handler) *HandlerManager {
    return &amp;HandlerManager{
        index:    -1,
        handlers: handlers,
    }
}

func (c *Context) Next(req map[string]interface{}) {
    c.index++
    for c.index &lt; len(c.handlers) &amp;&amp; c.index &lt;= MaxHandlersCnt {
        c.handlers[c.index](c, req)
        c.index++
    }
}

func (c *Context) Abort() {
    c.index = MaxHandlersCnt
}</code></pre><h2 id="">单例模式</h2><p><strong>含义：</strong> 结构体在整个进程中存在唯一的示例来供外部反复调用</p><p><strong>场景：</strong></p><ul><li>只允许存在一个实例的类，比如全局统一的监控或者统计模块</li><li>实例化时很耗费资源的类，比如连接池、第三方库的客户端等</li><li>负责业务领域分层的类，比如 domain、service、repo 等</li></ul><p><strong>实现：</strong></p><p>在单例模式的实现上，可以分为饿汉式和懒汉式两种类型：</p><p>• 饿汉式：应用一启动，就完成单例的初始化工作</p><p>• 懒汉式：手动执行单例的初始化工作</p><p>首先是饿汉模式</p><pre class="language-go lang-go"><code class="language-go lang-go">
var s *singleton

type Singleton interface {
    Work()
}

type singleton struct{}

func init() {
    s = &amp;singleton{}
}

func (s *singleton) Work() {
}

func GetSingletonIns() Singleton {
    return s
}</code></pre><p>然后是懒汉模式</p><p>懒汉模式的第一种做法是锁 + double check.</p><pre class="language-go lang-go"><code class="language-go lang-go">package main

import &quot;sync&quot;

var (
    s   Singleton
    mux sync.Mutex
)

type Singleton interface {
    Work()
}

type singleton struct{}

func newSingleton() Singleton {
    return &amp;singleton{}
}

func (s *singleton) Work() {
}

func GetSingletonIns() Singleton {
    if s != nil {
        return s
    }

    mux.Lock()

    defer mux.Unlock()

    // double check
    if s != nil {
        return s
    }

    s = newSingleton()

    return s

}</code></pre><p>懒汉模式的第二种做法是使用 sync.Once, 原理也是锁 + double check, Once 结构体有一个 uint32 类型的 done 字段，表示 once 保护的函数是否已经被执行过。( 通过 atomic.StoreUint32(&amp;o.done, 1) 对 done 字段进行原子操作的修改 )</p><pre class="language-go lang-go"><code class="language-go lang-go">package main

import &quot;sync&quot;

var (
    s    Singleton
    once sync.Once
)

type Singleton interface {
    Work()
}

type singleton struct{}

func newSingleton() Singleton {
    return &amp;singleton{}
}

func (s *singleton) Work() {
}

func GetSingletonIns() Singleton {
    once.Do(func() {
        s = newSingleton()
    })

    return s
}</code></pre><h2 id="">工厂模式</h2><p><strong>含义：</strong> 在类和使用方之间添加一个工厂类中间层，实现了代码的防腐和解耦</p><p><strong>场景：</strong></p><ul><li>业务方法和类之间产生过高的耦合度</li><li>类的定义发生变更，代码中类的构造流程都需要改动</li></ul><p><strong>实现：</strong></p><ul><li><p>简单工厂模式: 工厂模式中最简单直观的实现方式，有很好的切面效果，但是类扩展时无法满足开闭原则</p></li><li><p>工厂方法模式: 一个类对应一个工厂类，工程类实现工厂接口, 存在代码冗余，但可以在类扩展时满足开闭原则</p></li><li><p>抽象工厂模式:  工厂方法模式针对的是同一类或同等级产品(美式，拿铁，卡布奇诺),而抽象工厂模式针对的是多种类的产品设计（星巴克的拿铁，瑞幸的拿铁...）</p></li></ul><p>首先是是简单工厂：</p><p>工厂负责生成咖啡，入口是下面的 <code>CoffeeMaker</code> 函数。</p><pre class="language-go lang-go"><code class="language-go lang-go">package main

import &quot;fmt&quot;

type Coffee interface {
}

type americanoCoffee struct {
}

type latteCoffee struct {
}

type cappuccinoCoffee struct {
}

func NewAmericanoCoffee() Coffee {
    return &amp;americanoCoffee{}
}

func NewLatteCoffee() Coffee {
    return &amp;latteCoffee{}
}

func NewCappuccinoCoffee() Coffee {
    return &amp;cappuccinoCoffee{}
}

type coffeeMaker func() Coffee

type CoffeeFactory struct {
    coffeeList map[string]coffeeMaker
}

func NewCoffeeFactory() *CoffeeFactory {
    return &amp;CoffeeFactory{
        coffeeList: map[string]coffeeMaker{
            &quot;americano&quot;:  NewAmericanoCoffee,
            &quot;latte&quot;:      NewLatteCoffee,
            &quot;cappuccino&quot;: NewCappuccinoCoffee,
        },
    }
}

// 工厂入口
func (f *CoffeeFactory) MakeCoffee(typ string) (Coffee, error) {
    coffeeMaker, ok := f.coffeeList[typ]

    if !ok {
        return nil, fmt.Errorf(&quot;coffee type %s is not exist&quot;, typ)
    }

    return coffeeMaker, nil
}</code></pre><p>简单工厂有个问题，如果在加一种类型的咖啡，会直接修改 coffeeList, 不遵循开闭原则。 所以有个下面的工厂方法模式，每个具体的咖啡类后都有一个工厂类。这样做的好处是，以后再添加一种类型的咖啡，比如猫屎咖啡，只需要加猫屎咖啡类和其工厂类就可以了。</p><pre class="language-go lang-go"><code class="language-go lang-go">package main

type Coffee interface {
}

type CoffeeFactory interface {
    CreateCoffee() Coffee
}

type americanoCoffee struct {
}

// 美式咖啡的工厂类
type AmericanoFactory struct {
}


func NewAmericanoFactory() CoffeeFactory {
    return &amp;AmericanoFactory{}
}

func (a *AmericanoFactory) CreateCoffee() Coffee {
    return americanoCoffee{}
}

type latteCoffee struct {
}

// 拿铁的工厂类
type LatteFactory struct {
}


func NewLatteFactory() CoffeeFactory {
    return &amp;AmericanoFactory{}
}

func (a *LatteFactory) CreateCoffee() Coffee {
    return americanoCoffee{}
}

type cappuccinoCoffee struct {
}

// 卡布奇诺的工厂类
type CappuccinoFactory struct {
}

func NewCappuccinoFactory() CoffeeFactory {
    return &amp;CappuccinoFactory{}
}

func (a *CappuccinoFactory) CreateCoffee() Coffee {
    return cappuccinoCoffee{}
}

func NewCapCoffee() Coffee {
    return &amp;americanoCoffee{}
}

func NewLatteCoffee() Coffee {
    return &amp;latteCoffee{}
}

func NewCappuccinoCoffee() Coffee {
    return &amp;cappuccinoCoffee{}
}</code></pre><p>工厂方法模式针对的是同一类或同等级产品(美式，拿铁，卡布奇诺只有一层咖啡的维度)，而抽象工厂模式针对的是多种类的产品设计（星巴克的拿铁，瑞幸的拿铁...）也就是增添了品牌方的维度。</p><p>美式咖啡 <code>AmericanoCoffee</code>，拿铁咖啡 <code>LatteCoffee</code> 是接口，创建拿铁和美式的工厂(<code>CoffeeFactory</code>)也是接口，不同品牌方（星巴克，瑞幸），对美式 (<code>StarbucksAmericanoCoffee</code>)，拿铁，以及他们的工厂接口(<code>StarbucksCafeFactory</code>)做实现。</p><pre class="language-go lang-go"><code class="language-go lang-go">package main

type CoffeeFactory interface {
    CreateAmericanoCoffee() AmericanoCoffee
    CreateLatteCoffee() LatteCoffee
}

type AmericanoCoffee interface{}

type LatteCoffee interface{}

// 星巴克美式
type StarbucksAmericanoCoffee struct {
}

func NewStarbucksAmericanoCoffee() AmericanoCoffee {
    return &amp;StarbucksAmericanoCoffee{}
}

// 星巴克拿铁
type StarbucksLatteCoffee struct {
}

func NewStarbucksLatteCoffee() LatteCoffee {
    return &amp;StarbucksLatteCoffee{}
}

// 星巴克品牌实现
type StarbucksCafeFactory struct{}

func (s *StarbucksCafeFactory) CreateAmericanoCoffee() AmericanoCoffee {
    return NewStarbucksAmericanoCoffee()
}

func (s *StarbucksCafeFactory) CreateLatteCoffee() LatteCoffee {
    return NewStarbucksLatteCoffee()
}</code></pre><p>如果再加一个品牌方，只需要增加对美式和拿铁的接口实现(上面代码例子中的 <code>StarbucksAmericanoCoffee</code>, <code>StarbucksLatteCoffee</code>) 以及品牌方工厂实现(上面代码例子中的 <code>StarbucksCafeFactory</code>)。</p>
<h2 id="">装饰器模式</h2><p>含义： 在不改变其原有的结构和功能为对象添加新功能的模式其实就叫做装饰器模式，装饰比继承更加灵活,可以实现装饰者和被装饰者之间松耦合</p><p>功能：功能之间耦合太深，并且该功能可能要随时新增或者撤销。</p><p>实现：</p><p>圆形和矩形都是形状(接口 <code>Shape</code>), 都可以实现画一个圆形/矩形的功能，在装饰器模式的基础上，在新增一个给圆形/矩形的边框画成绿色。如果不用装饰器，你可以直接在圆形/矩形的 Draw 方法里写该功能，但是耦合度高。要根据自己的业务需求来选择合适的模式。</p><pre class="language-go lang-go"><code class="language-go lang-go">package main

import &quot;fmt&quot;

type Shape interface {
    Draw()
}

type Circle struct {
}

func (c *Circle) Draw() {
    fmt.Println(&quot;绘制圆形&quot;)
}

type Rectangle struct {
}

func (c *Rectangle) Draw() {
    fmt.Println(&quot;绘制矩形&quot;)
}

// 装饰器声明
type DecoratorShape Shape

func NewColorfulShape(s Shape) DecoratorShape {
    return s
}

type RedColorFullShape struct {
    DecoratorShape
}

func NewRedColorFullShape(s DecoratorShape) DecoratorShape {
    return &amp;RedColorFullShape{s}
}

func (r *RedColorFullShape) Draw() {
    r.DecoratorShape.Draw()
    fmt.Println(&quot;圆形/矩形边框画成了红色&quot;)
}</code></pre><h2 id="">观察者模式</h2><p><strong>场景：</strong></p><ul><li>mq 发布订阅</li><li>etcd watcher 功能</li></ul><p>在观察者模式中，核心的角色包含三类：</p><ul><li>Observer：观察者.</li><li>Event：事物的变更事件. 其中 Topic 标识了事物的身份以及变更的类型，Val 是变更详情</li><li>EventBus：事件总线， 负责维护管理观察者，并且在事物发生变更时，将情况同步给每个观察者.</li></ul><p>首先定义这三个角色：</p><pre class="language-go lang-go"><code class="language-go lang-go">type Event struct {
    Topic string
    Val   interface{}
}

type Observer interface {
    OnChange(ctx context.Context, e *Event) error
}

type EventBus interface {
    Subscribe(topic string, o Observer)
    UnSubscribe(topic string, o Observer)
    Publish(ctx context.Context, e *Event)
}</code></pre><p><strong>实现 Observer</strong></p><pre class="language-go lang-go"><code class="language-go lang-go">type SimpleObserver struct {
    name string
}

func NewSimpleObserver() Observer {
    return &amp;SimpleObserver{}
}

func (s *SimpleObserver) OnChange(ctx context.Context, e *Event) error {
    fmt.Printf(&quot;observer: %s, event key: %s, event val: %v&quot;, s.name, e.Topic, e.Val)
    return nil
}</code></pre><p><strong>实现 BaseEventBus</strong></p><pre class="language-go lang-go"><code class="language-go lang-go">type BaseEventBus struct {
    mux       sync.RWMutex
    observers map[string]map[Observer]struct{}
}

func NewBaseEventBus() *BaseEventBus {
    return &amp;BaseEventBus{
        observers: make(map[string]map[Observer]struct{}),
    }
}

func (b *BaseEventBus) Subscribe(topic string, o Observer) {
    b.mux.Lock()
    defer b.mux.Unlock()
    _, ok := b.observers[topic]
    if !ok {
        b.observers[topic] = make(map[Observer]struct{})
    }
    b.observers[topic][o] = struct{}{}
}

func (b *BaseEventBus) UnSubscribe(topic string, o Observer) {
    b.mux.Lock()
    defer b.mux.Unlock()
    delete(b.observers[topic], o)
}</code></pre><p><strong>实现 SyncEventBus</strong></p><p>在同步模式下，采用串行遍历的方式对每个观察者进行通知，并对处理流程中遇到的错误进行聚合, 统一处理.</p><pre class="language-go lang-go"><code class="language-go lang-go">type SyncEventBus struct {
    *BaseEventBus
}

func NewSyncEventBus(base *BaseEventBus) EventBus {
    return &amp;SyncEventBus{
        base,
    }
}

// 同步 event bus
func (s *SyncEventBus) Publish(ctx context.Context, e *Event) {
    s.mux.RLock()
    subscribers := s.observers[e.Topic]
    s.mux.RUnlock()

    errs := make(map[Observer]error)
    for subscriber := range subscribers {
        if err := subscriber.OnChange(ctx, e); err != nil {
            errs[subscriber] = err
        }
    }
    // 自定义错误处理
    // s.handleErr(ctx, errs)
}</code></pre><p><strong>实现 AsyncEventBus</strong>, 异步通知观察者，错误由后台的错误协程异步管理。</p><pre class="language-go lang-go"><code class="language-go lang-go">type AsyncEventBus struct {
    *BaseEventBus
    errC chan *ObserverWithErr
    ctx  context.Context
    stop context.CancelFunc
}

type ObserverWithErr struct {
    o   Observer
    err error
}

func NewAsyncEventBus(base *BaseEventBus) EventBus {
    asyncBus := &amp;AsyncEventBus{
        BaseEventBus: base,
    }

    asyncBus.ctx, asyncBus.stop = context.WithCancel(context.Background())

    go asyncBus.handleErr()

    return asyncBus

}

func (s *AsyncEventBus) Publish(ctx context.Context, e *Event) {
    s.mux.RLock()
    subscribers := s.observers[e.Topic]
    s.mux.RUnlock()

    for subscriber := range subscribers {
        // shadow 避免并发问题
        subscriber := subscriber

        go func() {
            if err := subscriber.OnChange(s.ctx, e); err != nil {
                select {
                case &lt;-s.ctx.Done():
                case s.errC &lt;- &amp;ObserverWithErr{
                    err: err,
                    o:   subscriber,
                }:
                }
            }
        }()
    }
}

func (s *AsyncEventBus) Stop() {
    s.stop()
}

func (s *AsyncEventBus) handleErr() {
    for {
        select {
        case &lt;-s.ctx.Done():
            return
        case respErr := &lt;-s.errC:
            // 处理 err
            fmt.Printf(&quot;observer: %v, err: %v&quot;, respErr.o, respErr.err)
        }
    }
}</code></pre><h2 id="">构造者模式</h2><p><strong>含义：</strong> 类实例化的过程，初始化类属性。</p><p><strong>场景：</strong> 很多第三方库当中，大家都见过这样的代码 <code>NewRocketMQ(withHost(&#x27;localhost&#x27;))</code> 这就是构造者模式的思想。</p><p><strong>实现：</strong></p><pre class="language-go lang-go"><code class="language-go lang-go">package main

import &quot;fmt&quot;

type Options struct {
    host     string
    username string
    password string
}

type Option func(opts *Options)

func WithHost(host string) Option {
    return func(opts *Options) {
        opts.host = host
    }
}

func WithPassword(password string) Option {
    return func(opts *Options) {
        opts.password = password
    }
}

func WithUsername(username string) Option {
    return func(opts *Options) {
        opts.username = username
    }
}

// 兜底默认值
func repairOptions(opts *Options) {
    if opts.username == &quot;&quot; {
        opts.username = &quot;root&quot;
    }
}

func NewOptions(opts ...Option) *Options {
    optionsIns := &amp;Options{}

    for _, opt := range opts {
        opt(optionsIns)
    }

    repairOptions(optionsIns)

    return optionsIns
}

// 应用 options 构造者模式
func main() {
    opt := NewOptions(WithHost(&quot;localhost&quot;), WithPassword(&quot;654321&quot;))
    fmt.Printf(&quot;options %+v&quot;, opt)
}</code></pre></div><p style="text-align:right"><a href="https://benlifec.com/posts/golang/godesignpattern#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://benlifec.com/posts/golang/godesignpattern</link><guid isPermaLink="true">https://benlifec.com/posts/golang/godesignpattern</guid><dc:creator><![CDATA[benjamin]]></dc:creator><pubDate>Thu, 20 Feb 2025 07:23:24 GMT</pubDate></item><item><title><![CDATA[小猫进化史]]></title><description><![CDATA[<link rel="preload" as="image" href="https://benlifec.com/api/v2/objects/file/sdii3mgd9pv0ddkbak.jpg"/><link rel="preload" as="image" href="https://benlifec.com/api/v2/objects/file/4yzaa1poao0n8hsk1g.jpg"/><link rel="preload" as="image" href="https://benlifec.com/api/v2/objects/file/x6ojtwc9yffbgztdv4.jpg"/><link rel="preload" as="image" href="https://benlifec.com/api/v2/objects/file/wq0zuzh4w9z9y1is3l.jpg"/><link rel="preload" as="image" href="https://benlifec.com/api/v2/objects/file/euyh5mshmo1kh6bdk6.jpg"/><link rel="preload" as="image" href="https://benlifec.com/api/v2/objects/file/cgvch1d7m2zdjli9r8.jpg"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://benlifec.com/notes/2">https://benlifec.com/notes/2</a></blockquote><div><p>猫猫名字叫：长公主</p><p>刚带回来的时候，两个月</p><p><img src="https://benlifec.com/api/v2/objects/file/sdii3mgd9pv0ddkbak.jpg" alt="两个月" height="1282" width="960"/>
<img src="https://benlifec.com/api/v2/objects/file/4yzaa1poao0n8hsk1g.jpg" alt="两个月" height="1706" width="1279"/>
<img src="https://benlifec.com/api/v2/objects/file/x6ojtwc9yffbgztdv4.jpg" alt="两个月" height="1282" width="960"/></p>
<p>五个月
<img src="https://benlifec.com/api/v2/objects/file/wq0zuzh4w9z9y1is3l.jpg" alt="五个月" height="1279" width="1299"/></p>
<p>八个月
<img src="https://benlifec.com/api/v2/objects/file/euyh5mshmo1kh6bdk6.jpg" alt="八个月" height="1282" width="960"/></p><p>11个月
<img src="https://benlifec.com/api/v2/objects/file/cgvch1d7m2zdjli9r8.jpg" alt="11个月" height="960" width="1280"/></p><p>最近再做绝育，每天带着头套(戴 7 天，防止她舔舐伤口)</p></div><p style="text-align:right"><a href="https://benlifec.com/notes/2#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://benlifec.com/notes/2</link><guid isPermaLink="true">https://benlifec.com/notes/2</guid><dc:creator><![CDATA[benjamin]]></dc:creator><pubDate>Tue, 18 Feb 2025 05:21:50 GMT</pubDate></item></channel></rss>