<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" 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">
    <channel>
        <title>younggglcy</title>
        <link>https://younggglcy.com/</link>
        <description>younggglcy' Blog</description>
        <lastBuildDate>Sat, 28 Feb 2026 16:49:32 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>younggglcy</title>
            <url>https://younggglcy.com/avatar.jpg</url>
            <link>https://younggglcy.com/</link>
        </image>
        <copyright>CC BY-NC-SA 4.0 2022 © younggglcy</copyright>
        <atom:link href="https://younggglcy.com/feed.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[深入浅出 TypeScript 装饰器]]></title>
            <link>https://younggglcy.com/posts/TS-decorator</link>
            <guid isPermaLink="true">https://younggglcy.com/posts/TS-decorator</guid>
            <pubDate>Mon, 07 Oct 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[本文分入门、进阶、深入三个章节，循序渐进地介绍了 TypeScript 中装饰器的原理与实现，希望能对读者有所启发]]></description>
            <content:encoded><![CDATA[<p>[[toc]]</p>
<h1>入门：了解装饰器</h1>
<blockquote>
<p>本章节首发于同事 <a href="https://github.com/xcatliu">xcatliu</a> 的 <a href="https://ts.xcatliu.com/">TypeScript 入门教程</a>，欢迎想了解或想对 TypeScript 有更深理解的你阅读~</p>
</blockquote>
<p>写在前面：本章只介绍 TypeScript 5.0+ 的装饰器用法，对于 5.0 以下的版本，请参考 <a href="https://www.typescriptlang.org/docs/handbook/decorators.html">TypeScript 官方文档</a></p>
<h2>什么是装饰器</h2>
<p>首先，什么是装饰器呢？<a href="https://en.wikipedia.org/wiki/Decorator_pattern">维基百科</a>是这么说的：</p>
<blockquote>
<p>In <a href="https://en.wikipedia.org/wiki/Object-oriented_programming">object-oriented programming</a>, the <strong>decorator pattern</strong> is a <a href="https://en.wikipedia.org/wiki/Design_pattern_(computer_science)">design pattern</a> that allows behavior to be added to an individual <a href="https://en.wikipedia.org/wiki/Object_(computer_science)">object</a>, dynamically, without affecting the behavior of other instances of the same <a href="https://en.wikipedia.org/wiki/Class_(computer_science)">class</a>.</p>
</blockquote>
<p>本人的蹩足翻译：在 OOP (面向对象编程)中，装饰器模式是一种允许动态地往一个对象上添加自定义行为，而又不影响该对象所属的类的其他实例的一种设计模式。</p>
<blockquote>
<p>什么是 OOP 和类？<a href="https://ts.xcatliu.com/advanced/class.html">前面的章节</a>做过介绍。</p>
</blockquote>
<p>这句话未免过于拗口了，我们不妨换个角度去切入。</p>
<h2>装饰器的使用场景</h2>
<p>要知道，一切设计模式的诞生，都是为了解决某个问题。在 JavaScript 的世界中，装饰器通常出现于以下场景：</p>
<ol>
<li>
<p>提供一种易读且容易实现的方式，修改类或者类的方法，避免出现大量重复的代码。</p>
<p>下面以修改类的方法为例。</p>
<p>首先，假设我们有一个 <code>Animal</code> 类：</p>
<pre><code class="language-tsx">class Animal {
  type: string
  constructor(type: string) {
    this.type = type
  }
  
  greet() {
    console.log(`Hello, I'm a(n) ${this.type}!`)
  }
}

const xcat = new Animal('cat')
xcat.greet() // Hello, I'm a(n) cat!
</code></pre>
<p>该类有一个 greet 方法，和调用方打招呼。</p>
<p>假如说，我还希望根据不同的 <code>type</code>，往 console 打印不同动物的叫声呢？</p>
<p>聪明的你或许想到了，这不就是<strong>类的继承</strong>吗！在子类的 <code>greet()</code> 方法中，实现不同的逻辑，再调用 <code>super.greet()</code> 即可。</p>
<pre><code class="language-tsx">class Xcat extends Animal {
  constructor() {
    super('cat')
  }
  
  greet() {
    console.log('meow~ meow~')
    super.greet()
  }
}

const xcat = new Xcat()
xcat.greet() // meow~ meow~
             // Hello, I'm a(n) cat!
</code></pre>
<p>用装饰器实现，也不妨为一种思路，比如在 <code>Animal</code> 类中，为 <code>greet()</code> 方法添加「打印不同动物叫声的」行为:</p>
<pre><code class="language-tsx">class Animal {
  type: string
  constructor(type: string) {
    this.type = type
  }

  @yelling
  greet() {
    console.log(`Hello, I'm a(n) ${this.type}!`)
  }
}

const typeToYellingMap = {
  cat: 'meow~ meow~'
}

function yelling(originalMethod: any, context: ClassMethodDecoratorContext) {
  return function(...args: any[]) {
    console.log(typeToYellingMap[this.type])
    originalMethod.call(this, ...args)
  }
}

const xcat = new Animal('cat')
xcat.greet() // meow~ meow~
             // Hello, I'm a(n) cat!
</code></pre>
<p>在 <code>Animal.greet()</code> 方法上出现的 <code>@yelling</code> ，就是 TypeScript 中装饰器的写法，即 @ + 函数名的组合。</p>
<p>上述示例对装饰器的应用属于<strong>方法装饰器</strong>，此类装饰器本身接收两个参数，一是被装饰的方法，二是方法装饰器的上下文。方法装饰器应返回一个函数，此函数在运行时真正被执行。在上述例子中，我们在装饰器返回的函数中做了两件事情：</p>
<ol>
<li>打印相应类别的动物的叫声。</li>
<li>调用 <code>originalMethod.call(this, …args)</code> ，确保原方法（即装饰器所装饰的方法）能够正确地被执行。</li>
</ol>
</li>
<li>
<p>结合「<strong>依赖注入</strong>」这一设计模式，优化模块与 class 的依赖关系。</p>
<p>什么是依赖注入呢？引用同事 <a href="https://github.com/ziofat">zio</a> 的原话：</p>
<blockquote>
<p><strong>依赖注入其实是将一个模块所依赖的部分作为参数传入，而不是由模块自己去构造。</strong></p>
</blockquote>
<p>可见，依赖注入解决了实际工程项目中，类、模块间依赖关系层级复杂的问题，将构造单例的行为交由实现依赖注入的框架去处理。</p>
<p>举个例子：</p>
<pre><code class="language-tsx">@injectable
class Dog implements IAnimal {
  sayHi() {
    console.log('woof woof woof')
  }
}

@injectable
class Cat implements IAnimal {
  sayHi() {
    console.log('meow meow meow')
  }
}

class AnimalService {
  constructor(
    @inject dog: Dog
    @inject cat: Cat
  ) {
    this._dog = dog
    this._cat = cat
  }
  
  sayHiByDog() {
    this._dog.sayHi()
  }
  
  sayHiByCat() {
    this._cat.sayHi()
  }
}
</code></pre>
<p>在上述代码中，<code>@injectable</code> 将一个类标记为「可被注入的」，在面向业务的类（即 <code>AnimalService</code>）中，使用 <code>@inject</code> 注入此类的单例，实现了「依赖倒置」。注意到这里的 <code>implements IAnimal</code> 用法，也是实战中依赖注入运用的精妙之处 —— 关心接口，而非具体实现。</p>
</li>
<li>
<p>实现「AOP」，即 Aspect-oriented programming，面向切面编程。</p>
<p>所谓的「切面」，可以理解成，在复杂的各个业务维度中，只关注一个维度的事务。</p>
<p>例如，使用装饰器，实现对类的某个方法的执行时间记录：</p>
<pre><code class="language-tsx">class MyService {
  @recordExecution
  myFn() {
    // do something...
  }
}

function recordExecution(originalMethod: any, context: ClassMethodDecoratorContext) {
  return function(...args: any[]) {
    console.time('mark execution')
    originalMethod.call(this, ...args)
    console.timeEnd('mark execution')
  }
}
</code></pre>
</li>
</ol>
<h2>装饰器的类别</h2>
<p>通过以上例子，相信读者已经对装饰器有一定了解，且认识到了装饰器在一些场景的强大之处。在此引用<a href="https://es6.ruanyifeng.com/#docs/decorator#%E7%AE%80%E4%BB%8B%EF%BC%88%E6%96%B0%E8%AF%AD%E6%B3%95%EF%BC%89">阮一峰 es6 教程</a>稍做总结：</p>
<blockquote>
<p>装饰器是一种函数，写成<code>@ + 函数名</code>，可以用来装饰四种类型的值。</p>
<ul>
<li>类</li>
<li>类的属性</li>
<li>类的方法</li>
<li>属性存取器（accessor, getter, setter）</li>
</ul>
</blockquote>
<blockquote>
<p>装饰器的执行步骤如下。</p>
<ol>
<li>计算各个装饰器的值，按照从左到右，从上到下的顺序。</li>
<li>调用方法装饰器。</li>
<li>调用类装饰器。</li>
</ol>
</blockquote>
<p>不管是哪种类型的装饰器，它们的函数签名都可以认为是一致的，即均接收 <code>value</code>, <code>context</code> 两个参数，前者指被装饰的对象，后者指一个存储了上下文信息的对象。</p>
<h2>context 与 metadata 二三讲</h2>
<p>四种装饰器的 context，均包含以下信息：</p>
<ul>
<li>
<p>kind</p>
<p>描述被装饰的 value 的类型，可取 <code>class</code>, <code>method</code>, <code>field</code>, <code>getter</code>, <code>setter</code>, <code>accessor</code> 这些值。</p>
</li>
<li>
<p>name</p>
<p>描述被装饰的 value 的名字。</p>
</li>
<li>
<p>addInitializer</p>
<p>一个方法，接收一个回调函数，使得开发者可以侵入 value 的初始化过程作修改。</p>
<p>对 <code>class</code> 来说，这个回调函数会在类定义最终确认后调用，即相当于在初始化过程的最后一步。</p>
<p>对其他的 value 来说，如果是被 <code>static</code> 所修饰的，则会在类定义期间被调用，且早于其他静态属性的赋值过程；否则，会在类初始化期间被调用，且早于 value 自身的初始化。</p>
<p>以下是 <code>@bound</code> 类方法装饰器的例子，该装饰器自动为方法绑定 <code>this</code>：</p>
<pre><code class="language-tsx">const bound = (value, context: ClassMemberDecoratorContext) {
  if (context.private) throw new TypeError(&quot;Not supported on private methods.&quot;);
  context.addInitializer(function () {
      this[context.name] = this[context.name].bind(this);
  });
}
</code></pre>
</li>
<li>
<p>metadata</p>
<p>和装饰器类似，<a href="https://github.com/tc39/proposal-decorator-metadata">metadata</a> 也是处于 stage 3 阶段的一个提案。装饰器只能访问到类原型链、类实例的相关数据，而 metadata 给了开发者更大的自由，让程序于运行时访问到编译时决定的元数据。</p>
<p>举个例子：</p>
<pre><code class="language-tsx">function meta(key, value) {
  return (_, context) =&gt; {
    context.metadata[key] = value;
  };
}

@meta('a', 'x')
class C {
  @meta('b', 'y')
  m() {}
}

C[Symbol.metadata].a; // 'x'
C[Symbol.metadata].b; // 'y'
</code></pre>
<p>在上述程序中，我们通过访问类的 <code>Symbol.metadata</code> ，读取到了 meta 装饰器所写入的元数据。对元数据的访问，有且仅有这一种形式。</p>
<p>注意一点，metadata 是作用在类上的，即使它的位置在类方法上。想实现细粒度的元数据存储，可以考虑手动维护若干 <code>WeakMap</code>。</p>
</li>
</ul>
<p>除了类装饰器以外，其他3种装饰器的 context 还拥有以下 3 个字段：</p>
<ul>
<li>
<p>static</p>
<p>布尔值，描述 value 是否为 static 所修饰。</p>
</li>
<li>
<p>private</p>
<p>布尔值，描述 value 是否为 private 所修饰。</p>
</li>
<li>
<p>access</p>
<p>一个对象，可在运行时访问 value 相关数据。</p>
<p>以类方法装饰器为例，用 <code>access.get</code> 可在运行时读取方法值，<code>access.has</code> 可在运行时查询对象上是否有某方法，举个例子：</p>
<pre><code class="language-tsx">const typeToYellingMap = {
  cat: 'meow~ meow~',
}

let yellingMethodContext: ClassMethodDecoratorContext

class Animal {
  type: string
  constructor(type: string) {
    this.type = type
  }

  @yelling
  greet() {
    console.log(`Hello, I'm a(n) ${this.type}!`)
  }

  accessor y = 1
}

function yelling(originalMethod: any, context: ClassMethodDecoratorContext) {
  yellingMethodContext = context
  return function (this: any, ...args: any[]) {
    console.log(typeToYellingMap[this.type as keyof typeof typeToYellingMap])
    originalMethod.call(this, ...args)
  }
}

const xcat = new Animal('cat')
xcat.greet() // meow~ meow~
// Hello, I'm a(n) cat!
yellingMethodContext.access.get(xcat).call(xcat) // meow~ meow~
// Hello, I'm a(n) cat!
console.log(yellingMethodContext.access.has(xcat)) // true
</code></pre>
<p><code>getter</code> 类别的装饰器，其 <code>context.access</code> 同样拥有 <code>has</code>, <code>get</code> 两个方法。</p>
<p>对于 <code>setter</code> 类别的装饰器，则是 <code>has</code> 与 <code>set</code> 方法。</p>
<p><code>filed</code> 与 <code>accessor</code> 类别的装饰器，拥有 <code>has</code>, <code>get</code>, <code>set</code> 全部三个方法。</p>
</li>
</ul>
<h1>进阶：one step further</h1>
<h2>了解装饰器提案的演进</h2>
<p>在「入门」章节的开头，有提到该章只介绍 TS 5.0+ 的装饰器实现。这是因为，TS 5.0+ 的装饰器是对 <a href="https://github.com/tc39/proposal-decorators/blob/a81149ffa1253601329b64542123ac52f839d139/README.md">stage3 装饰器草案</a>的实现，而 TS &lt; 5.0 的装饰器是对 <a href="https://tc39.es/proposal-decorators/">stage2 装饰器草案</a>的实现。</p>
<h3>简单介绍 TS &lt; 5.0 装饰器</h3>
<p>在 &lt;5.0 的版本，需要在 <code>tscofig.json</code> 的 <a href="https://www.typescriptlang.org/tsconfig#compilerOptions">**<code>compilerOptions</code></a>** 选项中，声明 <code>&quot;[experimentalDecorators](https://www.typescriptlang.org/tsconfig#experimentalDecorators)&quot;: true</code> ，才能让 stage2 装饰器生效。</p>
<p>和 stage3 装饰器不同的，有以下几点：</p>
<blockquote>
<p>推荐阅读： <a href="https://github.com/tc39/proposal-decorators?tab=readme-ov-file#how-does-this-proposal-compare-to-other-versions-of-decorators">https://github.com/tc39/proposal-decorators?tab=readme-ov-file#how-does-this-proposal-compare-to-other-versions-of-decorators</a></p>
</blockquote>
<ol>
<li>
<p>stage2 装饰器多出了 <code>parameter decorator</code> ，也即类中的参数装饰器，可用于类的构造函数或者方法中。</p>
<p>举个用法上的例子，在这里我们不关注具体实现。</p>
<pre><code class="language-tsx">class FontController {
  // 给富文本编辑器选取中的文字加粗
  bold(
    @Editor editor: unknown   // 主要关注这一行
  ) {
    return editor.formats.bold(editor.getRange())
  }
}
</code></pre>
</li>
<li>
<p>stage2 装饰器的写法更复杂，更难记，类型支持不太好。</p>
</li>
<li>
<p>stage2 装饰器中的 <code>target</code> ，是所修饰的类或者其原型链，而 stage3 装饰器并未暴露出直接操作类本身的能力，在设计上更为合理。</p>
<blockquote>
<p>事实上，类成员上的装饰器所修饰的，均是类的原型链，而带有 <code>static</code> 的类成员，其装饰器所修饰的是类本身。</p>
</blockquote>
</li>
<li>
<p>stage2 装饰器对 <code>target</code> 的侵入，是通过拿到 <code>descriptor</code> 并对其操作而实现的，而 stage3 通过 <code>context</code> ，只对外暴露必要接口，在设计上更为合理。</p>
</li>
<li>
<p>stage2 装饰器会先跑作用在实例上的装饰器，再跑 <code>static</code> 装饰器，而 stage3 装饰器的顺序完全由程序所决定</p>
</li>
</ol>
<p>综合来看，从 stage2 到 stage3，装饰器提案做出了许多好的改进。笔者唯一的槽点是，参数装饰器被去掉了。</p>
<h2>使用 TS Playground 分析原理</h2>
<p>对背景知识有所了解后，我们可以借助 TypeScript Playground，查看编译出来的 JavaScript 代码，来剖析装饰器的实现原理。</p>
<p>以上文最近的一个完整代码片段为例子，TypeScript Playground 链接在<a href="https://www.typescriptlang.org/play/?#code/MYewdgzgLgBFCeAHApgFRATWQG2wSzAHMBZAQ0RgF4YBvAKBhmFKgC4YByAW2RAHcAfjB78BHADR0AvnTrZkseDnxFiCgBYgAJgGFwUZAA82MHdlIQIaqJq0ARZKABOLEE71gDx2cHOWYAIJgeFyk2LQMcEjI7NBOBISRoJBQTgCuwFBuABQIKLGpCQCUEYyMNngQAHR5yFRRKJEykQACSrgJkYROyArZJfRlTOAQIPJV2CCE2QAGABLKIOIwAJLcMKTZYCUAJDQV1bVSAIQzRU2yjKTAwMiWbjDw9QCM0rIAZmlgmXjgj8oJbJuPCEAhhay2dikMDwZbJLwmMwWKwabQOZyudz6IxQAaRdoqEio3TY4z1eE4yI9KBpJxgGCfb5QX703LqSpQmHLKo80hOQgQTnwADaAF08UNkqNxpNprV0FgOqpyMKDjVohsIDAANbIeAgd4NXiG+WYAHKxDiyKMYGgsDg4lVZi4NmVbm8-kQc6MGTNKWwQzMWDUMDIPiBYKhbDZDhBjjnQMsKrdXpQfowAD0GeEvEEOdEdCzMAWuCWq3Wm22TBYxzoBISEO0HgRVWut0syb6idxTrC0e7JSLIjzw4EhezJcmyzWXA2WxKQdrUrGyAmU2y9dUxObONbNzu1XUFmyA8H2dSaWQdCAA">这里</a>，可以发现，核心在于 <code>__esDecorate</code> 函数的实现与使用。其代码如下：</p>
<pre><code class="language-tsx">// 关注此方法 ⬇️
var __esDecorate = (this &amp;&amp; this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
    function accept(f) { if (f !== void 0 &amp;&amp; typeof f !== &quot;function&quot;) throw new TypeError(&quot;Function expected&quot;); return f; }
    var kind = contextIn.kind, key = kind === &quot;getter&quot; ? &quot;get&quot; : kind === &quot;setter&quot; ? &quot;set&quot; : &quot;value&quot;;
    var target = !descriptorIn &amp;&amp; ctor ? contextIn[&quot;static&quot;] ? ctor : ctor.prototype : null;
    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
    var _, done = false;
    for (var i = decorators.length - 1; i &gt;= 0; i--) {
        var context = {};
        for (var p in contextIn) context[p] = p === &quot;access&quot; ? {} : contextIn[p];
        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
        context.addInitializer = function (f) { if (done) throw new TypeError(&quot;Cannot add initializers after decoration has completed&quot;); extraInitializers.push(accept(f || null)); };
        var result = (0, decorators[i])(kind === &quot;accessor&quot; ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
        if (kind === &quot;accessor&quot;) {
            if (result === void 0) continue;
            if (result === null || typeof result !== &quot;object&quot;) throw new TypeError(&quot;Object expected&quot;);
            if (_ = accept(result.get)) descriptor.get = _;
            if (_ = accept(result.set)) descriptor.set = _;
            if (_ = accept(result.init)) initializers.unshift(_);
        }
        else if (_ = accept(result)) {
            if (kind === &quot;field&quot;) initializers.unshift(_);
            else descriptor[key] = _;
        }
    }
    if (target) Object.defineProperty(target, contextIn.name, descriptor);
    done = true;
};

let Animal = (() =&gt; {
    var _a, _Animal_y_accessor_storage;
    let _instanceExtraInitializers = [];
    let _greet_decorators;
    return _a = class Animal {
            constructor(type) {
                this.type = __runInitializers(this, _instanceExtraInitializers);
                _Animal_y_accessor_storage.set(this, 1);
                this.type = type;
            }
            greet() {
                console.log(`Hello, I'm a(n) ${this.type}!`);
            }
            get y() { return __classPrivateFieldGet(this, _Animal_y_accessor_storage, &quot;f&quot;); }
            set y(value) { __classPrivateFieldSet(this, _Animal_y_accessor_storage, value, &quot;f&quot;); }
        },
        _Animal_y_accessor_storage = new WeakMap(),
        (() =&gt; {
            const _metadata = typeof Symbol === &quot;function&quot; &amp;&amp; Symbol.metadata ? Object.create(null) : void 0;
            _greet_decorators = [yelling];
            // 关注这里 ⬇️
            __esDecorate(_a, null, _greet_decorators, { kind: &quot;method&quot;, name: &quot;greet&quot;, static: false, private: false, access: { has: obj =&gt; &quot;greet&quot; in obj, get: obj =&gt; obj.greet }, metadata: _metadata }, null, _instanceExtraInitializers);
            // 关注这里 ⬆️
            if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
        })(),
        _a;
})();
</code></pre>
<h3>函数签名</h3>
<pre><code class="language-tsx">function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
  // do something...
}
</code></pre>
<ul>
<li><code>ctor</code> ，表示类的构造函数。</li>
<li><code>descriptorIn</code> ，表示现有的被装饰的属性的 descriptor，可以为空。</li>
<li><code>decorators</code> ，表示装饰器函数数组，因为装饰器可以存在多个。</li>
<li><code>contextIn</code> ，表示装饰器的 context 信息，何为 context 上文有简单介绍。</li>
<li><code>initializers</code> ，与函数外部的初始化逻辑相关，暂时忽略。</li>
<li><code>extraInitializers</code> ，与函数外部的初始化逻辑相关，暂时忽略。</li>
</ul>
<h3>函数主体</h3>
<p>在函数主体部分，主要做的逻辑就是，根据 context 上下文信息，拿到 <code>descriptor</code> 和其他相关变量，然后从后往前遍历装饰器，在每个循环，都创建一个独立的 context，和 descriptor 一起作为参数，传给装饰器并调用得到结果，最后更新 descriptor。</p>
<p>可见在语言层面，装饰器的实现本质上还是通过操作 <code>descriptor</code> 而实现的。</p>
<h1>深入：TypeScript 源码阅读</h1>
<p>在上一章，我们从 TS Playground 看到了 TS 编译后的产物。但好奇的你可能会继续发问：TS 是如何将代码编译成这个样子的呢？TS 对于装饰器语法，在编译器内，又是如何处理的呢？那这个时候，我们就需要 clone TS 源码过来阅读一下了。</p>
<img src="./assets-TS-decorator/ts-decoratormodern_complier_constction.jpeg" rounded-lg>
<p>附上 TS/C# 作者 Anders 在数年前一场关于现代编译器架构的 Talk 中，手绘的一张图。</p>
<pre><code class="language-mermaid">graph LR
  A[Lexer] --&gt; B[Parser]
  B --&gt; C[Type Checker]
  C --&gt; D[Code generator]
  D --&gt; E[Emitter]
</code></pre>
<p>对于编译原理，相信各位学得一定比笔者更好，笔者在此便不班门弄斧了。对于装饰器相关实现而言，我们更关心这里 <code>Lexer</code>, <code>Parser</code> 整体上是如何把装饰器代码处理为 AST Node 的，以及 AST Node 是如何被转换到生成的 JavaScript 产物的。</p>
<p>TypeScript 的源码相对比较多，深究技术细节的话，可能很容易一天就过去了。按笔者的经验，打 debugger 调试是切入并了解复杂项目，最有效的手段。</p>
<h2>setup</h2>
<p>笔者选择的方式比较笨，是直接在 TS 源码中，写上 <code>debugger</code> ，再使用 <code>hereby local</code> 命令将编译器产物输出到本地，最后使用产物中的 <code>tsc.js</code> ，调试 demo 项目。</p>
<p>笔者准备的 demo 项目也相当简单，目录结构如下：</p>
<img src="./assets-TS-decorator/ts-decoratorimage.png" rounded-lg>
<p><code>src/index.ts</code> 使用了「入门」篇中的例子，稍做修改免去了类型错误：</p>
<pre><code class="language-tsx">class Animal {
  type: string
  constructor(type: string) {
    this.type = type
  }

  @yelling
  greet() {
    console.log(`Hello, I'm a(n) ${this.type}!`)
  }
}

const typeToYellingMap = {
  cat: 'meow~ meow~'
}

function yelling(originalMethod: any, context: ClassMethodDecoratorContext) {
  return function(this: Animal, ...args: any[]) {
    console.log(typeToYellingMap[this.type as keyof typeof typeToYellingMap])
    originalMethod.call(this, ...args)
  }
}

const xcat = new Animal('cat')
xcat.greet() // meow~ meow~
             // Hello, I'm a(n) cat!

</code></pre>
<p><code>tsconfig.json</code> 做了很简单的配置：</p>
<pre><code class="language-tsx">{
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;es6&quot;,
    &quot;module&quot;: &quot;commonjs&quot;,
    &quot;strict&quot;: true,
    &quot;esModuleInterop&quot;: true,
    &quot;skipLibCheck&quot;: true,
    &quot;forceConsistentCasingInFileNames&quot;: true,
    &quot;outDir&quot;: &quot;./dist&quot;
  },
  &quot;include&quot;: [&quot;src&quot;],
  &quot;exclude&quot;: [&quot;node_modules&quot;]
}

</code></pre>
<p>准备好之后，笔者使用 VS Code 自带的 JavaScript Debug Terminal，在 TypeScript 目录下运行 <code>node built/local/tsc.js -p path-to-your-demo</code> ，即可在 VS Code 下进行调试。</p>
<h2>寻找入口</h2>
<p>以 main 分支 <code>8230bc66a7eb4b88cfb6cdaa4ea8324808b39a07</code> commit 为例子，以当前方式使用命令行时，入口是在 <code>src/compiler/executeCommandLine.ts</code> 的 <code>executeCommandLine</code> 方法，最终会调用 <code>performCompilation</code> 方法应用编译。</p>
<p>该方法中，一个重要调用是，使用 <code>createProgram</code> 创建了 Program 对象实例。Program 在 TypeScript 是个非常重要的存在，它是一个可被编译的最小单元，由若干 <code>SourceFile</code> 与 <code>CompilerOptions</code> 组成。前者是源文件的 AST 表达，而后者主要来源于项目的 <code>tsconfig.json</code> 。</p>
<p>因此可以看到，需要关注的重点，在于 Program 是如何创建 <code>SourceFile</code> 的。</p>
<p>不难发现，<code>src/compiler/program.ts</code> L1853，在创建 Program 时，会处理项目根级入口。</p>
<img src="./assets-TS-decorator/ts-decoratorimage 1.png" rounded-lg />
<p>此步骤在经过一系列必要处理后，最终由 <code>src/compiler/parser.ts</code> 的 <code>createSourceFile</code> 方法，将源文件处理成 <code>SourceFile</code> 对象。</p>
<h2>Lexer</h2>
<p>Lexer，也即 Lexical Analyzer，其主要职责为词法分析，把原文件字符串处理成 tokens。在 TypeScript 中，Lexer 逻辑聚集在 <code>src/compiler/scanner.ts</code> 处。scanner 在语义上和 Lexer 也是相通的，下文以 scanner 代指 lexer。</p>
<p>scanner 的 <code>scan</code> 方法，是取 token 的逻辑。</p>
<img src="./assets-TS-decorator/ts-decoratorimage 2.png" rounded-lg>
<p><code>scan</code> 取了当前位置的码点值，根据码点值，匹配不同的处理逻辑。返回的 token 是名为 <code>SyntaxKind</code> 的枚举。</p>
<img src="./assets-TS-decorator/ts-decoratorimage 3.png" rounded-lg>
<p>从上图可以看到，对于装饰器的场景，<code>@</code> 和函数名是作为两个单独 token 存在的，前者为 AtToken 标识，后者为 Identifier。我们不妨大胆猜测，在 Parser 阶段，会把这两个 token 组合成一个 AST Node。</p>
<h2>Parser</h2>
<p>Parser 的一个重要职责，是基于 scanner 输出的 tokens 进行语法分析，构建 AST。</p>
<p>TypeScript Parser 是一种 <strong>Recursive Descent Parser</strong>，这种 Parser 主要的技术特征，是使用一系列递归的函数，去处理语法中的符号。JavaScript 的语法是十分贴近 LL(1) 型的，因此即使 TypeScript 丰富了语法糖，在多数场景下，Parser 多往后消费一个 token 即可完成语法分析（look ahead），这使得 TypeScript Parser 是相对快的。</p>
<p>下面，让我们来研究一下，Parser 是如何工作的，并以装饰器为例，观察 token → AST Node 的过程。</p>
<img src="./assets-TS-decorator/ts-decoratorimage 4.png" rounded-lg>
<p>如上图源码，Parser 在创建 SourceFile 的过程中，会将源文件以 <code>ParsingContext</code> 为粒度，解析获得 statements，再由 <code>createSourceFile</code> ，得到包含 AST 信息的 SourceFile 对象。</p>
<p>ParsingContext 是一个枚举，可理解为 Parser 进行递归语法分析的作用域，此处 TypeScript 源码注释写得十分清晰。</p>
<img src="./assets-TS-decorator/ts-decoratorimage 5.png" rounded-lg>
<h3>parseList</h3>
<p>TypeScript 递归下降解析语法的步骤，就体现在 <code>parseList</code> 函数上。</p>
<img src="./assets-TS-decorator/ts-decoratorimage 6.png" rounded-lg>
<p>每个不同的 ParsingContext，其入口都是 <code>parseList</code> 函数。</p>
<p>在我们的 demo 中，第一个 token 是 <code>class</code> 关键词，于是 Parser 采用 <code>parseClassDeclaration</code> 的策略进行处理，在该调用栈中，会依次解析各个类成员。我们主要关心的是，装饰器 —— 比如 demo 中的方法装饰器 —— 是如何起作用的？</p>
<blockquote>
<p>建议配合 <a href="http://astexplorer.net">astexplorer.net</a> 或者 <a href="https://ts-ast-viewer.com/">https://ts-ast-viewer.com/</a> 食用。看过 AST Tree 或者熟悉 TS 原理的你可能已经发现了，装饰器在方法的 AST Node 中，存在于 modifiers 字段，而非 body。</p>
</blockquote>
<p>Parser 在递归调用中，会对每个 classElement，检查其 modifiers。</p>
<img src="./assets-TS-decorator/ts-decoratorimage 7.png" rounded-lg>
<p>此时，Parser 在处理完 <code>constructor</code> 的 AST Node 后，scanner 的当前 token 来到了 AtToken。Parser 发现了 AtToken 的存在，会尝试去解析装饰器语法，和我们预想的一致。</p>
<img src="./assets-TS-decorator/ts-decoratorimage 8.png" rounded-lg>
<img src="./assets-TS-decorator/ts-decoratorimage 9.png" rounded-lg>
<p>scanner 取了下一个 token，该 token 为 identifier(demo 中的 <code>yelling</code>)，于是，identifier 根据 token 信息，从 factory 中创建出一个 AST Node，挂在装饰器 Node 的 expression 字段上，大功告成。</p>
<p>最终，parseList 将 demo 中的 <code>src/index.ts</code> ，按照根级 <code>ParsingContext</code> ，分成了 5 个根级 AST Node，如同 astexplorer 和 ts-ast-viewer 所展示的那样。</p>
<img src="./assets-TS-decorator/ts-decoratorimage 10.png" rounded-lg>
<p>结合这些信息，Parser 便完成了 SourceFile 的创建。</p>
<h2>Codegen &amp; Emitter</h2>
<p>回到 <code>performCompilation</code> 函数的源码，我们对其进行追溯，发现最终还是用到了 <code>Program.emit</code> 方法。还记得上文提到的吗，Program 是一个可被编译的最小单元，它也承担了代码生成的任务。</p>
<h3>Transformers</h3>
<p>在 codegen 管线中，每个 SourceFile 都会被 <code>transformers</code> 依次转换，得到最后的结果交给 Printer 生成文字，写入磁盘。</p>
<img src="./assets-TS-decorator/ts-decoratorimage 11.png" rounded-lg>
<p>如上图，tranformers 管线将 SourceFile 的 AST，转换成了另一颗 AST。</p>
<blockquote>
<p>TypeScript Playground 中，可开启插件查看 Transform 各阶段的情况。但遗憾的是，截止写作此段的当天（2024-10-05），此插件无法作用在 demo 代码上。</p>
<img src="./assets-TS-decorator/ts-decoratorimage 12.png" rounded-lg>
</blockquote>
<p>对于装饰器语法来说，对应的 Transformer 为源码中的 <code>src/compiler/transformers/esDecorators.ts</code> 。每个 transformer 的核心，都在于其 <code>visitor</code> 方法。esDecorators 文件的 visitor 方法，针对 tc39 decorator stage3 proposal 具体规则，做了相应的实现。esDecorators Transform 之后，AST 就会带有 <code>__runInitializers</code>，<code>__esDecorate</code> 这些 Node。</p>
<p>以 demo 代码为例，SourceFile 中的 SyntaxKind.ClassDeclaration 结构的 AST Node（即 class 声明那一块）会被 <code>visitClassDeclaration</code> 处理返回。</p>
<img src="./assets-TS-decorator/ts-decoratorimage 13.png" rounded-lg>
<p>由于没有可视的 AST 结构，以笔者的水平，光靠 debugger 剖析过于困难，此处等哪天心血来潮了再详细完善吧。</p>
<p>本章水了很多字和图，其实只是通过一些源码片段，告诉读者装饰器是如何从字符串成为 AST Node 的，又是如何从 AST Node 成为 JavaScript 产物的。Type-Checker，LSP 等模块，TypeScript 整体精妙的架构设计与性能优化等，此处并未涉及，但若有兴趣，是比较值得研究的。</p>
<h1>EOF</h1>
]]></content:encoded>
            <author>younggglcy@gmail.com (younggglcy)</author>
        </item>
        <item>
            <title><![CDATA[毕业之随笔]]></title>
            <link>https://younggglcy.com/posts/graduation</link>
            <guid isPermaLink="true">https://younggglcy.com/posts/graduation</guid>
            <pubDate>Tue, 18 Jun 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[今日早晨，坐上开往深圳北的高铁，伴随着记忆里第一次乘坐一等座的些许新鲜感，一边时断时续地看着 NBA 总决赛 G5，一边写下此篇随笔，以总结这 4 年的大学生活。]]></description>
            <content:encoded><![CDATA[<p>[[toc]]</p>
<h2>前言</h2>
<p>在未更新博客的这段时间，我的主线时间花在了实习和学业上，空余时间大多放在玩 <s>原神</s> 游戏、刷手机、体育活动上。期间有十分长一段时间情绪都比较糟糕，屡次提笔记录下当前时间切片的「快照」，但均未成文。长久没有写东西，明显感觉到自己的语言表达能力、组织能力等等不如从前了，遣词造句花的时间也更久了，写作也杂乱无章，抓不住重点。</p>
<p>转念一想，本文的体裁既是「随笔」，草一点也无妨，无须像高中应试教育要求的议论文一般。当下也难得拥有几小时长的，相对安静的个人时间，也正适合随性行笔。</p>
<blockquote>
<p>复兴号一等座确实是舒适又安静，这下乡下人没见过世面了😭</p>
</blockquote>
<h2>毕业历程</h2>
<p>上个月底向公司请了长假，回校冲刺毕设项目和论文。因为离交初稿的 deadline 所剩无几，我在起飞的前一晚，写论文熬夜到凌晨 3、4 点，睡了 4 小时左右便启程赶往宝安机场了。但回到寝室后，室友营造的轻松氛围，让我焦虑的心情却逐渐平静下来了。睡在宿舍 90 厘米的床上，也并没有想象中的不适应，虽然这一年多来没回来住过几次。</p>
<p>是的，其实截止时间没有那么的紧，本科的答辩也没有那么严，是自己过度焦虑了。我答辩顺序靠后，学院要求的时间是 10 + 10，即学生答辩和答辩组老师提问各自占 10 分钟。前面的几位都是被严格把控在 20 分钟，而且老师们在学术、论文缺陷方面都提出了很刁钻的意见。轮到我时，已是过去近 5 小时。答辩组也早早做出调整，方向是普通研发类的论文，答辩直接视频演示作品，问答环节也快上不少。印象里是不到 5 分钟，我的答辩就结束了，精心做了几天的 PPT 也没讲，老师问的也较泛，很容易就答下来了。</p>
<p>答辩完到离校的这 10 多天，是我大学生涯以来最轻松的一段时光之一。只剩下按答辩组意见修改，提交论文最终稿，与办理毕业离校一系列手续这些事情要做。剩下来就是玩，哈哈。</p>
<h2>关于篮球的一章插播</h2>
<blockquote>
<p>见引言，当时正在高铁上边看球，边写作此文。</p>
</blockquote>
<p>插播：离 G5 结束还剩 3 分 40 秒，凯尔特人领先 26 分，比赛已经失去悬念，恭喜绿军 4-1 战胜独行侠，拿下 23-24 赛季 NBA 总冠军。从私心来讲，我更偏向东契奇领军的独行侠。我开始接触篮球是 14 年小学毕业左右，开始打球，看球是在初二，对应 15-16 赛季。那时，「大胡子」詹姆斯·哈登就已经带领休斯顿火箭打出不错的表现，哈登更是频频打出史诗级的个人表现，我也因此成为哈登粉丝。东契奇和哈登定位很像，都是持球大核型 MVP，都是球队顶级进攻火力，都有不俗的视野和传控，都是高球商等。今年独行侠的失败，是否又证实了，以这种打法的球星为核建队夺冠是不可行的？体系至上才是唯一解？</p>
<p>又说回哈登，感觉大概率会职业生涯无缘总冠军了，真挺惋惜的，18 年差一点跨过宇宙勇，篮网时期腿筋受伤 + 雄鹿不光彩地赢下系列赛 + 三巨头不欢而散，76 人时期和软蛋恩比德打不出化学反应（你别老站外线啊！），而今快船时期，不知道四巨头还能一起打多久。现在的哈登个人能力不如从前了，但是持球梳理进攻依然得心应手，也愿意防守了。</p>
<p>不继续写了，我不太懂球，诸君就图一乐罢。</p>
<h2>回顾</h2>
<p>简单回顾我的大学四年，在生活上是蛮平淡的，也没太多值得写的故事。</p>
<h3>大一</h3>
<p>踏入大学前，我满怀着对生活的憧憬与对知识的渴求。在畅想中，大学是一个思想自由、学术自由、生活自由的半社会化场所，记得和朋友聊天时，还放出大一 GPA 4.0+，勇攀学术高峰之类的豪言。然而，冰冷的现实打破了我的幻想，我不得不承认，大学在教育环境与制度上，没比高中强多少。</p>
<h4>关于教育的碎碎念</h4>
<p>我对于大学的期待，其实很大程度上来源于对义务教育的不满。</p>
<p>我是在当地最好、也是最严的一所私立中学念的初中。我很讨厌初中，但是即使坐时光机回到过去，我依然会去读那所中学，因为这是我身为小镇做题家的唯一出路。</p>
<p>和初中同学比，我家条件算是比较差的。家里老一辈，奶奶和外婆都是文盲，且只会说方言，不会说普通话。爷爷识一点字，会说足够日常生活使用的普通话。外公和我最疏远，但他文化水平是家里老人中最高的，识字，会说普通话，会书写。父亲长年酗酒，脾气古怪，并未很多地参与进对我的养育中，只是和天下父亲一样，望子成龙。父亲不满于家里现状，但是又没有能力改变什么。奶奶是典型的农村妇女形象，迷信，「笨」，只接受自己认知范围内的事物，以自己固化的那套旧理念待人接物处世，却又一辈子也没有勇气迈出认识哪怕一步。16 年浙江高考卷的阅读理解，是作家何家槐的《母亲》。《母亲》的主人公阿南婶长年在家操劳，听见火车经过的汽笛声总是心驰神往，叨唠着要是能看一看火车，邻居也劝她进城看一看，但她怎么样也没有去看，终生囿于自己的认知范围之内。阿南婶的文学形象，第一时间就使我想到了奶奶与许多亲戚。</p>
<p>我家是一家五口人住在乡镇，我的童年都伴随着对奶奶的讨厌，从来没有听过我的意见，固执已见地要喂我吃什么，给我穿什么，哪怕我知道奶奶是出于好心，我只要表演就可以让奶奶开心，但我就是做不到。父亲现在的无作为一定程度上也源于奶奶的认知毁掉了他的前途。父亲本可去读高中，去参军，或是出门闯荡寻求机会，但是奶奶让他留在农村干活了，因为以上罗列的未来都超出了奶奶的认知。去年过年回家，爷爷奶奶还跟我讲，深圳是落后的地方。</p>
<p>好在母亲起码受过中专程度的教育，也愿意接纳新鲜事物，愿意为了我的前途操心，以至于我可以走出小镇，来到武汉读大学，又到深圳工作。我很感激母亲的爱，但是母亲在一些场合也很「独断」。</p>
<p>童年种种加上初中变态的军事化管理和学习压力，让我极度讨厌义务教育对人的异化 —— 抹杀个性，剥夺自由，遏制思想，把人作为工具管理，同时也极度讨厌家 —— 在家里让我感到压抑。我承认我有一定的心理问题，我有时很难做到在一段亲密关系中，当面表达自己的要求，尤其对方和自己产生冲突时，以使得我总是把负面情绪积压在心底，导致亲密关系产生裂痕，不论是亲情还是爱情。</p>
<p>总而言之，我无比期望一个更加自由，更有人文关怀的环境。</p>
<p>说回大一，居然还要上晚自习，不准带笔记本电脑，形式主义的工作还不少。</p>
<blockquote>
<p>武汉理工请反思一下，计算机系不给带电脑我学什么😅？我大一军训期间还听话到在手机上敲代码😅</p>
</blockquote>
<p>学院的培养计划不能说差，计算机基本面都有涉及，但是教学内容浅薄，本科学又学不精，课还拖沓，浪费时间，还有近代史、大学物理这些没多大用的课要学。学院的规矩又死板，做不到让我跳出教学框架自学，再加上我学业水平有限，没达到申请免修、免听之类的条件，以至于我在大一下期间，就彻底放弃学业，开始思考未来，规划方向了。</p>
<p>氛围的缺失也是让我做出这一决定的重要一点。身边的同龄人大多数要么是压根不想上进，要么是自我感动式努力读书，要么是随波逐流地干这个干那个。这也是教育和社会的悲哀吧，主流声音都在谈论成绩，谈论梦想，谈论自我，谈论创造与突破的声音，小到让人听不到了。</p>
<p>说来惭愧，大一上册还尝试打过一段时间的 ACM，可惜太菜了，校集训队的选拔也以失败告终，遂放弃了算法这条路。</p>
<p>了解互联网开发是从高三毕业的暑假，在家读 <em>C Primer Plus</em> 一书，在水群时得知校 ACM 集训队的学长拿下了腾讯的 offer，月薪 2w+，给没见过世面的我留下了不小的震撼。诸君请勿笑话，中学时代的我只知道思春，打游戏，且被逼在学校封闭式地读书，又能对社会，对世界了解多少呢？在当时的我的认知里，7、8k 在县城中已经算是很高的薪资了。</p>
<blockquote>
<p>说来这也是教育缺失的一环吧？批评教育并非因我是愤青，而且切实地希望教育体系能尽可能地补上缺点，更加完善，培养真正的人才，而非耗材。</p>
</blockquote>
<p>书接上文，在大一入学时的百团大战期间，了解到 <a href="https://itoken.team/">Token（现名 Uni）团队</a> 在进行招新，特别感兴趣，于是就通过面试进入了技术部，开始接触互联网开发。所幸至今仍然保持着当初对于技术的热枕，并往当时的梦想 —— 成为优秀的全栈工程师而努力，奈何精力与能力十分有限，光是前端，还有很多没明白的地方。工作后，又疲于应付业务，距离成为全栈工程师遥遥无期。在 Token 时也遇到了很多非常不错的前辈，对我给予了很大的帮助。</p>
<p>大一还有值得一谈的一个乐子，和一个悲伤的故事。乐子是，我在考大学物理前，因熬夜复习过于疲惫，直接睡过头导致缺考，致使挂科。难过的是，和前任因异地而分手了，一整个学期都间歇性的郁郁寡欢，也怪自己不善经营感情，对她投入的也不够多。穷也是一方面，生活费堪堪只够自用，。时间细水长流，却是褪去了当初鲜艳的色彩，留下布满皱褶的回忆。</p>
<p>最重要的是，在大一，我想清楚了未来希望做什么，想要什么样的生活，并为每个阶段制定了大致的计划。回过头看，虽然许多计划都没有完成，我也并没有成为想成为的人，过上想要的生活，但好在我沿着自己划定的路线曲折地前进中。</p>
<h3>大二</h3>
<p>虽说在大一已经定下了目标，战略性放弃了学业，但未能学习计算机更深、更底层、更前沿的领域的知识，多少有些遗憾。</p>
<p>我的记忆像是一个极小、且采用 LRU 策略存储的内存。对于大二一整年，也没剩下多少印象了，也或许是情绪触发了大脑的某种保护机制，封印住了不常用空间。</p>
<p>这一年的主旋律，是痛苦。</p>
<p>一是生活上的痛苦。大一有个精神异常（字面意思，非贬义）的逆天室友，时常吼叫加情绪崩溃，好在后来出去住了，不然我也要情绪崩溃了。大二因专业分流换校区，宿舍也换到了新建的六人寝。但是舍友的作息极其逆天，哪怕是白天有课，也都是凌晨2点以后才睡觉。不是在聊天，就是在打游戏。我不得不每天跟着一起熬夜，再加带上耳塞睡觉，长此以往，感觉自己身体有些垮，精神也有些虚弱。</p>
<p>二是技术成长上的痛苦。大一做了不少项目，有微信小程序，C 端的外包项目和杂七杂八的小项目，在前端开发的领域，基本上是入门了。可是在成为真正工程师的路上，还只是迈出了一小步。我的计算机知识体系，算法基础，后端开发能力，Linux 相关基础都很差。因此整个大二期间，就相对没有目标地，在技能树各个分支上胡乱加点，结果倒不令我太满意，十八般武艺，样样疏忽，皆是三脚猫功夫。</p>
<h3>大三——大四</h3>
<p>我的学习能力和技术能力，在认识的同龄人中，都算不上强，但好在笨鸟先飞，在简历上多少能包装糊弄下。在大三上册，我就开始为日常实习做准备了，刷题，背面经，看看源码。</p>
<p>自打我念大学以来，武汉就处于时不时疫情封控的状态。22 年的年末，武汉各大高校叕封控了。忘了是哪个高校开了个头放假，武汉大学生就一股脑地都放假了。这一放也打乱了我的节奏，遂准备先前往已拿到 offer 的上海一家小公司，实习一段时间再做打算。</p>
<blockquote>
<p>也是第一个 offer，面试过程很愉快</p>
</blockquote>
<p>好不容易租好房，安顿下来，结果刚实习两天，就在周五晚上 8 点多接到了腾讯 HR 的 offer call。于是决定去腾讯日常实习，领了 500 元的两天实习工资就离职了。HR 说部门在上海也有办公的地方，不过没给实习生留位置，外加大本营在深圳，因此回到家中等待正式的 offer 邮件。</p>
<blockquote>
<p>反观腾讯，是我认为面试体验最差的公司之一。</p>
</blockquote>
<blockquote>
<p>p.s，从上海回来当天就阳了，HR 那边也因阳了在家休假，晚发一周 offer，导致我错过了腾讯的开工利是。</p>
</blockquote>
<blockquote>
<p>p.p.s，「利是」据我所知，是广东地区的专有说法。我理解和红包是一个意思。</p>
</blockquote>
<h4>开始实习</h4>
<p>23 年 1 月初，在深圳的实习生活正式开始。尽管坐落于城中村的出租屋不大，是个大概 20 平左右的大单间，但是床、办公桌、独立卫浴、厨房和该有的家具都有，满足了我的人生愿望之一 —— 「独立生活」。</p>
<p>「独立生活」是指我自初中以来就有的人生愿望。拥有属于自己的一片空间，让我无比享受。但紧接着，是山顶后陡峭的滑坡，让我坠向低谷。</p>
<p>一方面是，我很难把握住工作/生活之中的一个平衡点，经常顾此失彼。如果埋头工作，晚上回到家就没有足够的时间留给自己；如果早点下班回家，工作上又没有足够的产出，项目无法延期交付。我实习了近一年，才勉强找到这个平衡点。</p>
<p>另一方面是，感觉工作上糊业务，并没有给我带来多少技术上的成长。保证实习期间产出加持续面试寻找机会，也带来了不少压力。</p>
<p>运气使然，4 月得到机会，来到腾讯文档实习。工作内容相对普通的开发，更有挑战一些，故而算不上讨厌。秋招因准备不足，没能斩获 offer，因此便留在腾讯文档一直实习到毕业了。</p>
<p>这一年多来，每月都有一两天晚上是失眠的，身体感觉异常疲惫，精神反倒很清醒。好则 4-5 点入睡，坏则彻夜无眠。也不清楚是哪造成的问题，如何去缓解。总之最近的状态有所好转，也在尝试尽量早睡，改掉熬夜的陋习。</p>
<p>在离校当天，发了一条朋友圈，写着「希望大家都前途似锦，活出自己想要的人生」，实则也是对自己的激励。我活出自己想要的人生了吗？起码目前没有。</p>
<p>熬夜的坏习惯没有改过来，身体不太健康。工作能力在组里处于低下水平。想养一只猫，但是没有做好准备。打了多年的炉石传说，篮球，依然很菜。许多要做的事情，不能安排出合理地行程去完成。</p>
<p>希望这些问题都能慢慢被克服，与诸君共勉。</p>
<h2>总结</h2>
<p>题笔至此，却已是 25 日的凌晨 12 点半。趁 618 购入了不少小米的智能家居，生活体验有所改善，对生活整体仍有些不满意的地方。少年心气早便被磨灭至尽，而今这样得过且过的躺平生活，竟也差强人意。编程算是个为数不多的爱好，是个与世界沟通的枢纽，在最后，希望能多留下些代码，提升下水平吧。</p>
]]></content:encoded>
            <author>younggglcy@gmail.com (younggglcy)</author>
        </item>
        <item>
            <title><![CDATA[`@antfu/ni` - 一个简单又好用的工具]]></title>
            <link>https://younggglcy.com/posts/ni</link>
            <guid isPermaLink="true">https://younggglcy.com/posts/ni</guid>
            <pubDate>Thu, 14 Jul 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[[`ni`](https://github.com/antfu/ni) 是由 antfu 开源的一个工具，能让开发者使用正确的包管理工具(npm, yarn, pnpm, bun)，非常简单易用。其源码实现也不是太困难，让我们来一起研究一下。]]></description>
            <content:encoded><![CDATA[<p>[[toc]]</p>
<h2>什么是 <code>ni</code></h2>
<p><a href="https://github.com/antfu/ni"><code>ni</code></a> 是为了能让开发者使用正确的包管理而诞生的，它的原理是检测你的 lockfile ,即(<code>yarn.lock</code> / <code>pnpm-lock.yaml</code> / <code>package-lock.json</code> / <code>bun.lockb</code>)，或者根据 <code>package.json</code> 里的 <code>packageManager</code> 字段来检测你用的是哪个包管理。比如一个 pnpm 项目，在终端下敲 <code>ni</code>，就会执行 <code>pnpm install</code>; 敲 <code>nr dev</code>，就会执行 <code>pnpm run dev</code>。</p>
<blockquote>
<p>很巧的是，就在本文开始撰写的当天，<code>ni</code> 发布了新版本，支持了 <a href="https://bun.sh/"><code>bun</code></a> 这个新兴的 js 运行时</p>
</blockquote>
<p>它一共有这么些命令可供使用</p>
<ul>
<li><code>ni</code> - install</li>
<li><code>nr</code> - run</li>
<li><code>nx</code> - execute</li>
<li><code>nu</code> - upgrade</li>
<li><code>nun</code> - uninstall</li>
<li><code>nci</code> - clean install</li>
<li><code>na</code> - agent alias</li>
</ul>
<h2>为什么要用 <code>ni</code></h2>
<p>因为懒！相信 antfu 开发这款工具时一定也是抱着相同的心态。每次 <code>clone</code> 一个项目之后，都要先去看一看作者用的是什么包管理工具，再去跑 <code>scripts</code>，太麻烦了。而且 <code>ni</code> 提供的命令十分简短、易记，实在是太香了！</p>
<p><s><em><code>npm i</code> in a yarn project, again? F* * k!</em></s></p>
<h2><code>ni</code> 是如何实现的</h2>
<p>其核心逻辑如下</p>
<ol>
<li>解析输入的命令</li>
<li>检测包管理工具</li>
<li>将其执行</li>
</ol>
<p>命令的源码都在 src/commands 下，大多数源码，都形如以下形式</p>
<pre><code class="language-typescript">// src/commands/ni.ts
import { parseNi } from '../parse'
import { runCli } from '../runner'

runCli(parseNi)
</code></pre>
<p><code>runCli</code> 函数的源码是</p>
<pre><code class="language-typescript">// src/runner.ts
export type Runner = (agent: Agent, args: string[], ctx?: RunnerContext) =&gt; Promise&lt;string | undefined&gt; | string | undefined

export async function runCli(fn: Runner, options: DetectOptions = {}) {
  const args = process.argv.slice(2).filter(Boolean)
  try {
    await run(fn, args, options)
  }
  catch (error) {
    process.exit(1)
  }
}
</code></pre>
<p>其通过 <code>process.argv</code> 拿到用户在命令行所输入的附带的参数，然后执行 <code>run(fn, args, options)</code></p>
<p>可以看到核心就是 <code>run</code> 函数了，让我们来分析分析。</p>
<pre><code class="language-typescript">export async function run(fn: Runner, args: string[], options: DetectOptions = {}) {
  // 01  是否开启debug模式
  const debug = args.includes(DEBUG_SIGN)
  if (debug)
    remove(args, DEBUG_SIGN)

  let cwd = process.cwd()
  let command

  // 02 -C option, 表示Change directory
  if (args[0] === '-C') {
    cwd = resolve(cwd, args[1])
    args.splice(0, 2)
  }

  // 03 处理 带有-g 的命令 
  const isGlobal = args.includes('-g')
  if (isGlobal) {
    command = await fn(await getGlobalAgent(), args)
  }
  else {
    // 04 检测包管理工具
    let agent = await detect({ ...options, cwd }) || await getDefaultAgent()
    if (agent === 'prompt') {
      agent = (await prompts({
        name: 'agent',
        type: 'select',
        message: 'Choose the agent',
        choices: agents.filter(i =&gt; !i.includes('@')).map(value =&gt; ({ title: value, value })),
      })).agent
      if (!agent)
        return
    }
    // 05 生成相应命令
    command = await fn(agent as Agent, args, {
      hasLock: Boolean(agent),
      cwd,
    })
  }

  if (!command)
    return

  // 06 针对 volta 做特殊处理
  const voltaPrefix = getVoltaPrefix()
  if (voltaPrefix)
    command = voltaPrefix.concat(' ').concat(command)

  if (debug) {
    // eslint-disable-next-line no-console
    console.log(command)
    return
  }
	
  // 07 执行相应命令
  await execaCommand(command, { stdio: 'inherit', encoding: 'utf-8', cwd })
}
</code></pre>
<p>笔者将逻辑剖析成了7部分</p>
<h3>01 debug模式</h3>
<p>在同文件下，有一个 <code>DEBUG_SIGN</code> 变量</p>
<pre><code class="language-typescript">// src/runner.ts
const DEBUG_SIGN = '?'
</code></pre>
<p>见 <code>run()</code> 函数的49-53行</p>
<pre><code class="language-typescript">if (debug) {
  // eslint-disable-next-line no-console
  console.log(command)
  return
}
</code></pre>
<p>即在 debug 模式下，会在控制台打印解析出来的命令，不会执行。我们可以试试，以 <code>ni</code> 本身项目为例子</p>
<img src="./assets-ni/ni-1-dark.png" rounded-lg img-dark alt="ni debug mode example" />
<img src="./assets-ni/ni-1-light.png" rounded-lg img-light alt="ni debug mode example" />
<h3>02 -C option</h3>
<p><code>-C</code> 代表 change directory,即更换相应的目录。</p>
<pre><code class="language-typescript">if (args[0] === '-C') {
  // resolve是从node的path包中导入的
  // 在这里，cwd更新为新的目录,并把-C [name]从args中删除
  cwd = resolve(cwd, args[1])
  args.splice(0, 2)
}
</code></pre>
<p>同样的，举个例子</p>
<img src="./assets-ni/ni-2-dark.png" rounded-lg img-dark alt="ni with -C option" />
<img src="./assets-ni/ni-2-light.png" rounded-lg img-light alt="ni with -C option" />
<p><code>-C ni</code> 代表我们切换到了 <code>projects/ni</code> 目录</p>
<h3>03 -g</h3>
<p>既然带有 <code>-g</code> 参数，那么策略就是试图去寻找全局的包管理。反映在代码中，可以看到给 fn 的第一个参数是 <code>await getGlobalAgent()</code>。antfu 采用的逻辑是，先看 <code>package.json</code> 中是否存在 <code>packageManager</code>，若没有的话，则采用 <code>ni</code> 的全局配置(在 <code>.nirc</code> 文件中)，还是没有，那么就给默认值(<code>npm</code>)</p>
<p>其实现如下</p>
<pre><code class="language-typescript">import { findUp } from 'find-up'
import ini from 'ini'

interface Config {
  defaultAgent: Agent | 'prompt'
  globalAgent: Agent
}

const defaultConfig: Config = {
  defaultAgent: 'prompt',
  globalAgent: 'npm',
}

let config: Config | undefined

export async function getConfig(): Promise&lt;Config&gt; {
  // 这个条件相当于做了“缓存“处理，优化了细节，学习了
  if (!config) {
    // 这里的findUp是从find-up中导入的。find-up是一个不错的包，用于寻找某文件
    const result = await findUp('package.json') || ''
    let packageManager = ''
    if (result)
      packageManager = JSON.parse(fs.readFileSync(result, 'utf8')).packageManager ?? ''
    
    // 利用正则的()去做捕获组，捕获出agent和version，学习了
    // Object.values(LOCKS)的结果是[&quot;npm&quot;, &quot;pnpm&quot;, &quot;yarn@berry&quot;,&quot;yarn&quot;,&quot;pnpm@6&quot;,&quot;bun&quot;]
    const [, agent, version] = packageManager.match(new RegExp(`^(${Object.values(LOCKS).join('|')})@(\d).*?$`)) || []
    if (agent)
      config = Object.assign({}, defaultConfig, { defaultAgent: (agent === 'yarn' &amp;&amp; parseInt(version) &gt; 1) ? 'yarn@berry' : agent })
    else if (!fs.existsSync(rcPath))
      config = defaultConfig
    else
      config = Object.assign({}, defaultConfig, ini.parse(fs.readFileSync(rcPath, 'utf-8')))
  }
  return config
}

export async function getGlobalAgent() {
  const { globalAgent } = await getConfig()
  return globalAgent
}
</code></pre>
<p>那么，<code>command = await fn(await getGlobalAgent(), args)</code>中，<code>fn</code> 的实现又是怎么样的？别急，在[05](###05 生成相应命令)会讲</p>
<h3>04 检测包管理工具</h3>
<p>核心在这一行</p>
<pre><code class="language-typescript">let agent = await detect({ ...options, cwd }) || await getDefaultAgent()
</code></pre>
<p><code>getDefaultAgent()</code> 的实现和 <code>getGlobalAgent()</code> 十分类似</p>
<pre><code class="language-typescript">export async function getDefaultAgent() {
  const { defaultAgent } = await getConfig()
  // process.env.CI又是一个细节
  // 这里的CI就是我们老生常谈的CI/CD中的CI
  // 像Github Actions, Netlify这种做CI的工具，会将process.env.CI设置成true
  if (defaultAgent === 'prompt' &amp;&amp; process.env.CI)
    return 'npm'
  return defaultAgent
}
</code></pre>
<p><code>detect()</code> 实现如下</p>
<pre><code class="language-typescript">export interface DetectOptions {
  autoInstall?: boolean
  cwd?: string
}

// LOCKS 定义如下
LOCKS: Record&lt;string, Agent&gt; = {
  'bun.lockb': 'bun',
  'pnpm-lock.yaml': 'pnpm',
  'yarn.lock': 'yarn',
  'package-lock.json': 'npm',
  'npm-shrinkwrap.json': 'npm',
}

export async function detect({ autoInstall, cwd }: DetectOptions) {
  let agent: Agent | null = null

  const lockPath = await findUp(Object.keys(LOCKS), { cwd })
  let packageJsonPath: string | undefined

  if (lockPath)
    packageJsonPath = path.resolve(lockPath, '../package.json')
  else
    packageJsonPath = await findUp('package.json', { cwd })

  // read `packageManager` field in package.json
  if (packageJsonPath &amp;&amp; fs.existsSync(packageJsonPath)) {
    try {
      const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
      if (typeof pkg.packageManager === 'string') {
        const [name, version] = pkg.packageManager.split('@')
        if (name === 'yarn' &amp;&amp; parseInt(version) &gt; 1)
          agent = 'yarn@berry'
        else if (name === 'pnpm' &amp;&amp; parseInt(version) &lt; 7)
          agent = 'pnpm@6'
        else if (name in AGENTS)
          agent = name
        else
          console.warn('[ni] Unknown packageManager:', pkg.packageManager)
      }
    }
    catch {}
  }

  // detect based on lock
  if (!agent &amp;&amp; lockPath)
    agent = LOCKS[path.basename(lockPath)]

  // auto install
  if (agent &amp;&amp; !cmdExists(agent.split('@')[0])) {
    if (!autoInstall) {
      console.warn(`[ni] Detected ${agent} but it doesn't seem to be installed.\n`)

      if (process.env.CI)
        process.exit(1)

      const link = terminalLink(agent, INSTALL_PAGE[agent])
			// prompts包用于命令行交互
      // 即用户未下载该包管理时，提示其是否需要下载
      const { tryInstall } = await prompts({
        name: 'tryInstall',
        type: 'confirm',
        message: `Would you like to globally install ${link}?`,
      })
      if (!tryInstall)
        process.exit(1)
    }

    // 从execa包中导入的，用于执行命令
    await execaCommand(`npm i -g ${agent}`, { stdio: 'inherit', cwd })
  }

  return agent
}
</code></pre>
<p>可以看到，<code>packageManager</code> 字段拥有最高的优先级，其次是根据 <code>lockfile</code> 获取相应的包管理。</p>
<p>如果都没取到结果，那么轮到 <code>getDefaultAgent()</code> 执行，它的结果是 <code>prompt</code></p>
<p>结合代码可知，是让用户自己选择一款包管理</p>
<h3>05 生成相应命令</h3>
<pre><code class="language-typescript">// 05 生成相应命令
command = await fn(agent as Agent, args, {
  hasLock: Boolean(agent),
  cwd,
})
</code></pre>
<p>到这里，就要回头看看 <code>fn</code> 是如何实现的。以 <code>parseNi</code> 为例</p>
<pre><code class="language-typescript">// AGENTS	这一结构存储了相应的包管理以及其命令
// 如
/* AGENTS = {
  'npm': {
    // ...
    'install': 'npm i {0}',
    // ...
  },
  // ...
}
*/
export function getCommand(
  agent: Agent,
  command: Command,
  args: string[] = [],
) {
  if (!(agent in AGENTS))
    throw new Error(`Unsupported agent &quot;${agent}&quot;`)

  // 取出相应的原始命令
  const c = AGENTS[agent][command]

  if (typeof c === 'function')
    return c(args)

  if (!c)
    throw new Error(`Command &quot;${command}&quot; is not support by agent &quot;${agent}&quot;`)
  // 替换成可执行的命令
  return c.replace('{0}', args.join(' ')).trim()
}

export const parseNi = &lt;Runner&gt;((agent, args, ctx) =&gt; {
  if (args.length === 1 &amp;&amp; args[0] === '-v') {
    // eslint-disable-next-line no-console
    console.log(`@antfu/ni v${version}`)
    process.exit(0)
  }

  // bun use `-d` instead of `-D`, #90
  if (agent === 'bun')
    args = args.map(i =&gt; i === '-D' ? '-d' : i)

  if (args.includes('-g'))
    return getCommand(agent, 'global', exclude(args, '-g'))

  if (args.includes('--frozen-if-present')) {
    args = exclude(args, '--frozen-if-present')
    return getCommand(agent, ctx?.hasLock ? 'frozen' : 'install', args)
  }

  if (args.includes('--frozen'))
    return getCommand(agent, 'frozen', exclude(args, '--frozen'))

  if (args.length === 0 || args.every(i =&gt; i.startsWith('-')))
    return getCommand(agent, 'install', args)

  return getCommand(agent, 'add', args)
})
</code></pre>
<p>逻辑：替换成相应包管理的相应命令（提前写好的常量），并将参数置入，得到一个可执行的命令</p>
<h3>06 针对 volta 做特殊处理</h3>
<p><a href="https://volta.sh/">volta</a> 是一款JS工具管理器，本文不过多介绍</p>
<h3>07 执行相应命令</h3>
<p>用到了 <code>execa</code> 包的 <code>execaCommand</code> 来执行</p>
<h2>小结</h2>
<p>这回读了源码，了解到了Node的些许知识，以及<code>fast-glob</code>，<code>execa</code>, <code>unbuild</code> 这些很不错的第三方库，收获不少</p>
]]></content:encoded>
            <author>younggglcy@gmail.com (younggglcy)</author>
        </item>
        <item>
            <title><![CDATA[阅读 Koa 源码小记]]></title>
            <link>https://younggglcy.com/posts/Koa</link>
            <guid isPermaLink="true">https://younggglcy.com/posts/Koa</guid>
            <pubDate>Fri, 20 May 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[Koa 向来以小而美著称，简洁且易于上手，是小项目开发的不二选择。
笔者旨在学习 Koa2.13.1 源码的设计与实现]]></description>
            <content:encoded><![CDATA[<p>[[toc]]</p>
<h2>前置工作</h2>
<p>上 Github 找到相应的 <a href="https://github.com/koajs/koa/releases/tag/2.13.1">Koa 版本</a>，下载过来并解压</p>
<h2>初窥全貌</h2>
<p>浏览 Koa 的 <code>package.json</code> 文件，发现</p>
<pre><code class="language-json">{
  &quot;main&quot;: &quot;lib/application.js&quot;,
  &quot;exports&quot;: {
    &quot;.&quot;: {
      &quot;require&quot;: &quot;./lib/application.js&quot;,
      &quot;import&quot;: &quot;./dist/koa.mjs&quot;
    }
  }
}

</code></pre>
<p>由此不难看出，lib 文件夹下的 <code>application.js</code> 正是 Koa 的主体所在。此外，Koa 的源码全部在 lib 文件夹下，而 lib 文件夹十分简洁，只有四个文件</p>
<ul>
<li>application.js       Koa 主体</li>
<li>context.js           Koa 上下文</li>
<li>request.js           封装request</li>
<li>response.js         封装response</li>
</ul>
<p>总的来看 <code>application.js</code> 的代码，能发现 Koa 的实现其实并不复杂。其核心部分如下</p>
<pre><code class="language-js">const Emitter = require('events')

module.exports = class Application extends Emitter {
  constructor(options) {
    super()

    // initalize...
  }

  // 起一个服务器
  listen(...args) { }

  // 注册middleware
  use(fn) { }
}
</code></pre>
<p>是的，就是这么简单，和我们使用 Koa 一样简单！</p>
<p>整个 Koa 应用是一个基于 Node 里面的事件触发器的类。它在 Node 中有着相当重要的地位，我们看看<a href="http://nodejs.cn/api/events.html">官网</a>是怎么说的</p>
<blockquote>
<p>Node.js 的大部分核心 API 都是围绕惯用的异步事件驱动架构构建的，在该架构中，某些类型的对象（称为&quot;触发器&quot;）触发命名事件，使 <code>Function</code> 对象（&quot;监听器&quot;）被调用。</p>
<p>例如：<a href="http://nodejs.cn/api/net.html#class-netserver"><code>net.Server</code></a> 对象在每次有连接时触发事件；<a href="http://nodejs.cn/api/fs.html#class-fsreadstream"><code>fs.ReadStream</code></a> 在打开文件时触发事件；<a href="http://nodejs.cn/api/stream.html">流</a>在每当有数据可供读取时触发事件。</p>
<p>所有触发事件的对象都是 <code>EventEmitter</code> 类的实例。这些对象暴露了 <code>eventEmitter.on()</code> 函数，允许将一个或多个函数绑定到对象触发的命名事件。</p>
</blockquote>
<blockquote>
<p>以下示例展示了使用单个监听器的简单的 <code>EventEmitter</code> 实例。 <code>eventEmitter.on()</code> 方法用于注册监听器，<code>eventEmitter.emit()</code> 方法用于触发事件。</p>
<pre><code class="language-js">const EventEmitter = require('node:events')

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter()
myEmitter.on('event', () =&gt; {
  console.log('an event occurred!')
})
myEmitter.emit('event')
</code></pre>
</blockquote>
<p>这种经典的 <code>.on</code> 式的回调写法，在 Node 中无处不在，如 http.Server 等。可以说 <code>EventEmitter</code> 是 Node 异步 IO 机制的基石</p>
<h2>中间件模式</h2>
<p>提到 Koa，就免不了要提到其中间件模式。它正是 Koa 设计上的精髓所在。请求到了服务器，依次按序被注册的中间件所处理。其相关实现并不复杂</p>
<pre><code class="language-js">class Application extends Emitter {
  constructor(options) {
    super()
    this.middleware = [] // 存放中间件
    // ...
  }

  use(fn) {
    if (typeof fn !== 'function')
      throw new TypeError('middleware must be a function!')
    debug('use %s', fn._name || fn.name || '-')
    this.middleware.push(fn)
    return this // 提供链式调用的能力
  }
}
</code></pre>
<p>处理中间件模式的 <code>Server</code> 结构是这样的</p>
<pre><code class="language-js">const compose = require('koa-compose')
const onFinished = require('on-finished')

class Application extends Emitter {
  listen(...args) {
    debug('listen')
    const server = http.createServer(this.callback()) // 通过原生的http.createServer创建
    return server.listen(...args)
  }

  // 为Node的原生http server返回一个request handler
  callback() {
    // 将所有的中间件组合成一个函数，该函数返回一个Promise
    const fn = compose(this.middleware)

    // 如果没有注册error回调，则注册默认的onerror
    if (!this.listenerCount('error'))
      this.on('error', this.onerror)

    const handleRequest = (req, res) =&gt; {
      const ctx = this.createContext(req, res)
      return this.handleRequest(ctx, fn)
    }

    return handleRequest
  }

  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res
    res.statusCode = 404
    const onerror = err =&gt; ctx.onerror(err)
    const handleResponse = () =&gt; respond(ctx)
    // on-finished包的作用是
    // Execute a callback when a HTTP request closes, finishes, or errors.
    // 这行代码会在res出错时，执行onerror函数
    onFinished(res, onerror)
    // ctx会作为最初的context，传给中间件
    return fnMiddleware(ctx).then(handleResponse).catch(onerror)
  	}

  // 根据请求与响应，创建上下文
  createContext(req, res) {

  }

  // 默认error handler
  onerror(err) {

  }
}

// response helper
function respond(ctx) {
  // ...
}
</code></pre>
<h3>koa-compose</h3>
<p>接上，我们先来看 <a href="https://github.com/koajs/compose">koa-compose</a> 这个工具，它用于将中间件整合起来。其源码只有短短48行，相当精简。直接贴上附带注释的源码</p>
<pre><code class="language-js">'use strict'

/**
 * Expose compositor.
 */

module.exports = compose

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

function compose(middleware) {
  if (!Array.isArray(middleware))
    throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function')
      throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch(i) {
      if (i &lt;= index)
        return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length)
        fn = next
      if (!fn)
        return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      }
      catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
</code></pre>
<p>其核心就在于 <code>dispatch</code> 函数。该函数依次取出 <code>middleware</code> 中的函数，并将其通过 <code>Promise.resolve()</code> 串联在一起。只要 <code>middleware</code> 中的函数执行了 <code>next()</code>，下一个函数也将紧跟着执行，直到遍历完整个 <code>middleware</code> 数组。得益于 <strong>Event Loop</strong>，<code>Promise.resolve()</code> 使所有异步函数依次在微任务队列里执行完。这里还用 <code>index</code> 指向上一个被调用的 <code>middleware function</code>，所以出现了闭包结构</p>
<h2>Settings</h2>
<p>Koa 还支持<a href="https://koajs.com/#application">设置实例的一些属性</a>，如 <code>app.env</code>, <code>app.keys</code> 等</p>
<p>这个就比较简单了,其相关实现如下</p>
<pre><code class="language-js">/**
    *
    * @param {object} [options] Application options
    * @param {string} [options.env='development'] Environment
    * @param {string[]} [options.keys] Signed cookie keys
    * @param {boolean} [options.proxy] Trust proxy headers
    * @param {number} [options.subdomainOffset] Subdomain offset
    * @param {string} [options.proxyIpHeader] Proxy IP header, defaults to X-Forwarded-For
    * @param {number} [options.maxIpsCount] Max IPs read from proxy IP header, default to 0 (means infinity)
    *
    */

  constructor (options) {
    super()
    options = options || {}
    this.proxy = options.proxy || false
    this.subdomainOffset = options.subdomainOffset || 2
    this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For'
    this.maxIpsCount = options.maxIpsCount || 0
    this.env = options.env || process.env.NODE_ENV || 'development'
    if (options.keys) this.keys = options.keys
  }
</code></pre>
<h2>request，response，context</h2>
<p>在 Node 的原生 http 模块中，<a href="http://nodejs.cn/api/http.html#httpcreateserveroptions-requestlistener"><code>http.createServer</code></a> 接受 <code>requestListener</code> 回调函数作为其参数。该函数的两个参数 <code>request</code> 和 <code>response</code>，是分别基于 <code>http.IncomingMessage</code> 类和 <code>http.ServerResponse</code> 类的。在 Koa 中，为了简化、方便开发者对其的处理，Koa 自己封装了 <code>request</code> 和 <code>response</code>，可以理解为对原生 <code>IncomingMessage</code> 和 <code>ServerResponse</code> 的一层抽象</p>
<p><code>context</code> 则是将 <code>request</code> 和 <code>response</code> 对象封装成一个对象，为开发提供了很多有用的属性与 API</p>
<p>lib 文件夹的三个相应的文件，正是书写了这三者的 <code>Prototype</code>，在 Koa 主体中以 <code>Object.create(proto)</code> 的方式使用</p>
<p><code>context.js</code> 中还利用了 <code>delegates</code> 这个年久失修的包，凭借委托的设计模式来控制 <code>context</code> 上的 <code>request</code> 和 <code>response</code> 的行为</p>
<p>例如</p>
<pre><code class="language-js">const delegate = require('delegates')

const proto = module.exports = { }

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
</code></pre>
<p>其<a href="https://github.com/tj/node-delegates/blob/master/index.js#L107">源代码</a>里居然还出现了用 <code>proto.__defineGetter__</code> 来改写[[ Get ]]行为的写法。17年有位老哥提了个用 <code>Object.defineProperty</code> 代替的 <a href="https://github.com/tj/node-delegates/pull/20">PR</a>，也没人管...</p>
<p>详细内容，读者若有兴趣可自行查阅，文档与源码照着一起看</p>
<h2>小结</h2>
<p>这应该是笔者首次尝试去阅读一个开源项目的源码，受益良多。希望日后能不断地阅读优秀源码，不断变强</p>
]]></content:encoded>
            <author>younggglcy@gmail.com (younggglcy)</author>
        </item>
        <item>
            <title><![CDATA[JS 之原型篇]]></title>
            <link>https://younggglcy.com/posts/JS-prototype</link>
            <guid isPermaLink="true">https://younggglcy.com/posts/JS-prototype</guid>
            <pubDate>Wed, 11 May 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[深入浅出，揭开 **Prototype** 的神秘面纱！]]></description>
            <content:encoded><![CDATA[<p>[[toc]]</p>
<h2>原型与原型链</h2>
<p>一言以蔽之：<strong>原型，是一种机制，对象的存在与行为正是建立于这种机制之上的;对象之间基于原型建立起来的联系，就是原型链</strong></p>
<h3>Prototype-based VS Class-based</h3>
<p>原型和对象是紧密关联的。笔者希望从对象来入手，剖析原型。作为程序员的你，对于<strong>对象</strong>这个词一定不会陌生。然而，JS 里的对象不同于 C++, Java 里的。前者是基于原型的，后者是基于类的</p>
<p>来点硬核的，我们直接上 <a href="https://262.ecma-international.org/12.0/#sec-objects">ECMAScript 标准</a>，看看这两者的区别是什么</p>
<blockquote>
<p>In a class-based object-oriented language, in general, state is carried by instances, methods are carried by classes, and inheritance is only of structure and behaviour. In ECMAScript, the state and methods are carried by objects, while structure, behaviour, and state are all inherited</p>
<p>译：在基于类的面向对象语言中，一般情况下，(对象的)状态保存在实例中，方法保存在类中，继承只是结构和行为。在 ECMAScript 中，状态和方法由对象承载，而结构、行为和状态都是继承的</p>
</blockquote>
<p><a href="https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/Object_prototypes#%E5%9F%BA%E4%BA%8E%E5%8E%9F%E5%9E%8B%E7%9A%84%E8%AF%AD%E8%A8%80%EF%BC%9F">MDN上的解释</a>是</p>
<blockquote>
<p>在传统的 OOP 中，首先定义“类”，此后创建对象实例时，类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制——而是在对象实例和它的构造器之间建立一个链接（它是__proto__属性，是从构造函数的 <code>prototype</code> 属性派生的），之后通过上溯原型链，在构造器中找到这些属性和方法</p>
</blockquote>
<p>对比了两者的解释，个人对 Class-based 方式在此存疑。上文所述 ECMA 标准中，如果 <em>carried</em> 的意思是&quot;对内存持有引用&quot;，那么实例中的方法是对类中方法的引用而非复制，这和MDN上的解释是冲突的。咨询了<a href="https://github.com/CodingPlatelets">血小板</a>学长之后，个人倾向于 ECMA 标准的解释</p>
<p>上面两段话十分深奥，难以理解。不过没关系，读者只需要知道JS里的对象是特殊的、不同于传统OOP的即可</p>
<p>回到正题，在 JS 中，通常我们有两种方式去声明一个对象</p>
<ul>
<li>
<p>字面量声明形式</p>
<pre><code class="language-js">const a = {
  name: 'zhangsan'
}
</code></pre>
</li>
<li>
<p>构造形式（<strong>new</strong> 操作符调用 constructor 函数）</p>
<pre><code class="language-js">function b() {
  this.name = 'zhangsan'
}

const a = new b()
</code></pre>
</li>
</ul>
<p>事实上，字面量声明形式可以看作是一种语法糖。我们可以这样子等价改写成构造形式</p>
<pre><code class="language-js">const a = new Object()
a.name = 'zhangsan'
</code></pre>
<p>所以说不论是哪种形式的声明，都可以看作是 <strong>new</strong> + <strong>constructor 函数</strong>这种形式。在这种形式下，<code>new</code> 操作符会让 JS 引擎做如下事情</p>
<ol>
<li>
<p>创建（或者说构造）一个全新的对象</p>
</li>
<li>
<p>这个新对象会被执行 [[ Prototype ]] 连接</p>
</li>
<li>
<p>这个新对象会绑定到函数调用的 this</p>
</li>
<li>
<p>如果函数没有返回其他对象，那么 new 表达式中的函数调用会自动返回这个新对象</p>
</li>
</ol>
<p>上个图:<br>
<img src="./assets-JS-prototype/prototype1.png" rounded-lg alt="prototype description graph" /></p>
<h3>内部插槽</h3>
<p>对不熟悉[[ ]]它的朋友们，让笔者先来解释一下第二步的[[ Prototype ]]是什么意思<br>
[[ ]]这个东西怎么叫它无所谓，笔者比较喜欢叫它内部插槽。内部插槽是由 JS 引擎来实现的，是 JS 语言“内部”的属性或者实现。通常来说作为开发者，是接触不到这些底层的实现的。但是现在有相当多的浏览器都把它给暴露出来了，在 Chrome 下就可以通过 <code>__proto__</code> 这一属性来访问[[ Prototype ]]链接所指向的原型对象。注意了，<code>__proto__</code> 不是 JS 所拥有的东西，它只是引擎为了让我们能够访问[[ Prototype ]]而做出的某种实现</p>
<blockquote>
<p>而且据笔者发现，很多诸如__prop__这样的属性，其实都是引擎通过某种方法暴露了内部插槽</p>
</blockquote>
<blockquote>
<p>另外，<code>Object.getPrototypeOf()</code> 这个API也给我们提供了访问[[ Prototype ]]的能力。推荐使用</p>
</blockquote>
<p>到这里，我们已经明白了 <code>new</code> 操作符做了一些什么事情。接着看图，我们会发现 constructor 函数有一个 <strong>prototype 属性</strong>，同样指向原型对象</p>
<p>事实上，<strong>所有的函数，都有一个 prototype 属性</strong>。它的值是相应的原型对象。原型对象也是一个对象，包含一些属性和方法，这些属性和方法可以被所有对象实例共享</p>
<h3>constructor 属性</h3>
<p>原型对象默认只会拥有一个不可枚举的 constructor 属性，它的值是对构造函数的引用</p>
<p>更新一下上图<br>
<img src="./assets-JS-prototype/prototype2.png" rounded-lg alt="prototype description graph" /></p>
<p>举个例子直观地理解一下</p>
<pre><code class="language-js">function Proto() {
  this.name = 'zhangsan'
}
const obj = new Proto()
console.log(Object.getPrototypeOf(obj).constructor === Proto) // true
</code></pre>
<p>但是，注意了，<strong>这里有个坑！</strong></p>
<pre><code class="language-js">function Proto() {
  this.name = 'zhangsan'
}

function Parent() { }
Parent.prototype = new Proto()

console.log(Parent.prototype.constructor) // Proto
</code></pre>
<p>按图来说，<code>Parent.prototype.constructor === Parent</code> 才是我们想要的结果。这是因为我们手动将整个 <code>Parent.prototype</code> 改写了，破坏了默认行为</p>
<p>再衍生一下，所有对象（使用 Object.create(null) 创建的对象除外）都将具有 constructor 属性。在<strong>没有显式使用构造函数的情况</strong>下，创建的对象（例如对象和数组文本）将具有 constructor 属性，这个属性指向该对象的基本对象构造函数类型。(这段话是我从 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor">MDN</a> 上抄的)</p>
<pre><code class="language-js">const o = {}
o.constructor === Object // true

const o = new Object()
o.constructor === Object // true

const a = []
a.constructor === Array // true

const a = []
a.constructor === Array // true

const n = new Number(3)
n.constructor === Number // true
</code></pre>
<h3>原型链</h3>
<p>读者可能已经想到了，如果一个对象实例的构造函数封装了其他的构造函数，会怎么样呢？恭喜你，这就是原型链最大的秘密所在！</p>
<img src="./assets-JS-prototype/prototype3.png" rounded-lg alt="prototype description graph" />
<p>原型对象通过[[ Prototype ]]串联起来，形成一个原型链</p>
<h3>[[ Get ]]</h3>
<p>在访问一个对象的属性时，会发生什么事情？</p>
<pre><code class="language-js">const obj = {
  name: 'zhangsan'
}

obj.name // 'zhangsan'
</code></pre>
<p>不就是搜索作用域，找到 obj，再访问其内存吗？</p>
<p>对也不对。事实上，JS 引擎做了一个[[ Get ]]操作。它是这么做的</p>
<ol>
<li>查找对象中是否有相同名称属性，有则返回</li>
<li>如果没有，则遍历[[ Prototype ]]链上的所有对象，执行第一步</li>
<li>遍历完了还是没有？返回 undefined</li>
</ol>
<pre><code class="language-js">const arr = [3, 2, 1]
arr.sort((a, b) =&gt; a - b)
</code></pre>
<p>看上述代码。调用 <code>arr.sort</code> 实际上是找到了 <code>Array.prototype</code> 对象上的 <code>sort</code> 属性，再加以调用</p>
<h2>ES6 Class</h2>
<p>注意了，ES6 新增的 <code>class</code> 关键字所声明的对象，<strong>仍然是基于原型的，<code>class</code> 事实上只是一个语法糖</strong>。当然，<code>super</code>, <code>extends</code> 这些和 class 搭配的关键字同样也是一个语法糖。所以说，不需要对 JS 中的 class 参杂一些其他的理解，它仅仅是一个语法糖而已，而且严格来说，作为一个动态语言，不存在 &quot;class&quot; 这个概念</p>
<p>当然，<code>class</code> 语法糖也有其设计上的优点</p>
<ol>
<li><code>extends</code> 给了我们轻松继承父类的能力，也能轻松扩展 <code>Array</code>, <code>RegExp</code> 等内置对象，在 ES6 之前想要实现这个功能是比较困难繁琐的</li>
<li><code>super</code> 引用父类的原型，这样就可以访问父类的属性和方法了，也能轻松给父类构造函数传参</li>
</ol>
<h2>ES6 Proxy</h2>
<p>代理事实上针对内部插槽做了拦截,比如 <code>get()</code> 拦截了<code>[[ Get ]]</code>，<code>defineProperty(..)</code> 拦截了<code>[[ DefineOwnProperty ]]</code>，然后执行相应的逻辑。因此代理的 <code>get()</code> 操作对原型链追溯有一定影响</p>
<h2>设计模式： 组合胜过继承</h2>
<p>诚然，原型机制赋予了 JS 继承的能力，但是其弊端也很让人头疼</p>
<ol>
<li>
<p>原型中包含的引用值在所有实例间共享</p>
<p>举个简单的例子：</p>
<pre><code class="language-js">function Proto() {
  this.nums = [1, 2, 3]
}

function Parent() {}
Parent.prototype = new Proto()

const child1 = new Parent()
const child2 = new Parent()

child1.nums.push(4)
console.log(child1.nums, child2.nums) // [1, 2, 3, 4], [1, 2, 3, 4]
</code></pre>
</li>
<li>
<p>子类型在实例化时不能给父类型的构造函数传参</p>
<p>举个例子</p>
<pre><code class="language-js">function Proto() {
  this.nums = [1, 2, 3]
}

function Parent(name) {
  this.name = name || 'zhangsan'
}
Parent.prototype = new Proto()
</code></pre>
<p>如果想给子类设置 age 属性的值，只能在其实例化后手动设置</p>
<p>于是，前人经过总结，得出以下几种实现</p>
</li>
</ol>
<h3>盗用构造函数</h3>
<p>这种技术名字听起来似乎很复杂，但它其实只是借助了 <code>call()</code> (或者 <code>apply()</code> )的能力，将 <code>this</code> 绑定到实例上</p>
<pre><code class="language-js">function Proto(name) {
  this.nums = [1, 2, 3]
  this.name = name
}
Proto.prototype.hello = function () {
  return `hello, ${this.name}!`
}

function Parent(name) {
  Proto.call(this, name ?? 'zhangsan')
}

const child1 = new Parent()
const child2 = new Parent('luoxiang')

console.log(child1.name, child2.name) // &quot;zhangsan&quot;, &quot;luoxiang&quot;

child1.nums.push(4)
console.log(child1.nums, child2.nums) // [1, 2, 3, 4], [1, 2, 3]

console.log(child1.hello()) // TypeError: child1.hello is not a function
</code></pre>
<p>它虽然解决了原型机制的两个痛点，但从这个例子中我们也能看出来它的缺点。它人为破坏了原型链， <code>child1</code> 无法访问 <code>Proto</code> 的原型对象中的属性。另外，如果我们在 <code>Proto</code> 中书写方法，如 <code>this.func = function() { }</code>，那么在内存中，每个实例都会开辟一块空间去存储 <code>func</code>，极大消耗了内存，这点是不如原型链机制下的继承的</p>
<h3>组合继承</h3>
<p>它结合了上述两种思路，保留了两者的优点。即利用盗用构造函数来继承属性，利用原型链继承方法</p>
<pre><code class="language-js">function Proto(name) {
  this.nums = [1, 2, 3]
  this.name = name
}
Proto.prototype.hello = function () {
  return `hello, ${this.name}!`
}

function Parent(name) {
  Proto.call(this, name ?? 'zhangsan') // 第二次调用Proto
}
Parent.prototype = new Proto() // 第一次调用Proto
Parent.prototype.constructor = Parent

const child1 = new Parent()
const child2 = new Parent('luoxiang')

console.log(child1.hello()) // &quot;hello, zhangsan!&quot;
console.log(child2.hello()) // &quot;hello, luoxiang!&quot;

child1.nums.push(4)
console.log(child1.nums, child2.nums) // [1, 2, 3, 4], [1, 2, 3]
</code></pre>
<p>然而其也有一个效率问题，它调用了两次 <code>Proto</code>，这带来了一个小小的问题</p>
<p>以 <code>child2</code> 对象为例，它拥有 <code>nums</code> 和 <code>name</code> 属性，它通过<code>[[ Prototype ]]</code>所关联的原型对象也拥有这两个属性</p>
<pre><code class="language-js">console.log(child2)
console.log(Object.getPrototypeOf(child2))
</code></pre>
<img src="./assets-JS-prototype/prototype4-light.png" img-light rounded-lg alt="the result of the codes above" />
<img src="./assets-JS-prototype/prototype4-dark.png" img-dark rounded-lg alt="the result of the codes above" />
<p>这个问题出现的关键点在于这一行代码</p>
<pre><code class="language-js">Parent.prototype = new Proto()
</code></pre>
<p>我们在这里也调用了 <code>new</code> 操作符。因此 <code>Parent.prototype</code> 不是一个纯粹的原型对象。我们期望原型对象上只存在方法</p>
<h3>寄生组合继承</h3>
<p>这应该是继承方案的最优解了</p>
<p>只需要改一行代码即可</p>
<pre><code class="language-js">// Parent.prototype = new Proto()
Parent.prototype = Object.create(Proto.prototype)
</code></pre>
<p>再看看控制台</p>
<img src="./assets-JS-prototype/prototype5-light.png" img-light rounded-lg alt="the result of the codes above" />
<img src="./assets-JS-prototype/prototype5-dark.png" img-dark rounded-lg alt="the result of the codes above" />
<p><code>Object.create()</code> 干了什么事？看看它的简易 <code>Polyfill</code> 代码，相信你就明白了</p>
<pre><code class="language-js">Object.create = function (proto, propertiesObject) {
  // ...

  function F() {}
  F.prototype = proto

  return new F()
}
</code></pre>
<p>没错，我们“绕过了” <code>Proto</code> 函数，只提供了它的 <code>prototype</code></p>
<h3>组合</h3>
<p>固然我们实现了优异的继承，但是从设计模式的理念出发，组合是胜过继承的。组合能带来更优雅的代码结构、更低的耦合程度</p>
<h2>写在最后</h2>
<p>希望通过笔者的阐述，初学 JS 的读者们能够对原型有一个大致清晰的认知。笔者刚接触 JS 时，遇到的第一个难关就是原型，到处查资料、翻书，花了好大功夫</p>
<p>参考:</p>
<ul>
<li>《你不知道的JavaScript》</li>
<li>《JavaScript高级程序设计》</li>
<li><a href="https://developer.mozilla.org/zh-CN/">MDN</a></li>
<li><a href="https://juejin.cn/post/6844903974378668039">神三元 —— 原生JS灵魂之问(上)</a></li>
</ul>
]]></content:encoded>
            <author>younggglcy@gmail.com (younggglcy)</author>
        </item>
        <item>
            <title><![CDATA[JS 之类型转换篇]]></title>
            <link>https://younggglcy.com/posts/JS-type-casting</link>
            <guid isPermaLink="true">https://younggglcy.com/posts/JS-type-casting</guid>
            <pubDate>Wed, 03 Nov 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[主要对 JS 的类型转换规则进行了大体阐述，以经典图片 *Thanks for inventing JavaScript* 为例讲述了一些例子]]></description>
            <content:encoded><![CDATA[<p>[[toc]]</p>
<h2>JS 中有哪些数据类型？</h2>
<p>在 JS 中，有以下 7 种内置的基本类型，也叫做原始类型：</p>
<ul>
<li>
<p><code>number</code></p>
</li>
<li>
<p><code>boolean</code></p>
</li>
<li>
<p><code>string</code></p>
</li>
<li>
<p><code>symbol</code></p>
</li>
<li>
<p><code>null</code></p>
</li>
<li>
<p><code>undefined</code></p>
</li>
<li>
<p><code>bigint</code></p>
</li>
</ul>
<p>和一个引用类型，也叫做复杂数据类型或者复合类型：</p>
<ul>
<li><code>object</code></li>
</ul>
<p>像由 <code>Function()</code>, <code>Array()</code>, <code>RegExp()</code>, <code>Date()</code>, <code>Math()</code>, <code>Number()</code>, <code>String()</code>, <code>Boolean()</code>, <code>Proxy()</code>, <code>Promise()</code>,<code>Map()</code> 等等这些内置的构造函数所产生的对象，都可以将其归于引用类型的范畴</p>
<pre><code class="language-js">// example
const date = new Date()
console.log(typeof date) // object
console.log(date instanceof Object) // true
</code></pre>
<h2>类型转换</h2>
<p>需要注意的一点是，JS 作为一门动态语言，它在编译时不会检查你所声明的变量的值和类型是否匹配，当运行时，我们才知道变量的值和类型。换句话说，**变量可以持有任何值， 值持有其类型。**这就涉及到类型转换这个十分令人头疼的问题</p>
<p>当对不同类型的变量之间做运算和比较时，都会涉及到类型转换</p>
<blockquote>
<p>事实上，JS 引擎有着十分复杂的编译、执行流程，它甚至会采用一些延迟编译、重编译等手段来优化执行效率</p>
</blockquote>
<p>附上经典图片一张：</p>
<img alt="thanks for inventing javascript" src="./assets-JS-type-casting/thanks-for-inventing-javascript.jpg" rounded-lg >
<blockquote>
<p>简单讲解一下不涉及到类型转换的坑</p>
<ol>
<li>
<p><code>typeof NaN === 'number'</code>, <code>NaN</code> 是一个全局对象的属性，不可写、不可配置、不可枚举</p>
<p><code>console.log(Object.getOwnPropertyDescriptor(window, NaN))</code>在 Chrome 控制台下结果是</p>
<img src="./assets-JS-type-casting/type-casting1-dark.png" rounded-lg img-dark alt="the result of console.log(Object.getOwnPropertyDescriptor(window, NaN))" />
<img src="./assets-JS-type-casting/type-casting1-light.png" rounded-lg img-light alt="the result of console.log(Object.getOwnPropertyDescriptor(window, NaN))" />
`NaN` 意为 *not a number*，但其实 ECMA 标准将其归于 Number 的范畴，所以不用过于纠结这个问题。<a src="https://262.ecma-international.org/5.1/#sec-4.3.23" target="_blank">可参考这里</a>
</li>
<li>
<p>关于好几个 9 变成 10 的 n 次方，是由于绝对值大于或等于 2^53 的数值文本过大，无法用整数准确表示</p>
<p>为什么是 2^53？因为 IEEE754 标准规定了浮点数由 64 位二进制数表示，有效数字由 52 位二进制数表示。但有效数字第一位总是 1 且不保存在 64 位之中，所以有 53 位数用来表示一个浮点数，那么大于 2^53 就会导致精度缺失，没有多余的位来描述这个数了</p>
</li>
<li>
<pre><code class="language-js">console.log(0.5 + 0.1 === 0.6) // true
console.log(0.1 + 0.2 === 0.3) // false
</code></pre>
<p>事实上，浮点数类型的值精度最高可达到小数点后 17 位（也和 IEEE754 标准有关喔）。这样子会带来一定精度上误差，是无法避免的。0.1 转化为二进制的结果是 0.0001100110011...，是个无限循环的数。它和 0.2 转化为二进制的结果相加，再转化为十进制，结果是 0.300 000 000 000 000 04。如果想做到在允许误差范围内比较，可以利用 <code>Number.EPSILON</code>，它代表了 1 和从 1 的左边最趋近于 1 的数的差值</p>
<pre><code class="language-js">const a = 0.1 + 0.2
const b = 0.3
console.log(Math.abs(a - b) &lt; Number.EPSILON) // true，可以认为 a 和 b 相等
</code></pre>
</li>
<li>
<pre><code class="language-js">console.log(Math.max()) // -Infinity
console.log(Math.min()) // Infinity
</code></pre>
<p>其实 <code>Math.max()</code> 的默认值就是 -Infinity。<code>Math.max()</code> 接受若干个数字作为参数，并且返回其中的最大值。</p>
</li>
</ol>
</blockquote>
<p>接下来让我们简单了解类型转换的规则：</p>
<h3>基本类型相关</h3>
<ul>
<li>
<p>其他类型转化为 <code>Boolean</code> (使用 Boolean()，表格基于《JavaScript 高级程序设计第 4 版》，做了些许删改)</p>
<table>
<thead>
<tr>
<th>数据类型</th>
<th>转换为 <code>true</code> 的值</th>
<th>转换为 <code>false</code> 的值</th>
</tr>
</thead>
<tbody>
<tr>
<td>String</td>
<td>非空字符串</td>
<td>&quot;&quot;（空字符串）</td>
</tr>
<tr>
<td>Number</td>
<td>非零数值（包括无穷值）</td>
<td>0、NaN</td>
</tr>
<tr>
<td>Undefined</td>
<td></td>
<td>undefined</td>
</tr>
<tr>
<td>Null</td>
<td></td>
<td>null</td>
</tr>
<tr>
<td>Symbol</td>
<td>都是true</td>
<td></td>
</tr>
<tr>
<td>BigInt</td>
<td>非零数值（包括无穷值）</td>
<td>0n、NaN</td>
</tr>
</tbody>
</table>
</li>
</ul>
<blockquote>
<p>tips: JS 的假值只有以下几个：</p>
<ul>
<li>null</li>
<li>undefined</li>
<li>0</li>
<li>&quot;&quot;</li>
<li>NaN</li>
<li>0n</li>
<li>false</li>
</ul>
</blockquote>
<ul>
<li>
<p>其他类型转化为 <code>Number</code> (讨论使用 <code>Number()</code> 的情况)</p>
<ul>
<li>
<p><code>String</code>：</p>
<ul>
<li>如果字符串包含数值字符，包括数值字符前面带加、减号的情况，则转换为一个十进制数值</li>
</ul>
<p>因此，Number(&quot;1&quot;) 返回 1，Number(&quot;123&quot;) 返回 123，Number(&quot;011&quot;) 返回 11（忽略前面</p>
<p>的零）</p>
<ul>
<li>
<p>如果字符串包含有效的浮点值格式如&quot;1.1&quot;，则会转换为相应的浮点值（同样，忽略前面的零）</p>
</li>
<li>
<p>如果字符串包含有效的十六进制格式如&quot;0xf&quot;，则会转换为与该十六进制值对应的十进制整</p>
</li>
</ul>
<p>数值</p>
<ul>
<li>
<p>如果是空字符串（不包含字符），则返回 0</p>
</li>
<li>
<p>如果字符串包含除上述情况之外的其他字符，则返回 NaN</p>
</li>
</ul>
</li>
<li>
<p><code>Boolean</code>:  true转化为1， false转化为0</p>
</li>
<li>
<p><code>null</code>： 0</p>
</li>
<li>
<p><code>undefined</code>： <code>NaN</code></p>
</li>
<li>
<p><code>BigInt</code>:  转化成相应的数值或者无穷，超过“安全范围”会出现精度缺失</p>
</li>
<li>
<p><code>Symbol</code>： 无法转换为 <code>Number</code></p>
</li>
</ul>
</li>
<li>
<p>其他类型转化为 <code>String</code>（讨论使用 <code>String()</code> 的情况）</p>
<ul>
<li><code>Number</code>: 直接转化为字符串，但是十六进制数和二进制数会先转化为十进制数</li>
<li><code>null</code>: &quot;null&quot;</li>
<li><code>undefined</code>: &quot;undefined&quot;</li>
<li><code>Boolean</code>: true 转化为&quot;true&quot;，false 转化为 &quot;false&quot;</li>
<li><code>Symbol</code>:  比如 <code>Symbol('foo')</code> 为转化成字符串字面量 “Symbol(foo)”</li>
<li><code>BigInt</code>: 同 <code>Number</code></li>
</ul>
</li>
</ul>
<h3>复杂类型相关</h3>
<p>对于任何转换，Object 会优先调用内置的<code>[[ToPrimitive]]</code>，可以通过给对象添加<code>[Symbol.toPrimitive]</code>属性，改写内置的<code>[[ToPrimitive]]</code>。需要注意的是，<code>[Symbol.toPrimitive]() {}</code>只允许返回一个基本类型值</p>
<pre><code class="language-js">const obj = {
  valueOf() {
    return 123
  },
  toString() {
    return 'I am an Object'
  },
  [Symbol.toPrimitive]() {
    return true
  }
}
console.log(String(obj)) // 'true'
console.log(obj == 1) // true
</code></pre>
<p>如果没有该属性，其次是调用 <code>valueOf()</code>，如果 <code>valueOf()</code> 不存在或者不返回一个基本类型值，最后再调用 <code>toString()</code>。<code>toString()</code> 也不返回呢？那么转换就失败了，会出现 error</p>
<p>这是对一个对象做转换所调用方法的顺序</p>
<h3>操作符相关</h3>
<ol>
<li><strong>==</strong></li>
</ol>
<p><code>==</code>仅仅比较等号两边的值是否相等，故其允许对两边的值在比较过程中做隐式转换，这也牵扯到很多类型转换的问题</p>
<ul>
<li>布尔值与其他值比较，先将布尔值转换成 number 类型</li>
<li>对象与基本类型的值比较，先让对象转化为原始值，即按照上文的顺序转换</li>
<li>字符串与数字比较，先将字符串转化为数值</li>
<li><code>console.log(null == undefined) // true</code></li>
</ul>
<ol start="2">
<li><strong>+</strong></li>
</ol>
<ul>
<li>
<p>一元运算符<code>+</code>可以让值强制转换为数值</p>
</li>
<li>
<p>二元运算符<code>+</code>，如果一个操作数是字符串（或者是可以转化为字符串的对象），则执行字符串拼接，否则执行加法操作</p>
</li>
</ul>
<ol start="3">
<li><strong>-</strong></li>
</ol>
<ul>
<li><code>-</code>则会尝试让操作数都变成数值</li>
</ul>
<ol start="4">
<li><strong>字位操作符（^, | ,~）</strong></li>
</ol>
<ul>
<li>让操作数强制转化为32位整数，再对其进行位运算</li>
</ul>
<ol start="5">
<li><strong>!</strong></li>
</ol>
<ul>
<li>强制让操作数转化为真值或者假值</li>
</ul>
<h3>解答</h3>
<p>罗列了这么多规则，那现在我们给出<em>Thanks for inventing JavaScript</em>一图中的答案了</p>
<ul>
<li>
<p><code>[] + [];   // &quot;&quot;</code></p>
<p>在<code>+</code>的转化规则下，两个空数组都调用 <code>toString()</code> 成为空字符串再进行拼接，答案自然是空字符串</p>
</li>
<li>
<pre><code class="language-js">console.log([] + {}) // &quot;[object Object]&quot;
console.log({} + []) // 0
</code></pre>
<p>在<code>[] + {}</code>中，JS 引擎解析这条语句，认为它在做空数组 + 空对象这一操作，而<code>{}</code>被转化为字符串的结果是<code>[object Object]</code></p>
<p>而在<code>{} + []</code>中，引擎认为<code>{}</code>是一个空的作用域，把它忽略了，所以变成了一元操作符 <code>+</code> 再跟上一个空数组。所以，<code>+</code>把<code>[]</code>转化为 0</p>
</li>
<li>
<pre><code class="language-js">console.log(true + true + true === 3) // true
console.log(true - true) // 0
console.log(true == 1) // true
console.log(true === 1) // false
</code></pre>
<ol>
<li><code>true + true + true</code>，true 是布尔值，不满足以下两个条件之一：</li>
<li>是字符串</li>
<li>是可以调用<code>[[ToPrimitive]]</code>转化为字符串的对象</li>
</ol>
<p>故<code>+</code>被解释为数字加法，true 转化为1，进行相加</p>
<ol start="2">
<li><code>true - true</code>，true 被转化为1</li>
<li><code>==</code>将 true 转化为1</li>
<li><code>===</code>是严格相等，同时比较值和类型是否相等</li>
</ol>
</li>
<li>
<p><code>(!+[] + [] + ![]).length;  //9</code></p>
<p>第一眼看上去可能比较迷惑，但也不复杂。</p>
<ol>
<li><code>!+[]</code>，先执行<code>+[]</code>操作，结果为0。0是假值，<code>!0</code>得到布尔值<code>true</code></li>
<li>现在式子变成了<code>true + [] + ![]</code>，<code>[]</code>能够转换为空字符串，故<code>true + []</code>结果为<code>&quot;true&quot;</code>，式子变为<code>&quot;true&quot; + ![]</code></li>
<li><code>[]</code>是真值，<code>![]</code>得到布尔值 false。<code>&quot;true&quot; + false</code>结果是<code>&quot;truefalse&quot;</code>，<code>length</code>属性返回其长度9</li>
</ol>
</li>
<li>
<p>剩下三个比较简单，就不在此赘述</p>
</li>
</ul>
<h2>小结</h2>
<p>可以发现，JS 所涉及到的类型转换是比较复杂的。应该在经验的累积上，进行记忆。我们在日常开发时，也应该考虑清楚这部分的代码怎么写，力求简洁易读</p>
]]></content:encoded>
            <author>younggglcy@gmail.com (younggglcy)</author>
        </item>
    </channel>
</rss>