<?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" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ 若川 - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ 若川 - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 25 May 2026 19:55:31 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/author/lxchuan12/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 一文看懂 JS 的继承 ]]>
                </title>
                <description>
                    <![CDATA[ 在这篇文章里，我们分析一下这个常见的面试问题：JS 的继承。 用过React的读者知道，我们经常用extends继承React.Component。 // 部分源码 function Component(props, context, updater) {   // ... } Component.prototype.setState = function(partialState, callback){     // ... } const React = {     Component,     // ... } // 使用 class index extends React.Component{   ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/inheritance-in-js/</link>
                <guid isPermaLink="false">5f2b7c87c8da7105cbc148e0</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Thu, 06 Oct 2022 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/08/1596700278410.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>在这篇文章里，我们分析一下这个常见的面试问题：JS 的继承。</p><p>用过<code>React</code>的读者知道，我们经常用<code>extends</code>继承<code>React.Component</code>。</p><pre><code>// 部分源码
function Component(props, context, updater) {
  // ...
}
Component.prototype.setState = function(partialState, callback){
    // ...
}
const React = {
    Component,
    // ...
}
// 使用
class index extends React.Component{
    // ...
}</code></pre><p><a href="https://github.com/facebook/react/blob/master/packages/react/src/ReactBaseClasses.js" rel="nofollow noopener noreferrer">点击这里查看 React GitHub 源码</a></p><p>面试官可以顺着这个问<code>JS</code>继承的相关问题，比如：<strong><code>ES6</code>的<code>class</code>继承用 ES5 如何实现？</strong>据说很多人答得不好。</p><h2 id="-">构造函数、原型对象和实例之间的关系</h2><p>要弄懂 extends 继承之前，我们先来复习一下构造函数、原型对象和实例之间的关系。 代码表示：</p><pre><code>function F(){}
var f = new F();
// 构造器
F.prototype.constructor === F; // true
F.__proto__ === Function.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true

// 实例
f.__proto__ === F.prototype; // true
F.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true</code></pre><p>笔者画了一张图表示：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/09/2.png" class="kg-image" alt="2" width="1060" height="596" loading="lazy"></figure><h2 id="es6-extends-"><code>ES6 extends</code> 继承做了什么操作</h2><p>我们先看看这段包含静态方法的<code>ES6</code>继承代码：</p><pre><code>// ES6
class Parent{
    constructor(name){
        this.name = name;
    }
    static sayHello(){
        console.log('hello');
    }
    sayName(){
        console.log('my name is ' + this.name);
        return this.name;
    }
}
class Child extends Parent{
    constructor(name, age){
        super(name);
        this.age = age;
    }
    sayAge(){
        console.log('my age is ' + this.age);
        return this.age;
    }
}
let parent = new Parent('Parent');
let child = new Child('Child', 18);
console.log('parent: ', parent); // parent:  Parent&nbsp;{name: "Parent"}
Parent.sayHello(); // hello
parent.sayName(); // my name is Parent
console.log('child: ', child); // child:  Child&nbsp;{name: "Child", age: 18}
Child.sayHello(); // hello
child.sayName(); // my name is Child
child.sayAge(); // my age is 18</code></pre><p>其中这段代码里有两条原型链，不信看具体代码。</p><pre><code>// 1、构造器原型链
Child.__proto__ === Parent; // true
Parent.__proto__ === Function.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
// 2、实例原型链
child.__proto__ === Child.prototype; // true
Child.prototype.__proto__ === Parent.prototype; // true
Parent.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true</code></pre><p>一图胜千言，笔者也画了一张图表示，如图所示：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/09/3.png" class="kg-image" alt="3" width="1188" height="1007" loading="lazy"></figure><p>结合代码和图可以知道。 <code>ES6 extends</code> 继承，主要就是：</p><ol><li>把子类构造函数(<code>Child</code>)的原型(<code>__proto__</code>)指向了父类构造函数(<code>Parent</code>)，</li><li>把子类实例<code>child</code>的原型对象(<code>Child.prototype</code>) 的原型(<code>__proto__</code>)指向了父类<code>parent</code>的原型对象(<code>Parent.prototype</code>)。</li></ol><p>这两点也就是图中用不同颜色标记的两条线。</p><ol><li>子类构造函数<code>Child</code>继承了父类构造函数<code>Preant</code>的里的属性。使用<code>super</code>调用的(<code>ES5</code>则用<code>call</code>或者<code>apply</code>调用传参)。 也就是图中用不同颜色标记的两条线。</li></ol><p>看过《JavaScript高级程序设计-第3版》 章节<code>6.3继承</code>的读者应该知道，这<code>2和3小点</code>，正是<strong>寄生组合式继承</strong>，书中例子没有<code>第1小点</code>。 <code>1和2小点</code>都是相对于设置了<code>__proto__</code>链接。那问题来了，什么可以设置了<code>__proto__</code>链接呢。</p><h2 id="new-object-create-object-setprototypeof-__proto__"><code>new</code>、<code>Object.create</code>和 <code>Object.setPrototypeOf</code>可以设置<code>__proto__</code></h2><p>说明一下，<code>__proto__</code>这种写法是浏览器厂商自己的实现。 再结合一下图和代码看一下的<code>new</code>，<code>new</code>出来的实例的__proto__指向构造函数的<code>prototype</code>，这就是<code>new</code>做的事情。 摘抄一下之前写过文章的一段。<a href="https://juejin.im/post/6844903704663949325">面试官问：能否模拟实现JS的new操作符</a>，有兴趣的读者可以点击查看。</p><h3 id="new-"><strong><code>new</code>做了什么：</strong></h3><ol><li>创建了一个全新的对象。</li><li>这个对象会被执行<code>[[Prototype]]</code>（也就是<code>__proto__</code>）链接。</li><li>生成的新对象会绑定到函数调用的<code>this</code>。</li><li>通过<code>new</code>创建的每个对象将最终被<code>[[Prototype]]</code>链接到这个函数的<code>prototype</code>对象上。</li><li>如果函数没有返回对象类型<code>Object</code>(包含<code>Functoin</code>, <code>Array</code>, <code>Date</code>, <code>RegExg</code>, <code>Error</code>)，那么<code>new</code>表达式中的函数调用会自动返回这个新的对象。</li></ol><h3 id="object-create-es5-"><code>Object.create</code> <code>ES5提供的</code></h3><p><code>Object.create(proto, [propertiesObject])</code>方法创建一个新对象，使用现有的对象来提供新创建的对象的__proto__。 它接收两个参数，不过第二个可选参数是属性描述符（不常用，默认是<code>undefined</code>）。对于不支持<code>ES5</code>的浏览器，<code>MDN</code>上提供了<code>ployfill</code>方案。 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create" rel="nofollow noopener noreferrer">MDN Object.create()</a></p><pre><code>// 简版：也正是应用了new会设置__proto__链接的原理。
if(typeof Object.create !== 'function'){
    Object.create = function(proto){
        function F() {}
        F.prototype = proto;
        return new F();
    }
}</code></pre><h3 id="object-setprototypeof-es6-"><code>Object.setPrototypeOf</code> <code>ES6提供的</code></h3><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf" rel="nofollow noopener noreferrer"><code>Object.setPrototypeOf</code> <code>MDN</code></a></p><p><code>Object.setPrototypeOf()</code> 方法设置一个指定的对象的原型 ( 即, 内部<code>[[Prototype]]</code>属性）到另一个对象或 &nbsp;<code>null</code>。 <code>Object.setPrototypeOf(obj, prototype)</code></p><pre><code>`ployfill`
// 仅适用于Chrome和FireFox，在IE中不工作：
Object.setPrototypeOf = Object.setPrototypeOf || function (obj, proto) {
  obj.__proto__ = proto;
  return obj; 
}</code></pre><p><code>nodejs</code>源码就是利用这个实现继承的工具函数的。 <a href="https://github.com/nodejs/node/blob/master/lib/util.js#L295-L313" rel="nofollow noopener noreferrer">nodejs utils inherits</a></p><pre><code>function inherits(ctor, superCtor) {
  if (ctor === undefined || ctor === null)
    throw new ERR_INVALID_ARG_TYPE('ctor', 'Function', ctor);

  if (superCtor === undefined || superCtor === null)
    throw new ERR_INVALID_ARG_TYPE('superCtor', 'Function', superCtor);

  if (superCtor.prototype === undefined) {
    throw new ERR_INVALID_ARG_TYPE('superCtor.prototype',
                                   'Object', superCtor.prototype);
  }
  Object.defineProperty(ctor, 'super_', {
    value: superCtor,
    writable: true,
    configurable: true
  });
  Object.setPrototypeOf(ctor.prototype, superCtor.prototype);
}</code></pre><h2 id="es6-extends-es5-"><code>ES6</code>的<code>extends</code>的<code>ES5</code>版本实现</h2><p>知道了<code>ES6 extends</code>继承做了什么操作和设置<code>__proto__</code>的知识点后，把上面<code>ES6</code>例子的用<code>ES5</code>就比较容易实现了，也就是说<strong>实现寄生组合式继承</strong>，简版代码就是：</p><pre><code>// ES5 实现ES6 extends的例子
function Parent(name){
    this.name = name;
}
Parent.sayHello = function(){
    console.log('hello');
}
Parent.prototype.sayName = function(){
    console.log('my name is ' + this.name);
    return this.name;
}

function Child(name, age){
    // 相当于super
    Parent.call(this, name);
    this.age = age;
}
// new
function object(){
    function F() {}
    F.prototype = proto;
    return new F();
}
function _inherits(Child, Parent){
    // Object.create
    Child.prototype = Object.create(Parent.prototype);
    // __proto__
    // Child.prototype.__proto__ = Parent.prototype;
    Child.prototype.constructor = Child;
    // ES6
    // Object.setPrototypeOf(Child, Parent);
    // __proto__
    Child.__proto__ = Parent;
}
_inherits(Child,  Parent);
Child.prototype.sayAge = function(){
    console.log('my age is ' + this.age);
    return this.age;
}
var parent = new Parent('Parent');
var child = new Child('Child', 18);
console.log('parent: ', parent); // parent:  Parent&nbsp;{name: "Parent"}
Parent.sayHello(); // hello
parent.sayName(); // my name is Parent
console.log('child: ', child); // child:  Child&nbsp;{name: "Child", age: 18}
Child.sayHello(); // hello
child.sayName(); // my name is Child
child.sayAge(); // my age is 18</code></pre><p>我们完全可以把上述<code>ES6的例子</code>通过<a href="https://babeljs.io/repl" rel="nofollow noopener noreferrer"><code>babeljs</code></a>转码成<code>ES5</code>来查看，更严谨的实现。</p><pre><code>// 对转换后的代码进行了简要的注释
"use strict";
// 主要是对当前环境支持Symbol和不支持Symbol的typeof处理
function _typeof(obj) {
    if (typeof Symbol === "function" &amp;&amp; typeof Symbol.iterator === "symbol") {
        _typeof = function _typeof(obj) {
            return typeof obj;
        };
    } else {
        _typeof = function _typeof(obj) {
            return obj &amp;&amp; typeof Symbol === "function" &amp;&amp; obj.constructor === Symbol &amp;&amp; obj !== Symbol.prototype ? "symbol" : typeof obj;
        };
    }
    return _typeof(obj);
}
// _possibleConstructorReturn 判断Parent。call(this, name)函数返回值 是否为null或者函数或者对象。
function _possibleConstructorReturn(self, call) {
    if (call &amp;&amp; (_typeof(call) === "object" || typeof call === "function")) {
        return call;
    }
    return _assertThisInitialized(self);
}
// 如何 self 是void 0 （undefined） 则报错
function _assertThisInitialized(self) {
    if (self === void 0) {
        throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
    }
    return self;
}
// 获取__proto__
function _getPrototypeOf(o) {
    _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
        return o.__proto__ || Object.getPrototypeOf(o);
    };
    return _getPrototypeOf(o);
}
// 寄生组合式继承的核心
function _inherits(subClass, superClass) {
    if (typeof superClass !== "function" &amp;&amp; superClass !== null) {
        throw new TypeError("Super expression must either be null or a function");
    }
    // Object.create()方法创建一个新对象，使用现有的对象来提供新创建的对象的__proto__。 
    // 也就是说执行后 subClass.prototype.__proto__ === superClass.prototype; 这条语句为true
    subClass.prototype = Object.create(superClass &amp;&amp; superClass.prototype, {
        constructor: {
            value: subClass,
            writable: true,
            configurable: true
        }
    });
    if (superClass) _setPrototypeOf(subClass, superClass);
}
// 设置__proto__
function _setPrototypeOf(o, p) {
    _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
        o.__proto__ = p;
        return o;
    };
    return _setPrototypeOf(o, p);
}
// instanceof操作符包含对Symbol的处理
function _instanceof(left, right) {
    if (right != null &amp;&amp; typeof Symbol !== "undefined" &amp;&amp; right[Symbol.hasInstance]) {
        return right[Symbol.hasInstance](left);
    } else {
        return left instanceof right;
    }
}

function _classCallCheck(instance, Constructor) {
    if (!_instanceof(instance, Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}
// 按照它们的属性描述符 把方法和静态属性赋值到构造函数的prototype和构造器函数上
function _defineProperties(target, props) {
    for (var i = 0; i &lt; props.length; i++) {
        var descriptor = props[i];
        descriptor.enumerable = descriptor.enumerable || false;
        descriptor.configurable = true;
        if ("value" in descriptor) descriptor.writable = true;
        Object.defineProperty(target, descriptor.key, descriptor);
    }
}
// 把方法和静态属性赋值到构造函数的prototype和构造器函数上
function _createClass(Constructor, protoProps, staticProps) {
    if (protoProps) _defineProperties(Constructor.prototype, protoProps);
    if (staticProps) _defineProperties(Constructor, staticProps);
    return Constructor;
}

// ES6
var Parent = function () {
    function Parent(name) {
        _classCallCheck(this, Parent);
        this.name = name;
    }
    _createClass(Parent, [{
        key: "sayName",
        value: function sayName() {
            console.log('my name is ' + this.name);
            return this.name;
        }
    }], [{
        key: "sayHello",
        value: function sayHello() {
            console.log('hello');
        }
    }]);
    return Parent;
}();

var Child = function (_Parent) {
    _inherits(Child, _Parent);
    function Child(name, age) {
        var _this;
        _classCallCheck(this, Child);
        // Child.__proto__ =&gt; Parent
        // 所以也就是相当于Parent.call(this, name); 是super(name)的一种转换
        // _possibleConstructorReturn 判断Parent.call(this, name)函数返回值 是否为null或者函数或者对象。
        _this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name));
        _this.age = age;
        return _this;
    }
    _createClass(Child, [{
        key: "sayAge",
        value: function sayAge() {
            console.log('my age is ' + this.age);
            return this.age;
        }
    }]);
    return Child;
}(Parent);

var parent = new Parent('Parent');
var child = new Child('Child', 18);
console.log('parent: ', parent); // parent:  Parent&nbsp;{name: "Parent"}
Parent.sayHello(); // hello
parent.sayName(); // my name is Parent
console.log('child: ', child); // child:  Child&nbsp;{name: "Child", age: 18}
Child.sayHello(); // hello
child.sayName(); // my name is Child
child.sayAge(); // my age is 18</code></pre><p>如果对 JS 继承相关还是不太明白的读者，推荐阅读以下书籍的相关章节，可以自行找到相应的<code>pdf</code>版本。</p><h2 id="-js-">推荐阅读JS继承相关的书籍章节</h2><p>《JavaScript 高级程序设计第 3 版》-第 6 章 面向对象的程序设计，6种继承的方案，分别是原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承。<a href="http://www.ituring.com.cn/book/946" rel="nofollow noopener noreferrer">图灵社区本书地址</a>，后文放出<code>GitHub</code>链接，里面包含这几种继承的代码<code>demo</code>。</p><p>《JavaScript 面向对象编程第2版》-第6章 继承，12种继承的方案。1.原型链法（仿传统）、2.仅从原型继承法、3.临时构造器法、4.原型属性拷贝法、5.全属性拷贝法（即浅拷贝法）、6.深拷贝法、7.原型继承法、8.扩展与增强模式、9.多重继承法、10.寄生继承法、11.构造器借用法、12.构造器借用与属性拷贝法。</p><p><a href="http://es6.ruanyifeng.com/#docs/class-extends" rel="nofollow noopener noreferrer">ES6 标准入门-第 21 章 class 的继承</a></p><p><a href="https://oshotokill.gitbooks.io/understandinges6-simplified-chinese/content/chapter_9.html" rel="nofollow noopener noreferrer">《深入理解<code>ES6</code>》-第9章</a> <code>JavaScript</code>中的类</p><p>《你不知道的<code>JavaScript</code>-上卷》第6章 行为委托和附录 A <code>ES6中的class</code></p><h2 id="--1">总结</h2><p>继承对于 JS 来说就是父类拥有的方法和属性、静态方法等，子类也要拥有。子类中可以利用原型链查找，也可以在子类调用父类，或者从父类拷贝一份到子类等方案。 继承方法可以有很多，重点在于必须理解并熟 悉这些对象、原型以及构造器的工作方式，剩下的就简单了。<strong>寄生组合式继承</strong>是开发者使用比较多的。 回顾寄生组合式继承。主要就是三点：</p><ul><li>子类构造函数的<code>__proto__</code>指向父类构造器，继承父类的静态方法</li><li>子类构造函数的<code>prototype</code>的<code>__proto__</code>指向父类构造器的<code>prototype</code>，继承父类的方法。</li><li>子类构造器里调用父类构造器，继承父类的属性。 行文到此，文章就基本写完了。文章代码和图片等资源放在这里 <a href="https://github.com/lxchuan12/html5/tree/gh-pages/JS%E7%9B%B8%E5%85%B3/oop/inherit" rel="nofollow noopener noreferrer">GitHub inhert</a> 和<a href="http://lxchuan12.github.io/html5/JS%E7%9B%B8%E5%85%B3/oop/inherit/7.es6-extends.html" rel="nofollow noopener noreferrer"><code>demo</code>展示<code>es6-extends</code></a>，结合<code>console、source</code>面板查看更佳。</li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 为什么 Vue2 this 能够直接获取到 data 和 methods ]]>
                </title>
                <description>
                    <![CDATA[ 本文源于一次源码共读 [https://juejin.cn/pin/7005372623400435725]群里群友的提问，请问@若川，“为什么 data 中的数据可以用 this 直接获取到啊 ”，当时我翻阅源码做出了解答。想着如果下次有人再次问到，我还需要回答一次。当时打算有空写篇文章告诉读者自己探究原理，于是就有了这篇文章。 阅读本文，你将学到： 1. 如何学习调试 vue2 源码 2. data 中的数据为什么可以用 this 直接获取到 3. methods 中的方法为什么可以用 this 直接获取到 4. 学习源码中优秀代码和思想，投入到自己的项目中 本文不难，用过 Vue 的都看得懂，希望大家动手调试和学会看源码。 看源码可以大胆猜测，最后小心求证。 2. 示例：this 能够直接获取到 data 和 methods 众所周知，这样是可以输出我是若川的。好奇的人就会思考为啥 this 就能直接访问到呢。 const vm = new Vue({     data: {   ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/why-this-in-vue2-can-get-data-and-methods-directly/</link>
                <guid isPermaLink="false">61669b181cb0e006556dc535</guid>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Wed, 13 Oct 2021 08:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/10/caspar-camille-rubin-0qvBNep1Y04-unsplash-1.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>本文源于一次<a href="https://juejin.cn/pin/7005372623400435725" rel="noopener noreferrer">源码共读</a>群里群友的提问，请问@若川，“<strong>为什么 data 中的数据可以用 this 直接获取到啊</strong>”，当时我翻阅源码做出了解答。想着如果下次有人再次问到，我还需要回答一次。当时打算有空写篇文章告诉读者自己探究原理，于是就有了这篇文章。</p><p>阅读本文，你将学到：</p><pre><code class="language-js">1. 如何学习调试 vue2 源码
2. data 中的数据为什么可以用 this 直接获取到
3. methods 中的方法为什么可以用 this 直接获取到
4. 学习源码中优秀代码和思想，投入到自己的项目中
</code></pre><p>本文不难，用过 <code>Vue</code> 的都看得懂，希望大家动手调试和学会看源码。</p><p>看源码可以大胆猜测，最后小心求证。</p><h2 id="2-this-data-methods">2. 示例：this 能够直接获取到 data 和 methods</h2><p>众所周知，这样是可以输出<code>我是若川</code>的。好奇的人就会思考为啥 <code>this</code> 就能直接访问到呢。</p><pre><code class="language-js">const vm = new Vue({
    data: {
        name: '我是若川',
    },
    methods: {
        sayName(){
            console.log(this.name);
        }
    },
});
console.log(vm.name); // 我是若川
console.log(vm.sayName()); // 我是若川
</code></pre><p>那么为什么 <code>this.xxx</code> 能获取到<code>data</code>里的数据，能获取到 <code>methods</code> 方法。</p><p>我们自己构造写的函数，如何做到类似<code>Vue</code>的效果呢。</p><pre><code class="language-js">function Person(options){

}

const p = new Person({
    data: {
        name: '若川'
    },
    methods: {
        sayName(){
            console.log(this.name);
        }
    }
});

console.log(p.name);
// undefined
console.log(p.sayName());
// Uncaught TypeError: p.sayName is not a function
</code></pre><p>如果是你，你会怎么去实现呢？带着问题，我们来调试 <code>Vue2</code>源码学习。</p><h2 id="3-">3. 准备环境调试源码一探究竟</h2><p>可以在本地新建一个文件夹<code>examples</code>，新建文件<code>index.html</code>文件。 在<code>&lt;body&gt;&lt;/body&gt;</code>中加上如下<code>js</code>。</p><pre><code class="language-js">&lt;script src="https://unpkg.com/vue@2.6.14/dist/vue.js"&gt;&lt;/script&gt;
&lt;script&gt;
    const vm = new Vue({
        data: {
            name: '我是若川',
        },
        methods: {
            sayName(){
                console.log(this.name);
            }
        },
    });
    console.log(vm.name);
    console.log(vm.sayName());
&lt;/script&gt;
</code></pre><p>再全局安装<code>npm i -g http-server</code>启动服务。</p><pre><code class="language-js">npm i -g http-server
cd examples
http-server .
// 如果碰到端口被占用，也可以指定端口
http-server -p 8081 .
</code></pre><p>这样就能在<code>http://localhost:8080/</code>打开刚写的<code>index.html</code>页面了。</p><p>对于调试还不是很熟悉的读者，可以看这篇文章<a href="https://mp.weixin.qq.com/s/VOoDHqIo4gh3scHVNxk3lA" rel="noopener noreferrer">《前端容易忽略的 debugger 调试技巧》</a>，截图标注的很详细。</p><p>调试：在 <code>F12</code> 打开调试，<code>source</code> 面板，在例子中<code>const vm = new Vue({</code>打上断点。</p><figure class="kg-card kg-image-card"><img src="https://lxchuan12.gitee.io/assets/img/debugger.6a4010d8.png" class="kg-image" alt="如下图所示" width="600" height="400" loading="lazy"></figure><p>刷新页面后按<code>F11</code>进入函数，这时断点就走进了 Vue 构造函数。</p><h3 id="3-1-vue-">3.1 Vue 构造函数</h3><pre><code class="language-js">function Vue (options) {
    if (!(this instanceof Vue)
    ) {
        warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
}
// 初始化
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);
</code></pre><p>值得一提的是：<code>if (!(this instanceof Vue)){}</code> 判断是不是用了 <code>new</code> 关键词调用构造函数。 一般而言，我们平时应该不会考虑写这个。</p><p>当然看源码库也可以自己函数内部调用 <code>new</code> 。但 <code>vue</code> 一般一个项目只需要 <code>new Vue()</code> 一次，所以没必要。</p><p>而 <code>jQuery</code> 源码的就是内部 <code>new</code> ，对于使用者来说就是无<code>new</code>构造。</p><pre><code class="language-js">jQuery = function( selector, context ) {
  // 返回new之后的对象
  return new jQuery.fn.init( selector, context );
};
</code></pre><p>因为使用 <code>jQuery</code> 经常要调用。 其实 <code>jQuery</code> 也是可以 <code>new</code> 的。和不用 <code>new</code> 是一个效果。</p><p>如果不明白 <code>new</code> 操作符的用处，可以看我之前的文章：<a href="https://chinese.freecodecamp.org/news/javascript-new-operator/">《如何模拟实现 JS 的 new 操作符》</a>。</p><p>调试：继续在<code>this._init(options);</code>处打上断点，按<code>F11</code>进入函数。</p><h3 id="3-2-_init-">3.2 _init 初始化函数</h3><p>进入 <code>_init</code> 函数后，这个函数比较长，做了挺多事情，我们猜测跟<code>data</code>和<code>methods</code>相关的实现在<code>initState(vm)</code>函数里。</p><pre><code class="language-js">// 代码有删减
function initMixin (Vue) {
    Vue.prototype._init = function (options) {
      var vm = this;
      // a uid
      vm._uid = uid$3++;

      // a flag to avoid this being observed
      vm._isVue = true;
      // merge options
      if (options &amp;&amp; options._isComponent) {
        // optimize internal component instantiation
        // since dynamic options merging is pretty slow, and none of the
        // internal component options needs special treatment.
        initInternalComponent(vm, options);
      } else {
        vm.$options = mergeOptions(
          resolveConstructorOptions(vm.constructor),
          options || {},
          vm
        );
      }

      // expose real self
      vm._self = vm;
      initLifecycle(vm);
      initEvents(vm);
      initRender(vm);
      callHook(vm, 'beforeCreate');
      initInjections(vm); // resolve injections before data/props
      //  初始化状态
      initState(vm);
      initProvide(vm); // resolve provide after data/props
      callHook(vm, 'created');
    };
}
</code></pre><p>调试：接着我们在<code>initState(vm)</code>函数这里打算断点，按<code>F8</code>可以直接跳转到这个断点，然后按<code>F11</code>接着进入<code>initState</code>函数。</p><h3 id="3-3-initstate-">3.3 initState 初始化状态</h3><p>从函数名来看，这个函数主要实现功能是：</p><pre><code class="language-bash">初始化 props
初始化 methods
监测数据
初始化 computed
初始化 watch
</code></pre><pre><code class="language-js">function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.props) { initProps(vm, opts.props); }
    // 有传入 methods，初始化方法
    if (opts.methods) { initMethods(vm, opts.methods); }
    // 有传入 data，初始化 data
    if (opts.data) {
      initData(vm);
    } else {
      observe(vm._data = {}, true /* asRootData */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); }
    if (opts.watch &amp;&amp; opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
}
</code></pre><p>我们重点来看初始化 <code>methods</code>，之后再看初始化 <code>data</code>。</p><p>调试：在 <code>initMethods</code> 这句打上断点，同时在<code>initData(vm)</code>处打上断点，看完<code>initMethods</code>函数后，可以直接按<code>F8</code>回到<code>initData(vm)</code>函数。 继续按<code>F11</code>，先进入<code>initMethods</code>函数。</p><h3 id="3-4-initmethods-">3.4 initMethods 初始化方法</h3><pre><code class="language-js">function initMethods (vm, methods) {
    var props = vm.$options.props;
    for (var key in methods) {
      {
        if (typeof methods[key] !== 'function') {
          warn(
            "Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +
            "Did you reference the function correctly?",
            vm
          );
        }
        if (props &amp;&amp; hasOwn(props, key)) {
          warn(
            ("Method \"" + key + "\" has already been defined as a prop."),
            vm
          );
        }
        if ((key in vm) &amp;&amp; isReserved(key)) {
          warn(
            "Method \"" + key + "\" conflicts with an existing Vue instance method. " +
            "Avoid defining component methods that start with _ or $."
          );
        }
      }
      vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
    }
}
</code></pre><p><code>initMethods</code>函数，主要有一些判断。</p><pre><code class="language-js">判断 methods 中的每一项是不是函数，如果不是警告。
判断 methods 中的每一项是不是和 props 冲突了，如果是，警告。
判断 methods 中的每一项是不是已经在 new Vue实例 vm 上存在，而且是方法名是保留的 _ $ （在JS中一般指内部变量标识）开头，如果是警告。
</code></pre><p>除去这些判断，我们可以看出<code>initMethods</code>函数其实就是遍历传入的<code>methods</code>对象，并且使用<code>bind</code>绑定函数的this指向为<code>vm</code>，也就是<code>new Vue</code>的实例对象。</p><p><strong>这就是为什么我们可以通过<code>this</code>直接访问到<code>methods</code>里面的函数的原因</strong>。</p><p>我们可以把鼠标移上 <code>bind</code> 变量，按<code>alt</code>键，可以看到函数定义的地方，这里是<code>218行</code>，点击跳转到这里看 <code>bind</code> 的实现。</p><h4 id="3-4-1-bind-this-">3.4.1 bind 返回一个函数，修改 this 指向</h4><pre><code class="language-js">function polyfillBind (fn, ctx) {
    function boundFn (a) {
      var l = arguments.length;
      return l
        ? l &gt; 1
          ? fn.apply(ctx, arguments)
          : fn.call(ctx, a)
        : fn.call(ctx)
    }

    boundFn._length = fn.length;
    return boundFn
}

function nativeBind (fn, ctx) {
  return fn.bind(ctx)
}

var bind = Function.prototype.bind
  ? nativeBind
  : polyfillBind;
</code></pre><p>简单来说就是兼容了老版本不支持 原生的bind函数。同时兼容写法，对参数多少做出了判断，使用<code>call</code>和<code>apply</code>实现，据说是因为性能问题。</p><p>如果对于<code>call、apply、bind</code>的用法和实现不熟悉，可以查看我之前的文章：<a href="https://chinese.freecodecamp.org/news/javascript-call-and-apply/">《模拟实现 JavaScript 的 call 和 apply 方法》</a>、<a href="https://chinese.freecodecamp.org/news/javascript-bind-method/">《如何模拟实现 JS 的 bind 方法》</a>。</p><p>调试：看完了<code>initMethods</code>函数，按<code>F8</code>回到上文提到的<code>initData(vm)</code>函数断点处。</p><h3 id="3-5-initdata-data">3.5 initData 初始化 data</h3><p><code>initData</code> 函数也是一些判断。主要做了如下事情：</p><pre><code class="language-bash">先给 _data 赋值，以备后用。
最终获取到的 data 不是对象给出警告。
遍历 data ，其中每一项：
如果和 methods 冲突了，报警告。
如果和 props 冲突了，报警告。
不是内部私有的保留属性，做一层代理，代理到 _data 上。
最后监测 data，使之成为响应式的数据。
</code></pre><pre><code class="language-js">function initData (vm) {
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function'
      ? getData(data, vm)
      : data || {};
    if (!isPlainObject(data)) {
      data = {};
      warn(
        'data functions should return an object:\n' +
        'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
        vm
      );
    }
    // proxy data on instance
    var keys = Object.keys(data);
    var props = vm.$options.props;
    var methods = vm.$options.methods;
    var i = keys.length;
    while (i--) {
      var key = keys[i];
      {
        if (methods &amp;&amp; hasOwn(methods, key)) {
          warn(
            ("Method \"" + key + "\" has already been defined as a data property."),
            vm
          );
        }
      }
      if (props &amp;&amp; hasOwn(props, key)) {
        warn(
          "The data property \"" + key + "\" is already declared as a prop. " +
          "Use prop default value instead.",
          vm
        );
      } else if (!isReserved(key)) {
        proxy(vm, "_data", key);
      }
    }
    // observe data
    observe(data, true /* asRootData */);
}
</code></pre><h4 id="3-5-1-getdata-">3.5.1 getData 获取数据</h4><p>是函数时调用函数，执行获取到对象。</p><pre><code class="language-js">function getData (data, vm) {
    // #7573 disable dep collection when invoking data getters
    pushTarget();
    try {
      return data.call(vm, vm)
    } catch (e) {
      handleError(e, vm, "data()");
      return {}
    } finally {
      popTarget();
    }
}
</code></pre><h4 id="3-5-2-proxy-">3.5.2 proxy 代理</h4><p>其实就是用 <code>Object.defineProperty</code> 定义对象</p><p>这里用处是：<code>this.xxx</code> 则是访问的 <code>this._data.xxx</code>。</p><pre><code class="language-js">/**
   * Perform no operation.
   * Stubbing args to make Flow happy without leaving useless transpiled code
   * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
   */
function noop (a, b, c) {}
var sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
};

function proxy (target, sourceKey, key) {
    sharedPropertyDefinition.get = function proxyGetter () {
      return this[sourceKey][key]
    };
    sharedPropertyDefinition.set = function proxySetter (val) {
      this[sourceKey][key] = val;
    };
    Object.defineProperty(target, key, sharedPropertyDefinition);
}
</code></pre><h4 id="3-5-3-object-defineproperty-">3.5.3 Object.defineProperty 定义对象属性</h4><p><code>Object.defineProperty</code> 算是一个非常重要的API。还有一个定义多个属性的API：<code>Object.defineProperties(obj, props) (ES5)</code></p><p><code>Object.defineProperty</code> 涉及到比较重要的知识点，面试也常考。</p><pre><code class="language-bash">value——当试图获取属性时所返回的值。
writable——该属性是否可写。
enumerable——该属性在for in循环中是否会被枚举。
configurable——该属性是否可被删除。
set()——该属性的更新操作所调用的函数。
get()——获取属性值时所调用的函数。
</code></pre><p><a href="https://juejin.cn/post/6994976281053888519#heading-34" rel="noopener noreferrer">详细举例见此链接</a>。</p><h3 id="3-6-">3.6 文中出现的一些函数，最后统一解释下</h3><h4 id="3-6-1-hasown-">3.6.1 hasOwn 是否是对象本身拥有的属性</h4><p>调试模式下，按<code>alt</code>键，把鼠标移到方法名上，可以看到函数定义的地方。点击可以跳转。</p><pre><code class="language-js">/**
   * Check whether an object has the property.
   */
var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {
  return hasOwnProperty.call(obj, key)
}

hasOwn({ a: undefined }, 'a') // true
hasOwn({}, 'a') // false
hasOwn({}, 'hasOwnProperty') // false
hasOwn({}, 'toString') // false
// 是自己的本身拥有的属性，不是通过原型链向上查找的。
</code></pre><h4 id="3-6-2-isreserved-_-">3.6.2 isReserved 是否是内部私有保留的字符串$ 和 _ 开头</h4><pre><code class="language-js">/**
   * Check if a string starts with $ or _
   */
function isReserved (str) {
  var c = (str + '').charCodeAt(0);
  return c === 0x24 || c === 0x5F
}
isReserved('_data'); // true
isReserved('$options'); // true
isReserved('data'); // false
isReserved('options'); // false
</code></pre><h2 id="4-60-">4. 最后用60余行代码实现简化版</h2><pre><code class="language-js">function noop (a, b, c) {}
var sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
};
function proxy (target, sourceKey, key) {
    sharedPropertyDefinition.get = function proxyGetter () {
      return this[sourceKey][key]
    };
    sharedPropertyDefinition.set = function proxySetter (val) {
      this[sourceKey][key] = val;
    };
    Object.defineProperty(target, key, sharedPropertyDefinition);
}
function initData(vm){
  const data = vm._data = vm.$options.data;
  const keys = Object.keys(data);
  var i = keys.length;
  while (i--) {
    var key = keys[i];
    proxy(vm, '_data', key);
  }
}
function initMethods(vm, methods){
  for (var key in methods) {
    vm[key] = typeof methods[key] !== 'function' ? noop : methods[key].bind(vm);
  }
}

function Person(options){
  let vm = this;
  vm.$options = options;
  var opts = vm.$options;
  if(opts.data){
    initData(vm);
  }
  if(opts.methods){
    initMethods(vm, opts.methods)
  }
}

const p = new Person({
    data: {
        name: '若川'
    },
    methods: {
        sayName(){
            console.log(this.name);
        }
    }
});

console.log(p.name);
// 未实现前： undefined
// '若川'
console.log(p.sayName());
// 未实现前：Uncaught TypeError: p.sayName is not a function
// '若川'
</code></pre><h2 id="5-">5. 总结</h2><p>本文涉及到的基础知识主要有如下：</p><pre><code class="language-js">构造函数
this 指向
call、bind、apply
Object.defineProperty
等等基础知识。
</code></pre><p>本文源于解答源码共读群友的疑惑，通过详细的描述了如何调试 <code>Vue</code> 源码，来探寻答案。</p><p>解答文章开头提问：</p><p>通过<code>this</code>直接访问到<code>methods</code>里面的函数的原因是：因为<code>methods</code>里的方法通过 <code>bind</code> 指定了<code>this</code>为 new Vue的实例(<code>vm</code>)。</p><p>通过 <code>this</code> 直接访问到 <code>data</code> 里面的数据的原因是：data里的属性最终会存储到<code>new Vue</code>的实例（<code>vm</code>）上的 <code>_data</code>对象中，访问 <code>this.xxx</code>，是访问<code>Object.defineProperty</code>代理后的 <code>this._data.xxx</code>。</p><p><code>Vue</code>的这种设计，好处在于便于获取。也有不方便的地方，就是<code>props</code>、<code>methods</code> 和 <code>data</code>三者容易产生冲突。</p><p>文章整体难度不大，但非常建议读者朋友们自己动手调试下。调试后，你可能会发现：原来 <code>Vue</code> 源码，也没有想象中的那么难，也能看懂一部分。</p><p>启发：我们工作使用常用的技术和框架或库时，保持好奇心，多思考内部原理。能够做到知其然，知其所以然。就能远超很多人。</p><p>你可能会思考，为什么模板语法中，可以省略<code>this</code>关键词写法呢，内部模板编译时其实是用了<code>with</code>。有余力的读者可以探究这一原理。</p><p>最后，欢迎加我微信 <a href="https://juejin.cn/pin/7005372623400435725" rel="noopener noreferrer">ruochuan12</a> 交流，参与<a href="https://juejin.cn/pin/7005372623400435725" rel="noopener noreferrer">源码共读</a>活动，大家一起学习源码，共同进步。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 50 行代码串行 Promise，koa 洋葱模型原来这么实现？ ]]>
                </title>
                <description>
                    <![CDATA[ 之前我写过 koa 源码文章《学习 koa 源码的整体架构，浅析koa洋葱模型原理和co原理 [https://chinese.freecodecamp.org/news/learn-the-overall-architecture-of-koa-source-code/] 》，比较长，读者朋友大概率看不完，所以本文从koa-compose50行源码讲述。 本文涉及到的 koa-compose 仓库 [https://github.com/koajs/compose]文件，整个index.js文件代码行数虽然不到  50 行，而且测试用例test/test.js文件 300 余行，但非常值得我们学习。 歌德曾说：读一本好书，就是在和高尚的人谈话。同理可得：读源码，也算是和作者的一种学习交流的方式。 阅读本文，你将学到： 1. 熟悉 koa-compose 中间件源码、可以应对面试官相关问题 2. 学会使用测试用例调试源码 3. 学会 jest 部分用法 2. 环境准备 2.1 克隆 koa-compose 项目 本文仓库地址 koa-compose-analysis [ht ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/koa-compose/</link>
                <guid isPermaLink="false">6157fdc0dfb2500637a75cb5</guid>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Fri, 01 Oct 2021 06:35:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/10/pexels-josh-sorenson-1154504-1.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>之前我写过 koa 源码文章《<a href="https://chinese.freecodecamp.org/news/learn-the-overall-architecture-of-koa-source-code/">学习 koa 源码的整体架构，浅析koa洋葱模型原理和co原理</a>》，比较长，读者朋友大概率看不完，所以本文从<code>koa-compose</code>50行源码讲述。</p><p>本文涉及到的 <a href="https://github.com/koajs/compose" rel="noopener noreferrer">koa-compose 仓库</a>文件，整个<code>index.js</code>文件代码行数虽然不到 <code>50</code> 行，而且测试用例<code>test/test.js</code>文件 <code>300</code> 余行，但非常值得我们学习。</p><p>歌德曾说：读一本好书，就是在和高尚的人谈话。同理可得：读源码，也算是和作者的一种学习交流的方式。</p><p>阅读本文，你将学到：</p><pre><code class="language-bash">1. 熟悉 koa-compose 中间件源码、可以应对面试官相关问题
2. 学会使用测试用例调试源码
3. 学会 jest 部分用法
</code></pre><h2 id="2-">2. 环境准备</h2><h3 id="2-1-koa-compose-">2.1 克隆 koa-compose 项目</h3><p><a href="https://github.com/lxchuan12/koa-compose-analysis.git" rel="noopener noreferrer">本文仓库地址 koa-compose-analysis</a>，求个<code>star</code>~</p><pre><code class="language-bash"># 可以直接克隆我的仓库，我的仓库保留的 compose 仓库的 git 记录
git clone https://github.com/lxchuan12/koa-compose-analysis.git
cd koa-compose/compose
npm i
</code></pre><p>顺带说下：我是怎么保留 <code>compose</code> 仓库的 <code>git</code> 记录的。</p><pre><code class="language-bash"># 在 github 上新建一个仓库 `koa-compose-analysis` 克隆下来
git clone https://github.com/lxchuan12/koa-compose-analysis.git
cd koa-compose-analysis
git subtree add --prefix=compose https://github.com/koajs/compose.git main
# 这样就把 compose 文件夹克隆到自己的 git 仓库了。且保留的 git 记录
</code></pre><p>关于更多 <code>git subtree</code>，可以看这篇文章<a href="https://segmentfault.com/a/1190000003969060" rel="noopener noreferrer">用 Git Subtree 在多个 Git 项目间双向同步子项目，附简明使用手册</a>。</p><p>接着我们来看怎么根据开源项目中提供的测试用例调试源码。</p><h3 id="2-2-compose-">2.2 根据测试用例调试 compose 源码</h3><p>用<code>VSCode</code>（我的版本是 <code>1.60</code> ）打开项目，找到 <code>compose/package.json</code>，找到 <code>scripts</code> 和 <code>test</code> 命令。</p><pre><code class="language-json">// compose/package.json
{
    "name": "koa-compose",
    // debug （调试）
    "scripts": {
        "eslint": "standard --fix .",
        "test": "jest"
    },
}
</code></pre><p>在<code>scripts</code>上方应该会有<code>debug</code>或者<code>调试</code>字样。点击<code>debug</code>(调试)，选择 <code>test</code>。</p><figure class="kg-card kg-image-card"><img src="https://lxchuan12.gitee.io/assets/img/scripts-test-debugger.2a513076.png" class="kg-image" alt="VSCode 调试" width="600" height="400" loading="lazy"></figure><p>接着会执行测试用例<code>test/test.js</code>文件。终端输出如下图所示。</p><figure class="kg-card kg-image-card"><img src="https://lxchuan12.gitee.io/assets/img/jest-ternimal.baec9c72.png" class="kg-image" alt="koa-compose 测试用例输出结果" width="600" height="400" loading="lazy"></figure><p>接着我们调试 <code>compose/test/test.js</code> 文件。 我们可以在 <code>45行</code> 打上断点，重新点击 <code>package.json</code> =&gt; <code>srcipts</code> =&gt; <code>test</code> 进入调试模式。 如下图所示。</p><figure class="kg-card kg-image-card"><img src="https://lxchuan12.gitee.io/assets/img/test-compose-debugger.bae5b15c.png" class="kg-image" alt="koa-compose 调试" width="600" height="400" loading="lazy"></figure><p>接着按上方的按钮，继续调试。在<code>compose/index.js</code>文件中关键的地方打上断点，调试学习源码事半功倍。</p><p><a href="https://code.visualstudio.com/docs/nodejs/nodejs-debugging" rel="noopener noreferrer">更多 nodejs 调试相关 可以查看官方文档</a>。</p><p>顺便提一下几个调试相关按钮。</p><ol><li>继续（F5）</li></ol><ol><li>单步跳过（F10）</li></ol><ol><li>单步调试（F11）</li></ol><ol><li>单步跳出（Shift + F11）</li></ol><ol><li>重启（Ctrl + Shift + F5）</li></ol><ol><li>断开链接（Shift + F5）</li></ol><p>接下来，我们跟着测试用例学源码。</p><h2 id="3-">3. 跟着测试用例学源码</h2><p>分享一个测试用例小技巧：我们可以在测试用例处加上<code>only</code>修饰。</p><pre><code class="language-js">// 例如
it.only('should work', async () =&gt; {})
</code></pre><p>这样我们就可以只执行当前的测试用例，不关心其他的，不会干扰调试。</p><h3 id="3-1-">3.1 正常流程</h3><p>打开 <code>compose/test/test.js</code> 文件，看第一个测试用例。</p><pre><code class="language-js">// compose/test/test.js
'use strict'

/* eslint-env jest */

const compose = require('..')
const assert = require('assert')

function wait (ms) {
  return new Promise((resolve) =&gt; setTimeout(resolve, ms || 1))
}
// 分组
describe('Koa Compose', function () {
  it.only('should work', async () =&gt; {
    const arr = []
    const stack = []

    stack.push(async (context, next) =&gt; {
      arr.push(1)
      await wait(1)
      await next()
      await wait(1)
      arr.push(6)
    })

    stack.push(async (context, next) =&gt; {
      arr.push(2)
      await wait(1)
      await next()
      await wait(1)
      arr.push(5)
    })

    stack.push(async (context, next) =&gt; {
      arr.push(3)
      await wait(1)
      await next()
      await wait(1)
      arr.push(4)
    })

    await compose(stack)({})
    // 最后输出数组是 [1,2,3,4,5,6]
    expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6]))
  })
}
</code></pre><p>大概看完这段测试用例，<code>context</code>是什么，<code>next</code>又是什么。</p><p>在<a href="https://github.com/koajs/koa/blob/master/docs/guide.md#writing-middleware" rel="noopener noreferrer"><code>koa</code>的文档</a>上有个非常代表性的中间件 <code>gif</code> 图。</p><figure class="kg-card kg-image-card"><img src="https://lxchuan12.gitee.io/assets/img/middleware.104a11b9.gif" class="kg-image" alt="中间件 gif 图" width="600" height="400" loading="lazy"></figure><p>而<code>compose</code>函数作用就是把添加进中间件数组的函数按照上面 <code>gif</code> 图的顺序执行。</p><h4 id="3-1-1-compose-">3.1.1 compose 函数</h4><p>简单来说，<code>compose</code> 函数主要做了两件事情。</p><ol><li>接收一个参数，校验参数是数组，且校验数组中的每一项是函数。</li></ol><ol><li>返回一个函数，这个函数接收两个参数，分别是<code>context</code>和<code>next</code>，这个函数最后返回<code>Promise</code>。</li></ol><pre><code class="language-js">/**
 * 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){
      // 省略，下文讲述
    }
  }
}
</code></pre><p>接着我们来看 <code>dispatch</code> 函数。</p><h4 id="3-1-2-dispatch-">3.1.2 dispatch 函数</h4><pre><code class="language-js">function dispatch (i) {
  // 一个函数中多次调用报错
  // await next()
  // await next()
  if (i &lt;= index) return Promise.reject(new Error('next() called multiple times'))
  index = i
  // 取出数组里的 fn1, fn2, fn3...
  let fn = middleware[i]
  // 最后 相等，next 为 undefined
  if (i === middleware.length) fn = next
  // 直接返回 Promise.resolve()
  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>bind</code>函数是返回一个新的函数。第一个参数是函数里的this指向（如果函数不需要使用<code>this</code>，一般会写成<code>null</code>）。 这句<code>fn(context, dispatch.bind(null, i + 1)</code>，<code>i + 1</code> 是为了 <code>let fn = middleware[i]</code> 取<code>middleware</code>中的下一个函数。 也就是 <code>next</code> 是下一个中间件里的函数。也就能解释上文中的 <code>gif</code>图函数执行顺序。 测试用例中数组的最终顺序是<code>[1,2,3,4,5,6]</code>。</p><h4 id="3-1-3-compose-">3.1.3 简化 compose 便于理解</h4><p>自己动手调试之后，你会发现 <code>compose</code> 执行后就是类似这样的结构（省略 <code>try catch</code> 判断）。</p><pre><code class="language-js">// 这样就可能更好理解了。
// simpleKoaCompose
const [fn1, fn2, fn3] = stack;
const fnMiddleware = function(context){
    return Promise.resolve(
      fn1(context, function next(){
        return Promise.resolve(
          fn2(context, function next(){
              return Promise.resolve(
                  fn3(context, function next(){
                    return Promise.resolve();
                  })
              )
          })
        )
    })
  );
};
</code></pre><p>也就是说<code>koa-compose</code>返回的是一个<code>Promise</code>，从<code>中间件（传入的数组）</code>中取出第一个函数，传入<code>context</code>和第一个<code>next</code>函数来执行。<br>第一个<code>next</code>函数里也是返回的是一个<code>Promise</code>，从<code>中间件（传入的数组）</code>中取出第二个函数，传入<code>context</code>和第二个<code>next</code>函数来执行。<br>第二个<code>next</code>函数里也是返回的是一个<code>Promise</code>，从<code>中间件（传入的数组）</code>中取出第三个函数，传入<code>context</code>和第三个<code>next</code>函数来执行。<br>第三个...<br>以此类推。最后一个中间件中有调用<code>next</code>函数，则返回<code>Promise.resolve</code>。如果没有，则不执行<code>next</code>函数。 这样就把所有中间件串联起来了。这也就是我们常说的洋葱模型。<br></p><figure class="kg-card kg-image-card"><img src="https://lxchuan12.gitee.io/assets/img/middleware.dabded55.png" class="kg-image" alt="洋葱模型图如下图所示：" width="600" height="400" loading="lazy"></figure><p><strong>不得不说非常惊艳，“玩还是大神会玩”</strong>。</p><h3 id="3-2-">3.2 错误捕获</h3><pre><code class="language-js">it('should catch downstream errors', async () =&gt; {
  const arr = []
  const stack = []

  stack.push(async (ctx, next) =&gt; {
    arr.push(1)
    try {
      arr.push(6)
      await next()
      arr.push(7)
    } catch (err) {
      arr.push(2)
    }
    arr.push(3)
  })

  stack.push(async (ctx, next) =&gt; {
    arr.push(4)
    throw new Error()
  })

  await compose(stack)({})
  // 输出顺序 是 [ 1, 6, 4, 2, 3 ]
  expect(arr).toEqual([1, 6, 4, 2, 3])
})
</code></pre><p>相信理解了第一个测试用例和 <code>compose</code> 函数，也是比较好理解这个测试用例了。这一部分其实就是对应的代码在这里。</p><pre><code class="language-js">try {
    return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
  return Promise.reject(err)
}
</code></pre><h3 id="3-3-next-">3.3 next 函数不能调用多次</h3><pre><code class="language-js">it('should throw if next() is called multiple times', () =&gt; {
  return compose([
    async (ctx, next) =&gt; {
      await next()
      await next()
    }
  ])({}).then(() =&gt; {
    throw new Error('boom')
  }, (err) =&gt; {
    assert(/multiple times/.test(err.message))
  })
})
</code></pre><p>这一块对应的则是：</p><pre><code class="language-js">index = -1
dispatch(0)
function dispatch (i) {
  if (i &lt;= index) return Promise.reject(new Error('next() called multiple times'))
  index = i
}
</code></pre><p>调用两次后 <code>i</code> 和 <code>index</code> 都为 <code>1</code>，所以会报错。</p><p><code>compose/test/test.js</code>文件中总共 300余行，还有很多测试用例可以按照文中方法自行调试。</p><h2 id="4-">4. 总结</h2><p>虽然<code>koa-compose</code>源码 50行 不到，但如果是第一次看源码调试源码，还是会有难度的。其中混杂着高阶函数、闭包、<code>Promise</code>、<code>bind</code>等基础知识。</p><p>通过本文，我们熟悉了 <code>koa-compose</code> 中间件常说的洋葱模型，学会了部分 <a href="https://github.com/facebook/jest" rel="noopener noreferrer"><code>jest</code></a> 用法，同时也学会了如何使用现成的测试用例去调试源码。</p><p><strong>相信学会了通过测试用例调试源码后，会觉得源码也没有想象中的那么难</strong>。</p><p>开源项目，一般都会有很全面的测试用例。除了可以给我们学习源码调试源码带来方便的同时，也可以给我们带来的启发：自己工作中的项目，也可以逐步引入测试工具，比如 <a href="https://github.com/facebook/jest" rel="noopener noreferrer"><code>jest</code></a></p><p>此外，读开源项目源码是我们学习业界大牛设计思想和源码实现等比较好的方式。</p><p>看完本文，非常希望能自己动手实践调试源码去学习，容易吸收消化。另外，如果你有余力，可以继续看我的 <code>koa-compose</code> 源码文章：《<a href="https://juejin.cn/post/6844904088220467213https://chinese.freecodecamp.org/news/learn-the-overall-architecture-of-koa-source-code/">学习 koa 源码的整体架构，浅析koa洋葱模型原理和co原理</a>》。</p><p>欢迎在<a href="https://lxchuan12.gitee.io/">我的网站</a>查看更多内容。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 尤雨溪在 Vuex 源码中是怎么处理 this 指向丢失的 ]]>
                </title>
                <description>
                    <![CDATA[ 最近有小伙伴看我的 Vuex源码 [http://mp.weixin.qq.com/s?__biz=MzA5MjQwMzQyNw==&mid=2650744584&idx=1&sn=b14f8a762f132adcf0f7e3e075ee2ded&chksm=88662484bf11ad922ed27d45873af838298949eea381545e82a511cabf0c6fc6876a8370c6fb&scene=21#wechat_redirect] 文章，提到有一处this指向有点看不懂（好不容易终于有人看我的源码文章了，感动得要流泪了^_^）。于是我写篇文章答疑解惑，简单再说说 this 指向和尤大在 Vuex 源码中是怎么处理 this 指向丢失的。 2. 对象中的this指向 var person = {   name: '若川',   say: function(text){     console.log(this.name + ', ' + text);   } } console.log(person.name); console.log(person.sa ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/this-in-vuex/</link>
                <guid isPermaLink="false">61349c24abc6f30645ecce8c</guid>
                
                    <category>
                        <![CDATA[ Vuex ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Sun, 05 Sep 2021 10:35:08 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/09/glenn-carstens-peters-npxXWgQ33ZQ-unsplash-1.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>最近有小伙伴看我的 <a href="http://mp.weixin.qq.com/s?__biz=MzA5MjQwMzQyNw==&amp;mid=2650744584&amp;idx=1&amp;sn=b14f8a762f132adcf0f7e3e075ee2ded&amp;chksm=88662484bf11ad922ed27d45873af838298949eea381545e82a511cabf0c6fc6876a8370c6fb&amp;scene=21#wechat_redirect" rel="noopener noreferrer">Vuex源码</a>文章，提到有一处<code>this</code>指向有点看不懂（好不容易终于有人看我的源码文章了，感动得要流泪了<code>^_^</code>）。于是我写篇文章答疑解惑，简单再说说 <code>this</code> 指向和<code>尤大在 Vuex 源码中</code>是怎么处理 <code>this</code> 指向丢失的。</p><h2 id="2-this-">2. 对象中的this指向</h2><pre><code class="language-js">var person = {
  name: '若川',
  say: function(text){
    console.log(this.name + ', ' + text);
  }
}
console.log(person.name);
console.log(person.say('在写文章')); // 若川, 在写文章
var say = person.say;
say('在写文章'); // 这里的this指向就丢失了，指向window了。(非严格模式)
</code></pre><h2 id="3-this-">3. 类中的this指向</h2><h3 id="3-1-es5">3.1 ES5</h3><pre><code class="language-js">// ES5
var Person = function(){
  this.name = '若川';
}
Person.prototype.say = function(text){
  console.log(this.name + ', ' + text);
}
var person = new Person();
console.log(person.name); // 若川
console.log(person.say('在写文章'));
var say = person.say;
say('在写文章'); // 这里的this指向就丢失了，指向 window 了。
</code></pre><h3 id="3-2-es6">3.2 ES6</h3><pre><code class="language-js">// ES6
class Person{
  construcor(name = '若川'){
     this.name = name;
  }
  say(text){
    console.log(`${this.name}, ${text}`);
  }
}
const person = new Person();
person.say('在写文章')
// 解构
const { say } = person;
say('在写文章'); // 报错 this ，因为ES6 默认启用严格模式，严格模式下指向 undefined
</code></pre><h2 id="4-vuex-">4. 尤大在Vuex源码中是怎么处理的</h2><p>先看代码：</p><pre><code class="language-js">class Store{
  constructor(options = {}){
     this._actions = Object.create(null);
  // bind commit and dispatch to self
      // 给自己 绑定 commit 和 dispatch
      const store = this
      const { dispatch, commit } = this
      // 为何要这样绑定 ?
      // 说明调用commit和dispach 的 this 不一定是 store 实例
      // 这是确保这两个函数里的this是store实例
      this.dispatch = function boundDispatch (type, payload) {
        return dispatch.call(store, type, payload)
      }
      this.commit = function boundCommit (type, payload, options) {
        return commit.call(store, type, payload, options)
      }
  }
  dispatch(){
     console.log('dispatch', this);
  }
  commit(){
     console.log('commit', this);
  }
}
const store = new Store();
store.dispatch(); // 输出结果 this 是什么呢？

const { dispatch, commit } = store;
dispatch(); // 输出结果 this 是什么呢？
commit();  // 输出结果 this 是什么呢？
</code></pre><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/09/store-dispatch-example.jpg" class="kg-image" alt="store-dispatch-example" width="600" height="400" loading="lazy"></figure><p><strong>结论</strong>：非常巧妙的用了<code>call</code>把<code>dispatch</code>和<code>commit</code>函数的<code>this</code>指向强制绑定到<code>store</code>实例对象上。如果不这么绑定就报错了。</p><h3 id="4-1-actions-store">4.1 actions 解构 store</h3><p>其实<code>Vuex</code>源码里就有上面解构<code>const { dispatch, commit } = store;</code>的写法。想想我们平时是如何写<code>actions</code>的。<code>actions</code>中自定义函数的第一个参数其实就是 <code>store</code> 实例。</p><p>这时我们翻看下<code>actions文档</code>：<code>https://vuex.vuejs.org/zh/guide/actions.html</code></p><pre><code class="language-js">const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})
</code></pre><p>也可以用解构赋值的写法。</p><pre><code class="language-js">actions: {
  increment ({ commit }) {
    commit('increment')
  }
}
</code></pre><p>有了<code>Vuex</code>源码构造函数里的<code>call</code>绑定，这样<code>this</code>指向就被修正啦~<strong>不得不说祖师爷就是厉害</strong>。这一招，大家可以免费学走~</p><p>接着我们带着问题，为啥上文中的<code>context</code>就是<code>store</code>实例，有<code>dispatch</code>、<code>commit</code>这些方法呢。继续往下看。</p><h3 id="4-2-actions-store-">4.2 为什么 actions 对象里的自定义函数 第一个参数就是 store 实例。</h3><p>以下是简单源码，有缩减，感兴趣的可以看我的<a href="https://lxchuan12.gitee.io/vuex" rel="noopener noreferrer">Vuex源码文章</a>。</p><pre><code class="language-js">class Store{
 construcor(){
    // 初始化 根模块
    // 并且也递归的注册所有子模块
    // 并且收集所有模块的 getters 放在 this._wrappedGetters 里面
    installModule(this, state, [], this._modules.root)
 }
}
</code></pre><p>接着我们看<code>installModule</code>函数中的遍历注册 <code>actions</code> 实现</p><pre><code class="language-js">function installModule (store, rootState, path, module, hot) {
    // 省略若干代码
    // 循环遍历注册 action
    module.forEachAction((action, key) =&gt; {
      const type = action.root ? key : namespace + key
      const handler = action.handler || action
      registerAction(store, type, handler, local)
    })
}
</code></pre><p>接着看注册 <code>actions</code> 函数实现 <code>registerAction</code></p><pre><code class="language-js">/**
* 注册 mutation
* @param {Object} store 对象
* @param {String} type 类型
* @param {Function} handler 用户自定义的函数
* @param {Object} local local 对象
*/
function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  // payload 是actions函数的第二个参数
  entry.push(function wrappedActionHandler (payload) {
    /**
     * 也就是为什么用户定义的actions中的函数第一个参数有
     *  { dispatch, commit, getters, state, rootGetters, rootState } 的原因
     * actions: {
     *    checkout ({ commit, state }, products) {
     *        console.log(commit, state);
     *    }
     * }
     */
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload)
    // 源码有删减
}
</code></pre><p>比较容易发现调用顺序是 <code>new Store() =&gt; installModule(this) =&gt; registerAction(store) =&gt; let res = handler.call(store)</code>。</p><p>其中<code>handler</code> 就是 用户自定义的函数，也就是对应上文的例子<code>increment</code>函数。<code>store</code>实例对象一路往下传递，到<code>handler</code>执行时，也是用了<code>call</code>函数，强制绑定了第一个参数是<code>store</code>实例对象。</p><pre><code class="language-js">actions: {
  increment ({ commit }) {
    commit('increment')
  }
}
</code></pre><p>这也就是为什么 <code>actions</code> 对象中的自定义函数的第一个参数是 <code>store</code> 对象实例了。</p><p>好啦，文章到这里就基本写完啦~相对简短一些。应该也比较好理解。</p><h2 id="5-this-">5. 最后再总结下 this 指向</h2><p>摘抄下<a href="https://chinese.freecodecamp.org/news/javascript-this/">面试官问：this 指向</a>文章结尾。</p><p>如果要判断一个运行中函数的 <code>this</code> 绑定， 就需要找到这个函数的直接调用位置。 找到之后 就可以顺序应用下面这四条规则来判断 <code>this</code> 的绑定对象。<br></p><ol><li><code>new</code> 调用：绑定到新创建的对象，注意：显示<code>return</code>函数或对象，返回值不是新创建的对象，而是显式返回的函数或对象。<br></li><li><code>call</code> 或者 <code>apply</code>（ 或者 <code>bind</code>） 调用：严格模式下，绑定到指定的第一个参数。非严格模式下，<code>null</code>和<code>undefined</code>，指向全局对象（浏览器中是<code>window</code>），其余值指向被<code>new Object()</code>包装的对象。<br></li><li>对象上的函数调用：绑定到那个对象。<br></li><li>普通函数调用： 在严格模式下绑定到 <code>undefined</code>，否则绑定到全局对象。</li></ol><p><code>ES6</code> 中的箭头函数：不会使用上文的四条标准的绑定规则， 而是根据当前的词法作用域来决定<code>this</code>， 具体来说， 箭头函数会继承外层函数，调用的 this 绑定（ 无论 this 绑定到什么），没有外层函数，则是绑定到全局对象（浏览器中是<code>window</code>）。 这其实和 <code>ES6</code> 之前代码中的 <code>self = this</code> 机制一样。</p><p>欢迎在<a href="https://lxchuan12.gitee.io/">我的博客</a>阅读更多文章。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 初学者也能看懂的 Vue3 源码中那些实用的基础工具函数 ]]>
                </title>
                <description>
                    <![CDATA[ 1. 前言 本文通过学习Vue3源码中的工具函数模块的源码，学习源码为自己所用。 > 歌德曾说：读一本好书，就是在和高尚的人谈话。同理可得：读源码，也算是和作者的一种学习交流的方式。 阅读本文，你将学到：  * 如何学习 JavaScript 基础知识，会推荐很多学习资料  * 如何学习调试 vue 3 源码  * 如何学习源码中优秀代码和思想，投入到自己的项目中  * Vue 3 源码 shared 模块中的几十个实用工具函数  * 我的一些经验分享 shared模块中57个工具函数，本次阅读其中的30余个。 2. 环境准备 2.1 读开源项目 贡献指南 打开 vue-next [https://github.com/vuejs/vue-next]，开源项目一般都能在 README.md 或者  .github/contributing.md [https://github.com/vuejs/vue-next/blob/master/.github/contributing.md]找到贡献指南。 而贡献指南写了很多关于参与项目开发的信息。比如怎么跑起来，项目目录结构是怎样 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/vue3-source-code/</link>
                <guid isPermaLink="false">611b716d020863066536ab6a</guid>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Mon, 16 Aug 2021 08:21:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/08/trnava-university-BEEyeib-am8-unsplash.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="1-">1. 前言</h2><p>本文通过学习<code>Vue3</code>源码中的工具函数模块的源码，学习源码为自己所用。</p><blockquote>歌德曾说：读一本好书，就是在和高尚的人谈话。同理可得：读源码，也算是和作者的一种学习交流的方式。</blockquote><p>阅读本文，你将学到：</p><ul><li>如何学习 JavaScript 基础知识，会推荐很多学习资料</li><li>如何学习调试 vue 3 源码</li><li>如何学习源码中优秀代码和思想，投入到自己的项目中</li><li>Vue 3 源码 shared 模块中的几十个实用工具函数</li><li>我的一些经验分享</li></ul><p><code>shared</code>模块中<code>57个</code>工具函数，本次阅读其中的<code>30余个</code>。</p><h2 id="2-">2. 环境准备</h2><h3 id="2-1-">2.1 读开源项目 贡献指南</h3><p>打开 <a href="https://github.com/vuejs/vue-next" rel="noopener noreferrer">vue-next</a>，开源项目一般都能在 <code>README.md</code> 或者 <a href="https://github.com/vuejs/vue-next/blob/master/.github/contributing.md" rel="noopener noreferrer">.github/contributing.md </a>找到贡献指南。</p><p>而贡献指南写了很多关于参与项目开发的信息。比如怎么跑起来，项目目录结构是怎样的。怎么投入开发，需要哪些知识储备等。</p><p>我们可以在<a href="https://github.com/vuejs/vue-next/blob/master/.github/contributing.md#project-structure" rel="noopener noreferrer">项目目录结构</a>描述中，找到<code>shared</code>模块。</p><p><code>shared</code>: Internal utilities shared across multiple packages (especially environment-agnostic utils used by both runtime and compiler packages).</p><p><code>README.md</code> 和 <code>contributing.md</code> 一般都是英文的。可能会难倒一部分人。其实看不懂，完全可以可以借助划词翻译，整页翻译和百度翻译等翻译工具。再把英文加入后续学习计划。</p><p>本文就是讲<code>shared</code>模块，对应的文件路径是：<a href="https://github.com/vuejs/vue-next/blob/master/packages/shared/src/index.ts" rel="noopener noreferrer"><code>vue-next/packages/shared/src/index.ts</code></a></p><p>也可以用<code>github1s</code>访问，速度更快，<a href="https://github1s.com/vuejs/vue-next/blob/master/packages/shared/src/index.ts" rel="noopener noreferrer">github1s packages/shared/src/index.ts</a></p><h3 id="2-2-">2.2 按照项目指南 打包构建代码</h3><p>为了降低文章难度，我按照贡献指南中方法打包把<code>ts</code>转成了<code>js</code>。如果你需要打包，也可以参考下文打包构建。</p><p>你需要确保 <a href="http://nodejs.org/" rel="noopener noreferrer">Node.js</a> 版本是 <code>10+</code>, 而且 <code>yarn</code> 的版本是 <code>1.x</code> <a href="https://yarnpkg.com/en/docs/install" rel="noopener noreferrer">Yarn 1.x</a>。</p><p>你安装的 <code>Node.js</code> 版本很可能是低于 <code>10</code>。最简单的办法就是去官网重新安装。也可以使用 <code>nvm</code>等管理<code>Node.js</code>版本。</p><pre><code class="language-bash">node -v
# v14.16.0
# 全局安装 yarn
# 克隆项目
git clone https://github.com/vuejs/vue-next.git
cd vue-next

# 或者克隆我的项目
git clone https://github.com/lxchuan12/vue-next-analysis.git
cd vue-next-analysis

npm install --global yarn
yarn # install the dependencies of the project
# yarn —ignore-scripts 忽略一些安装，更快速
yarn build
</code></pre><p>可以得到 <code>vue-next/packages/shared/dist/shared.esm-bundler.js</code>，文件也就是纯<code>js</code>文件。也接下就是解释其中的一些方法。</p><p>当然，前面可能比较啰嗦。我可以直接讲 <code>3. 工具函数</code>。但通过我上文的介绍，即使是初学者，都能看懂一些开源项目源码，也许就会有一定的成就感。 另外，面试问到被类似的问题或者笔试题时，你说看<code>Vue3</code>源码学到的，面试官绝对对你刮目相看。</p><h3 id="2-3-sourcemap-vue-next-">2.3 如何生成 sourcemap 调试 vue-next 源码</h3><p>熟悉我的读者知道，我是经常强调生成<code>sourcemap</code>调试看源码，所以顺便提一下如何配置生成<code>sourcemap</code>，如何调试。这部分可以简单略过，动手操作时再仔细看。</p><p>其实<a href="https://github.com/vuejs/vue-next/blob/master/.github/contributing.md" rel="noopener noreferrer">贡献指南</a>里描述了。</p><p>Build with Source Maps Use the <code>--sourcemap</code> or <code>-s</code> flag to build with source maps. Note this will make the build much slower.</p><p>所以在 <code>vue-next/package.json</code> 追加 <code>"dev:sourcemap": "node scripts/dev.js --sourcemap"</code>，<code>yarn dev:sourcemap</code>执行，即可生成<code>sourcemap</code>，或者直接 <code>build</code>。</p><pre><code class="language-json">// package.json
{
    "version": "3.2.1",
    "scripts": {
        "dev:sourcemap": "node scripts/dev.js --sourcemap"
    }
}
</code></pre><p>会在控制台输出类似<code>vue-next/packages/vue/src/index.ts → packages/vue/dist/vue.global.js</code>的信息。</p><p>其中<code>packages/vue/dist/vue.global.js.map</code> 就是<code>sourcemap</code>文件了。</p><p>我们在 Vue3官网找个例子，在 <code>vue-next/examples/index.html</code>。其内容引入<code>packages/vue/dist/vue.global.js</code>。</p><pre><code class="language-js">// vue-next/examples/index.html
&lt;script src="../../packages/vue/dist/vue.global.js"&gt;&lt;/script&gt;
&lt;script&gt;
    const Counter = {
        data() {
            return {
                counter: 0
            }
        }
    }

    Vue.createApp(Counter).mount('#counter')
&lt;/script&gt;
</code></pre><p>然后我们新建一个终端窗口，<code>yarn serve</code>，在浏览器中打开<code>http://localhost:5000/examples/</code>，如下图所示，按<code>F11</code>等进入函数，就可以愉快的调试源码了。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/08/image-5.png" class="kg-image" alt="image-5" width="600" height="400" loading="lazy"></figure><h2 id="3-">3. 工具函数</h2><p>本文主要按照源码 <a href="https://github.com/vuejs/vue-next/blob/master/packages/shared/src/index.ts" rel="noopener noreferrer"><code>vue-next/packages/shared/src/index.ts</code></a> 的顺序来写。也省去了一些从外部导入的方法。</p><p>我们也可以通过<code>ts</code>文件，查看使用函数的位置。同时在<code>VSCode</code>运行调试JS代码，我们比较推荐韩老师写的<code>code runner</code>插件。</p><h3 id="3-1-babelparserdefaultplugins-babel-">3.1 babelParserDefaultPlugins babel 解析默认插件</h3><pre><code class="language-js">/**
 * List of @babel/parser plugins that are used for template expression
 * transforms and SFC script transforms. By default we enable proposals slated
 * for ES2020. This will need to be updated as the spec moves forward.
 * Full list at https://babeljs.io/docs/en/next/babel-parser#plugins
 */
const babelParserDefaultPlugins = [
    'bigInt',
    'optionalChaining',
    'nullishCoalescingOperator'
];
</code></pre><p>这里就是几个默认插件。感兴趣看英文注释查看。</p><h3 id="3-2-empty_obj-">3.2 EMPTY_OBJ 空对象</h3><pre><code class="language-js">const EMPTY_OBJ = (process.env.NODE_ENV !== 'production')
    ? Object.freeze({})
    : {};

// 例子：
// Object.freeze 是 冻结对象
// 冻结的对象最外层无法修改。
const EMPTY_OBJ_1 = Object.freeze({});
EMPTY_OBJ_1.name = '若川';
console.log(EMPTY_OBJ_1.name); // undefined

const EMPTY_OBJ_2 = Object.freeze({ props: { mp: '若川视野' } });
EMPTY_OBJ_2.props.name = '若川';
EMPTY_OBJ_2.props2 = 'props2';
console.log(EMPTY_OBJ_2.props.name); // '若川'
console.log(EMPTY_OBJ_2.props2); // undefined
console.log(EMPTY_OBJ_2);
/**
 * 
 * { 
 *  props: {
     mp: "若川视野",
     name: "若川"
    }
 * }
 * */
</code></pre><p><code>process.env.NODE_ENV</code> 是 <code>node</code> 项目中的一个环境变量，一般定义为：<code>development</code> 和<code>production</code>。根据环境写代码。比如开发环境，有报错等信息，生产环境则不需要这些报错警告。</p><h3 id="3-3-empty_arr-">3.3 EMPTY_ARR 空数组</h3><pre><code class="language-js">const EMPTY_ARR = (process.env.NODE_ENV !== 'production') ? Object.freeze([]) : [];

// 例子：
EMPTY_ARR.push(1) // 报错，也就是为啥生产环境还是用 []
EMPTY_ARR.length = 3;
console.log(EMPTY_ARR.length); // 0
</code></pre><h3 id="3-4-noop-">3.4 NOOP 空函数</h3><pre><code class="language-js">const NOOP = () =&gt; { };

// 很多库的源码中都有这样的定义函数，比如 jQuery、underscore、lodash 等
// 使用场景：1. 方便判断， 2. 方便压缩
// 1. 比如：
const instance = {
    render: NOOP
};

// 条件
const dev = true;
if(dev){
    instance.render = function(){
        console.log('render');
    }
}

// 可以用作判断。
if(instance.render === NOOP){
 console.log('i');
}
// 2. 再比如：
// 方便压缩代码
// 如果是 function(){} ，不方便压缩代码
</code></pre><h3 id="3-5-no-false-">3.5 NO 永远返回 false 的函数</h3><pre><code class="language-js">/**
 * Always return false.
 */
const NO = () =&gt; false;

// 除了压缩代码的好处外。
// 一直返回 false
</code></pre><h3 id="3-6-ison-on-on-">3.6 isOn 判断字符串是不是 on 开头，并且 on 后首字母不是小写字母</h3><pre><code class="language-js">const onRE = /^on[^a-z]/;
const isOn = (key) =&gt; onRE.test(key);

// 例子：
isOn('onChange'); // true
isOn('onchange'); // false
isOn('on3change'); // true
</code></pre><p><code>onRE</code> 是正则。<code>^</code>符号在开头，则表示是什么开头。而在其他地方是指非。</p><p>与之相反的是：<code>$</code>符合在结尾，则表示是以什么结尾。</p><p><code>[^a-z]</code>是指不是<code>a</code>到<code>z</code>的小写字母。</p><p>同时推荐一个正则在线工具。</p><p><a href="https://regex101.com/" rel="noopener noreferrer">regex101</a></p><p>另外正则看老姚的迷你书就够用了。</p><p><a href="https://juejin.cn/post/6844903501034684430" rel="noopener noreferrer">老姚：《JavaScript 正则表达式迷你书》问世了！</a></p><h3 id="3-7-ismodellistener-">3.7 isModelListener 监听器</h3><p>判断字符串是不是以<code>onUpdate:</code>开头</p><pre><code class="language-js">const isModelListener = (key) =&gt; key.startsWith('onUpdate:');

// 例子：
isModelListener('onUpdate:change'); // true
isModelListener('1onUpdate:change'); // false
// startsWith 是 ES6 提供的方法
</code></pre><p><a href="https://es6.ruanyifeng.com/#docs/string-methods" rel="noopener noreferrer">ES6入门教程：字符串的新增方法</a></p><p>很多方法都在《ES6入门教程》中有讲到，就不赘述了。</p><h3 id="3-8-extend-">3.8 extend 继承 合并</h3><p>说合并可能更准确些。</p><pre><code class="language-js">const extend = Object.assign;

// 例子：
const data = { name: '若川' };
const data2 = extend(data, { mp: '若川视野', name: '是若川啊' });
console.log(data); // { name: "是若川啊", mp: "若川视野" }
console.log(data2); // { name: "是若川啊", mp: "若川视野" }
console.log(data === data2); // true
</code></pre><h3 id="3-9-remove-">3.9 remove 移除数组的一项</h3><pre><code class="language-js">const remove = (arr, el) =&gt; {
    const i = arr.indexOf(el);
    if (i &gt; -1) {
        arr.splice(i, 1);
    }
};

// 例子：
const arr = [1, 2, 3];
remove(arr, 3);
console.log(arr); // [1, 2]
</code></pre><p><code>splice</code> 其实是一个很耗性能的方法。删除数组中的一项，其他元素都要移动位置。</p><p><strong>引申</strong>：<a href="https://github.com/axios/axios/blob/master/lib/core/InterceptorManager.js" rel="noopener noreferrer"><code>axios InterceptorManager</code> 拦截器源码</a>中，拦截器用数组存储的。但实际移除拦截器时，只是把拦截器置为 <code>null</code> 。而不是用<code>splice</code>移除。最后执行时为 <code>null</code> 的不执行，同样效果。<code>axios</code> 拦截器这个场景下，不得不说为性能做到了很好的考虑。</p><p>看如下 <code>axios</code> 拦截器代码示例：</p><pre><code class="language-js">// 代码有删减
// 声明
this.handlers = [];

// 移除
if (this.handlers[id]) {
    this.handlers[id] = null;
}

// 执行
if (h !== null) {
    fn(h);
}
</code></pre><h3 id="3-10-hasown-">3.10 hasOwn 是不是自己本身所拥有的属性</h3><pre><code class="language-js">const hasOwnProperty = Object.prototype.hasOwnProperty;
const hasOwn = (val, key) =&gt; hasOwnProperty.call(val, key);

// 例子：

// 特别提醒：__proto__ 是浏览器实现的原型写法，后面还会用到
// 现在已经有提供好几个原型相关的API
// Object.getPrototypeOf
// Object.setPrototypeOf
// Object.isPrototypeOf

// .call 则是函数里 this 显示指定以为第一个参数，并执行函数。

hasOwn({__proto__: { a: 1 }}, 'a') // false
hasOwn({ a: undefined }, 'a') // true
hasOwn({}, 'a') // false
hasOwn({}, 'hasOwnProperty') // false
hasOwn({}, 'toString') // false
// 是自己的本身拥有的属性，不是通过原型链向上查找的。
</code></pre><p>对象API可以看我之前写的一篇文章 <a href="https://mp.weixin.qq.com/s/Y3nL3GPcxiqb3zK6pEuycg" rel="noopener noreferrer">JavaScript 对象所有 API 解析</a>，写的还算全面。</p><h3 id="3-11-isarray-">3.11 isArray 判断数组</h3><pre><code class="language-js">const isArray = Array.isArray;

isArray([]); // true
const fakeArr = { __proto__: Array.prototype, length: 0 };
isArray(fakeArr); // false
fakeArr instanceof Array; // true
// 所以 instanceof 这种情况 不准确
</code></pre><h3 id="3-12-ismap-map-">3.12 isMap 判断是不是 Map 对象</h3><pre><code class="language-js">const isMap = (val) =&gt; toTypeString(val) === '[object Map]';

// 例子：
const map = new Map();
const o = { p: 'Hello World' };

map.set(o, 'content');
map.get(o); // 'content'
isMap(map); // true
</code></pre><p>ES6 提供了 Map 数据结构。它类似于对象，也是键值对的集合，但是“键”的范围不限于字符串，各种类型的值（包括对象）都可以当作键。也就是说，Object 结构提供了“字符串—值”的对应，Map 结构提供了“值—值”的对应，是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构，Map 比 Object 更合适。</p><h3 id="3-13-isset-set-">3.13 isSet 判断是不是 Set 对象</h3><pre><code class="language-js">const isSet = (val) =&gt; toTypeString(val) === '[object Set]';

// 例子：
const set = new Set();
isSet(set); // true
</code></pre><p><code>ES6</code> 提供了新的数据结构 <code>Set</code>。它类似于数组，但是成员的值都是唯一的，没有重复的值。</p><p><code>Set</code>本身是一个构造函数，用来生成 <code>Set</code> 数据结构。</p><h3 id="3-14-isdate-date-">3.14 isDate 判断是不是 Date 对象</h3><pre><code class="language-js">const isDate = (val) =&gt; val instanceof Date;

// 例子：
isDate(new Date()); // true

// `instanceof` 操作符左边是右边的实例。但不是很准，但一般够用了。原理是根据原型链向上查找的。

isDate({__proto__ : new Date()); // true
// 实际上是应该是 Object 才对。
// 所以用 instanceof 判断数组也不准确。
// 再比如
({__proto__: [] }) instanceof Array; // true
// 实际上是对象。
// 所以用 数组本身提供的方法 Array.isArray 是比较准确的。
</code></pre><h3 id="3-15-isfunction-">3.15 isFunction 判断是不是函数</h3><pre><code class="language-js">const isFunction = (val) =&gt; typeof val === 'function';
// 判断数组有多种方法，但这个是比较常用也相对兼容性好的。
</code></pre><h3 id="3-16-isstring-">3.16 isString 判断是不是字符串</h3><pre><code class="language-js">const isString = (val) =&gt; typeof val === 'string';

// 例子：
isString('') // true
</code></pre><h3 id="3-17-issymbol-symbol">3.17 isSymbol 判断是不是 Symbol</h3><pre><code class="language-js">const isSymbol = (val) =&gt; typeof val === 'symbol';

// 例子：
let s = Symbol();

typeof s;
// "symbol"
// Symbol 是函数，不需要用 new 调用。
</code></pre><p><code>ES6</code> 引入了一种新的原始数据类型<code>Symbol</code>，表示独一无二的值。</p><h3 id="3-18-isobject-">3.18 isObject 判断是不是对象</h3><pre><code class="language-js">const isObject = (val) =&gt; val !== null &amp;&amp; typeof val === 'object';

// 例子：
isObject(null); // false
isObject({name: '若川'}); // true
// 判断不为 null 的原因是 typeof null 其实 是 object
</code></pre><h3 id="3-19-ispromise-promise">3.19 isPromise 判断是不是 Promise</h3><pre><code class="language-js">const isPromise = (val) =&gt; {
    return isObject(val) &amp;&amp; isFunction(val.then) &amp;&amp; isFunction(val.catch);
};

// 判断是不是Promise对象
const p1 = new Promise(function(resolve, reject){
  resolve('若川');
});
isPromise(p1); // true

// promise 对于初学者来说可能比较难理解。但是重点内容，JS异步编程，要着重掌握。
// 现在 web 开发 Promise 和 async await 等非常常用。
</code></pre><p>可以根据文末推荐的书籍看<code>Promise</code>相关章节掌握。同时也推荐这本迷你书 <a href="http://liubin.org/promises-book/" rel="noopener noreferrer">JavaScript Promise 迷你书（中文版）</a>。</p><h3 id="3-20-objecttostring-">3.20 objectToString 对象转字符串</h3><pre><code class="language-js">const objectToString = Object.prototype.toString;

// 对象转字符串
</code></pre><h3 id="3-21-totypestring-">3.21 toTypeString 对象转字符串</h3><pre><code class="language-js">const toTypeString = (value) =&gt; objectToString.call(value);

// call 是一个函数，第一个参数是 执行函数里面 this 指向。
// 通过这个能获得 类似  "[object String]" 其中 String 是根据类型变化的
</code></pre><h3 id="3-22-torawtype-">3.22 toRawType 对象转字符串 截取后几位</h3><pre><code class="language-js">const toRawType = (value) =&gt; {
    // extract "RawType" from strings like "[object RawType]"
    return toTypeString(value).slice(8, -1);
};

// 截取到
toRawType('');  'String'
</code></pre><p>可以 截取到 <code>String</code> <code>Array</code> 等这些类型</p><p>是 <code>JS</code> 判断数据类型非常重要的知识点。</p><p><code>JS</code> 判断类型也有 typeof ，但不是很准确，而且能够识别出的不多。</p><p>这些算是基础知识</p><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/typeof" rel="noopener noreferrer">mdn typeof 文档</a>，文档比较详细，也实现了一个很完善的<code>type</code>函数，本文就不赘述了。</p><pre><code class="language-js">// typeof 返回值目前有以下8种 
'undefined'
'object'
'boolean'
'number'
'bigint'
'string'
'symobl'
'function'
</code></pre><h3 id="3-23-isplainobject-">3.23 isPlainObject 判断是不是纯粹的对象</h3><pre><code class="language-js">const objectToString = Object.prototype.toString;
const toTypeString = (value) =&gt; objectToString.call(value);
// 
const isPlainObject = (val) =&gt; toTypeString(val) === '[object Object]';

// 前文中 有 isObject 判断是不是对象了。
// isPlainObject 这个函数在很多源码里都有，比如 jQuery 源码和 lodash 源码等，具体实现不一样
// 上文的 isObject([]) 也是 true ，因为 type [] 为 'object'
// 而 isPlainObject([]) 则是false
const Ctor = function(){
    this.name = '我是构造函数';
}
isPlainObject({}); // true
isPlainObject(new Ctor()); // true
</code></pre><h3 id="3-24-isintegerkey-key-">3.24 isIntegerKey 判断是不是数字型的字符串 key 值</h3><pre><code class="language-js">const isIntegerKey = (key) =&gt; isString(key) &amp;&amp;
    key !== 'NaN' &amp;&amp;
    key[0] !== '-' &amp;&amp;
    '' + parseInt(key, 10) === key;

// 例子:
isIntegerKey('a'); // false
isIntegerKey('0'); // true
isIntegerKey('011'); // false
isIntegerKey('11'); // true
// 其中 parseInt 第二个参数是进制。
// 字符串能用数组取值的形式取值。
//  还有一个 charAt 函数，但不常用 
'abc'.charAt(0) // 'a'
// charAt 与数组形式不同的是 取不到值会返回空字符串''，数组形式取值取不到则是 undefined
</code></pre><h3 id="3-25-makemap-isreservedprop">3.25 makeMap &amp;&amp; isReservedProp</h3><p>传入一个以逗号分隔的字符串，生成一个 <code>map</code>(键值对)，并且返回一个函数检测 <code>key</code> 值在不在这个 <code>map</code> 中。第二个参数是小写选项。</p><pre><code class="language-js">/**
 * Make a map and return a function for checking if a key
 * is in that map.
 * IMPORTANT: all calls of this function must be prefixed with
 * \/\*#\_\_PURE\_\_\*\/
 * So that rollup can tree-shake them if necessary.
 */
function makeMap(str, expectsLowerCase) {
    const map = Object.create(null);
    const list = str.split(',');
    for (let i = 0; i &lt; list.length; i++) {
        map[list[i]] = true;
    }
    return expectsLowerCase ? val =&gt; !!map[val.toLowerCase()] : val =&gt; !!map[val];
}
const isReservedProp = /*#__PURE__*/ makeMap(
// the leading comma is intentional so empty string "" is also included
',key,ref,' +
    'onVnodeBeforeMount,onVnodeMounted,' +
    'onVnodeBeforeUpdate,onVnodeUpdated,' +
    'onVnodeBeforeUnmount,onVnodeUnmounted');

// 保留的属性
isReservedProp('key'); // true
isReservedProp('ref'); // true
isReservedProp('onVnodeBeforeMount'); // true
isReservedProp('onVnodeMounted'); // true
isReservedProp('onVnodeBeforeUpdate'); // true
isReservedProp('onVnodeUpdated'); // true
isReservedProp('onVnodeBeforeUnmount'); // true
isReservedProp('onVnodeUnmounted'); // true
</code></pre><h3 id="3-26-cachestringfunction-">3.26 cacheStringFunction 缓存</h3><pre><code class="language-js">const cacheStringFunction = (fn) =&gt; {
    const cache = Object.create(null);
    return ((str) =&gt; {
        const hit = cache[str];
        return hit || (cache[str] = fn(str));
    });
};
</code></pre><p>这个函数也是和上面 MakeMap 函数类似。只不过接收参数的是函数。《JavaScript 设计模式与开发实践》书中的第四章 JS单例模式也是类似的实现。</p><pre><code class="language-js">var getSingle = function(fn){ // 获取单例
    var result;
    return function(){
        return result || (result = fn.apply(this, arguments));
    }
};
</code></pre><p>以下是一些正则，系统学习正则推荐<a href="https://juejin.cn/post/6844903501034684430" rel="noopener noreferrer">老姚：《JavaScript 正则表达式迷你书》问世了！</a>看过的都说好。所以本文不会过多描述正则相关知识点。</p><pre><code class="language-js">// \w 是 0-9a-zA-Z_ 数字 大小写字母和下划线组成
// () 小括号是 分组捕获
const camelizeRE = /-(\w)/g;
/**
 * @private
 */
// 连字符 - 转驼峰  on-click =&gt; onClick
const camelize = cacheStringFunction((str) =&gt; {
    return str.replace(camelizeRE, (_, c) =&gt; (c ? c.toUpperCase() : ''));
});
// \B 是指 非 \b 单词边界。
const hyphenateRE = /\B([A-Z])/g;
/**
 * @private
 */

const hyphenate = cacheStringFunction((str) =&gt; str.replace(hyphenateRE, '-$1').toLowerCase());

// 举例：onClick =&gt; on-click
const hyphenateResult = hyphenate('onClick');
console.log('hyphenateResult', hyphenateResult); // 'on-click'

/**
 * @private
 */
// 首字母转大写
const capitalize = cacheStringFunction((str) =&gt; str.charAt(0).toUpperCase() + str.slice(1));
/**
 * @private
 */
// click =&gt; onClick
const toHandlerKey = cacheStringFunction((str) =&gt; (str ? `on${capitalize(str)}` : ``));

const result = toHandlerKey('click');
console.log(result, 'result'); // 'onClick'
</code></pre><h3 id="3-27-haschanged-">3.27 hasChanged 判断是不是有变化</h3><pre><code class="language-js">// compare whether a value has changed, accounting for NaN.
const hasChanged = (value, oldValue) =&gt; value !== oldValue &amp;&amp; (value === value || oldValue === oldValue);
// 例子：
// 认为 NaN 是不变的
hasChanged(NaN, NaN); // false
hasChanged(1, 1); // false
hasChanged(1, 2); // true
hasChanged(+0, -0); // false
// Obect.is 认为 +0 和 -0 不是同一个值
Object.is(+0, -0); // false           
// Object.is 认为  NaN 和 本身 相比 是同一个值
Object.is(NaN, NaN); // true
// 场景
// watch 监测值是不是变化了

// (value === value || oldValue === oldValue)
// 为什么会有这句 因为要判断 NaN 。认为 NaN 是不变的。因为 NaN === NaN 为 false
</code></pre><p>根据 <code>hasChanged</code> 这个我们继续来看看：<code>Object.is</code> <code>API</code>。</p><p><code>Object.is(value1, value2) (ES6)</code></p><p>该方法用来比较两个值是否严格相等。它与严格比较运算符（===）的行为基本一致。 不同之处只有两个：一是<code>+0</code>不等于<code>-0</code>，而是 <code>NaN</code> 等于自身。</p><pre><code class="language-js">Object.is('若川', '若川'); // true
Object.is({},{}); // false
Object.is(+0, -0); // false
+0 === -0; // true
Object.is(NaN, NaN); // true
NaN === NaN; // false
</code></pre><p><code>ES5</code>可以通过以下代码部署<code>Object.is</code>。</p><pre><code class="language-js">Object.defineProperty(Object, 'is', {
    value: function() {x, y} {
        if (x === y) {
           // 针对+0不等于-0的情况
           return x !== 0 || 1 / x === 1 / y;
        }
        // 针对 NaN的情况
        return x !== x &amp;&amp; y !== y;
    },
    configurable: true,
    enumerable: false,
    writable: true
});
</code></pre><p>根据举例可以说明。</p><h3 id="3-28-invokearrayfns-">3.28 invokeArrayFns 执行数组里的函数</h3><pre><code class="language-js">const invokeArrayFns = (fns, arg) =&gt; {
    for (let i = 0; i &lt; fns.length; i++) {
        fns[i](arg);
    }
};

// 例子：
const arr = [
    function(val){
        console.log(val + '的博客地址是：https://lxchuan12.gitee.io');
    },
    function(val){
        console.log('百度搜索 若川 可以找到' + val);
    },
    function(val){
        console.log('微信搜索 若川视野 可以找到关注' + val);
    },
]
invokeArrayFns(arr, '我');
</code></pre><p>为什么这样写，我们一般都是一个函数执行就行。</p><p>数组中存放函数，函数其实也算是数据。这种写法方便统一执行多个函数。</p><h3 id="3-29-def-">3.29 def 定义对象属性</h3><pre><code class="language-js">const def = (obj, key, value) =&gt; {
    Object.defineProperty(obj, key, {
        configurable: true,
        enumerable: false,
        value
    });
};
</code></pre><p><code>Object.defineProperty</code> 算是一个非常重要的<code>API</code>。还有一个定义多个属性的<code>API</code>：<code>Object.defineProperties(obj, props) (ES5)</code></p><p><code>Object.defineProperty</code> 涉及到比较重要的知识点。<br>在<code>ES3</code>中，除了一些内置属性（如：<code>Math.PI</code>），对象的所有的属性在任何时候都可以被修改、插入、删除。在<code>ES5</code>中，我们可以设置属性是否可以被改变或是被删除——在这之前，它是内置属性的特权。<code>ES5</code>中引入了<strong>属性描述符</strong>的概念，我们可以通过它对所定义的属性有更大的控制权。这些<strong>属性描述符</strong>（特性）包括：</p><p><code>value</code>——当试图获取属性时所返回的值。<br><code>writable</code>——该属性是否可写。<br><code>enumerable</code>——该属性在<code>for in</code>循环中是否会被枚举。<br><code>configurable</code>——该属性是否可被删除。<br><code>set()</code>——该属性的更新操作所调用的函数。<br><code>get()</code>——获取属性值时所调用的函数。<br></p><p>另外，<strong>数据描述符</strong>（其中属性为：<code>enumerable</code>，<code>configurable</code>，<code>value</code>，<code>writable</code>）与<strong>存取描述符</strong>（其中属性为<code>enumerable</code>，<code>configurable</code>，<code>set()</code>，<code>get()</code>）之间是有互斥关系的。在定义了<code>set()</code>和<code>get()</code>之后，描述符会认为存取操作已被 定义了，其中再定义<code>value</code>和<code>writable</code>会<strong>引起错误</strong>。</p><p>以下是<em>ES3</em>风格的属性定义方式：</p><pre><code class="language-js">var person = {};
person.legs = 2;
</code></pre><p>以下是等价的ES5通过<strong>数据描述符</strong>定义属性的方式：</p><pre><code class="language-js">var person = {};
Object.defineProperty(person, 'legs', {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true
});
</code></pre><p>其中， 除了value的默认值为<code>undefined</code>以外，其他的默认值都为<code>false</code>。这就意味着，如果想要通过这一方式定义一个可写的属性，必须显示将它们设为<code>true</code>。 或者，我们也可以通过<code>ES5</code>的存储描述符来定义：</p><pre><code class="language-js">var person = {};
Object.defineProperty(person, 'legs', {
    set:function(v) {
        return this.value = v;
    },
    get: function(v) {
        return this.value;
    },
    configurable: true,
    enumerable: true
});
person.legs = 2;
</code></pre><p>这样一来，多了许多可以用来描述属性的代码，如果想要防止别人篡改我们的属性，就必须要用到它们。此外，也不要忘了浏览器向后兼容<code>ES3</code>方面所做的考虑。例如，跟添加<code>Array.prototype</code>属性不一样，我们不能再旧版的浏览器中使用<code>shim</code>这一特性。 另外，我们还可以（通过定义<code>nonmalleable</code>属性），在具体行为中运用这些描述符：</p><pre><code class="language-js">var person = {};
Object.defineProperty(person, 'heads', {value: 1});
person.heads = 0; // 0
person.heads; // 1  (改不了)
delete person.heads; // false
person.heads // 1 (删不掉)
</code></pre><p>其他本文就不过多赘述了。更多对象 <code>API</code> 可以查看这篇文章 <a href="https://mp.weixin.qq.com/s/Y3nL3GPcxiqb3zK6pEuycg" rel="noopener noreferrer">JavaScript 对象所有 API 解析</a>。</p><h3 id="3-30-tonumber-">3.30 toNumber 转数字</h3><pre><code class="language-js">const toNumber = (val) =&gt; {
    const n = parseFloat(val);
    return isNaN(n) ? val : n;
};

toNumber('111'); // 111
toNumber('a111'); // 'a111'
parseFloat('a111'); // NaN
isNaN(NaN); // true
</code></pre><p>其实 <code>isNaN</code> 本意是判断是不是 <code>NaN</code> 值，但是不准确的。 比如：<code>isNaN('a')</code> 为 <code>true</code>。 所以 <code>ES6</code> 有了 <code>Number.isNaN</code> 这个判断方法，为了弥补这一个<code>API</code>。</p><pre><code class="language-js">Number.isNaN('a')  // false
Number.isNaN(NaN); // true
</code></pre><h3 id="3-31-getglobalthis-">3.31 getGlobalThis 全局对象</h3><pre><code class="language-js">let _globalThis;
const getGlobalThis = () =&gt; {
    return (_globalThis ||
        (_globalThis =
            typeof globalThis !== 'undefined'
                ? globalThis
                : typeof self !== 'undefined'
                    ? self
                    : typeof window !== 'undefined'
                        ? window
                        : typeof global !== 'undefined'
                            ? global
                            : {}));
};
</code></pre><p>获取全局 <code>this</code> 指向。</p><p>初次执行肯定是 <code>_globalThis</code> 是 <code>undefined</code>。所以会执行后面的赋值语句。</p><p>如果存在 <code>globalThis</code> 就用 <code>globalThis</code>。<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/globalThis" rel="noopener noreferrer">MDN globalThis</a></p><p>如果存在<code>self</code>，就用<code>self</code>。在 <code>Web Worker</code> 中不能访问到 <code>window</code> 对象，但是我们却能通过 <code>self</code> 访问到 <code>Worker</code> 环境中的全局对象。</p><p>如果存在<code>window</code>，就用<code>window</code>。</p><p>如果存在<code>global</code>，就用<code>global</code>。<code>Node</code>环境下，使用<code>global</code>。</p><p>如果都不存在，使用空对象。可能是微信小程序环境下。</p><p>下次执行就直接返回 <code>_globalThis</code>，不需要第二次继续判断了。这种写法值得我们学习。</p><h2 id="4-">4. 最后推荐一些文章和书籍</h2><p>先推荐我认为不错的<code>JavaScript API</code>的几篇文章和几本值得读的书。</p><p><a href="https://juejin.cn/post/6844903476720320525" rel="noopener noreferrer">JavaScript字符串所有API全解密</a></p><p><a href="https://juejin.cn/post/6844903476216987655" rel="noopener noreferrer">【深度长文】JavaScript数组所有API全解密</a></p><p><a href="https://juejin.cn/post/6844903469824868365" rel="noopener noreferrer">正则表达式前端使用手册</a></p><p><a href="https://juejin.cn/post/6844903501034684430" rel="noopener noreferrer">老姚：《JavaScript 正则表达式迷你书》问世了！</a></p><p><a href="https://mp.weixin.qq.com/s/Y3nL3GPcxiqb3zK6pEuycg" rel="noopener noreferrer">JavaScript 对象所有API解析</a> https://lxchuan12.gitee.io/js-object-api/</p><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript" rel="noopener noreferrer">MDN JavaScript</a></p><p><a href="https://book.douban.com/subject/35175321/" rel="noopener noreferrer">《JavaScript高级程序设计》第4版</a></p><p><a href="https://book.douban.com/subject/35396470/" rel="noopener noreferrer">《JavaScript 权威指南》第7版</a></p><p><a href="https://book.douban.com/subject/26302623/" rel="noopener noreferrer">《JavaScript面向对象编程2》 </a>面向对象讲的很详细。</p><p><a href="http://es6.ruanyifeng.com/" rel="noopener noreferrer">阮一峰老师：《ES6 入门教程》</a></p><p><a href="https://zh.javascript.info/" rel="noopener noreferrer">《现代 JavaScript 教程》</a></p><p><a href="https://book.douban.com/subject/26351021/" rel="noopener noreferrer">《你不知道的JavaScript》上中卷</a></p><p><a href="https://book.douban.com/subject/26382780/" rel="noopener noreferrer">《JavaScript 设计模式与开发实践》</a></p><p>我也是从小白看不懂书经历过来的。到现在写文章分享。</p><p>我看书的方法：多本书同时看，看相同类似的章节，比如函数。看完这本可能没懂，看下一本，几本书看下来基本就懂了，一遍没看懂，再看几遍，可以避免遗忘，巩固相关章节。当然，刚开始看书很难受，看不进。这些书大部分在微信读书都有，如果习惯看纸质书，那可以买来看。</p><p>这时可以看些视频和动手练习一些简单的项目。</p><p>比如：可以自己注册一个<code>github</code>账号，分章节小节，抄写书中的代码，提交到<code>github</code>，练习了才会更有感觉。</p><p>再比如 <a href="https://chinese.freecodecamp.org/" rel="noopener noreferrer">freeCodeCamp 中文在线学习网站</a>。看书是系统学习非常好的方法。后来我就是看源码较多，写文章分享出来给大家。</p><h2 id="5-">5. 总结</h2><p>文中主要通过学习 <code>shared</code> 模块下的几十个工具函数，比如有：<code>isPromise</code>、<code>makeMap</code>、<code>cacheStringFunction</code>、<code>invokeArrayFns</code>、<code>def</code>、<code>getGlobalThis</code>等等。</p><p>同时还分享了<code>vue</code>源码的调试技巧，推荐了一些书籍和看书籍的方法。</p><p>欢迎在我的<a href="https://lxchuan12.gitee.io/">个人网站</a>阅读更多。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 探究 vue-devtools「在编辑器中打开组件」功能实现原理 ]]>
                </title>
                <description>
                    <![CDATA[ 前言 不知道你们有没有碰到这样的场景，打开你自己（或者你同事）开发的页面，却短时间难以找到对应的源文件。 这时你可能会想要是能有点击页面按钮自动用编辑器打开对应文件的功能，那该多好啊。 而vue-devtools提供了这样的功能，也许你不知道。我觉得很大一部分人都不知道，因为感觉很多人都不常用vue-devtools。 你也许会问，我不用vue，我用react有没有类似功能啊，有啊，请看 react-dev-inspector [https://github.com/zthxxx/react-dev-inspector]。 本文就是根据学习尤大写的 launch-editor [https://github.com/yyx990803/launch-editor] 源码，本着 知其然，知其所以然的宗旨，探究 vue-devtools「在编辑器中打开组件」功能实现原理。 一句话简述其原理 code path/to/file 一句话简述原理：利用nodejs中的child_process，执行了类似code path/to/file 命令，于是对应编辑器就打开了相应的文件，而 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/vue-devtools/</link>
                <guid isPermaLink="false">60940ee30998fd05ae8c84ed</guid>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Thu, 06 May 2021 08:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/05/maxwell-nelson-taiuG8CPKAQ-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="-">前言</h2><p>不知道你们有没有碰到这样的场景，打开你自己（或者你同事）开发的页面，却短时间难以找到对应的源文件。</p><p>这时你可能会想要是能有<strong>点击页面按钮自动用编辑器打开对应文件</strong>的功能，那该多好啊。</p><p>而<code>vue-devtools</code>提供了这样的功能，也许你不知道。我觉得很大一部分人都不知道，因为感觉很多人都不常用<code>vue-devtools</code>。</p><figure class="kg-card kg-image-card"><img src="https://lxchuan12.gitee.io/assets/img/open-src-app.vue.51fd0181.png" class="kg-image" alt="open-in-editor" width="600" height="400" loading="lazy"></figure><p>你也许会问，我不用<code>vue</code>，我用<code>react</code>有没有类似功能啊，有啊，请看 <a href="https://github.com/zthxxx/react-dev-inspector" rel="noopener noreferrer">react-dev-inspector</a>。</p><p>本文就是根据学习尤大写的 <a href="https://github.com/yyx990803/launch-editor" rel="noopener noreferrer">launch-editor</a> 源码，本着<strong>知其然，知其所以然</strong>的宗旨，探究 <code>vue-devtools</code>「在编辑器中打开组件」功能实现原理。</p><h3 id="--1">一句话简述其原理</h3><pre><code class="language-sh">code path/to/file
</code></pre><p>一句话简述原理：利用<code>nodejs</code>中的<code>child_process</code>，执行了类似<code>code path/to/file</code>命令，于是对应编辑器就打开了相应的文件，而对应的编辑器则是通过在进程中执行<code>ps x</code>（<code>Window</code>则用<code>Get-Process</code>）命令来查找的，当然也可以自己指定编辑器。</p><h3 id="--2">打开编辑器无法打开组件的报错解决方法</h3><p>而你真正用这个功能时，你可能碰到报错，说不能打开这个文件。</p><pre><code class="language-sh">Could not open App.vue in the editor.

To specify an editor, specify the EDITOR env variable or add "editor" field to your Vue project config.
</code></pre><figure class="kg-card kg-image-card"><img src="https://lxchuan12.gitee.io/assets/img/open-in-editor-error.4fae42eb.png" class="kg-image" alt="控制台不能打开编辑器的错误提示" width="600" height="400" loading="lazy"></figure><p>这里说明下写这篇文章时用的是 <code>Windows</code> 电脑，在<code>Ubuntu</code>子系统下使用的终端工具。同时推荐我的文章<a href="https://mp.weixin.qq.com/s/MHngeDABRV3z2HmN5DRrEw" rel="noopener noreferrer">使用 ohmyzsh 打造 windows、ubuntu、mac 系统高效终端命令行工具</a>，<strong>用过的都说好</strong>。</p><p><strong>解决办法也简单，就是这句英文的意思</strong>。具体说明编辑器，在环境变量中说明指定编辑器。在<code>vue</code>项目的根目录下，对应本文则是：<code>vue3-project</code>，添加<code>.env.delelopment</code>文件，其内容是<code>EDITOR=code</code>。</p><pre><code class="language-sh"># .env.development
# 当然，我的命令行终端已经有了code这个命令。
EDITOR=code
</code></pre><p>不用指定编辑器的对应路径（<code>c/Users/lxchu/AppData/Local/Programs/Microsoft VS Code/bin/code</code>），因为会报错。为什么会报错，因为我看了源码且试过。因为会被根据空格截断，变成<code>c/Users/lxchu/AppData/Local/Programs/Microsoft</code>，当然就报错了。</p><p>接下来我们从源码角度探究「在编辑器中打开组件」功能的实现原理。</p><h2 id="vue-devtools-open-component-in-editor-">vue-devtools Open component in editor 文档</h2><p>探究原理之前，先来看看<code>vue-devtools</code>官方文档。</p><p><a href="https://github.com/vuejs/vue-devtools#open-component-in-editor" rel="noopener noreferrer">vuejs/vue-devtools</a> 文档</p><blockquote><strong>Open component in editor</strong><br>To enable this feature, follow <a href="https://github.com/vuejs/vue-devtools/blob/dev/docs/open-in-editor.md" rel="noopener noreferrer">this guide</a>.</blockquote><p>这篇指南中写了在<code>Vue CLI 3</code>中是<strong>开箱即用</strong>。</p><pre><code class="language-sh">Vue CLI 3 supports this feature out-of-the-box when running vue-cli-service serve.
</code></pre><p>也详细写了如何在<code>Webpack</code>下使用。</p><pre><code class="language-sh"># 1. Import the package:
var openInEditor = require('launch-editor-middleware')
# 2. In the devServer option, register the /__open-in-editor HTTP route:
devServer: {
  before (app) {
    app.use('/__open-in-editor', openInEditor())
  }
}
# 3. The editor to launch is guessed. You can also specify the editor app with the editor option. See the supported editors list.
# 用哪个编辑器打开会自动猜测。你也可以具体指明编辑器。这里显示更多的支持编辑器列表
openInEditor('code')
# 4. You can now click on the name of the component in the Component inspector pane (if the devtools knows about its file source, a tooltip will appear).
# 如果`vue-devtools`开发者工具有提示点击的组件的显示具体路径，那么你可以在编辑器打开。
</code></pre><p>同时也写了如何在<code>Node.js</code>中使用等。</p><blockquote><strong>Node.js</strong><br>You can use the <a href="https://github.com/yyx990803/launch-editor#usage" rel="noopener noreferrer">launch-editor</a> package to setup an HTTP route with the <code>/__open-in-editor</code> path. It will receive file as an URL variable.</blockquote><p>查看更多可以看<a href="https://github.com/vuejs/vue-devtools/blob/dev/docs/open-in-editor.md" rel="noopener noreferrer">这篇指南</a>。</p><h2 id="--3">环境准备工作</h2><p>熟悉我的读者，都知道我都是<strong>推荐调试看源码</strong>的，正所谓：<strong>哪里不会点哪里</strong>。而且调试一般都写得很详细，是希望能帮助到一部分人知道如何看源码。于是我特意新建一个仓库 <a href="https://github.com/lxchuan12/open-in-editor" rel="noopener noreferrer">open-in-editor </a><code>git clone https://github.com/lxchuan12/open-in-editor.git</code>，便于大家克隆学习。</p><p>安装<code>vue-cli</code></p><pre><code class="language-sh">npm install -g @vue/cli
# OR
yarn global add @vue/cli
</code></pre><pre><code class="language-sh">node -V
# v14.16.0
vue -V 
# @vue/cli 4.5.12
vue create vue3-project
# 这里选择的是vue3、vue2也是一样的。
# Please pick a preset: Default (Vue 3 Preview) ([Vue 3] babel, eslint)
npm install
# OR
yarn install
</code></pre><p>这里同时说明下我的vscode版本。</p><pre><code class="language-sh">code -v
1.55.2
</code></pre><p>前文提到的<code>Vue CLI 3</code>中<strong>开箱即用</strong>和<code>Webpack</code>使用方法。</p><p><code>vue3-project/package.json</code>中有一个<code>debug</code>按钮。</p><figure class="kg-card kg-image-card"><img src="https://lxchuan12.gitee.io/assets/img/debug.7f563e4f.png" class="kg-image" alt="debug示意图" width="600" height="400" loading="lazy"></figure><p>选择第一项，serve <code>vue-cli-service serve</code>。</p><p>我们来搜索下<code>'launch-editor-middleware'</code>这个中间件，一般来说搜索不到<code>node_modules</code>下的文件，需要设置下。当然也有个简单做法。就是「排除的文件」右侧旁边有个设置图标「使用“排查设置”与“忽略文件”」，点击下。</p><p>其他的就不赘述了。可以看这篇知乎回答：<a href="https://www.zhihu.com/question/309220217/answer/586510407" rel="noopener noreferrer">vscode怎么设置可以搜索包含node_modules中的文件？</a></p><p>这时就搜到了<code>vue3-project/node_modules/@vue/cli-service/lib/commands/serve.js</code>中有使用这个中间件。</p><h2 id="vue-devtools-">vue-devtools 开箱即用具体源码实现</h2><p>接着我们来看<code>Vue CLI 3</code>中<strong>开箱即用</strong>具体源码实现。</p><pre><code class="language-js">// vue3-project/node_modules/@vue/cli-service/lib/commands/serve.js
// 46行
const launchEditorMiddleware = require('launch-editor-middleware')
// 192行
before (app, server) {
    // launch editor support.
    // this works with vue-devtools &amp; @vue/cli-overlay
    app.use('/__open-in-editor', launchEditorMiddleware(() =&gt; console.log(
        `To specify an editor, specify the EDITOR env variable or ` +
        `add "editor" field to your Vue project config.\n`
    )))
    // 省略若干代码...
}
</code></pre><p>点击<code>vue-devtools</code>中的时，会有一个请求，<code>http://localhost:8080/__open-in-editor?file=src/App.vue</code>，不出意外就会打开该组件啦。</p><figure class="kg-card kg-image-card"><img src="https://lxchuan12.gitee.io/assets/img/open-src-app.vue.51fd0181.png" class="kg-image" alt="open src/App.vue in editor" width="600" height="400" loading="lazy"></figure><p>接着我们在<code>launchEditorMiddleware</code>的具体实现。</p><h2 id="launch-editor-middleware">launch-editor-middleware</h2><p>看源码时，先看调试截图。</p><figure class="kg-card kg-image-card"><img src="https://lxchuan12.gitee.io/assets/img/debug-launch.d30ca44e.png" class="kg-image" alt="debug-launch" width="600" height="400" loading="lazy"></figure><p>在<code>launch-editor-middleware</code>中间件中作用在于最终是调用 <code>launch-editor</code> 打开文件。</p><pre><code class="language-js">// vue3-project/node_modules/launch-editor-middleware/index.js
const url = require('url')
const path = require('path')
const launch = require('launch-editor')

module.exports = (specifiedEditor, srcRoot, onErrorCallback) =&gt; {
  // specifiedEditor =&gt; 这里传递过来的则是 () =&gt; console.log() 函数
  // 所以和 onErrorCallback 切换下，把它赋值给错误回调函数
  if (typeof specifiedEditor === 'function') {
    onErrorCallback = specifiedEditor
    specifiedEditor = undefined
  }

  // 如果第二个参数是函数，同样把它赋值给错误回调函数
  // 这里传递过来的是undefined
  if (typeof srcRoot === 'function') {
    onErrorCallback = srcRoot
    srcRoot = undefined
  }

  // srcRoot 是传递过来的参数，或者当前node进程的目录
  srcRoot = srcRoot || process.cwd()

  // 最后返回一个函数， express 中间件
  return function launchEditorMiddleware (req, res, next) {
    // 省略 ...
  }
}
</code></pre><p><strong>上一段中，这种切换参数的写法，在很多源码中都很常见。为的是方便用户调用时传参。虽然是多个参数，但可以传一个或者两个</strong>。</p><p>可以根据情况打上断点。比如这里我会在<code>launch(path.resolve(srcRoot, file), specifiedEditor, onErrorCallback)</code>打断点。</p><pre><code class="language-js">// vue3-project/node_modules/launch-editor-middleware/index.js
module.exports = (specifiedEditor, srcRoot, onErrorCallback) =&gt; {
  // 省略上半部分
  return function launchEditorMiddleware (req, res, next) {
    // 根据请求解析出file路径
    const { file } = url.parse(req.url, true).query || {}
    // 如果没有文件路径，则报错
    if (!file) {
      res.statusCode = 500
      res.end(`launch-editor-middleware: required query param "file" is missing.`)
    } else {
      // 否则拼接路径，用launch打开。
      launch(path.resolve(srcRoot, file), specifiedEditor, onErrorCallback)
      res.end()
    }
  }
}
</code></pre><h2 id="launch-editor">launch-editor</h2><p>跟着断点来看，走到了<code>launchEditor</code>函数。</p><pre><code class="language-js">// vue3-project/node_modules/launch-editor/index.js
function launchEditor (file, specifiedEditor, onErrorCallback) {
  // 解析出文件路径和行号列号等信息
  const parsed = parseFile(file)
  let { fileName } = parsed
  const { lineNumber, columnNumber } = parsed

  // 判断文件是否存在，不存在，直接返回。
  if (!fs.existsSync(fileName)) {
    return
  }
  // 所以和 onErrorCallback 切换下，把它赋值给错误回调函数
  if (typeof specifiedEditor === 'function') {
    onErrorCallback = specifiedEditor
    specifiedEditor = undefined
  }
  // 包裹一层函数
  onErrorCallback = wrapErrorCallback(onErrorCallback)

  // 猜测当前进程运行的是哪个编辑器
  const [editor, ...args] = guessEditor(specifiedEditor)
  if (!editor) {
    onErrorCallback(fileName, null)
    return
  }
  // 省略剩余部分，后文再讲述...
}
</code></pre><h3 id="wraperrorcallback-">wrapErrorCallback 包裹错误函数回调</h3><pre><code class="language-js">onErrorCallback = wrapErrorCallback(onErrorCallback)
</code></pre><p>这段的代码，我相信读者朋友能看懂，我单独拿出来讲述，主要是因为<strong>这种包裹函数的形式在很多源码里都很常见</strong>。 这里也就是文章开头终端错误图<code>Could not open App.vue in the editor.</code>输出的代码位置。</p><pre><code class="language-js">// vue3-project/node_modules/launch-editor/index.js
function wrapErrorCallback (cb) {
  return (fileName, errorMessage) =&gt; {
    console.log()
    console.log(
      chalk.red('Could not open ' + path.basename(fileName) + ' in the editor.')
    )
    if (errorMessage) {
      if (errorMessage[errorMessage.length - 1] !== '.') {
        errorMessage += '.'
      }
      console.log(
        chalk.red('The editor process exited with an error: ' + errorMessage)
      )
    }
    console.log()
    if (cb) cb(fileName, errorMessage)
  }
}
</code></pre><h3 id="guesseditor-">guessEditor 猜测当前正在使用的编辑器</h3><p>这个函数主要做了如下四件事情：</p><ol><li>如果具体指明了编辑器，则解析下返回。<br></li><li>找出当前进程中哪一个编辑器正在运行。<code>macOS</code> 和 <code>Linux</code> 用 <code>ps x</code> 命令<br><code>windows</code> 则用 <code>Get-Process</code> 命令<br></li><li>如果都没找到就用 <code>process.env.VISUAL</code>或者<code>process.env.EDITOR</code>。这就是为啥开头错误提示可以使用环境变量指定编辑器的原因。<br></li><li>最后还是没有找到就返回<code>[null]</code>，则会报错。</li></ol><pre><code class="language-js">const [editor, ...args] = guessEditor(specifiedEditor)
if (!editor) {
    onErrorCallback(fileName, null)
    return
}
</code></pre><pre><code class="language-js">// vue3-project/node_modules/launch-editor/guess.js
const shellQuote = require('shell-quote')

module.exports = function guessEditor (specifiedEditor) {
  // 如果指定了编辑器，则解析一下，这里没有传入。如果自己指定了路径。
  // 比如 c/Users/lxchu/AppData/Local/Programs/Microsoft VS Code/bin/code 
  //   会根据空格切割成 c/Users/lxchu/AppData/Local/Programs/Microsoft
  if (specifiedEditor) {
    return shellQuote.parse(specifiedEditor)
  }
  // We can find out which editor is currently running by:
  // `ps x` on macOS and Linux
  // `Get-Process` on Windows
  try {
    //  省略...
  } catch (error) {
    // Ignore...
  }

  // Last resort, use old skool env vars
  if (process.env.VISUAL) {
    return [process.env.VISUAL]
  } else if (process.env.EDITOR) {
    return [process.env.EDITOR]
  }

  return [null]
}
</code></pre><p>看完了 guessEditor 函数，我们接着来看 <code>launch-editor</code> 剩余部分。</p><h3 id="launch-editor-">launch-editor 剩余部分</h3><p>以下这段代码不用细看，调试的时候细看就行。</p><pre><code class="language-js">// vue3-project/node_modules/launch-editor/index.js
function launchEditor(){
  //  省略上部分...
  if (
    process.platform === 'linux' &amp;&amp;
    fileName.startsWith('/mnt/') &amp;&amp;
    /Microsoft/i.test(os.release())
  ) {
    // Assume WSL / "Bash on Ubuntu on Windows" is being used, and
    // that the file exists on the Windows file system.
    // `os.release()` is "4.4.0-43-Microsoft" in the current release
    // build of WSL, see: https://github.com/Microsoft/BashOnWindows/issues/423#issuecomment-221627364
    // When a Windows editor is specified, interop functionality can
    // handle the path translation, but only if a relative path is used.
    fileName = path.relative('', fileName)
  }

  if (lineNumber) {
    const extraArgs = getArgumentsForPosition(editor, fileName, lineNumber, columnNumber)
    args.push.apply(args, extraArgs)
  } else {
    args.push(fileName)
  }

  if (_childProcess &amp;&amp; isTerminalEditor(editor)) {
    // There's an existing editor process already and it's attached
    // to the terminal, so go kill it. Otherwise two separate editor
    // instances attach to the stdin/stdout which gets confusing.
    _childProcess.kill('SIGKILL')
  }

  if (process.platform === 'win32') {
    // On Windows, launch the editor in a shell because spawn can only
    // launch .exe files.
    _childProcess = childProcess.spawn(
      'cmd.exe',
      ['/C', editor].concat(args),
      { stdio: 'inherit' }
    )
  } else {
    _childProcess = childProcess.spawn(editor, args, { stdio: 'inherit' })
  }
  _childProcess.on('exit', function (errorCode) {
    _childProcess = null

    if (errorCode) {
      onErrorCallback(fileName, '(code ' + errorCode + ')')
    }
  })

  _childProcess.on('error', function (error) {
    onErrorCallback(fileName, error.message)
  })
}
</code></pre><p>这一大段中，主要的就是<strong>以下代码</strong>，用子进程模块。简单来说子进程模块有着执行命令的能力。</p><pre><code class="language-js">const childProcess = require('child_process')

if (process.platform === 'win32') {
    // On Windows, launch the editor in a shell because spawn can only
    // launch .exe files.
    _childProcess = childProcess.spawn(
        'cmd.exe',
        ['/C', editor].concat(args),
        { stdio: 'inherit' }
    )
    } else {
    _childProcess = childProcess.spawn(editor, args, { stdio: 'inherit' })
}
</code></pre><p>行文至此，就基本接近尾声了。</p><h2 id="--4">总结</h2><p>这里总结一下：首先文章开头通过提出「短时间找不到页面对应源文件的场景」，并针对容易碰到的报错情况给出了解决方案。 其次，配置了环境跟着调试学习了<code>vue-devtools</code>中使用的尤大写的 <a href="https://github.com/yyx990803/launch-editor" rel="noopener noreferrer">yyx990803/launch-editor</a>。</p><h3 id="--5">一句话简述其原理</h3><p>我们回顾下开头的原理内容。</p><pre><code class="language-sh">code path/to/file
</code></pre><p>一句话简述原理：利用<code>nodejs</code>中的<code>child_process</code>，执行了类似<code>code path/to/file</code>命令，于是对应编辑器就打开了相应的文件，而对应的编辑器则是通过在进程中执行<code>ps x</code>（<code>Window</code>则用<code>Get-Process</code>）命令来查找的，当然也可以自己指定编辑器。</p><p>最后还能做什么呢？</p><p>可以再看看 <a href="https://github.com/umijs/launch-editor" rel="noopener noreferrer">umijs/launch-editor</a> 和 <a href="https://github.com/facebook/create-react-app/blob/master/packages/react-dev-utils/launchEditor.js" rel="noopener noreferrer">react-dev-utils/launchEditor.js</a>，它们的代码几乎类似。</p><p>也可以利用<code>Node.js</code>做一些提高开发效率等工作，同时可以学习<code>child_process</code>等模块。</p><p>欢迎阅读我的<a href="https://www.lxchuan12.cn/">更多文章</a>。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何模拟实现 JS 的 bind 方法 ]]>
                </title>
                <description>
                    <![CDATA[ 本文通过分析和模拟实现 JavaScript 中常用的 bind 方法，帮助大家巩固前端开发知识。 用过React的同学都知道，经常会使用bind来绑定this。 import React, { Component } from 'react'; class TodoItem extends Component{     constructor(props){         super(props);         this.handleClick = this.handleClick.bind(this);     }    ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/javascript-bind-method/</link>
                <guid isPermaLink="false">5fbbb83539641a0517d51100</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Mon, 29 Mar 2021 08:25:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/11/carl-heyerdahl-KE0nC8-58MQ-unsplash--1-.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>本文通过分析和模拟实现 JavaScript 中常用的 bind 方法，帮助大家巩固前端开发知识。</p><p>用过<code>React</code>的同学都知道，经常会使用<code>bind</code>来绑定<code>this</code>。</p><pre><code class="language-js">import React, { Component } from 'react';
class TodoItem extends Component{
    constructor(props){
        super(props);
        this.handleClick = this.handleClick.bind(this);
    }
    handleClick(){
        console.log('handleClick');
    }
    render(){
        return  (
            &lt;div onClick={this.handleClick}&gt;点击&lt;/div&gt;
        );
    };
}
export default TodoItem;
</code></pre><p><strong>那么面试官可能会问是否想过</strong><code><strong>bind</strong></code><strong>到底做了什么，怎么模拟实现呢？</strong></p><p>附上之前写文章写过的一段话：已经有很多模拟实现<code>bind</code>的文章，为什么自己还要写一遍呢。学习就好比是座大山，人们沿着不同的路登山，分享着自己看到的风景。你不一定能看到别人看到的风景，体会到别人的心情。只有自己去登山，才能看到不一样的风景，体会才更加深刻。</p><p>先看一下<code>bind</code>是什么。从上面的<code>React</code>代码中，可以看出<code>bind</code>执行后是函数，并且每个函数都可以执行调用它。 眼见为实，耳听为虚。读者可以在控制台一步步点开<strong>例子1</strong>中的<code>obj</code>:</p><pre><code class="language-js">var obj = {};
console.log(obj);
console.log(typeof Function.prototype.bind); // function
console.log(typeof Function.prototype.bind());  // function
console.log(Function.prototype.bind.name);  // bind
console.log(Function.prototype.bind().name);  // bound
</code></pre><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/09/1.png" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><h3 id="-1-">因此可以得出结论1：</h3><p>1、<code>bind</code>是<code>Functoin</code>原型链中<code>Function.prototype</code>的一个属性，每个函数都可以调用它。<br>2、<code>bind</code>本身是一个函数名为<code>bind</code>的函数，返回值也是函数，函数名是<code>bound</code>。（打出来就是<code>bound加上一个空格</code>）。 知道了<code>bind</code>是函数，就可以传参，而且返回值<code>'bound '</code>也是函数，也可以传参，就很容易写出<strong>例子2</strong>：<br>后文统一 <code>bound</code> 指原函数<code>original</code> <code>bind</code>之后返回的函数，便于说明。</p><pre><code class="language-js">var obj = {
    name: '若川',
};
function original(a, b){
    console.log(this.name);
    console.log([a, b]);
    return false;
}
var bound = original.bind(obj, 1);
var boundResult = bound(2); // '若川', [1, 2]
console.log(boundResult); // false
console.log(original.bind.name); // 'bind'
console.log(original.bind.length); // 1
console.log(original.bind().length); // 2 返回original函数的形参个数
console.log(bound.name); // 'bound original'
console.log((function(){}).bind().name); // 'bound '
console.log((function(){}).bind().length); // 0
</code></pre><h3 id="-2-">由此可以得出结论2：</h3><p>1、调用<code>bind</code>的函数中的<code>this</code>指向<code>bind()</code>函数的第一个参数。</p><p>2、传给<code>bind()</code>的其他参数接收处理了，<code>bind()</code>之后返回的函数的参数也接收处理了，也就是说合并处理了。</p><p>3、并且<code>bind()</code>后的<code>name</code>为<code>bound + 空格 + 调用bind的函数名</code>。如果是匿名函数则是<code>bound + 空格</code>。</p><p>4、<code>bind</code>后的返回值函数，执行后返回值是原函数（<code>original</code>）的返回值。</p><p>5、<code>bind</code>函数形参（即函数的<code>length</code>）是<code>1</code>。<code>bind</code>后返回的<code>bound</code>函数形参不定，根据绑定的函数原函数（<code>original</code>）形参个数确定。</p><p>根据结论2：我们就可以简单模拟实现一个简版<code>bindFn</code></p><pre><code class="language-js">// 第一版 修改this指向，合并参数
Function.prototype.bindFn = function bind(thisArg){
    if(typeof this !== 'function'){
        throw new TypeError(this + 'must be a function');
    }
    // 存储函数本身
    var self = this;
    // 去除thisArg的其他参数 转成数组
    var args = [].slice.call(arguments, 1);
    var bound = function(){
        // bind返回的函数 的参数转成数组
        var boundArgs = [].slice.call(arguments);
        // apply修改this指向，把两个函数的参数合并传给self函数，并执行self函数，返回执行结果
        return self.apply(thisArg, args.concat(boundArgs));
    }
    return bound;
}
// 测试
var obj = {
    name: '若川',
};
function original(a, b){
    console.log(this.name);
    console.log([a, b]);
}
var bound = original.bindFn(obj, 1);
bound(2); // '若川', [1, 2]
</code></pre><p>如果面试官看到你答到这里，估计对你的印象60、70分应该是会有的。 但我们知道函数是可以用<code>new</code>来实例化的。那么<code>bind()</code>返回值函数会是什么表现呢。</p><p>接下来看<strong>例子3</strong>：</p><pre><code class="language-js">var obj = {
    name: '若川',
};
function original(a, b){
    console.log('this', this); // original {}
    console.log('typeof this', typeof this); // object
    this.name = b;
    console.log('name', this.name); // 2
    console.log('this', this);  // original {name: 2}
    console.log([a, b]); // 1, 2
}
var bound = original.bind(obj, 1);
var newBoundResult = new bound(2);
console.log(newBoundResult, 'newBoundResult'); // original {name: 2}
</code></pre><p>从<strong>例子3</strong>种可以看出<code>this</code>指向了<code>new bound()</code>生成的新对象。</p><h3 id="-3-">可以分析得出结论3：</h3><p>1、<code>bind</code>原先指向<code>obj</code>的失效了，其他参数有效。</p><p>2、<code>new bound</code>的返回值是以<code>original</code>原函数构造器生成的新对象。<code>original</code>原函数的<code>this</code>指向的就是这个新对象。 另外前不久写过一篇文章：<a href="https://juejin.im/post/5bde7c926fb9a049f66b8b52" rel="noopener noreferrer">面试官问：能否模拟实现JS的new操作符 (opens new window)</a>。简单摘要： <strong>new做了什么：</strong></p><ul><li>创建了一个全新的对象。</li><li>这个对象会被执行<code>[[Prototype]]</code>（也就是<code>__proto__</code>）链接。</li><li>生成的新对象会绑定到函数调用的this。</li><li>通过<code>new</code>创建的每个对象将最终被<code>[[Prototype]]</code>链接到这个函数的<code>prototype</code>对象上。</li><li>如果函数没有返回对象类型<code>Object</code>(包含<code>Functoin</code>, <code>Array</code>, <code>Date</code>, <code>RegExg</code>, <code>Error</code>)，那么<code>new</code>表达式中的函数调用会自动返回这个新的对象。</li></ul><p>所以相当于<code>new</code>调用时，<code>bind</code>的返回值函数<code>bound</code>内部要模拟实现<code>new</code>实现的操作。 话不多说，直接上代码。</p><pre><code class="language-js">// 第三版 实现new调用
Function.prototype.bindFn = function bind(thisArg){
    if(typeof this !== 'function'){
        throw new TypeError(this + ' must be a function');
    }
    // 存储调用bind的函数本身
    var self = this;
    // 去除thisArg的其他参数 转成数组
    var args = [].slice.call(arguments, 1);
    var bound = function(){
        // bind返回的函数 的参数转成数组
        var boundArgs = [].slice.call(arguments);
        var finalArgs = args.concat(boundArgs);
        // new 调用时，其实this instanceof bound判断也不是很准确。es6 new.target就是解决这一问题的。
        if(this instanceof bound){
            // 这里是实现上文描述的 new 的第 1, 2, 4 步
            // 1.创建一个全新的对象
            // 2.并且执行[[Prototype]]链接
            // 4.通过`new`创建的每个对象将最终被`[[Prototype]]`链接到这个函数的`prototype`对象上。
            // self可能是ES6的箭头函数，没有prototype，所以就没必要再指向做prototype操作。
            if(self.prototype){
                // ES5 提供的方案 Object.create()
                // bound.prototype = Object.create(self.prototype);
                // 但 既然是模拟ES5的bind，那浏览器也基本没有实现Object.create()
                // 所以采用 MDN ployfill方案 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create
                function Empty(){}
                Empty.prototype = self.prototype;
                bound.prototype = new Empty();
            }
            // 这里是实现上文描述的 new 的第 3 步
            // 3.生成的新对象会绑定到函数调用的`this`。
            var result = self.apply(this, finalArgs);
            // 这里是实现上文描述的 new 的第 5 步
            // 5.如果函数没有返回对象类型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`)，
            // 那么`new`表达式中的函数调用会自动返回这个新的对象。
            var isObject = typeof result === 'object' &amp;&amp; result !== null;
            var isFunction = typeof result === 'function';
            if(isObject || isFunction){
                return result;
            }
            return this;
        }
        else{
            // apply修改this指向，把两个函数的参数合并传给self函数，并执行self函数，返回执行结果
            return self.apply(thisArg, finalArgs);
        }
    };
    return bound;
}
</code></pre><p>面试官看到这样的实现代码，基本就是满分了，心里独白：这小伙子/小姑娘不错啊。不过可能还会问<code>this instanceof bound</code>不准确问题。 上文注释中提到<code>this instanceof bound</code>也不是很准确，<code>ES6 new.target</code>很好的解决这一问题，我们举个<strong>例子4：</strong></p><h3 id="instanceof-es6-new-target-"><code>instanceof</code> 不准确，<code>ES6 new.target</code>很好地解决这一问题</h3><pre><code class="language-js">function Student(name){
    if(this instanceof Student){
        this.name = name;
        console.log('name', name);
    }
    else{
        throw new Error('必须通过new关键字来调用Student。');
    }
}
var student = new Student('若');
var notAStudent = Student.call(student, '川'); // 不抛出错误，且执行了。
console.log(student, 'student', notAStudent, 'notAStudent');

function Student2(name){
    if(typeof new.target !== 'undefined'){
        this.name = name;
        console.log('name', name);
    }
    else{
        throw new Error('必须通过new关键字来调用Student2。');
    }
}
var student2 = new Student2('若');
var notAStudent2 = Student2.call(student2, '川');
console.log(student2, 'student2', notAStudent2, 'notAStudent2'); // 抛出错误
</code></pre><p>细心的同学可能会发现了这版本的代码没有实现<code>bind</code>后的<code>bound</code>函数的<code>name</code><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/name" rel="noopener noreferrer">MDN Function.name (opens new window)</a>和<code>length</code><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/length" rel="noopener noreferrer">MDN Function.length (opens new window)</a>。面试官可能也发现了这一点继续追问，如何实现，或者问是否看过<a href="https://github.com/es-shims/es5-shim/blob/master/es5-shim.js#L201-L335" rel="noopener noreferrer"><code>es5-shim</code>的源码实现<code>L201-L335</code> (opens new window)</a>。如果不限<code>ES</code>版本。其实可以用<code>ES5</code>的<code>Object.defineProperties</code>来实现。</p><pre><code class="language-js">Object.defineProperties(bound, {
    'length': {
        value: self.length,
    },
    'name': {
        value: 'bound ' + self.name,
    }
});
</code></pre><h3 id="es5-shim-bind"><code>es5-shim</code>的源码实现<code>bind</code></h3><p>直接附上源码（有删减注释和部分修改等）</p><pre><code class="language-js">var $Array = Array;
var ArrayPrototype = $Array.prototype;
var $Object = Object;
var array_push = ArrayPrototype.push;
var array_slice = ArrayPrototype.slice;
var array_join = ArrayPrototype.join;
var array_concat = ArrayPrototype.concat;
var $Function = Function;
var FunctionPrototype = $Function.prototype;
var apply = FunctionPrototype.apply;
var max = Math.max;
// 简版 源码更复杂些。
var isCallable = function isCallable(value){
    if(typeof value !== 'function'){
        return false;
    }
    return true;
};
var Empty = function Empty() {};
// 源码是 defineProperties
// 源码是bind笔者改成bindFn便于测试
FunctionPrototype.bindFn = function bind(that) {
    var target = this;
    if (!isCallable(target)) {
        throw new TypeError('Function.prototype.bind called on incompatible ' + target);
    }
    var args = array_slice.call(arguments, 1);
    var bound;
    var binder = function () {
        if (this instanceof bound) {
            var result = apply.call(
                target,
                this,
                array_concat.call(args, array_slice.call(arguments))
            );
            if ($Object(result) === result) {
                return result;
            }
            return this;
        } else {
            return apply.call(
                target,
                that,
                array_concat.call(args, array_slice.call(arguments))
            );
        }
    };
    var boundLength = max(0, target.length - args.length);
    var boundArgs = [];
    for (var i = 0; i &lt; boundLength; i++) {
        array_push.call(boundArgs, '$' + i);
    }
    // 这里是Function构造方式生成形参length $1, $2, $3...
    bound = $Function('binder', 'return function (' + array_join.call(boundArgs, ',') + '){ return binder.apply(this, arguments); }')(binder);

    if (target.prototype) {
        Empty.prototype = target.prototype;
        bound.prototype = new Empty();
        Empty.prototype = null;
    }
    return bound;
};
</code></pre><p>你说出<code>es5-shim</code>源码<code>bind</code>实现，感慨这代码真是高效、严谨。面试官心里独白可能是：你就是我要找的人，薪酬福利你可以和<code>HR</code>去谈下。</p><h2 id="-">总结</h2><p>1、<code>bind</code>是<code>Function</code>原型链中的<code>Function.prototype</code>的一个属性，它是一个函数，修改<code>this</code>指向，合并参数传递给原函数，返回值是一个新的函数。<br>2、<code>bind</code>返回的函数可以通过<code>new</code>调用，这时提供的<code>this</code>的参数被忽略，指向了<code>new</code>生成的全新对象。内部模拟实现了<code>new</code>操作符。<br>3、<code>es5-shim</code>源码模拟实现<code>bind</code>时用<code>Function</code>实现了<code>length</code>。<br>事实上，平时其实很少需要使用自己实现的投入到生成环境中。但面试官通过这个面试题能考察很多知识。比如<code>this</code>指向，原型链，闭包，函数等知识，可以扩展很多。<br>读者发现有不妥或可改善之处，欢迎指出。另外觉得写得不错，可以点个赞，也是对笔者的一种支持。</p><p>文章中的例子和测试代码放在<code>github</code>中<a href="https://github.com/lxchuan12/html5/tree/gh-pages/JS%E7%9B%B8%E5%85%B3/%E5%87%BD%E6%95%B0/bind%E6%A8%A1%E6%8B%9F%E5%AE%9E%E7%8E%B0" rel="noopener noreferrer">bind模拟实现 github (opens new window)</a>。<a href="http://lxchuan12.github.io/html5/JS%E7%9B%B8%E5%85%B3/%E5%87%BD%E6%95%B0/bind%E6%A8%A1%E6%8B%9F%E5%AE%9E%E7%8E%B0/bind-0.html" rel="noopener noreferrer">bind模拟实现 预览地址 (opens new window)</a><code>F12</code>看控制台输出，结合<code>source</code>面板查看效果更佳。</p><pre><code class="language-js">// 最终版 删除注释 详细注释版请看上文
Function.prototype.bind = Function.prototype.bind || function bind(thisArg){
    if(typeof this !== 'function'){
        throw new TypeError(this + ' must be a function');
    }
    var self = this;
    var args = [].slice.call(arguments, 1);
    var bound = function(){
        var boundArgs = [].slice.call(arguments);
        var finalArgs = args.concat(boundArgs);
        if(this instanceof bound){
            if(self.prototype){
                function Empty(){}
                Empty.prototype = self.prototype;
                bound.prototype = new Empty();
            }
            var result = self.apply(this, finalArgs);
            var isObject = typeof result === 'object' &amp;&amp; result !== null;
            var isFunction = typeof result === 'function';
            if(isObject || isFunction){
                return result;
            }
            return this;
        }
        else{
            return self.apply(thisArg, finalArgs);
        }
    };
    return bound;
}
</code></pre><h2 id="--1">参考</h2><ul><li><a href="https://oshotokill.gitbooks.io/understandinges6-simplified-chinese/content/chapter_3.html" rel="noopener noreferrer">OshotOkill翻译的 深入理解<code>ES6</code> 简体中文版 - 第三章 函数 (opens new window)</a>（虽然笔者是看的纸质书籍，但推荐下这本在线的书）</li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind" rel="noopener noreferrer">MDN Function.prototype.bind(opens new window)</a></li><li><a href="https://juejin.im/post/59093b1fa0bb9f006517b906" rel="noopener noreferrer">冴羽: JavaScript深入之bind的模拟实现(opens new window)</a></li><li><a href="https://www.jianshu.com/p/6958f99db769" rel="noopener noreferrer">《React状态管理与同构实战》侯策：从一道面试题，到“我可能看了假源码”</a></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ underscore 源码整体架构 ]]>
                </title>
                <description>
                    <![CDATA[ 源码类文章，一般阅读量不高。已经有能力看懂的，自己就看了。不想看，不敢看的就不会去看源码。 所以我的文章，尽量写得让想看源码又不知道怎么看的读者能看懂。 虽然看过挺多underscore.js分析类的文章，但总感觉少点什么。这也许就是纸上得来终觉浅，绝知此事要躬行吧。于是决定自己写一篇学习 underscore.js整体架构的文章。 本文章学习的版本是v1.9.1。 unpkg.com underscore 源码地址(opens new window) [https://unpkg.com/underscore@1.9.1/underscore.js]。 虽然很多人都没用过underscore.js，但看下官方文档都应该知道如何使用。 从一个官方文档_.chain简单例子看起： _.chain([1, 2, 3]).reverse().value(); // => [3, 2, 1] 看例子中可以看出，这是支持链式调用。 读者也可以顺着文章思路，自行打开下载源码进行调试，这样印象更加深刻。 2 链式调用 _.chain 函数源码： _.chain = functio ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/learn-the-overall-architecture-of-underscore-source-code/</link>
                <guid isPermaLink="false">600ed1e15f61e30501b5c15b</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Sun, 21 Feb 2021 09:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/01/dean-pugh-C8NDn4xk9zs-unsplash-1.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>源码类文章，一般阅读量不高。已经有能力看懂的，自己就看了。不想看，不敢看的就不会去看源码。<br>所以我的文章，尽量写得让想看源码又不知道怎么看的读者能看懂。</p><p>虽然看过挺多<code>underscore.js</code>分析类的文章，但总感觉少点什么。这也许就是<strong>纸上得来终觉浅，绝知此事要躬行</strong>吧。于是决定自己写一篇学习<code>underscore.js</code>整体架构的文章。</p><p>本文章学习的版本是<code>v1.9.1</code>。 <a href="https://unpkg.com/underscore@1.9.1/underscore.js" rel="noopener noreferrer"><code>unpkg.com</code> underscore 源码地址(opens new window)</a>。</p><p>虽然很多人都没用过<code>underscore.js</code>，但看下官方文档都应该知道如何使用。</p><p>从一个官方文档<code>_.chain</code>简单例子看起：</p><pre><code class="language-js">_.chain([1, 2, 3]).reverse().value();
// =&gt; [3, 2, 1]
</code></pre><p>看例子中可以看出，这是支持链式调用。</p><p>读者也可以顺着文章思路，自行打开下载源码进行调试，这样印象更加深刻。</p><h2 id="2-">2 链式调用</h2><p><code>_.chain</code> 函数源码：</p><pre><code class="language-js">_.chain = function(obj) {
	var instance = _(obj);
	instance._chain = true;
	return instance;
};
</code></pre><p>这个函数比较简单，就是传递<code>obj</code>调用<code>_()</code>。但返回值变量竟然是<code>instance</code>实例对象。添加属性<code>_chain</code>赋值为<code>true</code>，并返回<code>intance</code>对象。但再看例子，实例对象竟然可以调用<code>reverse</code>方法，再调用<code>value</code>方法。猜测支持<code>OOP</code>（面向对象）调用。</p><p>带着问题，笔者看了下定义 <code>_</code> 函数对象的代码。</p><h2 id="3-_-oop">3 <code>_</code> 函数对象 支持<code>OOP</code></h2><pre><code class="language-js">var _ = function(obj) {
	if (obj instanceof _) return obj;
	if (!(this instanceof _)) return new _(obj);
	this._wrapped = obj;
};
</code></pre><p>如果参数<code>obj</code>已经是<code>_</code>的实例了，则返回<code>obj</code>。 如果<code>this</code>不是<code>_</code>的实例，则手动 <code>new _(obj)</code>; 再次<code>new</code>调用时，把<code>obj</code>对象赋值给<code>_wrapped</code>这个属性。 也就是说最后得到的实例对象是这样的结构 <code>{ _wrapped: '参数obj', }</code> 它的原型<code>_(obj).__proto__</code> 是 <code>_.prototype</code>;</p><p>如果对这块不熟悉的读者，可以看下以下这张图(之前写<a href="https://juejin.im/post/5c433e216fb9a049c15f841b" rel="noopener noreferrer">面试官问：<code>JS的继承</code> (opens new window)</a>画的图)。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image-15.png" class="kg-image" alt="image-15" width="600" height="400" loading="lazy"></figure><p>继续分析官方的<code>_.chain</code>例子。这个例子拆开，写成三步。</p><pre><code class="language-js">var part1 = _.chain([1, 2, 3]);
var part2 = part1.reverse();
var part3 = part2.value();

// 没有后续part1.reverse()操作的情况下
console.log(part1); // {__wrapped: [1, 2, 3], _chain: true}

console.log(part2); // {__wrapped: [3, 2, 1], _chain: true}

console.log(part3); // [3, 2, 1]
</code></pre><p>思考问题：<code>reverse</code>本是<code>Array.prototype</code>上的方法呀。为啥支持链式调用呢。 搜索<code>reverse</code>，可以看到如下这段代码：</p><p>并将例子代入这段代码可得（怎么有种高中做数学题的既视感^_^）：</p><pre><code class="language-js">_.chain([1,2,3]).reverse().value()s
</code></pre><pre><code class="language-js">var ArrayProto = Array.prototype;
// 遍历 数组 Array.prototype 的这些方法，赋值到 _.prototype 上
_.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
	// 这里的`method`是 reverse 函数
	var method = ArrayProto[name];
	_.prototype[name] = function() {
	// 这里的obj 就是数组 [1, 2, 3]
	var obj = this._wrapped;
	// arguments  是参数集合，指定reverse 的this指向为obj，参数为arguments， 并执行这个函数函数。执行后 obj 则是 [3, 2, 1]
	method.apply(obj, arguments);
	if ((name === 'shift' || name === 'splice') &amp;&amp; obj.length === 0) delete obj[0];
	// 重点在于这里 chainResult 函数。
	return chainResult(this, obj);
	};
});
</code></pre><pre><code class="language-js">// Helper function to continue chaining intermediate results.
var chainResult = function(instance, obj) {
	// 如果实例中有_chain 为 true 这个属性，则返回实例 支持链式调用的实例对象  { _chain: true, this._wrapped: [3, 2, 1] }，否则直接返回这个对象[3, 2, 1]。
	return instance._chain ? _(obj).chain() : obj;
};
</code></pre><p><code>if ((name === 'shift' || name === 'splice') &amp;&amp; obj.length === 0) delete obj[0];</code> 提一下上面源码中的这一句，看到这句是百思不得其解。于是赶紧在<code>github</code>中搜索这句加上<code>""</code>双引号。表示全部搜索。</p><p>搜索到两个在官方库中的<code>ISSUE</code>，大概意思就是兼容IE低版本的写法。有兴趣的可以点击去看看。</p><p><a href="https://github.com/jashkenas/underscore/issues/2016" rel="noopener noreferrer">I don't understand the meaning of this sentence.(opens new window)</a></p><p><a href="https://github.com/jashkenas/underscore/issues/2773" rel="noopener noreferrer">why delete obj[0](opens new window)</a></p><h2 id="4-">4 基于流的编程</h2><p>至此就算是分析完了链式调用<code>_.chain()</code>和<code>_</code> 函数对象。这种把数据存储在实例对象<code>{_wrapped: '', _chain: true}</code> 中，<code>_chain</code>判断是否支持链式调用，来传递给下一个函数处理。这种做法叫做 <strong>基于流的编程</strong>。</p><p>最后数据处理完，要返回这个数据怎么办呢。<code>underscore</code>提供了一个<code>value</code>的方法。</p><pre><code class="language-js">_.prototype.value = function(){
	return this._wrapped;
}
</code></pre><p>顺便提供了几个别名。<code>toJSON</code>、<code>valueOf</code>。 _.prototype.valueOf = _.prototype.toJSON = _.prototype.value;</p><p>还提供了 <code>toString</code>的方法。</p><pre><code class="language-js">_.prototype.toString = function() {
	return String(this._wrapped);
};
</code></pre><p>这里的<code>String()</code> 和<code>new String()</code> 效果是一样的。 可以猜测内部实现和 <code>_</code>函数对象类似。</p><pre><code class="language-js">var String = function(){
	if(!(this instanceOf String)) return new String(obj);
}
</code></pre><pre><code class="language-js">var chainResult = function(instance, obj) {
	return instance._chain ? _(obj).chain() : obj;
};
</code></pre><p>细心的读者会发现<code>chainResult</code>函数中的<code>_(obj).chain()</code>，是怎么实现实现链式调用的呢。</p><p>而<code>_(obj)</code>是返回的实例对象<code>{_wrapped: obj}</code>呀。怎么会有<code>chain()</code>方法，肯定有地方挂载了这个方法到<code>_.prototype</code>上或者其他操作，这就是<code>_.mixin()</code>。</p><h2 id="5-_-mixin-_-prototype-">5 <code>_.mixin</code> 挂载所有的静态方法到 <code>_.prototype</code>， 也可以挂载自定义的方法</h2><p><code>_.mixin</code> 混入。但侵入性太强，经常容易出现覆盖之类的问题。记得之前<code>React</code>有<code>mixin</code>功能，<code>Vue</code>也有<code>mixin</code>功能。但版本迭代更新后基本都是慢慢的都不推荐或者不支持<code>mixin</code>。</p><pre><code class="language-js">_.mixin = function(obj) {
	// 遍历对象上的所有方法
	_.each(_.functions(obj), function(name) {
		// 比如 chain, obj['chain'] 函数，自定义的，则赋值到_[name] 上，func 就是该函数。也就是说自定义的方法，不仅_函数对象上有，而且`_.prototype`上也有
	var func = _[name] = obj[name];
	_.prototype[name] = function() {
		// 处理的数据对象
		var args = [this._wrapped];
		// 处理的数据对象 和 arguments 结合
		push.apply(args, arguments);
		// 链式调用  chain.apply(_, args) 参数又被加上了 _chain属性，支持链式调用。
		// _.chain = function(obj) {
		//	var instance = _(obj);
		//	instance._chain = true;
		//	return instance;
		};
		return chainResult(this, func.apply(_, args));
	};
	});
	// 最终返回 _ 函数对象。
	return _;
};

_.mixin(_);
</code></pre><p><code>_mixin(_)</code> 把静态方法挂载到了<code>_.prototype</code>上，也就是<code>_.prototype.chain</code>方法 也就是 <code>_.chain</code>方法。</p><p>所以<code>_.chain(obj)</code>和<code>_(obj).chain()</code>效果一样，都能实现链式调用。</p><p>关于上述的链式调用，笔者画了一张图，所谓一图胜千言。</p><figure class="kg-card kg-image-card"><img src="https://www.lxchuan12.cn/assets/img/underscore.js-chain.3af7400a.png" class="kg-image" alt="underscore.js 链式调用图解" width="600" height="400" loading="lazy"></figure><h3 id="5-1-_-mixin-">5.1 _.mixin 挂载自定义方法</h3><p>挂载自定义方法： 举个例子：</p><pre><code class="language-js">_.mixin({
	log: function(){
		console.log('哎呀，我被调用了');
	}
})
_.log() // 哎呀，我被调用了
_().log() // 哎呀，我被调用了
</code></pre><h3 id="5-2-_-functions-obj-">5.2 _.functions(obj)</h3><pre><code class="language-js">_.functions = _.methods = function(obj) {
	var names = [];
	for (var key in obj) {
	if (_.isFunction(obj[key])) names.push(key);
	}
	return names.sort();
};
</code></pre><p><code>_.functions</code> 和 <code>_.methods</code> 两个方法，遍历对象上的方法，放入一个数组，并且排序。返回排序后的数组。</p><h3 id="5-3-underscore-js-_-_-prototype-">5.3 <code>underscore.js</code> 究竟在<code>_</code>和<code>_.prototype</code>挂载了多少方法和属性</h3><p>再来看下<code>underscore.js</code>究竟挂载在<code>_函数对象</code>上有多少静态方法和属性，和挂载<code>_.prototype</code>上有多少方法和属性。</p><p>使用<code>for in</code>循环一试便知。看如下代码：</p><pre><code class="language-js">var staticMethods = [];
var staticProperty = [];
for(var name in _){
	if(typeof _[name] === 'function'){
		staticMethods.push(name);
	}
	else{
		staticProperty.push(name);
	}
}
console.log(staticProperty); // ["VERSION", "templateSettings"] 两个
console.log(staticMethods); // ["after", "all", "allKeys", "any", "assign", ...] 138个
</code></pre><pre><code class="language-js">var prototypeMethods = [];
var prototypeProperty = [];
for(var name in _.prototype){
	if(typeof _.prototype[name] === 'function'){
		prototypeMethods.push(name);
	}
	else{
		prototypeProperty.push(name);
	}
}
console.log(prototypeProperty); // []
console.log(prototypeMethods); // ["after", "all", "allKeys", "any", "assign", ...] 152个
</code></pre><p>根据这些，笔者又画了一张图<code>underscore.js</code> 原型关系图，毕竟一图胜千言。</p><figure class="kg-card kg-image-card"><img src="https://www.lxchuan12.cn/assets/img/underscore.js-prototype.c2832ff7.png" class="kg-image" alt=" 原型关系图" width="600" height="400" loading="lazy"></figure><h2 id="6-">6 整体架构概览</h2><h3 id="6-1-">6.1 匿名函数自执行</h3><pre><code class="language-js">(function(){

}());
</code></pre><p>这样保证不污染外界环境，同时隔离外界环境，不是外界影响内部环境。</p><p>外界访问不到里面的变量和函数，里面可以访问到外界的变量，但里面定义了自己的变量，则不会访问外界的变量。 匿名函数将代码包裹在里面，防止与其他代码冲突和污染全局环境。 关于自执行函数不是很了解的读者可以参看这篇文章。 <a href="https://segmentfault.com/a/1190000003985390" rel="noopener noreferrer">[译] JavaScript：立即执行函数表达式（IIFE）(opens new window)</a></p><h3 id="6-2-root-">6.2 root 处理</h3><pre><code class="language-js">var root = typeof self == 'object' &amp;&amp; self.self === self &amp;&amp; self ||
	typeof global == 'object' &amp;&amp; global.global === global &amp;&amp; global ||
	this ||
	{};
</code></pre><p>支持<code>浏览器环境</code>、<code>node</code>、<code>Web Worker</code>、<code>node vm</code>、<code>微信小程序</code>。</p><h3 id="6-3-">6.3 导出</h3><pre><code class="language-js">if (typeof exports != 'undefined' &amp;&amp; !exports.nodeType) {
	if (typeof module != 'undefined' &amp;&amp; !module.nodeType &amp;&amp; module.exports) {
	exports = module.exports = _;
	}
	exports._ = _;
} else {
	root._ = _;
}
</code></pre><p>关于<code>root处理</code>和<code>导出</code>的这两段代码的解释，推荐看这篇文章<a href="https://juejin.im/post/5a0bae515188252964213855" rel="noopener noreferrer">冴羽：underscore 系列之如何写自己的 underscore (opens new window)</a>，讲得真的太好了。笔者在此就不赘述了。 总之，<code>underscore.js</code>作者对这些处理也不是一蹴而就的，也是慢慢积累，和其他人提<code>ISSUE</code>之后不断改进的。</p><h3 id="6-4-amd-">6.4 支持 <code>amd</code> 模块化规范</h3><pre><code class="language-js">if (typeof define == 'function' &amp;&amp; define.amd) {
	define('underscore', [], function() {
		return _;
	});
}
</code></pre><h3 id="6-5-_-noconflict-">6.5 _.noConflict 防冲突函数</h3><p>源码：</p><pre><code class="language-js">// 暂存在 root 上， 执行noConflict时再赋值回来
var previousUnderscore = root._;
_.noConflict = function() {
	root._ = previousUnderscore;
	return this;
};
</code></pre><p>使用：</p><pre><code class="language-js">&lt;script&gt;
var _ = '我就是我，不一样的烟火，其他可不要覆盖我呀';
&lt;/script&gt;
&lt;script src="https://unpkg.com/underscore@1.9.1/underscore.js"&gt;
&lt;/script&gt;
&lt;script&gt;
var underscore = _.noConflict();
console.log(_); // '我就是我，不一样的烟火，其他可不要覆盖我呀'
underscore.isArray([]) // true
&lt;/script&gt;
</code></pre><h2 id="7-">7 总结</h2><p>全文根据官网提供的链式调用的例子， <code>_.chain([1, 2, 3]).reverse().value();</code>较为深入的调试和追踪代码，分析链式调用（<code>_.chain()</code> 和 <code>_(obj).chain()</code>）、<code>OOP</code>、基于流式编程、和<code>_.mixin(_)</code>在<code>_.prototype</code>挂载方法，最后整体架构分析。学习<code>underscore.js</code>整体架构，利于打造属于自己的函数式编程类库。</p><p>文章分析的源码整体结构。</p><pre><code class="language-js">(function() {
	var root = typeof self == 'object' &amp;&amp; self.self === self &amp;&amp; self ||
		typeof global == 'object' &amp;&amp; global.global === global &amp;&amp; global ||
		this ||
		{};
	var previousUnderscore = root._;

	var _ = function(obj) {
	  if (obj instanceof _) return obj;
	  if (!(this instanceof _)) return new _(obj);
	  this._wrapped = obj;
	};

	if (typeof exports != 'undefined' &amp;&amp; !exports.nodeType) {
	  if (typeof module != 'undefined' &amp;&amp; !module.nodeType &amp;&amp; module.exports) {
		exports = module.exports = _;
	  }
	  exports._ = _;
	} else {
	  root._ = _;
	}
	_.VERSION = '1.9.1';

	_.chain = function(obj) {
	  var instance = _(obj);
	  instance._chain = true;
	  return instance;
	};

	var chainResult = function(instance, obj) {
	  return instance._chain ? _(obj).chain() : obj;
	};

	_.mixin = function(obj) {
	  _.each(_.functions(obj), function(name) {
		var func = _[name] = obj[name];
		_.prototype[name] = function() {
		  var args = [this._wrapped];
		  push.apply(args, arguments);
		  return chainResult(this, func.apply(_, args));
		};
	  });
	  return _;
	};

	_.mixin(_);

	_.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
	  var method = ArrayProto[name];
	  _.prototype[name] = function() {
		var obj = this._wrapped;
		method.apply(obj, arguments);
		if ((name === 'shift' || name === 'splice') &amp;&amp; obj.length === 0) delete obj[0];
		return chainResult(this, obj);
	  };
	});

	_.each(['concat', 'join', 'slice'], function(name) {
	  var method = ArrayProto[name];
	  _.prototype[name] = function() {
		return chainResult(this, method.apply(this._wrapped, arguments));
	  };
	});

	_.prototype.value = function() {
	  return this._wrapped;
	};

	_.prototype.valueOf = _.prototype.toJSON = _.prototype.value;

	_.prototype.toString = function() {
	  return String(this._wrapped);
	};

	if (typeof define == 'function' &amp;&amp; define.amd) {
	  define('underscore', [], function() {
		return _;
	  });
	}
}());
</code></pre><p>下一篇文章可能是学习<code>lodash</code>的源码整体架构。</p><p>读者发现有不妥或可改善之处，欢迎评论指出。如果觉得写得不错，可以评论、转发，也是对笔者的一种支持。</p><h2 id="8-">8 推荐阅读</h2><p><a href="https://underscorejs.org/" rel="noopener noreferrer">underscorejs.org 官网(opens new window)</a><br><a href="https://yoyoyohamapi.gitbooks.io/undersercore-analysis/content/" rel="noopener noreferrer">undersercore-analysis(opens new window)</a><br><a href="https://juejin.im/post/5a0bae515188252964213855" rel="noopener noreferrer">underscore 系列之如何写自己的 underscore(opens new window)</a><br><br>欢迎阅读我的<a href="https://www.lxchuan12.cn/">更多文章</a>。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ koa 源码整体架构 ]]>
                </title>
                <description>
                    <![CDATA[ 如果你简历上一不小心写了熟悉koa，面试官大概率会问： > 1、koa洋葱模型怎么实现的 2、如果中间件中的next()方法报错了怎么办 3、co的原理是怎样的 等等问题。 导读 文章通过例子调试 koa，梳理koa的主流程，来理解 koa-compose 洋葱模型原理和 co 库的原理，相信你看完后一定会有所收获。 本文学习的koa版本是v2.11.0。克隆的官方仓库的master分支。 截至目前（2020年3月11日），最新一次commit是2020-01-04 07:41 Olle Jonsson eda27608，build: Drop unused Travis sudo: false directive (#1416)。 本文仓库在这里若川的 koa-analysis github 仓库 [https://github.com/lxchuan12/koa-analysishttps://github.com/lxchuan12/koa-analysis] 。求个star呀。 本文阅读最佳方式 先star一下我的仓库，再把它git clone https://git ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/learn-the-overall-architecture-of-koa-source-code/</link>
                <guid isPermaLink="false">601ff3096183a7054015640e</guid>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Sun, 07 Feb 2021 10:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/02/safar-safarov-LKsHwgzyk7c-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>如果你简历上一不小心写了熟悉<code>koa</code>，面试官大概率会问：</p><blockquote>1、<code>koa</code>洋葱模型怎么实现的<br>2、如果中间件中的<code>next()</code>方法报错了怎么办<br>3、<code>co</code>的原理是怎样的</blockquote><p>等等问题。</p><h2 id="-">导读</h2><p>文章通过例子调试 <code>koa</code>，梳理<code>koa</code>的主流程，来理解 <code>koa-compose</code> 洋葱模型原理和 <code>co</code> 库的原理，相信你看完后一定会有所收获。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/02/image.png" class="kg-image" alt="image" width="600" height="400" loading="lazy"></figure><p>本文学习的<code>koa</code>版本是<code>v2.11.0</code>。克隆的官方仓库的<code>master</code>分支。 截至目前（2020年3月11日），最新一次<code>commit</code>是<code>2020-01-04 07:41 Olle Jonsson</code> <code>eda27608</code>，<code>build: Drop unused Travis sudo: false directive (#1416)</code>。</p><p>本文仓库在这里<a href="https://github.com/lxchuan12/koa-analysishttps://github.com/lxchuan12/koa-analysis">若川的 koa-analysis github 仓库</a>。求个<code>star</code>呀。</p><h2 id="--1">本文阅读最佳方式</h2><p>先<code>star</code>一下我的仓库，再把它<code>git clone https://github.com/lxchuan12/koa-analysis.git</code>克隆下来。不用管你是否用过<code>nodejs</code>。会一点点<code>promise、generator、async、await</code>等知识即可看懂。如果一点点也不会，可以边看阮一峰老师的<a href="https://es6.ruanyifeng.com/#docs/generatorhttps://es6.ruanyifeng.com/#docs/generator">《ES6标准入门》</a>相关章节。<strong>跟着文章节奏调试和示例代码调试，动手调试（用<code>vscode</code>或者<code>chrome</code>）印象更加深刻</strong>。文章长段代码不用细看，可以调试时再细看。看这类源码文章百遍，可能不如自己多调试几遍。也欢迎加我微信交流<code>ruochuan12</code>。</p><pre><code class="language-bash"># 克隆我的这个仓库
git clone https://github.com/lxchuan12/koa-analysis.git
# chrome 调试：
# 全局安装 http-server
npm i -g http-server
hs koa/examples/
# 可以指定端口 -p 3001
# hs -p 3001 koa/examples/
# 浏览器中打开
# 然后在浏览器中打开localhost:8080，开心的把代码调试起来
</code></pre><p>这里把这个<code>examples</code>文件夹做个简单介绍。<br></p><ul><li><code>middleware</code>文件夹是用来<code>vscode</code>调试整体流程的。<br></li><li><code>simpleKoa</code> 文件夹是<code>koa</code>简化版，为了调试<code>koa-compose</code>洋葱模型如何串联起来各个中间件的。<br></li><li><code>koa-convert</code>文件夹是用来调试<code>koa-convert</code>和<code>co</code>源码的。<br></li><li><code>co-generator</code>文件夹是模拟实现<code>co</code>的示例代码。</li></ul><h2 id="vscode-koa-">vscode 调试 koa 源码方法</h2><p>之前，我在知乎回答了一个问题<a href="https://www.zhihu.com/question/350289336/answer/910970733" rel="noopener noreferrer">一年内的前端看不懂前端框架源码怎么办？</a>推荐了一些资料，阅读量还不错，大家有兴趣可以看看。主要有四点：</p><ul><li>借助调试</li><li>搜索查阅相关高赞文章</li><li>把不懂的地方记录下来，查阅相关文档</li><li>总结</li></ul><p>看源码，调试很重要，所以我详细写下 <code>koa</code> 源码调试方法，帮助一些可能不知道如何调试的读者。</p><pre><code class="language-bash"># 我已经克隆到我的koa-analysis仓库了
git clone https://github.com/koajs/koa.git
</code></pre><pre><code class="language-json">// package.json
{
  "name": "koa",
  "version": "2.11.0",
  "description": "Koa web app framework",
  "main": "lib/application.js",
}
</code></pre><p>克隆源码后，看<code>package.json</code>找到<code>main</code>，就知道入口文件是<code>lib/application.js</code>了。</p><p>大概看完项目结构后发现没有<code>examples</code>文件夹（一般项目都会有这个文件夹，告知用户如何使用该项目），这时仔细看<code>README.md</code>。 如果看英文<code>README.md</code>有些吃力，会发现在<code>Community</code>标题下有一个<a href="https://github.com/demopark/koa-docs-Zh-CN" rel="noopener noreferrer">中文文档 v2.x </a>。同时也有一个<a href="https://github.com/koajs/examples" rel="noopener noreferrer"><code>examples</code>仓库 </a>。</p><pre><code class="language-bash"># 我已经克隆下来到我的仓库了
git clone https://github.com/koajs/examples.git
</code></pre><p>这时再开心的把<code>examples</code>克隆到自己电脑。可以安装好依赖，逐个研究学习下这里的例子，然后可能就一不小心掌握了<code>koa</code>的基本用法。当然，我这里不详细写这一块了，我是自己手写一些例子来调试。</p><p>继续看文档会发现<strong>使用指南</strong>讲述<code>编写中间件</code>。</p><h3 id="-koa-compose-">使用文档中的中间件<code>koa-compose</code>例子来调试</h3><p>学习 <code>koa-compose</code> 前，先看两张图。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/02/image-1.png" class="kg-image" alt="image-1" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/02/image-2.png" class="kg-image" alt="image-2" width="600" height="400" loading="lazy"></figure><p>在<code>koa</code>中，请求响应都放在中间件的第一个参数<code>context</code>对象中了。</p><p>再引用<a href="https://github.com/demopark/koa-docs-Zh-CN/blob/master/guide.md#debugging-koa" rel="noopener noreferrer">Koa中文文档</a>中的一段：</p><p>如果您是前端开发人员，您可以将 <code>next()</code>; 之前的任意代码视为“捕获”阶段，这个简易的 <code>gif</code> 说明了 <code>async</code> 函数如何使我们能够恰当地利用堆栈流来实现请求和响应流：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/02/middleware.104a11b9.gif" class="kg-image" alt="middleware.104a11b9" width="600" height="400" loading="lazy"></figure><ol><li>创建一个跟踪响应时间的日期</li><li>等待下一个中间件的控制</li><li>创建另一个日期跟踪持续时间</li><li>等待下一个中间件的控制</li><li>将响应主体设置为“Hello World”</li><li>计算持续时间</li><li>输出日志行</li><li>计算响应时间</li><li>设置 <code>X-Response-Time</code> 头字段</li><li>交给 Koa 处理响应</li></ol><p>读者们看完这个gif图，也可以思考下如何实现的。根据表现，可以猜测是<code>next</code>是一个函数，而且返回的可能是一个<code>promise</code>，被<code>await</code>调用。</p><p>看到这个<code>gif</code>图，我把之前写的<code>examples/koa-compose</code>的调试方法<strong>含泪删除</strong>了。默默写上<code>gif</code>图上的这些代码，想着这个读者们更容易读懂。 我把这段代码写在这里 <a href="https://github.com/lxchuan12/koa-analysis/blob/master/koa/examples/middleware/app.js" rel="noopener noreferrer"><code>koa/examples/middleware/app.js</code> </a>便于调试。</p><p>在项目路径下配置新建<a href="https://github.com/lxchuan12/koa-analysis/blob/master/.vscode/launch.json" rel="noopener noreferrer">.vscode/launch.json </a>文件，<code>program</code>配置为自己写的<code>koa/examples/middleware/app.js</code>文件。</p><p>.vscode/launch.json 代码</p><pre><code class="language-json">{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "启动程序",
            "skipFiles": [
                "&lt;node_internals&gt;/**"
            ],
            "program": "${workspaceFolder}/koa/examples/middleware/app.js"
        }
    ]
}
</code></pre><p>按<code>F5键</code>开始调试，调试时先走主流程，必要的地方打上断点，不用一开始就关心细枝末节。</p><p><strong>断点调试要领：</strong><br><strong>赋值语句可以一步跳过，看返回值即可，后续详细再看。</strong><br><strong>函数执行需要断点跟着看，也可以结合注释和上下文倒推这个函数做了什么。</strong></p><p>上述比较啰嗦的写了一堆调试方法。主要是想着<code>授人予鱼不如授人予渔</code>，这样换成其他源码也会调试了。</p><p>简单说下<code>chrome</code>调试<code>nodejs</code>，<code>chrome</code>浏览器打开<code>chrome://inspect</code>，点击配置**configure...**配置<code>127.0.0.1:端口号</code>(端口号在<code>Vscode</code> 调试控制台显示了)。</p><p>更多可以查看<a href="https://nodejs.org/en/docs/inspector" rel="noopener noreferrer">English Debugging Guide</a>。</p><p><a href="https://nodejs.org/zh-cn/docs/guides/debugging-getting-started/" rel="noopener noreferrer">中文调试指南</a></p><p>喜欢看视频的读者也可以看慕课网这个视频<a href="https://www.imooc.com/learn/1093" rel="noopener noreferrer">node.js调试入门 </a>，讲得还是比较详细的。<br>不过我感觉在<code>chrome</code>调试<code>nodejs</code>项目体验不是很好（可能是我方式不对），所以我大部分具体的代码时都放在<code>html</code>文件<code>script</code>形式，在<code>chrome</code>调试了。</p><h2 id="-new-koa-app-">先看看 <code>new Koa()</code> 结果<code>app</code>是什么</h2><p>看源码我习惯性看<strong>它的实例对象结构</strong>，一般所有属性和方法都放在实例对象上了，而且会通过原型链查找形式查找最顶端的属性和方法。</p><p>用<code>koa/examples/middleware/app.js</code>文件调试时，先看下执行<code>new Koa()</code>之后，<code>app</code>是什么，有个初步印象。</p><pre><code class="language-js">// 文件 koa/examples/middleware/app.js
const Koa = require('../../lib/application');

// const Koa = require('koa');
// 这里打个断点
const app = new Koa();
// x-response-time

// 这里打个断点
app.use(async (ctx, next) =&gt; {

});
</code></pre><p>在调试控制台<code>ctrl + 反引号键（一般在</code>Tab<code>上方的按键）唤起</code>，输入<code>app</code>，按<code>enter</code>键打印<code>app</code>。会有一张这样的图。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/02/image-3.png" class="kg-image" alt="image-3" width="600" height="400" loading="lazy"></figure><p><code>VScode</code>也有一个代码调试神器插件<a href="https://marketplace.visualstudio.com/items?itemName=hediet.debug-visualizer" rel="noopener noreferrer"><code>Debug Visualizer</code></a>。</p><p>安装好后插件后，按<code>ctrl + shift + p</code>，输入<code>Open a new Debug Visualizer View</code>，来使用，输入<code>app</code>，显示是这样的。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/02/image-4.png" class="kg-image" alt="image-4" width="600" height="400" loading="lazy"></figure><p>不过目前体验来看，相对还比较鸡肋，只能显示一级，而且只能显示对象，相信以后会更好。更多玩法可以查看它的文档。</p><p>我把koa实例对象比较完整的用<code>xmind</code>画出来了，大概看看就好，有个初步印象。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/02/image-5.png" class="kg-image" alt="image-5" width="600" height="400" loading="lazy"></figure><p>接着，我们可以看下<code>app 实例、context、request、request</code>的官方文档。</p><h3 id="app-context-request-request-api-">app 实例、context、request、request 官方API文档</h3><ul><li><a href="https://github.com/demopark/koa-docs-Zh-CN/blob/master/api/index.md" rel="noopener noreferrer">index API</a> | <a href="https://github.com/demopark/koa-docs-Zh-CN/blob/master/api/context.md" rel="noopener noreferrer">context API</a> | <a href="https://github.com/demopark/koa-docs-Zh-CN/blob/master/api/request.md" rel="noopener noreferrer">request API </a>| <a href="https://github.com/demopark/koa-docs-Zh-CN/blob/master/api/response.md" rel="noopener noreferrer">response API</a></li></ul><p>可以真正使用的时候再去仔细看文档。</p><h2 id="koa-">koa 主流程梳理简化</h2><p>通过<code>F5启动调试（直接跳到下一个断点处）</code>、<code>F10单步跳过</code>、<code>F11单步调试</code>等，配合重要的地方断点，调试完整体代码，其实比较容易整理出如下主流程的代码。</p><pre><code class="language-js">class Emitter{
  // node 内置模块
  constructor(){
  }
}
class Koa extends Emitter{
  constructor(options){
    super();
    options = options || {};
    this.middleware = [];
    this.context = {
      method: 'GET',
      url: '/url',
      body: undefined,
      set: function(key, val){
        console.log('context.set', key, val);
      },
    };
  }
  use(fn){
    this.middleware.push(fn);
    return this;
  }
  listen(){
    const  fnMiddleware = compose(this.middleware);
    const ctx = this.context;
    const handleResponse = () =&gt; respond(ctx);
    const onerror = function(){
      console.log('onerror');
    };
    fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
}
function respond(ctx){
  console.log('handleResponse');
  console.log('response.end', ctx.body);
}
</code></pre><p>重点就在<code>listen</code>函数里的<code>compose</code>这个函数，接下来我们就详细来<strong>欣赏</strong>下这个函数。</p><h2 id="koa-compose-">koa-compose 源码（洋葱模型实现）</h2><p>通过<code>app.use()</code> 添加了若干函数，但是要把它们串起来执行呀。像上文的<code>gif</code>图一样。</p><p><code>compose</code>函数，传入一个数组，返回一个函数。对入参是不是数组和校验数组每一项是不是函数。</p><pre><code class="language-js">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!')
  }

 //  传入对象 context 返回Promise
  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>koa-compose</code>代码写在了一个文件中。<a href="https://github.com/lxchuan12/koa-analysis/blob/master/koa/examples/simpleKoa/koa-compose.js" rel="noopener noreferrer">koa/examples/simpleKoa/koa-compose.js</a></p><pre><code class="language-bash">hs koa/examples/
# 然后可以打开localhost:8080/simpleKoa，开心的把代码调试起来
</code></pre><p>不过这样好像还是有点麻烦，我还把这些代码放在<a href="https://codepen.io/lxchuan12/pen/wvarPEb" rel="noopener noreferrer"><code>codepen</code> https://codepen.io/lxchuan12/pen/wvarPEb</a> 中，<strong>直接可以在线调试啦</strong>。是不是觉得很贴心^_^，自己多调试几遍便于消化理解。</p><p>你会发现<code>compose</code>就是类似这样的结构（移除一些判断）。</p><pre><code class="language-js">// 这样就可能更好理解了。
// simpleKoaCompose
const [fn1, fn2, fn3] = this.middleware;
const fnMiddleware = function(context){
    return Promise.resolve(
      fn1(context, function next(){
        return Promise.resolve(
          fn2(context, function next(){
              return Promise.resolve(
                  fn3(context, function next(){
                    return Promise.resolve();
                  })
              )
          })
        )
    })
  );
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);
</code></pre><p>也就是说<code>koa-compose</code>返回的是一个<code>Promise</code>，<code>Promise</code>中取出第一个函数（<code>app.use</code>添加的中间件），传入<code>context</code>和第一个<code>next</code>函数来执行。<br>第一个<code>next</code>函数里也是返回的是一个<code>Promise</code>，<code>Promise</code>中取出第二个函数（<code>app.use</code>添加的中间件），传入<code>context</code>和第二个<code>next</code>函数来执行。<br>第二个<code>next</code>函数里也是返回的是一个<code>Promise</code>，<code>Promise</code>中取出第三个函数（<code>app.use</code>添加的中间件），传入<code>context</code>和第三个<code>next</code>函数来执行。<br>第三个...<br>以此类推。最后一个中间件中有调用<code>next</code>函数，则返回<code>Promise.resolve</code>。如果没有，则不执行<code>next</code>函数。 这样就把所有中间件串联起来了。这也就是我们常说的洋葱模型。<br></p><p><strong>不得不说非常惊艳，“玩还是大神会玩”</strong>。</p><p>这种把函数存储下来的方式，在很多源码中都有看到。比如<code>lodash</code>源码的惰性求值，<code>vuex</code>也是把<code>action</code>等函数存储下，最后才去调用。</p><p>搞懂了<code>koa-compose</code> 洋葱模型实现的代码，其他代码就不在话下了。</p><h2 id="--2">错误处理</h2><p><a href="https://github.com/demopark/koa-docs-Zh-CN/blob/master/error-handling.md" rel="noopener noreferrer">中文文档 错误处理</a></p><p>仔细看文档，文档中写了三种捕获错误的方式。</p><ul><li><code>ctx.onerror</code> 中间件中的错误捕获</li><li><code>app.on('error', (err) =&gt; {})</code> 最外层实例事件监听形式 也可以看看例子<a href="https://github.com/koajs/examples/blob/master/errors/app.js" rel="noopener noreferrer">koajs/examples/errors/app.js 文件</a></li><li><code>app.onerror = (err) =&gt; {}</code> 重写<code>onerror</code>自定义形式 也可以看<a href="https://github.com/lxchuan12/koa-analysis/blob/master/koa/test/context/onerror.js" rel="noopener noreferrer">测试用例 onerror</a></li></ul><pre><code class="language-js">// application.js 文件
class Application extends Emitter {
  // 代码有简化组合
  listen(){
    const  fnMiddleware = compose(this.middleware);
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    const onerror = err =&gt; ctx.onerror(err);
    fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
  onerror(err) {
    // 代码省略
    // ...
  }
}
</code></pre><p><strong>ctx.onerror</strong></p><p><code>lib/context.js</code>文件中，有一个函数<code>onerror</code>，而且有这么一行代码<code>this.app.emit('error', err, this)</code>。</p><pre><code class="language-js">module.exports = {
  onerror(){
    // delegate
    // app 是在new Koa() 实例
    this.app.emit('error', err, this);
  }
}
</code></pre><pre><code class="language-js">app.use(async (ctx, next) =&gt; {
  try {
    await next();
  } catch (err) {
    err.status = err.statusCode || err.status || 500;
    throw err;
  }
});
</code></pre><p><code>try catch</code> 错误或被<code>fnMiddleware(ctx).then(handleResponse).catch(onerror);</code>，这里的<code>onerror</code>是<code>ctx.onerror</code><br>而<code>ctx.onerror</code>函数中又调用了<code>this.app.emit('error', err, this)</code>，所以在最外围<code>app.on('error'，err =&gt; {})</code>可以捕获中间件链中的错误。 因为<code>koa</code>继承自<code>events模块</code>，所以有'emit'和<code>on</code>等方法）</p><h2 id="koa2-koa1-">koa2 和 koa1 的简单对比</h2><p><a href="https://github.com/demopark/koa-docs-Zh-CN/blob/master/migration.md" rel="noopener noreferrer">中文文档中描述了 koa2 和 koa1 的区别</a></p><p><code>koa1</code>中主要是<code>generator</code>函数。<code>koa2</code>中会自动转换<code>generator</code>函数。</p><pre><code class="language-js">// Koa 将转换
app.use(function *(next) {
  const start = Date.now();
  yield next;
  const ms = Date.now() - start;
  console.log(`${this.method} ${this.url} - ${ms}ms`);
});
</code></pre><h3 id="koa-convert-">koa-convert 源码</h3><p>在<code>vscode/launch.json</code>文件，找到这个<code>program</code>字段，修改为<code>"program": "${workspaceFolder}/koa/examples/koa-convert/app.js"</code>。</p><p>通过<code>F5启动调试（直接跳到下一个断点处）</code>、<code>F10单步跳过</code>、<code>F11单步调试</code>调试走一遍流程。重要地方断点调试。</p><p><code>app.use</code>时有一层判断，是否是<code>generator</code>函数，如果是则用<code>koa-convert</code>暴露的方法<code>convert</code>来转换重新赋值，再存入<code>middleware</code>，后续再使用。</p><pre><code class="language-js">class Koa extends Emitter{
  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }
}
</code></pre><p><code>koa-convert</code>源码挺多，核心代码其实是这样的。</p><pre><code class="language-js">function convert(){
 return function (ctx, next) {
    return co.call(ctx, mw.call(ctx, createGenerator(next)))
  }
  function * createGenerator (next) {
    return yield next()
  }
}
</code></pre><p>最后还是通过<code>co</code>来转换的。所以接下来看<code>co</code>的源码。</p><h3 id="co-">co 源码</h3><p><a href="https://github.com/tj/co" rel="noopener noreferrer">tj大神写的co 仓库</a></p><p>本小节的示例代码都在这个文件夹<a href="https://github.com/lxchuan12/koa-analysis/tree/master/koa/examples/co-generator" rel="noopener noreferrer"><code>koa/examples/co-generator</code> </a>中，<code>hs koa/example</code>，可以自行打开<code>https://localhost:8080/co-generator</code>调试查看。</p><p>看<code>co</code>源码前，先看几段简单代码。</p><pre><code class="language-js">// 写一个请求简版请求
function request(ms= 1000) {
  return new Promise((resolve) =&gt; {
    setTimeout(() =&gt; {
      resolve({name: '若川'});
    }, ms);
  });
}
</code></pre><pre><code class="language-js">// 获取generator的值
function* generatorFunc(){
  const res = yield request();
  console.log(res, 'generatorFunc-res');
}
generatorFunc(); // 报告，我不会输出你想要的结果的
</code></pre><p>简单来说<code>co</code>，就是把<code>generator</code>自动执行，再返回一个<code>promise</code>。 <strong><code>generator</code>函数这玩意它不自动执行呀，还要一步步调用<code>next()</code>，也就是叫它走一步才走一步</strong>。</p><p>所以有了<code>async、await</code>函数。</p><pre><code class="language-js">// await 函数 自动执行
async function asyncFunc(){
    const res = await request();
    console.log(res, 'asyncFunc-res await 函数 自动执行');
}
asyncFunc(); // 输出结果
</code></pre><p>也就是说<code>co</code>需要做的事情，是让<code>generator</code>向<code>async、await</code>函数一样自动执行。</p><h3 id="-co-">模拟实现简版 co（第一版）</h3><p>这时，我们来模拟实现第一版的<code>co</code>。根据<code>generator</code>的特性，其实容易写出如下代码。</p><pre><code class="language-js">// 获取generator的值
function* generatorFunc(){
  const res = yield request();
  console.log(res, 'generatorFunc-res');
}

function coSimple(gen){
  gen = gen();
  console.log(gen, 'gen');

  const ret = gen.next();
  const promise = ret.value;
  promise.then(res =&gt; {
    gen.next(res);
  });
}
coSimple(generatorFunc);
// 输出了想要的结果
// {name: "若川"}"generatorFunc-res"
</code></pre><h3 id="-co--1">模拟实现简版 co（第二版）</h3><p>但是实际上，不会上面那么简单的。有可能是多个<code>yield</code>和传参数的情况。 传参可以通过这如下两行代码来解决。</p><pre><code class="language-js">const args = Array.prototype.slice.call(arguments, 1);
gen = gen.apply(ctx, args);
</code></pre><p>两个<code>yield</code>，我大不了重新调用一下<code>promise.then</code>，搞定。</p><pre><code class="language-js">// 多个yeild，传参情况
function* generatorFunc(suffix = ''){
  const res = yield request();
  console.log(res, 'generatorFunc-res' + suffix);

  const res2 = yield request();
  console.log(res2, 'generatorFunc-res-2' + suffix);
}

function coSimple(gen){
  const ctx = this;
  const args = Array.prototype.slice.call(arguments, 1);
  gen = gen.apply(ctx, args);
  console.log(gen, 'gen');

  const ret = gen.next();
  const promise = ret.value;
  promise.then(res =&gt; {
    const ret = gen.next(res);
    const promise = ret.value;
      promise.then(res =&gt; {
        gen.next(res);
      });
  });
}

coSimple(generatorFunc, ' 哎呀，我真的是后缀');
</code></pre><h3 id="-co--2">模拟实现简版 co（第三版）</h3><p>问题是肯定不止两次，无限次的<code>yield</code>的呢，这时肯定要把重复的封装起来。而且返回是<code>promise</code>，这就实现了如下版本的代码。</p><pre><code class="language-js">function* generatorFunc(suffix = ''){
  const res = yield request();
  console.log(res, 'generatorFunc-res' + suffix);

  const res2 = yield request();
  console.log(res2, 'generatorFunc-res-2' + suffix);

  const res3 = yield request();
  console.log(res3, 'generatorFunc-res-3' + suffix);

  const res4 = yield request();
  console.log(res4, 'generatorFunc-res-4' + suffix);
}

function coSimple(gen){
  const ctx = this;
  const args = Array.prototype.slice.call(arguments, 1);
  gen = gen.apply(ctx, args);
  console.log(gen, 'gen');

  return new Promise((resolve, reject) =&gt; {

    onFulfilled();

    function onFulfilled(res){
      const ret = gen.next(res);
      next(ret);
    }

    function next(ret) {
      const promise = ret.value;
      promise &amp;&amp; promise.then(onFulfilled);
    }

  });
}

coSimple(generatorFunc, ' 哎呀，我真的是后缀');
</code></pre><p>但第三版的模拟实现简版<code>co</code>中，还没有考虑报错和一些参数合法的情况。</p><h3 id="-co--3">最终来看下<code>co</code>源码</h3><p>这时来看看<code>co</code>的源码，报错和错误的情况，错误时调用<code>reject</code>，是不是就好理解了一些呢。</p><pre><code class="language-js">function co(gen) {
  var ctx = this;
  var args = slice.call(arguments, 1)

  // we wrap everything in a promise to avoid promise chaining,
  // which leads to memory leak errors.
  // see https://github.com/tj/co/issues/180
  return new Promise(function(resolve, reject) {
    // 把参数传递给gen函数并执行
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    // 如果不是函数 直接返回
    if (!gen || typeof gen.next !== 'function') return resolve(gen);

    onFulfilled();

    /**
     * @param {Mixed} res
     * @return {Promise}
     * @api private
     */

    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    /**
     * @param {Error} err
     * @return {Promise}
     * @api private
     */

    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    /**
     * Get the next value in the generator,
     * return a promise.
     *
     * @param {Object} ret
     * @return {Promise}
     * @api private
     */

    // 反复执行调用自己
    function next(ret) {
      // 检查当前是否为 Generator 函数的最后一步，如果是就返回
      if (ret.done) return resolve(ret.value);
      // 确保返回值是promise对象。
      var value = toPromise.call(ctx, ret.value);
      // 使用 then 方法，为返回值加上回调函数，然后通过 onFulfilled 函数再次调用 next 函数。
      if (value &amp;&amp; isPromise(value)) return value.then(onFulfilled, onRejected);
      // 在参数不符合要求的情况下（参数非 Thunk 函数和 Promise 对象），将 Promise 对象的状态改为 rejected，从而终止执行。
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
    }
  });
}
</code></pre><h2 id="koa-express-">koa 和 express 简单对比</h2><p><a href="https://github.com/demopark/koa-docs-Zh-CN/blob/master/koa-vs-express.md" rel="noopener noreferrer">中文文档 koa 和 express 对比</a></p><p>文档里写的挺全面的。简单来说<code>koa2</code>语法更先进，更容易深度定制（<code>egg.js</code>、<code>think.js</code>、底层框架都是<code>koa</code>）。</p><h2 id="--3">总结</h2><p>文章通过<code>授人予鱼不如授人予鱼</code>的方式，告知如何调试源码，看完了<code>koa-compose</code>洋葱模型实现，<code>koa-convert</code>和<code>co</code>等源码。</p><p><code>koa-compose</code>是将<code>app.use</code>添加到<code>middleware</code>数组中的中间件（函数），通过使用<code>Promise</code>串联起来，<code>next()</code>返回的是一个<code>promise</code>。</p><p><code>koa-convert</code> 判断<code>app.use</code>传入的函数是否是<code>generator</code>函数，如果是则用<code>koa-convert</code>来转换，最终还是调用的<code>co</code>来转换。</p><p><code>co</code>源码实现原理：其实就是通过不断的调用<code>generator</code>函数的<code>next()</code>函数，来达到自动执行<code>generator</code>函数的效果（类似<code>async、await函数的自动自行</code>）。</p><p><code>koa</code>框架总结：主要就是四个核心概念，洋葱模型（把中间件串联起来），<code>http</code>请求上下文（<code>context</code>）、<code>http</code>请求对象、<code>http</code>响应对象。</p><pre><code class="language-bash">git clone https://github.com/lxchuan12/koa-analysis.git
</code></pre><p>再强烈建议下按照<strong>本文阅读最佳方式</strong>，克隆代码下来，<strong>动手调试代码学习更加深刻</strong>。</p><p>如果读者发现有不妥或可改善之处，再或者哪里没写明白的地方，欢迎评论指出，也欢迎加我微信交流<code>ruochuan12</code>。另外觉得写得不错，对你有些许帮助，可以评论、转发分享，也是对笔者的一种支持，万分感谢。</p><h3 id="--4">解答下开头的提问</h3><p>仅供参考</p><p>1、<code>koa</code>洋葱模型怎么实现的。</p><p>可以参考上文整理的简版<code>koa-compose</code>作答。</p><pre><code class="language-js">// 这样就可能更好理解了。
// simpleKoaCompose
const [fn1, fn2, fn3] = this.middleware;
const fnMiddleware = function(context){
    return Promise.resolve(
      fn1(context, function next(){
        return Promise.resolve(
          fn2(context, function next(){
              return Promise.resolve(
                  fn3(context, function next(){
                    return Promise.resolve();
                  })
              )
          })
        )
    })
  );
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);
</code></pre><p>答：app.use() 把中间件函数存储在<code>middleware</code>数组中，最终会调用<code>koa-compose</code>导出的函数<code>compose</code>返回一个<code>promise</code>，中间函数的第一个参数<code>ctx</code>是包含响应和请求的一个对象，会不断传递给下一个中间件。<code>next</code>是一个函数，返回的是一个<code>promise</code>。</p><p>2、如果中间件中的<code>next()</code>方法报错了怎么办。</p><p>可参考上文整理的错误处理作答。</p><pre><code class="language-js">ctx.onerror = function {
  this.app.emit('error', err, this);
};
  listen(){
    const  fnMiddleware = compose(this.middleware);
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    const onerror = err =&gt; ctx.onerror(err);
    fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
  onerror(err) {
    // 代码省略
    // ...
  }
</code></pre><p>答：中间件链错误会由<code>ctx.onerror</code>捕获，该函数中会调用<code>this.app.emit('error', err, this)</code>（因为<code>koa</code>继承自<code>events模块</code>，所以有'emit'和<code>on</code>等方法），可以使用<code>app.on('error', (err) =&gt; {})</code>，或者<code>app.onerror = (err) =&gt; {}</code>进行捕获。</p><p>3、<code>co</code>的原理是怎样的。<br>答：<code>co</code>的原理是通过不断调用<code>generator</code>函数的<code>next</code>方法来达到自动执行<code>generator</code>函数的，类似<code>async、await</code>函数自动执行。</p><p>答完，面试官可能觉得小伙子还是蛮懂<code>koa</code>的啊。当然也可能继续追问，直到答不出...</p><h3 id="--5">还能做些什么 ？</h3><p>学完了整体流程，<code>koa-compose</code>、<code>koa-convert</code>和<code>co</code>的源码。</p><p>还能仔细看看看<code>http</code>请求上下文（<code>context</code>）、<code>http</code>请求对象、<code>http</code>响应对象的具体实现。</p><p>还能根据我文章说的调试方式调试<a href="https://github.com/koajs" rel="noopener noreferrer">koa 组织</a>中的各种中间件，比如<code>koa-bodyparser</code>, <code>koa-router</code>，<code>koa-jwt</code>，<code>koa-session</code>、<code>koa-cors</code>等等。</p><p>还能把<a href="https://github.com/koajs/examples" rel="noopener noreferrer"><code>examples</code>仓库</a>克隆下来，我的这个仓库已经克隆了，挨个调试学习下源码。</p><p><code>web</code>框架有很多，比如<code>Express.js</code>，<code>Koa.js</code>、<code>Egg.js</code>、<code>Nest.js</code>、<code>Next.js</code>、<code>Fastify.js</code>、<code>Hapi.js</code>、<code>Restify.js</code>、<code>Loopback.io</code>、<code>Sails.js</code>、<code>Midway.js</code>等等。</p><p>还能把这些框架的优势劣势、设计思想等学习下。</p><p>还能继续学习<code>HTTP</code>协议、<code>TCP/IP</code>协议网络相关，虽然不属于<code>koa</code>的知识，但需深入学习掌握。</p><p>学无止境~~~</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Redux 源码整体架构 ]]>
                </title>
                <description>
                    <![CDATA[ 1. 本文阅读最佳方式 把我的redux源码仓库 git clone https://github.com/lxchuan12/redux-analysis.git克隆下来，顺便 star一下我的redux源码学习仓库 (opens new window) [https://github.com/lxchuan12/redux-analysis]^_^。跟着文章节奏调试和示例代码调试，用chrome 动手调试印象更加深刻。文章长段代码不用细看，可以调试时再细看。看这类源码文章百遍，可能不如自己多调试几遍。也欢迎加我微信交流ruochuan12。 2. git subtree 管理子仓库 写了很多源码文章，vuex、axios、koa等都是使用新的仓库克隆一份源码在自己仓库中。 虽然电脑可以拉取最新代码，看到原作者的git信息。但上传到 github后。读者却看不到原仓库作者的git信息了。于是我找到了git submodules 方案，但并不是很适合。再后来发现了git subtree。 简单说下 npm package和git subtree的区别。 npm package是单向 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/learn-the-overall-architecture-of-redux-source-code/</link>
                <guid isPermaLink="false">6016ba226183a70540156321</guid>
                
                    <category>
                        <![CDATA[ Redux ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Sat, 30 Jan 2021 09:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/01/safar-safarov-MSN8TFhJ0is-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="1-">1. 本文阅读最佳方式</h2><p>把我的<code>redux</code>源码仓库 <code>git clone https://github.com/lxchuan12/redux-analysis.git</code>克隆下来，顺便<code>star</code>一下<a href="https://github.com/lxchuan12/redux-analysis" rel="noopener noreferrer">我的redux源码学习仓库 (opens new window)</a>^_^。<strong>跟着文章节奏调试和示例代码调试，用<code>chrome</code>动手调试印象更加深刻</strong>。文章长段代码不用细看，可以调试时再细看。看这类源码文章百遍，可能不如自己多调试几遍。也欢迎加我微信交流<code>ruochuan12</code>。</p><h2 id="2-git-subtree-">2. git subtree 管理子仓库</h2><p>写了很多源码文章，<code>vuex</code>、<code>axios</code>、<code>koa</code>等都是使用新的仓库克隆一份源码在自己仓库中。 虽然电脑可以拉取最新代码，看到原作者的git信息。但上传到<code>github</code>后。读者却看不到原仓库作者的<code>git</code>信息了。于是我找到了<code>git submodules</code> 方案，但并不是很适合。再后来发现了<code>git subtree</code>。</p><p>简单说下 <code>npm package</code>和<code>git subtree</code>的区别。 <code>npm package</code>是单向的。<code>git subtree</code>则是双向的。</p><p>具体可以查看这篇文章<a href="https://segmentfault.com/a/1190000003969060" rel="noopener noreferrer">@德来（原有赞大佬）：用 Git Subtree 在多个 Git 项目间双向同步子项目，附简明使用手册(opens new window)</a></p><p>学会了<code>git subtree</code>后，我新建了<code>redux-analysis</code>项目后，把<code>redux</code>源码<code>4.x</code>（截止至2020年06月13日，<code>4.x</code>分支最新版本是<code>4.0.5</code>，<code>master</code>分支是<code>ts</code>，文章中暂不想让一些不熟悉<code>ts</code>的读者看不懂）分支克隆到了我的项目里的一个子项目，得以保留<code>git</code>信息。</p><p>对应命令则是：</p><pre><code class="language-bash">git subtree add --prefix=redux https://github.com/reduxjs/redux.git 4.x
</code></pre><h2 id="3-redux-">3. 调试 redux 源码准备工作</h2><p>之前，我在知乎回答了一个问题<a href="https://www.zhihu.com/question/350289336/answer/910970733" rel="noopener noreferrer">若川：一年内的前端看不懂前端框架源码怎么办？ (opens new window)</a>推荐了一些资料，阅读量还不错，大家有兴趣可以看看。主要有四点：<br>1.借助调试<br>2.搜索查阅相关高赞文章<br>3.把不懂的地方记录下来，查阅相关文档<br>4.总结</p><p>看源码调试很重要，所以我的每篇源码文章都详细描述（也许有人看来是比较啰嗦...）如何调试源码。</p><p><strong>断点调试要领：</strong><br><strong>赋值语句可以一步按<code>F10</code>跳过，看返回值即可，后续详细再看。</strong><br><strong>函数执行需要断点按<code>F11</code>跟着看，也可以结合注释和上下文倒推这个函数做了什么。</strong><br><strong>有些不需要细看的，直接按<code>F8</code>走向下一个断点</strong><br><strong>刷新重新调试按<code>F5</code></strong></p><p>调试源码前，先简单看看 <code>redux</code> 的工作流程，有个大概印象。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image-26.png" class="kg-image" alt="image-26" width="844" height="379" loading="lazy"></figure><h3 id="3-1-rollup-sourcemap-">3.1 rollup 生成 sourcemap 便于调试</h3><p>修改<code>rollup.config.js</code>文件，<code>output</code>输出的配置生成<code>sourcemap</code>。</p><pre><code class="language-js">// redux/rollup.config.js 有些省略
const sourcemap = {
  sourcemap: true,
};

output: {
    // ...
    ...sourcemap,
}
</code></pre><p>安装依赖</p><pre><code class="language-bash">git clone http://github.com/lxchuan12/redux-analysis.git
cd redux-analysi/redux
npm i
npm run build
# 编译结束后会生成 sourcemap .map格式的文件到 dist、es、lib 目录下。
</code></pre><p>仔细看看<code>redux/examples</code>目录和<code>redux/README</code>。</p><p>这时我在根路径下，新建文件夹<code>examples</code>，把原生<code>js</code>写的计数器<code>redux/examples/counter-vanilla/index.html</code>，复制到<code>examples/index.html</code>。同时把打包后的包含<code>sourcemap</code>的<code>redux/dist</code>目录，复制到<code>examples/dist</code>目录。</p><p>修改<code>index.html</code>的<code>script</code>的<code>redux.js</code>文件为<code>dist中的路径</code>。</p><p>为了便于区分和调试后续<code>html</code>文件，我把<code>index.html</code>重命名为<code>index.1.redux.getState.dispatch.html</code>。</p><pre><code class="language-bash"># redux-analysis 根目录
# 安装启动服务的npm包
npm i -g http-server
cd examples
hs -p 5000
</code></pre><p>就可以开心的调试啦。可以直接克隆我的项目<code>git clone http://github.com/lxchuan12/redux-analysis.git</code>。本地调试，动手实践，容易消化吸收。</p><h2 id="4-redux-">4. 通过调试计数器例子的学习 redux 源码</h2><p>接着我们来看<code>examples/index.1.redux.getState.dispatch.html</code>文件。先看<code>html</code>部分。只是写了几个 <code>button</code>，比较简单。</p><pre><code class="language-html">&lt;div&gt;
    &lt;p&gt;
    Clicked: &lt;span id="value"&gt;0&lt;/span&gt; times
    &lt;button id="increment"&gt;+&lt;/button&gt;
    &lt;button id="decrement"&gt;-&lt;/button&gt;
    &lt;button id="incrementIfOdd"&gt;Increment if odd&lt;/button&gt;
    &lt;button id="incrementAsync"&gt;Increment async&lt;/button&gt;
    &lt;/p&gt;
&lt;/div&gt;
</code></pre><p><code>js部分</code>，也比较简单。声明了一个<code>counter</code>函数，传递给<code>Redux.createStore(counter)</code>，得到结果<code>store</code>，而<code>store</code>是个对象。<code>render</code>方法渲染数字到页面。用<code>store.subscribe(render)</code>订阅的<code>render</code>方法。还有<code>store.dispatch({type: 'INCREMENT' })</code>方法，调用<code>store.dispatch</code>时会触发<code>render</code>方法。这样就实现了一个计数器。</p><pre><code class="language-js">function counter(state, action) {
    if (typeof state === 'undefined') {
        return 0
    }

    switch (action.type) {
        case 'INCREMENT':
        return state + 1
        case 'DECREMENT':
        return state - 1
        default:
        return state
    }
}

var store = Redux.createStore(counter)
var valueEl = document.getElementById('value')

function render() {
    valueEl.innerHTML = store.getState().toString()
}
render()
store.subscribe(render)

document.getElementById('increment')
.addEventListener('click', function () {
    store.dispatch({ type: 'INCREMENT' })
})

// 省略部分暂时无效代码...
</code></pre><p>思考：看了这段代码，你会在哪打断点来调试呢。</p><pre><code class="language-js">// 四处可以断点来看
// 1.
var store = Redux.createStore(counter)
// 2.
function render() {
valueEl.innerHTML = store.getState().toString()
}
render()
// 3.
store.subscribe(render)
// 4.
store.dispatch({ type: 'INCREMENT' })
</code></pre><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image-27.png" class="kg-image" alt="image-27" width="1312" height="649" loading="lazy"></figure><p>图中的右边<code>Scope</code>，有时需要关注下，会显示闭包、全局环境、当前环境等变量，还可以显示函数等具体代码位置，能帮助自己理解代码。</p><p>断点调试，按<code>F5</code>刷新页面后，按<code>F8</code>，把鼠标放在<code>Redux</code>和<code>store</code>上。</p><p>可以看到<code>Redux</code>上有好几个方法。分别是：</p><ul><li>__DO_NOT_USE__ActionTypes: {INIT: "@@redux/INITu.v.d.u.6.r", REPLACE: "@@redux/REPLACEg.u.u.7.c", PROBE_UNKNOWN_ACTION: ƒ}</li><li>applyMiddleware: ƒ applyMiddleware() 函数是一个增强器，组合多个中间件，最终增强<code>store.dispatch</code>函数，<code>dispatch</code>时，可以串联执行所有中间件。</li><li>bindActionCreators: ƒ bindActionCreators(actionCreators, dispatch) 生成actions，主要用于其他库，比如<code>react-redux</code>。</li><li>combineReducers: ƒ combineReducers(reducers) 组合多个<code>reducers</code>，返回一个总的<code>reducer</code>函数。</li><li>compose: ƒ compose() 组合多个函数，从右到左，比如：compose(f, g, h) 最终得到这个结果 (...args) =&gt; f(g(h(...args))).</li><li>createStore: ƒ createStore(reducer, preloadedState, enhancer) 生成 <code>store</code> 对象</li></ul><p>再看<code>store</code>也有几个方法。分别是：</p><ul><li>dispatch: ƒ dispatch(action) 派发动作，也就是把<code>subscribe</code>收集的函数，依次遍历执行</li><li>subscribe: ƒ subscribe(listener) 订阅收集函数存在数组中，等待触发<code>dispatch</code>依次执行。返回一个取消订阅的函数，可以取消订阅监听。</li><li>getState: ƒ getState() 获取存在<code>createStore</code>函数内部闭包的对象。</li><li>replaceReducer: ƒ replaceReducer(nextReducer) 主要用于<code>redux</code>开发者工具，对比当前和上一次操作的异同。有点类似时间穿梭功能。</li><li>Symbol(observable): ƒ observable()</li></ul><p>也就是<a href="https://redux.org.js/" rel="noopener noreferrer">官方文档redux.org.js (opens new window)</a>上的 <code>API</code>。</p><p>暂时不去深究每一个<code>API</code>的实现。重新按<code>F5</code>刷新页面，断点到<code>var store = Redux.createStore(counter)</code>。一直按<code>F11</code>，先走一遍主流程。</p><h3 id="4-1-redux-createsotre">4.1 Redux.createSotre</h3><p><code>createStore</code> 函数结构是这样的，是不是看起来很简单，最终返回对象<code>store</code>，包含<code>dispatch</code>、<code>subscribe</code>、<code>getState</code>、<code>replaceReducer</code>等方法。</p><pre><code class="language-js">// 省略了若干代码
export default function createStore(reducer, preloadedState, enhancer) {
    // 省略参数校验和替换
    // 当前的 reducer 函数
    let currentReducer = reducer
    // 当前state
    let currentState = preloadedState
    // 当前的监听数组函数
    let currentListeners = []
    // 下一个监听数组函数
    let nextListeners = currentListeners
    // 是否正在dispatch中
    let isDispatching = false
    function ensureCanMutateNextListeners() {
        if (nextListeners === currentListeners) {
        nextListeners = currentListeners.slice()
        }
    }
    function getState() {
        return currentState
    }
    function subscribe(listener) {}
    function dispatch(action) {}
    function replaceReducer(nextReducer) {}
    function observable() {}
    // ActionTypes.INIT @@redux/INITu.v.d.u.6.r
    dispatch({ type: ActionTypes.INIT })
    return {
        dispatch,
        subscribe,
        getState,
        replaceReducer,
        [$$observable]: observable
    }
}
</code></pre><h3 id="4-2-store-dispatch-action-">4.2 store.dispatch(action)</h3><pre><code class="language-js">function dispatch(action) {
    // 判断action是否是对象，不是则报错
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }
    // 判断action.type 是否存在，没有则报错
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }
    // 不是则报错
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
        // 调用完后置为 false
      isDispatching = false
    }
    //  把 收集的函数拿出来依次调用
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i &lt; listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
    // 最终返回 action
    return action
  }
</code></pre><pre><code class="language-js">var store = Redux.createStore(counter)
</code></pre><p>上文调试完了这句。</p><p>继续按<code>F11</code>调试。</p><pre><code class="language-js">function render() {
    valueEl.innerHTML = store.getState().toString()
}
render()
</code></pre><h3 id="4-3-store-getstate-">4.3 store.getState()</h3><p><code>getState</code>函数实现比较简单。</p><pre><code class="language-js">function getState() {
    // 判断正在dispatch中，则报错
    if (isDispatching) {
        throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
            'The reducer has already received the state as an argument. ' +
            'Pass it down from the top reducer instead of reading it from the store.'
        )
    }
    // 返回当前的state
    return currentState
}
</code></pre><h3 id="4-4-store-subscribe-listener-">4.4 store.subscribe(listener)</h3><p>订阅监听函数，存放在数组中，<code>store.dispatch(action)</code>时遍历执行。</p><pre><code class="language-js">function subscribe(listener) {
    // 订阅参数校验不是函数报错
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }
    // 正在dispatch中，报错
    if (isDispatching) {
      throw new Error(
        'You may not call store.subscribe() while the reducer is executing. ' +
          'If you would like to be notified after the store has been updated, subscribe from a ' +
          'component and invoke store.getState() in the callback to access the latest state. ' +
          'See https://redux.js.org/api-reference/store#subscribelistener for more details.'
      )
    }
    // 订阅为 true
    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    // 返回一个取消订阅的函数
    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }
      // 正在dispatch中，则报错
      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See https://redux.js.org/api-reference/store#subscribelistener for more details.'
        )
      }
      // 订阅为 false
      isSubscribed = false

      ensureCanMutateNextListeners()
    //   找到当前监听函数
      const index = nextListeners.indexOf(listener)
    //   在数组中删除
      nextListeners.splice(index, 1)
      currentListeners = null
    }
  }
</code></pre><p>到这里，我们就调试学习完了<code>Redux.createSotre</code>、<code>store.dispatch</code>、<code>store.getState</code>、<code>store.subscribe</code>的源码。</p><p>接下来，我们写个中间件例子，来调试中间件相关源码。</p><h2 id="5-redux-">5. Redux 中间件相关源码</h2><p>中间件是重点，面试官也经常问这类问题。</p><h3 id="5-1-redux-applymiddleware-middlewares-">5.1 Redux.applyMiddleware(...middlewares)</h3><h4 id="5-1-1-logger-">5.1.1 准备 logger 例子调试</h4><p>为了调试<code>Redux.applyMiddleware(...middlewares)</code>，我在<code>examples/js/middlewares.logger.example.js</code>写一个简单的<code>logger</code>例子。分别有三个<code>logger1</code>，<code>logger2</code>，<code>logger3</code>函数。由于都是类似，所以我在这里只展示<code>logger1</code>函数。</p><pre><code class="language-js">// examples/js/middlewares.logger.example.js
function logger1({ getState }) {
  return next =&gt; action =&gt; {
      console.log('will dispatch--1--next, action:', next, action)

      // Call the next dispatch method in the middleware chain.
      const returnValue = next(action)

      console.log('state after dispatch--1', getState())

      // This will likely be the action itself, unless
      // a middleware further in chain changed it.
      return returnValue
  }
}
// 省略 logger2、logger3
</code></pre><p><code>logger</code>中间件函数做的事情也比较简单，返回两层函数，<code>next</code>就是下一个中间件函数，调用返回结果。为了让读者能看懂，我把<code>logger1</code>用箭头函数、<code>logger2</code>则用普通函数。</p><p><code>写好例子后</code>，我们接着来看怎么调试<code>Redux.applyMiddleware(...middlewares))</code>源码。</p><pre><code class="language-bash">cd redux-analysis &amp;&amp; hs -p 5000
# 上文说过npm i -g http-server
</code></pre><p>打开<code>http://localhost:5000/examples/index.2.redux.applyMiddleware.compose.html</code>，按<code>F12</code>打开控制台，</p><p>先点击加号操作+1，把结果展示出来。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image-28.png" class="kg-image" alt="image-28" width="1316" height="605" loading="lazy"></figure><p>从图中可以看出，<code>next</code>则是下一个函数。先1-2-3，再3-2-1这样的顺序。</p><p>这种也就是我们常说的中间件，面向切面编程（AOP）。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image-29.png" class="kg-image" alt="image-29" width="470" height="411" loading="lazy"></figure><p>接下来调试，在以下语句打上断点和一些你觉得重要的地方打上断点。</p><pre><code class="language-js">// examples/index.2.redux.applyMiddleware.compose.html
var store = Redux.createStore(counter, Redux.applyMiddleware(logger1, logger2,  logger3))
</code></pre><h4 id="5-1-2-redux-applymiddleware-middlewares-">5.1.2 Redux.applyMiddleware(...middlewares) 源码</h4><pre><code class="language-js">// redux/src/applyMiddleware.js
/**
 * ...
 * @param {...Function} middlewares The middleware chain to be applied.
 * @returns {Function} A store enhancer applying the middleware.
 */
export default function applyMiddleware(...middlewares) {
  return createStore =&gt; (...args) =&gt; {
    const store = createStore(...args)
    let dispatch = () =&gt; {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) =&gt; dispatch(...args)
    }
    const chain = middlewares.map(middleware =&gt; middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}
</code></pre><pre><code class="language-js">// redux/src/createStore.js
export default function createStore(reducer, preloadedState, enhancer) {
  // 省略参数校验
  // 如果第二个参数`preloadedState`是函数，并且第三个参数`enhancer`是undefined，把它们互换一下。
  if (typeof preloadedState === 'function' &amp;&amp; typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    // enhancer 也就是`Redux.applyMiddleware`返回的函数
    // createStore 的 args 则是 `reducer, preloadedState`
    /**
     * createStore =&gt; (...args) =&gt; {
            const store = createStore(...args)
            return {
              ...store,
               dispatch,
            }
        }
     ** /
    // 最终返回增强的store对象。
    return enhancer(createStore)(reducer, preloadedState)
  }
  // 省略后续代码
}
</code></pre><p>把接收的中间件函数<code>logger1</code>, <code>logger2</code>, <code>logger3</code>放入到 了<code>middlewares</code>数组中。<code>Redux.applyMiddleware</code>最后返回两层函数。 把中间件函数都混入了参数<code>getState</code>和<code>dispatch</code>。</p><pre><code class="language-js">// examples/index.2.redux.applyMiddleware.compose.html
var store = Redux.createStore(counter, Redux.applyMiddleware(logger1, logger2,  logger3))
</code></pre><p>最后这句其实是返回一个增强了<code>dispatch</code>的<code>store</code>对象。</p><p>而增强的<code>dispatch</code>函数，则是用<code>Redux.compose(...functions)</code>进行串联起来执行的。</p><h3 id="5-2-redux-compose-functions-">5.2 Redux.compose(...functions)</h3><pre><code class="language-js">export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg =&gt; arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) =&gt; (...args) =&gt; a(b(...args)))
}
</code></pre><pre><code class="language-js">// applyMiddleware.js
dispatch = compose(...chain)(store.dispatch)
// compose
funcs.reduce((a, b) =&gt; (...args) =&gt; a(b(...args)))
</code></pre><p>这两句可能不是那么好理解，可以断点多调试几次。我把箭头函数转换成普通函数。</p><pre><code class="language-js">funcs.reduce(function(a, b){
  return function(...args){
    return a(b(...args));
  };
});
</code></pre><p>其实<code>redux</code>源码中注释很清晰了，这个<code>compose</code>函数上方有一堆注释，其中有一句：组合多个函数，从右到左，比如：<code>compose(f, g, h)</code> 最终得到这个结果 <code>(...args) =&gt; f(g(h(...args)))</code>.</p><h4 id="5-2-1-compose-">5.2.1 compose 函数演化</h4><p>看<code>Redux.compose(...functions)</code>函数源码后，还是不明白，不要急不要慌，吃完鸡蛋还有汤。仔细来看如何演化而来，先来简单看下如下需求。</p><p>传入一个数值，计算数值乘以10再加上10，再减去2。</p><p>实现起来很简单。</p><pre><code class="language-js">const calc = (num) =&gt; num * 10 + 10 - 2;
calc(10); // 108
</code></pre><p>但这样写有个问题，不好扩展，比如我想乘以<code>10</code>时就打印出结果。 为了便于扩展，我们分开写成三个函数。</p><pre><code class="language-js">const multiply = (x) =&gt; {
   const result = x * 10;
   console.log(result);
   return result;
};
const add = (y) =&gt; y + 10;
const minus = (z) =&gt; z - 2;

// 计算结果
console.log(minus(add(multiply(10))));
// 100
// 108
// 这样我们就把三个函数计算结果出来了。
</code></pre><p>再来实现一个相对通用的函数，计算这三个函数的结果。</p><pre><code class="language-js">const compose = (f, g, h) =&gt; {
  return function(x){
    return f(g(h(x)));
  }
}
const calc = compose(minus, add, multiply);
console.log(calc(10));
// 100
// 108
</code></pre><p>这样还是有问题，只支持三个函数。我想支持多个函数。 我们了解到数组的<code>reduce</code>方法就能实现这样的功能。 前一个函数</p><pre><code class="language-js">// 我们常用reduce来计算数值数组的总和
[1,2,3,4,5].reduce((pre, item, index, arr) =&gt; {
  console.log('(pre, item, index, arr)', pre, item, index, arr);
  // (pre, item, index, arr) 1 2 1 (5) [1, 2, 3, 4, 5]
  // (pre, item, index, arr) 3 3 2 (5) [1, 2, 3, 4, 5]
  // (pre, item, index, arr) 6 4 3 (5) [1, 2, 3, 4, 5]
  // (pre, item, index, arr) 10 5 4 (5) [1, 2, 3, 4, 5]
  return pre + item;
});
// 15
</code></pre><p><code>pre</code> 是上一次返回值，在这里是数值<code>1,3,6,10</code>。在下一个例子中则是匿名函数。</p><pre><code class="language-js">function(x){
  return a(b(x));
}
</code></pre><p><code>item</code>是<code>2,3,4,5</code>，在下一个例子中是<code>minus、add、multiply</code>。</p><pre><code class="language-js">const compose = (...funcs) =&gt; {
  return funcs.reduce((a, b) =&gt; {
    return function(x){
      return a(b(x));
    }
  })
}
const calc = compose(minus, add, multiply);
console.log(calc(10));
// 100
// 108
</code></pre><p>而<code>Redux.compose(...functions)</code>其实就是这样，只不过中间件是返回双层函数罢了。</p><p>所以返回的是<code>next函数</code>，他们串起来执行了，形成了中间件的洋葱模型。 人们都说一图胜千言。我画了一个相对简单的<code>redux</code>中间件原理图。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image-30.png" class="kg-image" alt="image-30" width="1286" height="731" loading="lazy"></figure><p>如果还不是很明白，建议按照我给出的例子，多调试。</p><pre><code class="language-bash">cd redux-analysis &amp;&amp; hs -p 5000
# 上文说过npm i -g http-server
</code></pre><p>打开<code>http://localhost:5000/examples/index.3.html</code>，按<code>F12</code>打开控制台调试。</p><h4 id="5-2-2-compose-">5.2.2 前端框架的 compose 函数的实现</h4><p><strong>lodash</strong>源码中 <code>compose</code>函数的实现，也是类似于数组的<code>reduce</code>，只不过是内部实现的<code>arrayReduce</code></p><p><a href="https://juejin.im/post/5d767e1d6fb9a06b032025ea#heading-20" rel="noopener noreferrer">引用自我的文章：学习lodash源码整体架构(opens new window)</a></p><pre><code class="language-js">// lodash源码
function baseWrapperValue(value, actions) {
	var result = value;
	// 如果是lazyWrapper的实例，则调用LazyWrapper.prototype.value 方法，也就是 lazyValue 方法
	if (result instanceof LazyWrapper) {
		result = result.value();
	}
	// 类似 [].reduce()，把上一个函数返回结果作为参数传递给下一个函数
	return arrayReduce(actions, function(result, action) {
		return action.func.apply(action.thisArg, arrayPush([result], action.args));
	}, result);
}
</code></pre><p><strong>koa-compose</strong>源码也有<code>compose</code>函数的实现。实现是循环加<code>promise</code>。 由于代码比较长我就省略了，具体看链接<a href="https://juejin.im/post/5e69925cf265da571e262fe6#heading-7" rel="noopener noreferrer">若川：学习 koa 源码的整体架构，浅析koa洋葱模型原理和co原理 (opens new window)</a>小节 <code>koa-compose 源码</code>（洋葱模型实现）</p><h2 id="6-redux-combinereducers-reducers-">6. Redux.combineReducers(reducers)</h2><p>打开<code>http://localhost:5000/examples/index.4.html</code>，按<code>F12</code>打开控制台，按照给出的例子，调试接下来的<code>Redux.combineReducers(reducers)</code>和<code>Redux.bindActionCreators(actionCreators, dispatch)</code>具体实现。由于文章已经很长了，这两个函数就不那么详细解释了。</p><p><code>combineReducers</code>函数简单来说就是合并多个<code>reducer</code>为一个函数<code>combination</code>。</p><pre><code class="language-js">export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  for (let i = 0; i &lt; reducerKeys.length; i++) {
    const key = reducerKeys[i]

    // 省略一些开发环境判断的代码...

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }

  // 经过一些处理后得到最后的finalReducerKeys
  const finalReducerKeys = Object.keys(finalReducers)

  // 省略一些开发环境判断的代码...

  return function combination(state = {}, action) {
    // ... 省略开发环境的一些判断

   // 用 hasChanged变量 记录前后 state 是否已经修改
    let hasChanged = false
    // 声明对象来存储下一次的state
    const nextState = {}
    //遍历 finalReducerKeys
    for (let i = 0; i &lt; finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key]
      // 执行 reducer
      const nextStateForKey = reducer(previousStateForKey, action)

      // 省略容错代码 ...

      nextState[key] = nextStateForKey
      // 两次 key 对比 不相等则发生改变
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 最后的 keys 数组对比 不相等则发生改变
    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length
    return hasChanged ? nextState : state
  }
}
</code></pre><h2 id="7-redux-bindactioncreators-actioncreators-dispatch-">7. Redux.bindActionCreators(actionCreators, dispatch)</h2><p>如果第一个参数是一个函数，那就直接返回一个函数。如果是一个对象，则遍历赋值，最终生成<code>boundActionCreators</code>对象。</p><pre><code class="language-js">function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  // ... 省略一些容错判断

  const boundActionCreators = {}
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}
</code></pre><p><code>redux</code>所提供的的<code>API</code> 除了<code>store.replaceReducer(nextReducer)</code>没分析，其他都分析了。</p><h2 id="8-vuex-redux-">8. vuex 和 redux 简单对比</h2><h3 id="8-1-">8.1 源码实现形式</h3><p>从源码实现上来看，<code>vuex</code>源码主要使用了构造函数，而<code>redux</code>则是多用函数式编程、闭包。</p><h3 id="8-2-">8.2 耦合度</h3><p><code>vuex</code> 与 <code>vue</code> 强耦合，脱离了<code>vue</code>则无法使用。而<code>redux</code>跟<code>react</code>没有关系，所以它可以使用于小程序或者<code>jQuery</code>等。如果需要和<code>react</code>使用，还需要结合<code>react-redux</code>库。</p><h3 id="8-3-">8.3 扩展</h3><pre><code class="language-js">// logger 插件，具体实现省略
function logger (store) {
  console.log('store', store);
}
// 作为数组传入
new Vuex.Store({
  state,
  getters,
  actions,
  mutations,
  plugins: process.env.NODE_ENV !== 'production'
    ? [logger]
    : []
})
// vuex 源码 插件执行部分
class Store{
  constructor(){
    // 把vuex的实例对象 store整个对象传递给插件使用
    plugins.forEach(plugin =&gt; plugin(this))
  }
}
</code></pre><p><code>vuex</code>实现扩展则是使用插件形式，而<code>redux</code>是中间件的形式。<code>redux</code>的中间件则是AOP（面向切面编程），<code>redux</code>中<code>Redux.applyMiddleware()</code>其实也是一个增强函数，所以也可以用户来实现增强器，所以<a href="https://www.redux.org.cn/docs/introduction/Ecosystem.html" rel="noopener noreferrer"><code>redux</code>生态 (opens new window)</a>比较繁荣。</p><h3 id="8-4-">8.4 上手难易度</h3><p>相对来说，<code>vuex</code>上手相对简单，<code>redux</code>相对难一些，<code>redux</code>涉及到一些函数式编程、高阶函数、纯函数等概念。</p><h2 id="9-">9. 总结</h2><p>文章主要通过一步步调试的方式循序渐进地讲述<code>redux</code>源码的具体实现。旨在教会读者调试源码，不惧怕源码。</p><p>面试官经常喜欢考写一个<code>redux</code>中间件，说说<code>redux</code>中间件的原理。</p><pre><code class="language-js">function logger1({ getState }) {
  return next =&gt; action =&gt; {
      const returnValue = next(action)
      return returnValue
  }
}
</code></pre><pre><code class="language-js">const compose = (...funcs) =&gt; {
  if (funcs.length === 0) {
    return arg =&gt; arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  // 箭头函数
  // return funcs.reduce((a, b) =&gt; (...args) =&gt; a(b(...args)))
  return funcs.reduce((a, b) =&gt; {
    return function(x){
      return a(b(x));
    }
  })
}
</code></pre><pre><code class="language-js">const enhancerStore = Redux.create(reducer, Redux.applyMiddleware(logger1, ...))
enhancerStore.dispatch(action)
</code></pre><p>用户触发<code>enhancerStore.dispatch(action)</code>是增强后的，其实就是第一个中间件函数，中间的<code>next</code>是下一个中间件函数，最后<code>next</code>是没有增强的<code>store.dispatch(action)</code>。</p><p>最后再来看张<code>redux</code>工作流程图</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/01/image-31.png" class="kg-image" alt="image-31" width="1440" height="1080" loading="lazy"></figure><p>是不是就更理解些了呢。</p><p>如果读者发现有不妥或可改善之处，再或者哪里没写明白的地方，欢迎评论指出。另外觉得写得不错，对你有些许帮助，可以评论、转发分享，也是对我的一种支持，非常感谢呀。<strong>要是有人说到怎么读源码，正在读文章的你能推荐我的源码系列文章，那真是太好了。</strong></p><p>欢迎阅读我的<a href="https://www.lxchuan12.cn/">更多文章</a>。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 全面解析 JavaScript 对象所有 API ]]>
                </title>
                <description>
                    <![CDATA[ 近日发现有挺多人对对象基础API不熟悉，举个开发中常见的需求，经常会有类似的封装http到原型Vue.prototype，一般人是这样封装的，但容易被篡改。 function Vue(){  console.log('test vue'); } function http(){   console.log('我是调用接口的http'); } Vue.prototype.$http = http; var vm = new Vue(); vm.$http() vm.$http = 1; // 一旦被修改，虽然一般正常情况下不会被修改 vm.$http(); // 再次调用报错 熟悉Object.defineProperty或者说熟悉对象API的人，一般是如下代码写的，则不会出现被修改的问题。 function Vue(){  console.log('test vue'); }; function http(){   console.log('我是调用接口的http'); }; Object.defineProperty(Vue.prototype, '$http', {     ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/javascript-object-api/</link>
                <guid isPermaLink="false">60098b7b5f61e30501b5c019</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ API ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Thu, 21 Jan 2021 09:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/01/essentialiving-yvG7vDXCzDE-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>近日发现有挺多人对对象基础<code>API</code>不熟悉，举个开发中常见的需求，经常会有类似的封装<code>http</code>到原型<code>Vue.prototype</code>，一般人是这样封装的，但容易被篡改。</p><pre><code class="language-js">function Vue(){
 console.log('test vue');
}
function http(){
  console.log('我是调用接口的http');
}
Vue.prototype.$http = http;
var vm = new Vue();
vm.$http()
vm.$http = 1; // 一旦被修改，虽然一般正常情况下不会被修改
vm.$http(); // 再次调用报错
</code></pre><p>熟悉<code>Object.defineProperty</code>或者说熟悉对象<code>API</code>的人，一般是如下代码写的，则不会出现被修改的问题。</p><pre><code class="language-js">function Vue(){
 console.log('test vue');
};
function http(){
  console.log('我是调用接口的http');
};
Object.defineProperty(Vue.prototype, '$http', {
    get(){
     return http;
    }
});
var vm = new Vue();
vm.$http();
vm.$http = 1; // 这里无法修改
vm.$http(); // 调用正常
</code></pre><p><a href="https://github.com/vuejs/vue-router/blob/dev/src/install.js#L38-L44" rel="noopener noreferrer">vue-router 源码里就是类似这样写的 (opens new window)</a>，<code>this.$router</code>，<code>this.$route</code>无法修改。</p><pre><code class="language-js">// vue-router 源码
Object.defineProperty(Vue.prototype, '$router', {
	get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
	get () { return this._routerRoot._route }
})
</code></pre><p>以下是正文~</p><p>之前看到<a href="http://louiszhai.github.io/2017/04/28/array/" rel="noopener noreferrer">【深度长文】JavaScript数组所有API全解密（opens new window</a>）和<a href="http://louiszhai.github.io/2016/01/12/js.String/" rel="noopener noreferrer">JavaScript字符串所有API全解密（opens new window</a>）这两篇高质量的文章。发现没写对象API解析（估计是博主觉得简单，就没写）。刚好我看到《JavaScript面向对象编程指南（第2版）》，觉得有必要写（或者说chao）一下，也好熟悉下对象的所有API用法。</p><p>创建对象的两种方式：</p><pre><code class="language-js">var o = new Object();
var o = {}; // 推荐
</code></pre><p>该构造器可以接受任何类型的参数，并且会自动识别参数的类型，并选择更合适的构造器来完成相关操作。比如：</p><pre><code class="language-js">var o = new Object('something');
o.constructor; // ƒ String() { [native code] }
var n = new Object(123);
n.constructor; // ƒ Number() { [native code] }
</code></pre><h2 id="object-">Object构造器的成员</h2><h3 id="object-prototype">Object.prototype</h3><p>该属性是所有对象的原型（包括 <code>Object</code>对象本身），语言中的其他对象正是通过对该属性上添加东西来实现它们之间的继承关系的。所以要小心使用。 比如：</p><pre><code class="language-js">var s = new String('若川');
Object.prototype.custom = 1;
console.log(s.custom); // 1
</code></pre><h2 id="object-prototype-">Object.prototype 的成员</h2><h3 id="object-prototype-constructor">Object.prototype.constructor</h3><p>该属性指向用来构造该函数对象的构造器，在这里为<code>Object()</code></p><pre><code class="language-js">Object.prototype.constructor === Object; // true
var o = new Object();
o.constructor === Object; // true
</code></pre><h3 id="object-prototype-tostring-radix-">Object.prototype.toString(radix)</h3><p>该方法返回的是一个用于描述目标对象的字符串。特别地，当目标是一个Number对象时，可以传递一个用于进制数的参数<code>radix</code>，该参数<code>radix</code>，该参数的默认值为10。</p><pre><code class="language-js">var o = {prop:1};
o.toString(); // '[object Object]'
var n = new Number(255);
n.toString(); // '255'
n.toString(16); // 'ff'
</code></pre><h3 id="object-prototype-tolocalestring-">Object.prototype.toLocaleString()</h3><p>该方法的作用与<code>toString()</code>基本相同，只不过它做一些本地化处理。该方法会根据当前对象的不同而被重写，例如<code>Date()</code>,<code>Number()</code>,<code>Array()</code>,它们的值都会以本地化的形式输出。当然，对于包括<code>Object()</code>在内的其他大多数对象来说，该方法与<code>toString()</code>是基本相同的。 在浏览器环境下，可以通过<code>BOM</code>对象<code>Navigator</code>的<code>language</code>属性（在<code>IE</code>中则是<code>userLanguage</code>）来了解当前所使用的语言：</p><pre><code class="language-js">navigator.language; //'en-US'
</code></pre><h3 id="object-prototype-valueof-">Object.prototype.valueOf()</h3><p>该方法返回的是用基本类型所表示的<code>this</code>值，如果它可以用基本类型表示的话。如果<code>Number</code>对象返回的是它的基本数值，而<code>Date</code>对象返回的是一个时间戳（<code>timestamp</code>）。如果无法用基本数据类型表示，该方法会返回<code>this</code>本身。</p><pre><code class="language-js">// Object
var o = {};
typeof o.valueOf(); // 'object'
o.valueOf() === o; // true
// Number
var n = new Number(101);
typeof n; // 'object'
typeof n.vauleOf; // 'function'
typeof n.valueOf(); // 'number'
n.valueOf() === n; // false
// Date
var d = new Date();
typeof d.valueOf(); // 'number'
d.valueOf(); // 1503146772355
</code></pre><h3 id="object-prototype-hasownproperty-prop-">Object.prototype.hasOwnProperty(prop)</h3><p>该方法仅在目标属性为对象自身属性时返回<code>true</code>,而当该属性是从原型链中继承而来或根本不存在时，返回<code>false</code>。</p><pre><code class="language-js">var o = {prop:1};
o.hasOwnProperty('prop'); // true
o.hasOwnProperty('toString'); // false
o.hasOwnProperty('formString'); // false
</code></pre><h3 id="object-prototype-isprototypeof-obj-">Object.prototype.isPrototypeOf(obj)</h3><p>如果目标对象是当前对象的原型，该方法就会返回<code>true</code>，而且，当前对象所在原型上的所有对象都能通过该测试，并不局限与它的直系关系。</p><pre><code class="language-js">var s = new String('');
Object.prototype.isPrototypeOf(s); // true
String.prototype.isPrototypeOf(s); // true
Array.prototype.isPrototypeOf(s); // false
</code></pre><h3 id="object-prototype-propertyisenumerable-prop-">Object.prototype.propertyIsEnumerable(prop)</h3><p>如果目标属性能在<code>for in</code>循环中被显示出来，该方法就返回<code>true</code></p><pre><code class="language-js">var a = [1,2,3];
a.propertyIsEnumerable('length'); // false
a.propertyIsEnumerable(0); // true
</code></pre><h2 id="-es5-object-">在<code>ES5</code>中附加的<code>Object</code>属性</h2><p>在<code>ES3</code>中，除了一些内置属性（如：<code>Math.PI</code>），对象的所有的属性在任何时候都可以被修改、插入、删除。在<code>ES5</code>中，我们可以设置属性是否可以被改变或是被删除——在这之前，它是内置属性的特权。<code>ES5</code>中引入了<strong>属性描述符</strong>的概念，我们可以通过它对所定义的属性有更大的控制权。这些<strong>属性描述符</strong>（特性）包括：</p><p><code>value</code>——当试图获取属性时所返回的值。 <code>writable</code>——该属性是否可写。 <code>enumerable</code>——该属性在<code>for in</code>循环中是否会被枚举 <code>configurable</code>——该属性是否可被删除。 <code>set()</code>——该属性的更新操作所调用的函数。 <code>get()</code>——获取属性值时所调用的函数。 另外，<strong>数据描述符</strong>（其中属性为：<code>enumerable</code>，<code>configurable</code>，<code>value</code>，<code>writable</code>）与<strong>存取描述符</strong>（其中属性为<code>enumerable</code>，<code>configurable</code>，<code>set()</code>，<code>get()</code>）之间是有互斥关系的。在定义了<code>set()</code>和<code>get()</code>之后，描述符会认为存取操作已被 定义了，其中再定义<code>value</code>和<code>writable</code>会<strong>引起错误</strong>。 以下是<em>ES3</em>风格的属性定义方式：</p><pre><code class="language-js">var person = {};
person.legs = 2;
</code></pre><p>以下是等价的ES5通过<strong>数据描述符</strong>定义属性的方式：</p><pre><code class="language-js">var person = {};
Object.defineProperty(person, 'legs', {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true
});
</code></pre><p>其中， 除了value的默认值为<code>undefined</code>以外，其他的默认值都为<code>false</code>。这就意味着，如果想要通过这一方式定义一个可写的属性，必须显示将它们设为<code>true</code>。 或者，我们也可以通过<code>ES5</code>的存储描述符来定义：</p><pre><code class="language-js">var person = {};
Object.defineProperty(person, 'legs', {
    set:function(v) {
        return this.value = v;
    },
    get: function(v) {
        return this.value;
    },
    configurable: true,
    enumerable: true
});
person.legs = 2;
</code></pre><p>这样一来，多了许多可以用来描述属性的代码，如果想要防止别人篡改我们的属性，就必须要用到它们。此外，也不要忘了浏览器向后兼容<code>ES3</code>方面所做的考虑。例如，跟添加<code>Array.prototype</code>属性不一样，我们不能再旧版的浏览器中使用<code>shim</code>这一特性。 另外，我们还可以（通过定义<code>nonmalleable</code>属性），在具体行为中运用这些描述符：</p><pre><code class="language-js">var person = {};
Object.defineProperty(person, 'heads', {value: 1});
person.heads = 0; // 0
person.heads; // 1  (改不了)
delete person.heads; // false
person.heads // 1 (删不掉)
</code></pre><h3 id="object-defineproperty-obj-prop-descriptor-es5-">Object.defineProperty(obj, prop, descriptor) (ES5)</h3><p>具体用法可参见上文，或者查看MDN。 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty" rel="noopener noreferrer">MDN Object.defineProperty(obj, descriptor)(opens new window)</a></p><p>Vue.js文档：<a href="https://cn.vuejs.org/v2/guide/reactivity.html" rel="noopener noreferrer"><strong>如何追踪变化</strong> (opens new window)</a>把一个普通 JavaScript 对象传给 Vue 实例的 data 选项，Vue 将遍历此对象所有的属性，并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是仅 ES5 支持，且无法 shim 的特性，这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器的原因。</p><h3 id="object-defineproperties-obj-props-es5-">Object.defineProperties(obj, props) (ES5)</h3><p>该方法的作用与<code>defineProperty()</code>基本相同，只不过它可以用来一次定义多个属性。 比如：</p><pre><code class="language-js">var glass = Object.defineProperties({}, {
    'color': {
        value: 'transparent',
        writable: true
    },
    'fullness': {
        value: 'half',
        writable: false
    }
});
glass.fullness; // 'half'
</code></pre><h3 id="object-getprototypeof-obj-es5-">Object.getPrototypeOf(obj) (ES5)</h3><p>之前在<code>ES3</code>中，我们往往需要通过<code>Object.prototype.isPrototypeOf()</code>去猜测某个给定的对象的原型是什么，如今在<code>ES5</code>中，我们可以直接询问改对象“你的原型是什么？”</p><pre><code class="language-js">Object.getPrototypeOf([]) === Array.prototype; // true
Object.getPrototypeOf(Array.prototype) === Object.prototype; // true
Object.getPrototypeOf(Object.prototype) === null; // true
</code></pre><h3 id="object-create-obj-descr-es5-">Object.create(obj, descr) (ES5)</h3><p>该方法主要用于创建一个新对象，并为其设置原型，用（上述）属性描述符来定义对象的原型属性。</p><pre><code class="language-js">var parent = {hi: 'Hello'};
var o = Object.create(parent, {
    prop: {
        value: 1
    }
});
o.hi; // 'Hello'
// 获得它的原型
Object.getPrototypeOf(parent) === Object.prototype; // true 说明parent的原型是Object.prototype
Object.getPrototypeOf(o); // {hi: "Hello"} // 说明o的原型是{hi: "Hello"}
o.hasOwnProperty('hi'); // false 说明hi是原型上的
o.hasOwnProperty('prop'); // true 说明prop是原型上的自身上的属性。
</code></pre><p>现在，我们甚至可以用它来创建一个完全空白的对象，这样的事情在<code>ES3</code>中可是做不到的。</p><pre><code class="language-js">var o = Object.create(null);
typeof o.toString(); // 'undefined'
</code></pre><h3 id="object-getownpropertydesciptor-obj-property-es5-">Object.getOwnPropertyDesciptor(obj, property) (ES5)</h3><p>该方法可以让我们详细查看一个属性的定义。甚至可以通过它一窥那些内置的，之前不可见的隐藏属性。</p><pre><code class="language-js">Object.getOwnPropertyDescriptor(Object.prototype, 'toString');
// {writable: true, enumerable: false, configurable: true, value: ƒ toString()}
</code></pre><h3 id="object-getownpropertynames-obj-es5-">Object.getOwnPropertyNames(obj) (ES5)</h3><p>该方法返回一个数组，其中包含了当前对象所有属性的名称（字符串），不论它们是否可枚举。当然，也可以用<code>Object.keys()</code>来单独返回可枚举的属性。</p><pre><code class="language-js">Object.getOwnPropertyNames(Object.prototype);
// ["__defineGetter__", "__defineSetter__", "hasOwnProperty", "__lookupGetter__", "__lookupSetter__", "propertyIsEnumerable", "toString", "valueOf", "__proto__", "constructor", "toLocaleString", "isPrototypeOf"]
Object.keys(Object.prototype);
// []
Object.getOwnPropertyNames(Object);
// ["length", "name", "arguments", "caller", "prototype", "assign", "getOwnPropertyDescriptor", "getOwnPropertyDescriptors", "getOwnPropertyNames", "getOwnPropertySymbols", "is", "preventExtensions", "seal", "create", "defineProperties", "defineProperty", "freeze", "getPrototypeOf", "setPrototypeOf", "isExtensible", "isFrozen", "isSealed", "keys", "entries", "values"]
Object.keys(Object);
// []
</code></pre><h3 id="object-preventextensions-obj-es5-">Object.preventExtensions(obj) (ES5)</h3><h3 id="object-isextensible-obj-es5-">Object.isExtensible(obj) (ES5)</h3><p><code>preventExtensions()</code>方法用于禁止向某一对象添加更多属性，而<code>isExtensible()</code>方法则用于检查某对象是否还可以被添加属性。</p><pre><code class="language-js">var deadline = {};
Object.isExtensible(deadline); // true
deadline.date = 'yesterday'; // 'yesterday'
Object.preventExtensions(deadline);
Object.isExtensible(deadline); // false
deadline.date = 'today';
deadline.date; // 'today'
// 尽管向某个不可扩展的对象中添加属性不算是一个错误操作，但它没有任何作用。
deadline.report = true;
deadline.report; // undefined
</code></pre><h3 id="object-seal-obj-es5-">Object.seal(obj) (ES5)</h3><h3 id="object-isseal-obj-es5-">Object.isSeal(obj) (ES5)</h3><p><code>seal()</code>方法可以让一个对象密封，并返回被密封后的对象。 <code>seal()</code>方法的作用与<code>preventExtensions()</code>基本相同，但除此之外，它还会将现有属性 设置成不可配置。也就是说，在这种情况下，我们只能变更现有属性的值，但不能删除或（用<code>defineProperty()</code>）重新配置这些属性，例如不能将一个可枚举的属性改成不可枚举。</p><pre><code class="language-js">var person = {legs:2};
// person === Object.seal(person); // true
Object.isSealed(person); // true
Object.getOwnPropertyDescriptor(person, 'legs');
// {value: 2, writable: true, enumerable: true, configurable: false}
delete person.legs; // false (不可删除，不可配置)
Object.defineProperty(person, 'legs',{value:2});
person.legs; // 2
person.legs = 1;
person.legs; // 1 (可写)
Object.defineProperty(person, "legs", { get: function() { return "legs"; } });
// 抛出TypeError异常
</code></pre><h3 id="object-freeze-obj-es5-">Object.freeze(obj) (ES5)</h3><h3 id="object-isfrozen-obj-es5-">Object.isFrozen(obj) (ES5)</h3><p><code>freeze()</code>方法用于执行一切不受<code>seal()</code>方法限制的属性值变更。<code>Object.freeze()</code> 方法可以冻结一个对象，冻结指的是不能向这个对象添加新的属性，不能修改其已有属性的值，不能删除已有属性，以及不能修改该对象已有属性的可枚举性、可配置性、可写性。也就是说，这个对象永远是不可变的。该方法返回被冻结的对象。</p><pre><code class="language-js">var deadline = Object.freeze({date: 'yesterday'});
deadline.date = 'tomorrow';
deadline.excuse = 'lame';
deadline.date; // 'yesterday'
deadline.excuse; // undefined
Object.isSealed(deadline); // true;
Object.isFrozen(deadline); // true
Object.getOwnPropertyDescriptor(deadline, 'date');
// {value: "yesterday", writable: false, enumerable: true, configurable: false} (不可配置，不可写)
Object.keys(deadline); // ['date'] (可枚举)
</code></pre><h3 id="object-keys-obj-es5-">Object.keys(obj) (ES5)</h3><p>该方法是一种特殊的<code>for-in</code>循环。它只返回当前对象的属性（不像<code>for-in</code>），而且这些属性也必须是可枚举的（这点和<code>Object.getOwnPropertyNames()</code>不同，不论是否可以枚举）。返回值是一个字符串数组。</p><pre><code class="language-js">Object.prototype.customProto = 101;
Object.getOwnPropertyNames(Object.prototype);
// [..., "constructor", "toLocaleString", "isPrototypeOf", "customProto"]
Object.keys(Object.prototype); // ['customProto']
var o = {own: 202};
o.customProto; // 101
Object.keys(o); // ['own']
</code></pre><h2 id="-es6-object-">四、在<code>ES6</code>中附加的<code>Object</code>属性</h2><h3 id="object-is-value1-value2-es6-">Object.is(value1, value2) (ES6)</h3><p>该方法用来比较两个值是否严格相等。它与严格比较运算符（===）的行为基本一致。 不同之处只有两个：一是<code>+0</code>不等于<code>-0</code>，而是<code>NaN</code>等于自身。</p><pre><code class="language-js">Object.is('若川', '若川'); // true
Object.is({},{}); // false
Object.is(+0, -0); // false
+0 === -0; // true
Object.is(NaN, NaN); // true
NaN === NaN; // false
</code></pre><p><code>ES5</code>可以通过以下代码部署<code>Object.is</code></p><pre><code class="language-js">Object.defineProperty(Object, 'is', {
    value: function() {x, y} {
        if (x === y) {
           // 针对+0不等于-0的情况
           return x !== 0 || 1 / x === 1 / y;
        }
        // 针对 NaN的情况
        return x !== x &amp;&amp; y !== y;
    },
    configurable: true,
    enumerable: false,
    writable: true
});
</code></pre><h3 id="object-assign-target-sources-es6-">Object.assign(target, ...sources) (ES6)</h3><p>该方法用来源对象（<code>source</code>）的所有可枚举的属性复制到目标对象（<code>target</code>）。它至少需要两个对象作为参数，第一个参数是目标对象<code>target</code>，后面的参数都是源对象（<code>source</code>）。只有一个参数不是对象，就会抛出<code>TypeError</code>错误。</p><pre><code class="language-js">var target = {a: 1};
var source1 = {b: 2};
var source2 = {c: 3};
obj = Object.assign(target, source1, source2);
target; // {a:1,b:2,c:3}
obj; // {a:1,b:2,c:3}
target === obj; // true
// 如果目标对象与源对象有同名属性，或多个源对象有同名属性，则后面的属性会覆盖前面的属性。
var source3 = {a:2,b:3,c:4};
Object.assign(target, source3);
target; // {a:2,b:3,c:4}
</code></pre><p><code>Object.assign</code>只复制自身属性，不可枚举的属性（<code>enumerable</code>为<code>false</code>）和继承的属性不会被复制。</p><pre><code class="language-js">Object.assign({b: 'c'},
    Object.defineProperty({}, 'invisible', {
        enumerable: false,
        value: 'hello'
    })
);
// {b: 'c'}
</code></pre><p>属性名为<code>Symbol</code>值的属性，也会被<code>Object.assign()</code>复制。</p><pre><code class="language-js">Object.assign({a: 'b'}, {[Symbol('c')]: 'd'});
// {a: 'b', Symbol(c): 'd'}
</code></pre><p>对于嵌套的对象，<code>Object.assign()</code>的处理方法是替换，而不是添加。</p><pre><code class="language-js">Object.assign({a: {b:'c',d:'e'}}, {a:{b:'hello'}});
// {a: {b:'hello'}}
</code></pre><p>对于数组，<code>Object.assign()</code>把数组视为属性名为0、1、2的对象。</p><pre><code class="language-js">Object.assign([1,2,3], [4,5]);
// [4,5,3]
</code></pre><h3 id="object-getownpropertysymbols-obj-es6-">Object.getOwnPropertySymbols(obj) (ES6)</h3><p>该方法会返回一个数组，该数组包含了指定对象自身的（非继承的）所有 <code>symbol</code> 属性键。 该方法和 <code>Object.getOwnPropertyNames()</code> 类似，但后者返回的结果只会包含字符串类型的属性键，也就是传统的属性名。</p><pre><code class="language-js">Object.getOwnPropertySymbols({a: 'b', [Symbol('c')]: 'd'});
// [Symbol(c)]
</code></pre><h3 id="object-setprototypeof-obj-prototype-es6-">Object.setPrototypeOf(obj, prototype) (ES6)</h3><p>该方法设置一个指定的对象的原型 ( 即, 内部<code>[[Prototype]]</code>属性）到另一个对象或 <code>null</code>。 <code>__proto__</code>属性用来读取或设置当前对象的<code>prototype</code>对象。目前，所有浏览器（包括<code>IE11</code>）都部署了这个属性。</p><pre><code class="language-js">// ES6写法
var obj = {
    method: function(){
        // code ...
    }
};
// obj.__proto__ = someOtherObj;
// ES5写法
var obj = Object.create(someOtherObj);
obj.method = function(){
    // code ...
};
</code></pre><p>该属性没有写入<code>ES6</code>的正文，而是写入了附录。<code>__proto__</code>前后的双下划线说明它本质上是一个内部属性，而不是正式对外的一个API。无论从语义的角度，还是从兼容性的角度，都不要使用这个属性。而是使用<code>Object.setPrototypeOf()</code>（写操作），<code>Object.getPrototypeOf()</code>（读操作），或<code>Object.create()</code>（生成操作）代替。 在实现上，<code>__proto__</code>调用的<code>Object.prototype.__proto__</code>。 <code>Object.setPrototypeOf()</code>方法的作用与<code>__proto__</code>作用相同，用于设置一个对象的<code>prototype</code>对象。它是<code>ES6</code>正式推荐的设置原型对象的方法。</p><h2 id="-es8-object-">在<code>ES8</code>中附加的<code>Object</code>属性</h2><h3 id="object-getownpropertydescriptors-obj-es8-">Object.getOwnPropertyDescriptors(obj) (ES8)</h3><p>该方法基本与<code>Object.getOwnPropertyDescriptor(obj, property)</code>用法一致，只不过它可以用来获取一个对象的所有自身属性的描述符。</p><pre><code class="language-js">Object.getOwnPropertyDescriptor(Object.prototype, 'toString');
// {writable: true, enumerable: false, configurable: true, value: ƒ toString()}
Object.getOwnPropertyDescriptors(Object.prototype); // 可以自行在浏览器控制台查看效果。
</code></pre><h3 id="object-values-obj-es8-">Object.values(obj) (ES8)</h3><p><code>Object.values()</code> 方法与<code>Object.keys</code>类似。返回一个给定对象自己的所有可枚举属性值的数组，值的顺序与使用<code>for...in</code>循环的顺序相同 ( 区别在于<code>for-in</code>循环枚举原型链中的属性 )。</p><pre><code class="language-js">var obj = {a:1,b:2,c:3};
Object.keys(obj); // ['a','b','c']
Object.values(obj); // [1,2,3]
</code></pre><h3 id="object-entries-obj-es8-">Object.entries(obj) (ES8)</h3><p><code>Object.entries()</code> 方法返回一个给定对象自己的可枚举属性<code>[key，value]</code>对的数组，数组中键值对的排列顺序和使用 <code>for...in</code> 循环遍历该对象时返回的顺序一致（区别在于一个<code>for-in</code>循环也枚举原型链中的属性）。</p><pre><code class="language-js">var obj = {a:1,b:2,c:3};
Object.keys(obj); // ['a','b','c']
Object.values(obj); // [1,2,3]
Object.entries(obj); // [['a',1],['b',2],['c',3]]
</code></pre><h2 id="-es10-object-">六、在<code>ES10</code>中附加的<code>Object</code>属性</h2><h3 id="object-fromentries-iterable-es10-">Object.fromEntries(iterable) (ES10)</h3><p><code>Object.fromEntries()</code>方法返回一个给定可迭代对象（类似<code>Array</code>、<code>Map</code>或其他可迭代对象）对应属性的新对象。</p><p><code>Object.fromEntries()</code> 是 <code>Object.entries()</code>的逆操作。</p><pre><code class="language-js">var arr = [['a',1],['b',2],['c',3]];
Object.fromEntries(obj); // {a: 1, b: 2, c: 3}
var entries = new Map([
  ['name', '若川'],
  ['age', 18]
]);
Object.fromEntries(entries) // {name: '若川', age: 18}
</code></pre><h2 id="-">小结</h2><p>细心的读者可能会发现<code>MDN</code>上还有一些<code>API</code>，本文没有列举到。因为那些是非标准的<code>API</code>。熟悉对象的API对理解原型和原型链相关知识会有一定帮助。常用的API主要有<code>Object.prototype.toString()</code>，<code>Object.prototype.hasOwnProperty()</code>， <code>Object.getPrototypeOf(obj)</code>，<code>Object.create()</code>，<code>Object.defineProperty</code>，<code>Object.keys(obj)</code>，<code>Object.assign()</code>。</p><h2 id="--1">参考资料</h2><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object" rel="noopener noreferrer">MDN Object API(opens new window)</a><br><a href="https://book.douban.com/subject/26302623/" rel="noopener noreferrer">JavaScript面向对象编程指南（第2版）（豆瓣读书链接）(opens new window)</a><br><a href="http://es6.ruanyifeng.com/" rel="noopener noreferrer">阮一峰 ES6标准入门2(opens new window)</a></p><p>欢迎阅读我的<a href="https://www.lxchuan12.cn/">更多文章</a>。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 模拟实现 JavaScript 的 call 和 apply 方法 ]]>
                </title>
                <description>
                    <![CDATA[ 先通过MDN认识call和apply call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数（MDN 文档：Function.prototype.call() [https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/call] ）。 语法： fun.call(thisArg, arg1, arg2, ...) thisArg 在fun函数运行时指定的this值。需要注意的是，指定的this值并不一定是该函数执行时真正的this值，如果这个函数处于非严格模式下，则指定为null和 undefined的this值会自动指向全局对象(浏览器中就是window对象)，同时值为原始值(数字，字符串，布尔值)的this会指向该原始值的自动包装对象。 arg1, arg2, ... 指定的参数列表 返回值 返回值是你调用的方法的返回值，若该方法没有返回值，则返回undefined。 apply() 方法调用一个具有给定this值的函 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/javascript-call-and-apply/</link>
                <guid isPermaLink="false">5fdf3f0039641a0517d52208</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Sat, 19 Dec 2020 07:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/12/nick-morrison-FHnnjk1Yj7Y-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="-mdn-call-apply">先通过<code>MDN</code>认识<code>call</code>和<code>apply</code></h2><p><code><strong>call()</strong></code> 方法使用一个指定的 <code>this</code> 值和单独给出的一个或多个参数来调用一个函数（<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/call" rel="nofollow noopener noreferrer">MDN 文档：Function.prototype.call()</a>）。</p><p><strong>语法：</strong></p><pre><code>fun.call(thisArg, arg1, arg2, ...)</code></pre><p><strong>thisArg</strong><br>在<code>fun</code>函数运行时指定的<code>this</code>值。需要注意的是，指定的<code>this</code>值并不一定是该函数执行时真正的<code>this</code>值，如果这个函数处于<strong>非严格模式</strong>下，则指定为<code>null</code>和<code>undefined</code>的<code>this</code>值会自动指向全局对象(浏览器中就是<code>window</code>对象)，同时值为原始值(数字，字符串，布尔值)的<code>this</code>会指向该原始值的自动包装对象。</p><p><strong>arg1, arg2, ...</strong><br>指定的参数列表</p><p><strong>返回值</strong><br>返回值是你调用的方法的返回值，若该方法没有返回值，则返回<code>undefined</code>。</p><p><strong><code>apply()</code></strong> 方法调用一个具有给定<code>this</code>值的函数，以及以一个数组（或<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Indexed_collections#Working_with_array-like_objects">类数组对象</a>）的形式提供的参数（<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/apply" rel="nofollow noopener noreferrer">MDN 文档：Function.prototype.apply()</a>）。</p><pre><code>func.apply(thisArg, [argsArray])</code></pre><p><strong>thisArg</strong><br>可选的。在 <code>func</code> 函数运行时使用的 <code>this</code> 值。请注意，<code>this</code>可能不是该方法看到的实际值：如果这个函数处于<strong>非严格模式</strong>下，则指定为 <code>null</code> 或 <code>undefined</code> 时会自动替换为指向全局对象，原始值会被包装。</p><p><strong>argsArray</strong><br>可选的。一个数组或者类数组对象，其中的数组元素将作为单独的参数传给 <code>func</code> 函数。如果该参数的值为 <code>null</code> 或 &nbsp;<code>undefined</code>，则表示不需要传入任何参数。从<code>ECMAScript 5</code> 开始可以使用类数组对象。</p><p><strong>返回值</strong><br>调用有指定this值和参数的函数的结果。 直接先看<strong>例子1</strong></p><h2 id="call-apply-"><code>call</code> 和 <code>apply</code> 的异同</h2><p><strong>相同点：</strong><br>1、<code>call</code>和<code>apply</code>的第一个参数<code>thisArg</code>，都是<code>func</code>运行时指定的<code>this</code>。而且，<code>this</code>可能不是该方法看到的实际值：如果这个函数处于<strong>非严格模式</strong>下，则指定为 <code>null</code> 或 <code>undefined</code> 时会自动替换为指向全局对象，原始值会被包装。</p><p>2、都可以只传递一个参数。</p><p><strong>不同点：</strong><code>apply</code>只接收两个参数，第二个参数可以是数组也可以是类数组，其实也可以是对象，后续的参数忽略不计。<code>call</code>接收第二个及以后一系列的参数。</p><p>看两个简单例子1和2：</p><pre><code>// 例子1：浏览器环境 非严格模式下
var doSth = function(a, b){
    console.log(this);
    console.log([a, b]);
}
doSth.apply(null, [1, 2]); // this是window  // [1, 2]
doSth.apply(0, [1, 2]); // this 是 Number(0) // [1, 2]
doSth.apply(true); // this 是 Boolean(true) // [undefined, undefined]
doSth.call(undefined, 1, 2); // this 是 window // [1, 2]
doSth.call('0', 1, {a: 1}); // this 是 String('0') // [1, {a: 1}]</code></pre><pre><code>// 例子2：浏览器环境 严格模式下
'use strict';
var doSth2 = function(a, b){
    console.log(this);
    console.log([a, b]);
}
doSth2.call(0, 1, 2); // this 是 0 // [1, 2]
doSth2.apply('1'); // this 是 '1' // [undefined, undefined]
doSth2.apply(null, [1, 2]); // this 是 null // [1, 2]</code></pre><p><code>typeof</code> 有<code>7</code>种类型（<code>undefined number string boolean symbol object function</code>），笔者都验证了一遍：<strong>更加验证了相同点第一点，严格模式下，函数的<code>this</code>值就是<code>call</code>和<code>apply</code>的第一个参数<code>thisArg</code>，非严格模式下，<code>thisArg</code>值被指定为 <code>null</code> 或 <code>undefined</code> 时<code>this</code>值会自动替换为指向全局对象，原始值则会被自动包装，也就是<code>new Object()</code></strong>。</p><p>重新认识了<code>call</code>和<code>apply</code>会发现：<strong>它们作用都是一样的，改变函数里的<code>this</code>指向为第一个参数<code>thisArg</code>，如果明确有多少参数，那可以用<code>call</code>，不明确则可以使用<code>apply</code>。也就是说完全可以不使用<code>call</code>，而使用<code>apply</code>代替。</strong><br>也就是说，我们只需要模拟实现<code>apply</code>，<code>call</code>可以根据参数个数都放在一个数组中，给到<code>apply</code>即可。</p><h2 id="-apply">模拟实现 <code>apply</code></h2><p>既然准备模拟实现<code>apply</code>，那先得看看<code>ES5</code>规范。<a href="http://es5.github.io/#x15.3.4.3" rel="nofollow noopener noreferrer"><code>ES5规范 英文版</code></a>，<a href="http://yanhaijing.com/es5/#322" rel="nofollow noopener noreferrer"><code>ES5规范 中文版</code></a>。<code>apply</code>的规范下一个就是<code>call</code>的规范，可以点击打开新标签页去查看，这里摘抄一部分。</p><p><strong>Function.prototype.apply (thisArg, argArray)</strong><br>当以 <code>thisArg</code> 和 <code>argArray</code> 为参数在一个 <code>func</code> 对象上调用 <code>apply</code> 方法，采用如下步骤：</p><ul><li>如果 <code>IsCallable(func)</code> 是 <code>false</code>, 则抛出一个 <code>TypeError</code> 异常。</li><li>如果 <code>argArray</code> 是 <code>null</code> 或 <code>undefined</code>, 则返回提供 <code>thisArg</code> 作为 <code>this</code> 值并以空参数列表调用 <code>func</code> 的 <code>[[Call]]</code> 内部方法的结果。</li><li>返回提供 <code>thisArg</code> 作为 <code>this</code> 值并以空参数列表调用 <code>func</code> 的 <code>[[Call]]</code> 内部方法的结果。</li><li>如果 <code>Type(argArray)</code> 不是 <code>Object</code>, 则抛出一个 <code>TypeError</code> 异常。<br>5~8 略</li><li>提供 <code>thisArg</code> 作为 <code>this</code> 值并以 <code>argList</code> 作为参数列表，调用 <code>func</code> 的 <code>[[Call]]</code> 内部方法，返回结果。<br><code>apply</code> 方法的 <code>length</code> 属性是 <code>2</code>。</li></ul><p>在外面传入的 <code>thisArg</code> 值会修改并成为 <code>this</code> 值。<code>thisArg</code> 是 <code>undefined</code> 或 <code>null</code> 时它会被替换成全局对象，所有其他值会被应用 <code>ToObject</code> 并将结果作为 <code>this</code> 值，这是第三版引入的更改。</p><p>结合上文和规范，如何将函数里的<code>this</code>指向第一个参数<code>thisArg</code>呢，这是一个问题。 这时候请出<strong>例子 3</strong>：</p><pre><code>// 浏览器环境 非严格模式下
var doSth = function(a, b){
    console.log(this);
    console.log(this.name);
    console.log([a, b]);
}
var student = {
    name: '若川',
    doSth: doSth,
};
student.doSth(1, 2); // this === student // true // '若川' // [1, 2]
doSth.apply(student, [1, 2]); // this === student // true // '若川' // [1, 2]</code></pre><p>可以<strong>得出结论1</strong>：在对象<code>student</code>上加一个函数<code>doSth</code>，再执行这个函数，这个函数里的<code>this</code>就指向了这个对象。那也就是可以在<code>thisArg</code>上新增调用函数，执行后删除这个函数即可。 知道这些后，我们试着容易实现第一版本：</p><pre><code>// 浏览器环境 非严格模式
function getGlobalObject(){
    return this;
}
Function.prototype.applyFn = function apply(thisArg, argsArray){ // `apply` 方法的 `length` 属性是 `2`。
    // 1.如果 `IsCallable(func)` 是 `false`, 则抛出一个 `TypeError` 异常。
    if(typeof this !== 'function'){
        throw new TypeError(this + ' is not a function');
    }

    // 2.如果 argArray 是 null 或 undefined, 则
    // 返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。
    if(typeof argsArray === 'undefined' || argsArray === null){
        argsArray = [];
    }
    
    // 3.如果 Type(argArray) 不是 Object, 则抛出一个 TypeError 异常 .
    if(argsArray !== new Object(argsArray)){
        throw new TypeError('CreateListFromArrayLike called on non-object');
    }

    if(typeof thisArg === 'undefined' || thisArg === null){
        // 在外面传入的 thisArg 值会修改并成为 this 值。
        // ES3: thisArg 是 undefined 或 null 时它会被替换成全局对象 浏览器里是window
        thisArg = getGlobalObject();
    }

    // ES3: 所有其他值会被应用 ToObject 并将结果作为 this 值，这是第三版引入的更改。
    thisArg = new Object(thisArg);
    var __fn = '__fn';
    thisArg[__fn] = this;
    // 9.提供 thisArg 作为 this 值并以 argList 作为参数列表，调用 func 的 [[Call]] 内部方法，返回结果
    var result = thisArg[__fn](...argsArray);
    delete thisArg[__fn];
    return result;
};</code></pre><h2 id="-">实现第一版后，很容易找出两个问题：</h2><p>1.<code>__fn</code> 同名覆盖问题，<code>thisArg</code>对象上有<code>__fn</code>，那就被覆盖了然后被删除了。</p><p><strong>针对问题 1 </strong>解决方案一：采用<code>ES6</code> <code>Sybmol()</code> 独一无二的。可以本来就是模拟<code>ES3</code>的方法。如果面试官不允许用呢。 解决方案二：自己用<code>Math.random()</code>模拟实现独一无二的<code>key</code>。面试时可以直接用生成时间戳即可。</p><pre><code>// 生成UUID 通用唯一识别码
// 大概生成 这样一串 '18efca2d-6e25-42bf-a636-30b8f9f2de09'
function generateUUID(){
    var i, random;
    var uuid = '';
    for (i = 0; i &lt; 32; i++) {
        random = Math.random() * 16 | 0;
        if (i === 8 || i === 12 || i === 16 || i === 20) {
            uuid += '-';
        }
        uuid += (i === 12 ? 4 : (i === 16 ? (random &amp; 3 | 8) : random))
            .toString(16);
    }
    return uuid;
}
// 简单实现
// '__' + new Date().getTime();</code></pre><p>如果这个<code>key</code>万一这对象中还是有，为了保险起见，可以做一次缓存操作。比如如下代码：</p><pre><code>var student = {
    name: '若川',
    doSth: 'doSth',
};
var originalVal = student.doSth;
var hasOriginalVal = student.hasOwnProperty('doSth');
student.doSth = function(){};
delete student.doSth;
// 如果没有，`originalVal`则为undefined，直接赋值新增了一个undefined，这是不对的，所以需判断一下。
if(hasOriginalVal){
    student.doSth = originalVal;
}
console.log('student:', student); // { name: '若川', doSth: 'doSth' }</code></pre><p>2.使用了<code>ES6</code>扩展符<code>...</code><br>解决方案一：采用<code>eval</code>来执行函数。</p><p><code>eval</code>把字符串解析成代码执行。<br><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/eval" rel="nofollow noopener noreferrer">MDN 文档：eval</a></p><p><strong>语法：</strong></p><pre><code>eval(string)</code></pre><p><strong>参数：</strong><br><strong>string</strong><br>表示<code>JavaScript</code>表达式，语句或一系列语句的字符串。表达式可以包含变量以及已存在对象的属性。</p><p><strong>返回值</strong><br>执行指定代码之后的返回值。如果返回值为空，返回<code>undefined</code><br>解决方案二：但万一面试官不允许用<code>eval</code>呢，毕竟<code>eval</code>是魔鬼。可以采用<code>new Function()</code>来生成执行函数。 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function" rel="nofollow noopener noreferrer">MDN 文档：Function</a></p><p><strong>语法：</strong></p><pre><code>new Function ([arg1[, arg2[, ...argN]],] functionBody)</code></pre><p><strong>参数</strong><br><strong>arg1, arg2, ... argN</strong></p><p>被函数使用的参数的名称必须是合法命名的。参数名称是一个有效的<code>JavaScript</code>标识符的字符串，或者一个用逗号分隔的有效字符串的列表;例如<code>“×”</code>，<code>“theValue”</code>，或<code>“A，B”</code>。</p><p><strong>functionBody</strong><br>一个含有包括函数定义的<code>JavaScript</code>语句的字符串。<br>接下来看两个例子：</p><pre><code>简单例子：
var sum = new Function('a', 'b', 'return a + b');
console.log(sum(2, 6));</code></pre><pre><code>// 稍微复杂点的例子：
var student = {
    name: '若川',
    doSth: function(argsArray){
        console.log(argsArray);
        console.log(this.name);
    }
};
// var result = student.doSth(['Rowboat', 18]);
// 用new Function()生成函数并执行返回结果
var result = new Function('return arguments[0][arguments[1]](arguments[2][0], arguments[2][1])')(student, 'doSth', ['Rowboat', 18]);
// 个数不定
// 所以可以写一个函数生成函数代码：
function generateFunctionCode(argsArrayLength){
    var code = 'return arguments[0][arguments[1]](';
    for(var i = 0; i &lt; argsArrayLength; i++){
        if(i &gt; 0){
            code += ',';
        }
        code += 'arguments[2][' + i + ']';
    }
    code += ')';
    // return arguments[0][arguments[1]](arg1, arg2, arg3...)
    return code;
}</code></pre><h2 id="-es3-es5-undefined-">你可能不知道在<code>ES3、ES5</code>中 <code>undefined</code> 是能修改的</h2><p>可能大部分人不知道。<code>ES5</code>中虽然在全局作用域下不能修改，但在局部作用域中也是能修改的，不信可以复制以下测试代码在控制台执行下。虽然一般情况下是不会的去修改它。</p><pre><code>function test(){
    var undefined = 3;
    console.log(undefined); // chrome下也是 3
}
test();</code></pre><p>所以判断一个变量<code>a</code>是不是<code>undefined</code>，更严谨的方案是<code>typeof a === 'undefined'</code>或者<code>a === void 0;</code>这里面用的是<code>void</code>，<code>void</code>的作用是计算表达式，始终返回<code>undefined</code>，也可以这样写<code>void(0)</code>。 更多可以查看<code>韩子迟</code>的这篇文章：<a href="https://github.com/hanzichi/underscore-analysis/issues/1" rel="nofollow noopener noreferrer">为什么用「void 0」代替「undefined」</a>解决了这几个问题，比较容易实现如下代码。</p><h2 id="-new-function-apply">使用 <code>new Function()</code> 模拟实现的<code>apply</code></h2><pre><code>// 浏览器环境 非严格模式
function getGlobalObject(){
    return this;
}
function generateFunctionCode(argsArrayLength){
    var code = 'return arguments[0][arguments[1]](';
    for(var i = 0; i &lt; argsArrayLength; i++){
        if(i &gt; 0){
            code += ',';
        }
        code += 'arguments[2][' + i + ']';
    }
    code += ')';
    // return arguments[0][arguments[1]](arg1, arg2, arg3...)
    return code;
}
Function.prototype.applyFn = function apply(thisArg, argsArray){ // `apply` 方法的 `length` 属性是 `2`。
    // 1.如果 `IsCallable(func)` 是 `false`, 则抛出一个 `TypeError` 异常。
    if(typeof this !== 'function'){
        throw new TypeError(this + ' is not a function');
    }
    // 2.如果 argArray 是 null 或 undefined, 则
    // 返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。
    if(typeof argsArray === 'undefined' || argsArray === null){
        argsArray = [];
    }
    // 3.如果 Type(argArray) 不是 Object, 则抛出一个 TypeError 异常 .
    if(argsArray !== new Object(argsArray)){
        throw new TypeError('CreateListFromArrayLike called on non-object');
    }
    if(typeof thisArg === 'undefined' || thisArg === null){
        // 在外面传入的 thisArg 值会修改并成为 this 值。
        // ES3: thisArg 是 undefined 或 null 时它会被替换成全局对象 浏览器里是window
        thisArg = getGlobalObject();
    }
    // ES3: 所有其他值会被应用 ToObject 并将结果作为 this 值，这是第三版引入的更改。
    thisArg = new Object(thisArg);
    var __fn = '__' + new Date().getTime();
    // 万一还是有 先存储一份，删除后，再恢复该值
    var originalVal = thisArg[__fn];
    // 是否有原始值
    var hasOriginalVal = thisArg.hasOwnProperty(__fn);
    thisArg[__fn] = this;
    // 9.提供 `thisArg` 作为 `this` 值并以 `argList` 作为参数列表，调用 `func` 的 `[[Call]]` 内部方法，返回结果。
    // ES6版
    // var result = thisArg[__fn](...args);
    var code = generateFunctionCode(argsArray.length);
    var result = (new Function(code))(thisArg, __fn, argsArray);
    delete thisArg[__fn];
    if(hasOriginalVal){
        thisArg[__fn] = originalVal;
    }
    return result;
};</code></pre><h2 id="-apply-call">利用模拟实现的<code>apply</code>模拟实现<code>call</code></h2><pre><code>Function.prototype.callFn = function call(thisArg){
    var argsArray = [];
    var argumentsLength = arguments.length;
    for(var i = 0; i &lt; argumentsLength - 1; i++){
        // argsArray.push(arguments[i + 1]);
        argsArray[i] = arguments[i + 1];
    }
    console.log('argsArray:', argsArray);
    return this.applyFn(thisArg, argsArray);
}
// 测试例子
var doSth = function (name, age){
    var type = Object.prototype.toString.call(this);
    console.log(typeof doSth);
    console.log(this === firstArg);
    console.log('type:', type);
    console.log('this:', this);
    console.log('args:', [name, age], arguments);
    return 'this--';
};

var name = 'window';

var student = {
    name: '若川',
    age: 18,
    doSth: 'doSth',
    __fn: 'doSth',
};
var firstArg = student;
var result = doSth.applyFn(firstArg, [1, {name: 'Rowboat'}]);
var result2 = doSth.callFn(firstArg, 1, {name: 'Rowboat'});
console.log('result:', result);
console.log('result2:', result2);</code></pre><p>细心的你会发现注释了这一句<code>argsArray.push(arguments[i + 1]);</code>，事实上<code>push</code>方法，内部也有一层循环。所以理论上不使用<code>push</code>性能会更好些。面试官也可能根据这点来问时间复杂度和空间复杂度的问题。</p><pre><code>// 看看V8引擎中的具体实现：
function ArrayPush() {
    var n = TO_UINT32( this.length );    // 被push的对象的length
    var m = %_ArgumentsLength();     // push的参数个数
    for (var i = 0; i &lt; m; i++) {
        this[ i + n ] = %_Arguments( i );   // 复制元素     (1)
    }
    this.length = n + m;      // 修正length属性的值    (2)
    return this.length;
};</code></pre><p>行文至此，就基本结束了，你可能还发现就是写的非严格模式下，<code>thisArg</code>原始值会包装成对象，添加函数并执行，再删除。而严格模式下还是原始值这个没有实现，而且万一这个对象是冻结对象呢，<code>Object.freeze({})</code>，是无法在这个对象上添加属性的。所以这个方法只能算是非严格模式下的简版实现。最后来总结一下。</p><h2 id="--1">总结</h2><p>通过<code>MDN</code>认识<code>call</code>和<code>apply</code>，阅读<code>ES5</code>规范，到模拟实现<code>apply</code>，再实现<code>call</code>。<br>就是使用在对象上添加调用<code>apply</code>的函数执行，这时的调用函数的<code>this</code>就指向了这个<code>thisArg</code>，再返回结果。引出了<code>ES6 Symbol</code>，<code>ES6</code>的扩展符<code>...</code>、<code>eval</code>、<code>new Function()</code>，严格模式等。</p><p>事实上，现实业务场景不需要去模拟实现<code>call</code>和<code>apply</code>,毕竟是<code>ES3</code>就提供的方法。但面试官可以通过这个面试题考察候选人很多基础知识。如：<code>call</code>、<code>apply</code>的使用。<code>ES6 Symbol</code>，<code>ES6</code>的扩展符<code>...</code>，<code>eval</code>，<code>new Function()</code>，严格模式，甚至时间复杂度和空间复杂度等。</p><p>读者发现有不妥或可改善之处，欢迎指出。另外觉得写得不错，可以点个赞，也是对笔者的一种支持。</p><pre><code>// 最终版版 删除注释版，详细注释看文章
// 浏览器环境 非严格模式
function getGlobalObject(){
    return this;
}
function generateFunctionCode(argsArrayLength){
    var code = 'return arguments[0][arguments[1]](';
    for(var i = 0; i &lt; argsArrayLength; i++){
        if(i &gt; 0){
            code += ',';
        }
        code += 'arguments[2][' + i + ']';
    }
    code += ')';
    return code;
}
Function.prototype.applyFn = function apply(thisArg, argsArray){
    if(typeof this !== 'function'){
        throw new TypeError(this + ' is not a function');
    }
    if(typeof argsArray === 'undefined' || argsArray === null){
        argsArray = [];
    }
    if(argsArray !== new Object(argsArray)){
        throw new TypeError('CreateListFromArrayLike called on non-object');
    }
    if(typeof thisArg === 'undefined' || thisArg === null){
        thisArg = getGlobalObject();
    }
    thisArg = new Object(thisArg);
    var __fn = '__' + new Date().getTime();
    var originalVal = thisArg[__fn];
    var hasOriginalVal = thisArg.hasOwnProperty(__fn);
    thisArg[__fn] = this;
    var code = generateFunctionCode(argsArray.length);
    var result = (new Function(code))(thisArg, __fn, argsArray);
    delete thisArg[__fn];
    if(hasOriginalVal){
        thisArg[__fn] = originalVal;
    }
    return result;
};
Function.prototype.callFn = function call(thisArg){
    var argsArray = [];
    var argumentsLength = arguments.length;
    for(var i = 0; i &lt; argumentsLength - 1; i++){
        argsArray[i] = arguments[i + 1];
    }
    return this.applyFn(thisArg, argsArray);
}</code></pre> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何模拟实现JS的new操作符 ]]>
                </title>
                <description>
                    <![CDATA[ 用过Vuejs的同学都知道，需要用new操作符来实例化。 new Vue({     el: '#app',     mounted(){}, }); 我们在面试的时候，面试官可能会问：是否想过new到底做了什么，怎么模拟实现呢？ 附上我在之前的文章中写过的一段话：已经有很多模拟实现new 操作符的文章，为什么我还要写一遍呢？学习就好比是座大山，人们沿着不同的路登山，分享着自己看到的风景。你不一定能看到别人看到的风景，体会到别人的心情。只有自己去登山，才能看到不一样的风景，体会才更加深刻。 new 操作符做了什么 先看简单例子1： // 例子1 function Student(){ } var student = new Student(); console.log(student); // {} // student 是一个对象。 console.log(Object.prototype.toString.call(student)); // [object Object] // 我们知道平时声明对象也可以用new Object(); 只是看起来更复杂 // 顺便提一下  ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/javascript-new-operator/</link>
                <guid isPermaLink="false">5fa90b745f583f0565090fb5</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 面向对象 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Wed, 11 Nov 2020 05:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/photo-1544980919-e17526d4ed0a.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>用过<code>Vuejs</code>的同学都知道，需要用<code>new</code>操作符来实例化。</p><pre><code>new Vue({
    el: '#app',
    mounted(){},
});</code></pre><p><strong>我们在面试的时候，面试官可能会问：是否想过<code>new</code>到底做了什么，怎么模拟实现呢？</strong></p><p>附上我在之前的文章中写过的一段话：已经有很多模拟实现<code>new</code>操作符的文章，为什么我还要写一遍呢？学习就好比是座大山，人们沿着不同的路登山，分享着自己看到的风景。你不一定能看到别人看到的风景，体会到别人的心情。只有自己去登山，才能看到不一样的风景，体会才更加深刻。</p><h3 id="new-"><code>new</code> 操作符做了什么</h3><p>先看简单<strong>例子1</strong>：</p><pre><code>// 例子1
function Student(){
}
var student = new Student();
console.log(student); // {}
// student 是一个对象。
console.log(Object.prototype.toString.call(student)); // [object Object]
// 我们知道平时声明对象也可以用new Object(); 只是看起来更复杂
// 顺便提一下 `new Object`(不推荐)和Object()也是一样的效果
// 可以猜测内部做了一次判断，用new调用
/** if (!(this instanceof Object)) {
*    return new Object();
*  }
*/
var obj = new Object();
console.log(obj) // {}
console.log(Object.prototype.toString.call(student)); // [object Object]

typeof Student === 'function' // true
typeof Object === 'function' // true</code></pre><p>从这里例子中，我们可以看出：一个函数用<code>new</code>操作符来调用后，生成了一个全新的对象。而且<code>Student</code>和<code>Object</code>都是函数，只不过<code>Student</code>是我们自定义的，<code>Object</code>是<code>JS</code>本身就内置的。 再来看下控制台输出图，感兴趣的读者可以在控制台试试。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-12.png" class="kg-image" alt="image-12" width="573" height="870" loading="lazy"></figure><p>与<code>new Object()</code> 生成的对象不同的是<code>new Student()</code>生成的对象中间还嵌套了一层<code>__proto__</code>，它的<code>constructor</code>是<code>Student</code>这个函数。</p><pre><code>// 也就是说：
student.constructor === Student;
Student.prototype.constructor === Student;</code></pre><h4 id="-1-new-">小结1：从这个简单例子来看，<code>new</code>操作符做了两件事：</h4><ol><li>创建了一个全新的对象。</li><li>这个对象会被执行<code>[[Prototype]]</code>（也就是<code>__proto__</code>）链接。</li></ol><p>接下来我们再来看升级版的<strong>例子2</strong>：</p><pre><code>// 例子2
function Student(name){
    console.log('赋值前-this', this); // {}
    this.name = name;
    console.log('赋值后-this', this); // {name: '若川'}
}
var student = new Student('若川');
console.log(student); // {name: '若川'}</code></pre><p>由此可以看出：这里<code>Student</code>函数中的<code>this</code>指向<code>new Student()</code>生成的对象<code>student</code>。</p><h4 id="-2-new-">小结2：从这个例子来看，<code>new</code>操作符又做了一件事：</h4><ol><li>生成的新对象会绑定到函数调用的<code>this</code>。</li></ol><p>接下来继续看升级版<strong>例子3</strong>：</p><pre><code>// 例子3
function Student(name){
    this.name = name;
    // this.doSth();
}
Student.prototype.doSth = function() {
    console.log(this.name);
};
var student1 = new Student('若');
var student2 = new Student('川');
console.log(student1, student1.doSth()); // {name: '若'} '若'
console.log(student2, student2.doSth()); // {name: '川'} '川'
student1.__proto__ === Student.prototype; // true
student2.__proto__ === Student.prototype; // true
// __proto__ 是浏览器实现的查看原型方案。
// 用ES5 则是：
Object.getPrototypeOf(student1) === Student.prototype; // true
Object.getPrototypeOf(student2) === Student.prototype; // true</code></pre><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-13.png" class="kg-image" alt="image-13" width="628" height="898" loading="lazy"></figure><p>关于JS的原型关系笔者之前看到这张图，觉得很不错，分享给大家。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-14.png" class="kg-image" alt="image-14" width="1019" height="741" loading="lazy"></figure><h4 id="-3-3-1-2-prototype-__proto__-new-student-prototype-student-protytype-">小结3：这个例子3再一次验证了<strong>小结1</strong>中的<strong>第2点</strong>。也就是这个对象会被执行<code>[[Prototype]]</code>（也就是<code>__proto__</code>）链接。并且通过<code>new Student()</code>创建的每个对象将最终被<code>[[Prototype]]</code>链接到这个<code>Student.protytype</code>对象上。</h4><p>细心的同学可能会发现这三个例子中的函数都没有返回值。那么有返回值会是怎样的情形呢。 那么接下来请看<strong>例子4</strong></p><pre><code>// 例子4
function Student(name){
    this.name = name;
    // Null（空） null
    // Undefined（未定义） undefined
    // Number（数字） 1
    // String（字符串）'1'
    // Boolean（布尔） true
    // Symbol（符号）（第六版新增） symbol
    
    // Object（对象） {}
        // Function（函数） function(){}
        // Array（数组） []
        // Date（日期） new Date()
        // RegExp（正则表达式）/a/
        // Error （错误） new Error() 
    // return /a/;
}
var student = new Student('若川');
console.log(student); {name: '若川'}</code></pre><p>笔者测试这七种类型后<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/A_re-introduction_to_JavaScript" rel="nofollow noopener noreferrer">MDN JavaScript类型</a>，得出的结果是：前面六种基本类型都会正常返回<code>{name: '若川'}</code>，后面的<code>Object</code>(包含<code>Functoin</code>, <code>Array</code>, <code>Date</code>, <code>RegExg</code>, <code>Error</code>)都会直接返回这些值。</p><h4 id="-4-">由此得出 小结4：</h4><ol><li>如果函数没有返回对象类型<code>Object</code>(包含<code>Functoin</code>, <code>Array</code>, <code>Date</code>, <code>RegExg</code>, <code>Error</code>)，那么<code>new</code>表达式中的函数调用会自动返回这个新的对象。</li></ol><p>结合这些小结，整理在一起就是：</p><ol><li>创建了一个全新的对象。</li><li>这个对象会被执行<code>[[Prototype]]</code>（也就是<code>__proto__</code>）链接。</li><li>生成的新对象会绑定到函数调用的<code>this</code>。</li><li>通过<code>new</code>创建的每个对象将最终被<code>[[Prototype]]</code>链接到这个函数的<code>prototype</code>对象上。</li><li>如果函数没有返回对象类型<code>Object</code>(包含<code>Functoin</code>, <code>Array</code>, <code>Date</code>, <code>RegExg</code>, <code>Error</code>)，那么<code>new</code>表达式中的函数调用会自动返回这个新的对象。</li></ol><h3 id="new--1">new 模拟实现</h3><p>知道了这些现象，我们就可以模拟实现<code>new</code>操作符。直接贴出代码和注释</p><pre><code>/**
 * 模拟实现 new 操作符
 * @param  {Function} ctor [构造函数]
 * @return {Object|Function|Regex|Date|Error}      [返回结果]
 */
function newOperator(ctor){
    if(typeof ctor !== 'function'){
      throw 'newOperator function the first param must be a function';
    }
    // ES6 new.target 是指向构造函数
    newOperator.target = ctor;
    // 1.创建一个全新的对象，
    // 2.并且执行[[Prototype]]链接
    // 4.通过`new`创建的每个对象将最终被`[[Prototype]]`链接到这个函数的`prototype`对象上。
    var newObj = Object.create(ctor.prototype);
    // ES5 arguments转成数组 当然也可以用ES6 [...arguments], Aarry.from(arguments);
    // 除去ctor构造函数的其余参数
    var argsArr = [].slice.call(arguments, 1);
    // 3.生成的新对象会绑定到函数调用的`this`。
    // 获取到ctor函数返回结果
    var ctorReturnResult = ctor.apply(newObj, argsArr);
    // 小结4 中这些类型中合并起来只有Object和Function两种类型 typeof null 也是'object'所以要不等于null，排除null
    var isObject = typeof ctorReturnResult === 'object' &amp;&amp; ctorReturnResult !== null;
    var isFunction = typeof ctorReturnResult === 'function';
    if(isObject || isFunction){
        return ctorReturnResult;
    }
    // 5.如果函数没有返回对象类型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`)，那么`new`表达式中的函数调用会自动返回这个新的对象。
    return newObj;
}</code></pre><p>最后用模拟实现的<code>newOperator</code>函数验证下之前的<strong>例子3</strong>：</p><pre><code>// 例子3 多加一个参数
function Student(name, age){
    this.name = name;
    this.age = age;
    // this.doSth();
    // return Error();
}
Student.prototype.doSth = function() {
    console.log(this.name);
};
var student1 = newOperator(Student, '若', 18);
var student2 = newOperator(Student, '川', 18);
// var student1 = new Student('若');
// var student2 = new Student('川');
console.log(student1, student1.doSth()); // {name: '若'} '若'
console.log(student2, student2.doSth()); // {name: '川'} '川'

student1.__proto__ === Student.prototype; // true
student2.__proto__ === Student.prototype; // true
// __proto__ 是浏览器实现的查看原型方案。
// 用ES5 则是：
Object.getPrototypeOf(student1) === Student.prototype; // true
Object.getPrototypeOf(student2) === Student.prototype; // true</code></pre><p>可以看出，很符合<code>new</code>操作符。读者发现有不妥或可改善之处，欢迎指出。 回顾这个模拟<code>new</code>函数<code>newOperator</code>实现，最大的功臣当属于<code>Object.create()</code>这个<code>ES5</code>提供的<code>API</code>。</p><h3 id="object-create-">Object.create() 用法举例</h3><p>笔者之前整理的一篇文章中也有讲过，可以翻看<a href="https://segmentfault.com/a/1190000010753942" rel="nofollow noopener noreferrer">JavaScript 对象所有API解析</a></p><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create" rel="nofollow noopener noreferrer">MDN Object.create()</a></p><p><code>Object.create(proto, [propertiesObject])</code>方法创建一个新对象，使用现有的对象来提供新创建的对象的__proto__。 它接收两个参数，不过第二个可选参数是属性描述符（不常用，默认是<code>undefined</code>）。</p><pre><code>var anotherObject = {
    name: '若川'
};
var myObject = Object.create(anotherObject, {
    age: {
        value：18,
    },
});
// 获得它的原型
Object.getPrototypeOf(anotherObject) === Object.prototype; // true 说明anotherObject的原型是Object.prototype
Object.getPrototypeOf(myObject); // {name: "若川"} // 说明myObject的原型是{name: "若川"}
myObject.hasOwnProperty('name'); // false; 说明name是原型上的。
myObject.hasOwnProperty('age'); // true 说明age是自身的
myObject.name; // '若川'
myObject.age; // 18;</code></pre><p>对于不支持<code>ES5</code>的浏览器，<code>MDN</code>上提供了<code>ployfill</code>方案。</p><pre><code>if (typeof Object.create !== "function") {
    Object.create = function (proto, propertiesObject) {
        if (typeof proto !== 'object' &amp;&amp; typeof proto !== 'function') {
            throw new TypeError('Object prototype may only be an Object: ' + proto);
        } else if (proto === null) {
            throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");
        }

        if (typeof propertiesObject != 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");

        function F() {}
        F.prototype = proto;
        return new F();
    };
}</code></pre><p>到此，文章就基本写完了。感谢读者看到这里。</p><h3 id="-">最后总结一下：</h3><ol><li><code>new</code>做了什么：</li></ol><ol><li>创建了一个全新的对象。</li><li>这个对象会被执行<code>[[Prototype]]</code>（也就是<code>__proto__</code>）链接。</li><li>生成的新对象会绑定到函数调用的<code>this</code>。</li><li>通过<code>new</code>创建的每个对象将最终被<code>[[Prototype]]</code>链接到这个函数的<code>prototype</code>对象上。</li><li>如果函数没有返回对象类型<code>Object</code>(包含<code>Functoin</code>, <code>Array</code>, <code>Date</code>, <code>RegExg</code>, <code>Error</code>)，那么<code>new</code>表达式中的函数调用会自动返回这个新的对象。</li><li>怎么模拟实现</li></ol><pre><code>// 去除了注释
function newOperator(ctor){
    if(typeof ctor !== 'function'){
      throw 'newOperator function the first param must be a function';
    }
    newOperator.target = ctor;
    var newObj = Object.create(ctor.prototype);
    var argsArr = [].slice.call(arguments, 1);
    var ctorReturnResult = ctor.apply(newObj, argsArr);
    var isObject = typeof ctorReturnResult === 'object' &amp;&amp; ctorReturnResult !== null;
    var isFunction = typeof ctorReturnResult === 'function';
    if(isObject || isFunction){
        return ctorReturnResult;
    }
    return newObj;
}</code></pre><p>读者发现有不妥或可改善之处，欢迎指出。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ JavaScript 的 this 指向 ]]>
                </title>
                <description>
                    <![CDATA[ 面试官出很多考题，基本都会变着方式来考察this指向，看候选人对 JavaScript 基础知识是否扎实。 读者可以先拉到底部看总结，再谷歌（或各技术平台）搜索几篇类似文章，看笔者写的文章和别人有什么不同（欢迎在评论区评论不同之处），对比来看，验证与自己现有知识是否有盲点，多看几篇，自然就会完善自身知识。 附上之前写文章写过的一段话：已经有很多关于this 的文章，为什么自己还要写一遍呢。学习就好比是座大山，人们沿着不同的路登山，分享着自己看到的风景。你不一定能看到别人看到的风景，体会到别人的心情。只有自己去登山，才能看到不一样的风景，体会才更加深刻。 函数的this在调用时绑定的，完全取决于函数的调用位置（也就是函数的调用方法）。为了搞清楚this的指向是什么，必须知道相关函数是如何调用的。 全局上下文 非严格模式和严格模式中this都是指向顶层对象（浏览器中是window）。 this === window // true 'use strict' this === window; this.name = '若川'; console.log(this.name); // 若 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/javascript-this/</link>
                <guid isPermaLink="false">5f8d6eef5f583f0565090b28</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 函数 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Mon, 19 Oct 2020 08:40:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/05/daria-shevtsova-zbWFT4eVopE-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>面试官出很多考题，基本都会变着方式来考察<code>this</code>指向，看候选人对 <code>JavaScript</code> 基础知识是否扎实。 读者可以先拉到底部看总结，再谷歌（或各技术平台）搜索几篇类似文章，看笔者写的文章和别人有什么不同（欢迎在评论区评论不同之处），对比来看，验证与自己现有知识是否有盲点，多看几篇，自然就会完善自身知识。</p><p>附上之前写文章写过的一段话：已经有很多关于<code>this</code>的文章，为什么自己还要写一遍呢。学习就好比是座大山，人们沿着不同的路登山，分享着自己看到的风景。你不一定能看到别人看到的风景，体会到别人的心情。只有自己去登山，才能看到不一样的风景，体会才更加深刻。</p><p>函数的<code>this</code>在调用时绑定的，完全取决于函数的调用位置（也就是函数的调用方法）。为了搞清楚<code>this</code>的指向是什么，必须知道相关函数是如何调用的。</p><h2 id="-">全局上下文</h2><p>非严格模式和严格模式中this都是指向顶层对象（浏览器中是<code>window</code>）。</p><pre><code class="language-js">this === window // true
'use strict'
this === window;
this.name = '若川';
console.log(this.name); // 若川
</code></pre><h2 id="--1">函数上下文</h2><h3 id="--2">普通函数调用模式</h3><pre><code class="language-js">// 非严格模式
var name = 'window';
var doSth = function(){
    console.log(this.name);
}
doSth(); // 'window'
</code></pre><p>你可能会误以为<code>window.doSth()</code>是调用的，所以是指向<code>window</code>。虽然本例中<code>window.doSth</code>确实等于<code>doSth</code>。<code>name</code>等于<code>window.name</code>。上面代码中这是因为在<code>ES5</code>中，全局变量是挂载在顶层对象（浏览器是<code>window</code>）中。 事实上，并不是如此。</p><pre><code class="language-js">// 非严格模式
let name2 = 'window2';
let doSth2 = function(){
    console.log(this === window);
    console.log(this.name2);
}
doSth2() // true, undefined
</code></pre><p>这个例子中<code>let</code>没有给顶层对象中（浏览器是window）添加属性，<code>window.name2和window.doSth</code>都是<code>undefined</code>。</p><p>严格模式中，普通函数中的<code>this</code>则表现不同，表现为<code>undefined</code>。</p><pre><code class="language-js">// 严格模式
'use strict'
var name = 'window';
var doSth = function(){
    console.log(typeof this === 'undefined');
    console.log(this.name);
}
doSth(); // true，// 报错，因为this是undefined
</code></pre><p>看过的《你不知道的<code>JavaScript</code>》上卷的读者，应该知道书上将这种叫做默认绑定。 对<code>call</code>，<code>apply</code>熟悉的读者会类比为：</p><pre><code class="language-js">doSth.call(undefined);
doSth.apply(undefined);
</code></pre><p>效果是一样的，<code>call</code>，<code>apply</code>作用之一就是用来修改函数中的<code>this</code>指向为第一个参数的。 第一个参数是<code>undefined</code>或者<code>null</code>，非严格模式下，是指向<code>window</code>。严格模式下，就是指向第一个参数。后文详细解释。<br>经常有这类代码（回调函数），其实也是普通函数调用模式。</p><pre><code class="language-js">var name = '若川';
setTimeout(function(){
    console.log(this.name);
}, 0);
// 语法
setTimeout(fn | code, 0, arg1, arg2, ...)
// 也可以是一串代码。也可以传递其他函数
// 类比 setTimeout函数内部调用fn或者执行代码`code`。
fn.call(undefined, arg1, arg2, ...);
</code></pre><h3 id="--3">对象中的函数（方法）调用模式</h3><pre><code class="language-js">var name = 'window';
var doSth = function(){
    console.log(this.name);
}
var student = {
    name: '若川',
    doSth: doSth,
    other: {
        name: 'other',
        doSth: doSth,
    }
}
student.doSth(); // '若川'
student.other.doSth(); // 'other'
// 用call类比则为：
student.doSth.call(student);
// 用call类比则为：
student.other.doSth.call(student.other);
</code></pre><p>但往往会有以下场景，把对象中的函数赋值成一个变量了。 这样其实又变成普通函数了，所以使用普通函数的规则（默认绑定）。</p><pre><code class="language-js">var studentDoSth = student.doSth;
studentDoSth(); // 'window'
// 用call类比则为：
studentDoSth.call(undefined);
</code></pre><h3 id="call-apply-bind-"><code>call、apply、bind</code> 调用模式</h3><p>上文提到<code>call</code>、<code>apply</code>，这里详细解读一下。先通过<code>MDN</code>认识下<code>call</code>和<code>apply</code> <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/call" rel="noopener noreferrer">MDN 文档：Function.prototype.call()</a>。</p><p><strong>语法</strong></p><pre><code class="language-js">fun.call(thisArg, arg1, arg2, ...)
</code></pre><p><strong>thisArg</strong><br>在<code>fun</code>函数运行时指定的<code>this</code>值。需要注意的是，指定的<code>this</code>值并不一定是该函数执行时真正的<code>this</code>值，如果这个函数处于<strong>非严格模式</strong>下，则指定为<code>null</code>和<code>undefined</code>的<code>this</code>值会自动指向全局对象(浏览器中就是<code>window</code>对象)，同时值为原始值(数字，字符串，布尔值)的<code>this</code>会指向该原始值的自动包装对象。<br><strong>arg1, arg2, ...</strong><br>指定的参数列表<br><strong>返回值</strong><br>返回值是你调用的方法的返回值，若该方法没有返回值，则返回<code>undefined</code>。<br><code>apply</code>和<code>call</code>类似。只是参数不一样。它的参数是数组（或者类数组）。</p><p>根据参数<code>thisArg</code>的描述，可以知道，<code>call</code>就是改变函数中的<code>this</code>指向为<code>thisArg</code>，并且执行这个函数，这也就使 <code>JavaScript</code> 灵活很多。严格模式下，<code>thisArg</code>是原始值是值类型，也就是原始值。不会被包装成对象。举个例子：</p><pre><code class="language-js">var doSth = function(name){
    console.log(this);
    console.log(name);
}
doSth.call(2, '若川'); // Number{2}, '若川'
var doSth2 = function(name){
    'use strict';
    console.log(this);
    console.log(name);
}
doSth2.call(2, '若川'); // 2, '若川'
</code></pre><p>虽然一般不会把<code>thisArg</code>参数写成值类型。但还是需要知道这个知识。 之前写过一篇文章：<a href="https://juejin.im/post/5bf6c79bf265da6142738b29" rel="noopener noreferrer">面试官问：能否模拟实现<code>JS</code>的<code>call</code>和<code>apply</code>方法</a> 就是利用对象上的函数<code>this</code>指向这个对象，来模拟实现<code>call</code>和<code>apply</code>的。感兴趣的读者思考如何实现，再去看看笔者的实现。</p><p><code>bind</code>和<code>call</code>和<code>apply</code>类似，第一个参数也是修改<code>this</code>指向，只不过返回值是新函数，新函数也能当做构造函数（<code>new</code>）调用。 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind" rel="noopener noreferrer">MDN Function.prototype.bind</a></p><p><code>bind()</code>方法创建一个新的函数， 当这个新函数被调用时<code>this</code>键值为其提供的值，其参数列表前几项值为创建时指定的参数序列。<br><strong>语法：</strong> fun.bind(thisArg[, arg1[, arg2[, ...]]])<br><strong>参数：</strong> <strong>thisArg</strong> 调用绑定函数时作为this参数传递给目标函数的值。 如果使用<code>new</code>运算符构造绑定函数，则忽略该值。当使用<code>bind</code>在<code>setTimeout</code>中创建一个函数（作为回调提供）时，作为<code>thisArg</code>传递的任何原始值都将转换为<code>object</code>。如果没有提供绑定的参数，则执行作用域的<code>this</code>被视为新函数的<code>thisArg</code>。 <strong>arg1, arg2, ...</strong> 当绑定函数被调用时，这些参数将置于实参之前传递给被绑定的方法。 <strong>返回值</strong> 返回由指定的<code>this</code>值和初始化参数改造的原函数拷贝。</p><p>之前也写过一篇文章：<a href="https://chinese.freecodecamp.org/news/javascript-bind-method/">如何模拟实现 JS 的 bind 方法</a>，就是利用<code>call</code>和<code>apply</code>指向这个<code>thisArg</code>参数，来模拟实现<code>bind</code>的。感兴趣的读者思考如何实现，再去看看笔者的实现。</p><h3 id="--4">构造函数调用模式</h3><pre><code class="language-js">function Student(name){
    this.name = name;
    console.log(this); // {name: '若川'}
    // 相当于返回了
    // return this;
}
var result = new Student('若川');
</code></pre><p>使用<code>new</code>操作符调用函数，会自动执行以下步骤。</p><ol><li>创建了一个全新的对象。</li><li>这个对象会被执行<code>[[Prototype]]</code>（也就是<code>__proto__</code>）链接。</li><li>生成的新对象会绑定到函数调用的<code>this</code>。</li><li>通过<code>new</code>创建的每个对象将最终被<code>[[Prototype]]</code>链接到这个函数的<code>prototype</code>对象上。</li><li>如果函数没有返回对象类型<code>Object</code>(包含<code>Functoin</code>, <code>Array</code>, <code>Date</code>, <code>RegExg</code>, <code>Error</code>)，那么<code>new</code>表达式中的函数调用会自动返回这个新的对象。</li></ol><p>由此可以知道：<code>new</code>操作符调用时，<code>this</code>指向生成的新对象。 <strong>特别提醒一下，<code>new</code>调用时的返回值，如果没有显式返回对象或者函数，才是返回生成的新对象</strong>。</p><pre><code class="language-js">function Student(name){
    this.name = name;
    // return function f(){};
    // return {};
}
var result = new Student('若川');
console.log(result); {name: '若川'}
// 如果返回函数f，则result是函数f，如果是对象{}，则result是对象{}
</code></pre><p>很多人或者文章都忽略了这一点，直接简单用<code>typeof</code>判断对象。虽然实际使用时不会显示返回，但面试官会问到。</p><p>之前也写了一篇文章<a href="https://juejin.im/post/5bde7c926fb9a049f66b8b52" rel="noopener noreferrer">面试官问：能否模拟实现<code>JS</code>的<code>new</code>操作符</a>，是使用apply来把this指向到生成的新生成的对象上。感兴趣的读者思考如何实现，再去看看笔者的实现。</p><h3 id="--5">原型链中的调用模式</h3><pre><code class="language-js">function Student(name){
    this.name = name;
}
var s1 = new Student('若川');
Student.prototype.doSth = function(){
    console.log(this.name);
}
s1.doSth(); // '若川'
</code></pre><p>会发现这个似曾相识。这就是对象上的方法调用模式。自然是指向生成的新对象。 如果该对象继承自其它对象。同样会通过原型链查找。 上面代码使用 <code>ES6</code>中<code>class</code>写法则是：</p><pre><code class="language-js">class Student{
    constructor(name){
        this.name = name;
    }
    doSth(){
        console.log(this.name);
    }
}
let s1 = new Student('若川');
s1.doSth();
</code></pre><p><code>babel</code> <code>es6</code>转换成<code>es5</code>的结果，可以去<a href="https://babeljs.io/" rel="noopener noreferrer"><code>babeljs网站转换测试</code></a>自行试试。</p><pre><code class="language-js">'use strict';

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Student = function () {
    function Student(name) {
        _classCallCheck(this, Student);

        this.name = name;
    }

    _createClass(Student, [{
        key: 'doSth',
        value: function doSth() {
            console.log(this.name);
        }
    }]);

    return Student;
}();

var s1 = new Student('若川');
s1.doSth();
</code></pre><p>由此看出，<code>ES6</code>的<code>class</code>也是通过构造函数模拟实现的，是一种语法糖。</p><h3 id="--6">箭头函数调用模式</h3><p>先看箭头函数和普通函数的重要区别：</p><p>1、没有自己的<code>this</code>、<code>super</code>、<code>arguments</code>和<code>new.target</code>绑定。 2、不能使用<code>new</code>来调用。 3、没有原型对象。 4、不可以改变<code>this</code>的绑定。 5、形参名称不能重复。</p><p>箭头函数中没有<code>this</code>绑定，必须通过查找作用域链来决定其值。 如果箭头函数被非箭头函数包含，则<code>this</code>绑定的是最近一层非箭头函数的<code>this</code>，否则<code>this</code>的值则被设置为全局对象。 比如：</p><pre><code class="language-js">var name = 'window';
var student = {
    name: '若川',
    doSth: function(){
        // var self = this;
        var arrowDoSth = () =&gt; {
            // console.log(self.name);
            console.log(this.name);
        }
        arrowDoSth();
    },
    arrowDoSth2: () =&gt; {
        console.log(this.name);
    }
}
student.doSth(); // '若川'
student.arrowDoSth2(); // 'window'
</code></pre><p>其实就是相当于箭头函数外的<code>this</code>是缓存的该箭头函数上层的普通函数的<code>this</code>。如果没有普通函数，则是全局对象（浏览器中则是<code>window</code>）。 也就是说无法通过<code>call</code>、<code>apply</code>、<code>bind</code>绑定箭头函数的<code>this</code>(它自身没有<code>this</code>)。而<code>call</code>、<code>apply</code>、<code>bind</code>可以绑定缓存箭头函数上层的普通函数的<code>this</code>。 比如：</p><pre><code class="language-js">var student = {
    name: '若川',
    doSth: function(){
        console.log(this.name);
        return () =&gt; {
            console.log('arrowFn:', this.name);
        }
    }
}
var person = {
    name: 'person',
}
student.doSth().call(person); // '若川'  'arrowFn:' '若川'
student.doSth.call(person)(); // 'person' 'arrowFn:' 'person'
</code></pre><h3 id="dom-"><code>DOM</code>事件处理函数调用</h3><h4 id="addeventerlistener-attachevent-onclick">addEventerListener、attachEvent、onclick</h4><pre><code class="language-js">&lt;button class="button"&gt;onclick&lt;/button&gt;
&lt;ul class="list"&gt;
    &lt;li&gt;1&lt;/li&gt;
    &lt;li&gt;2&lt;/li&gt;
    &lt;li&gt;3&lt;/li&gt;
&lt;/ul&gt;
&lt;script&gt;
    var button = document.querySelector('button');
    button.onclick = function(ev){
        console.log(this);
        console.log(this === ev.currentTarget); // true
    }
    var list = document.querySelector('.list');
    list.addEventListener('click', function(ev){
        console.log(this === list); // true
        console.log(this === ev.currentTarget); // true
        console.log(this);
        console.log(ev.target);
    }, false);
&lt;/script&gt;
</code></pre><p><code>onclick</code>和<code>addEventerListener</code>是指向绑定事件的元素。 一些浏览器，比如<code>IE6~IE8</code>下使用<code>attachEvent</code>，<code>this</code>指向是<code>window</code>。 顺便提下：面试官也经常考察<code>ev.currentTarget</code>和<code>ev.target</code>的区别。 <code>ev.currentTarget</code>是绑定事件的元素，而<code>ev.target</code>是当前触发事件的元素。比如这里的分别是<code>ul</code>和<code>li</code>。 但也可能点击的是<code>ul</code>，这时<code>ev.currentTarget</code>和<code>ev.target</code>就相等了。</p><h4 id="--7">内联事件处理函数调用</h4><pre><code class="language-html">&lt;button class="btn1" onclick="console.log(this === document.querySelector('.btn1'))"&gt;点我呀&lt;/button&gt;
&lt;button onclick="console.log((function(){return this})());"&gt;再点我呀&lt;/button&gt;
</code></pre><p>第一个是<code>button</code>本身，所以是<code>true</code>，第二个是<code>window</code>。这里跟严格模式没有关系。 当然我们现在不会这样用了，但有时不小心写成了这样，也需要了解。</p><p>其实<code>this</code>的使用场景还有挺多，比如对象<code>object</code>中的<code>getter</code>、<code>setter</code>的<code>this</code>，<code>new Function()</code>、<code>eval</code>。 但掌握以上几种，去分析其他的，就自然迎刃而解了。 使用比较多的还是普通函数调用、对象的函数调用、<code>new</code>调用、<code>call、apply、bind</code>调用、箭头函数调用。 那么他们的优先级是怎样的呢。</p><h3 id="--8">优先级</h3><p>而箭头函数的<code>this</code>是上层普通函数的<code>this</code>或者是全局对象（浏览器中是<code>window</code>），所以排除，不算优先级。</p><pre><code class="language-js">var name = 'window';
var person = {
    name: 'person',
}
var doSth = function(){
    console.log(this.name);
    return function(){
        console.log('return:', this.name);
    }
}
var Student = {
    name: '若川',
    doSth: doSth,
}
// 普通函数调用
doSth(); // window
// 对象上的函数调用
Student.doSth(); // '若川'
// call、apply 调用
Student.doSth.call(person); // 'person'
new Student.doSth.call(person);
</code></pre><p>试想一下，如果是<code>Student.doSth.call(person)</code>先执行的情况下，那<code>new</code>执行一个函数。是没有问题的。 然而事实上，这代码是报错的。<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Operator_Precedence" rel="noopener noreferrer">运算符优先级</a>是<code>new</code>比点号低，所以是执行<code>new (Student.doSth.call)(person)</code> 而<code>Function.prototype.call</code>，虽然是一个函数（<code>apply</code>、<code>bind</code>也是函数），跟箭头函数一样，不能用<code>new</code>调用。所以报错了。</p><pre><code class="language-js">Uncaught TypeError: Student.doSth.call is not a constructor
</code></pre><p>这是因为函数内部有两个不同的方法：<code>[[Call]]</code>和<code>[[Constructor]]</code>。 当使用普通函数调用时，<code>[[Call]]</code>会被执行。当使用构造函数调用时，<code>[[Constructor]]</code>会被执行。<code>call</code>、<code>apply</code>、<code>bind</code>和箭头函数内部没有<code>[[Constructor]]</code>方法。</p><p>从上面的例子可以看出普通函数调用优先级最低，其次是对象上的函数。 <code>call（apply、bind）</code>调用方式和<code>new</code>调用方式的优先级，在《你不知道的JavaScript》是对比<code>bind</code>和<code>new</code>，引用了<code>mdn</code>的<code>bind</code>的<code>ployfill</code>实现，<code>new</code>调用时bind之后的函数，会忽略<code>bind</code>绑定的第一个参数，(<code>mdn</code>的实现其实还有一些问题，感兴趣的读者，可以看我之前的文章：<a href="https://juejin.im/post/5bec4183f265da616b1044d7" rel="noopener noreferrer">面试官问：能否模拟实现<code>JS</code>的<code>bind</code>方法</a>)，说明<code>new</code>的调用的优先级最高。 所以它们的优先级是<code>new</code> 调用 &gt; <code>call、apply、bind</code> 调用 &gt; 对象上的函数调用 &gt; 普通函数调用。</p><h2 id="--9">总结</h2><p>如果要判断一个运行中函数的 <code>this</code> 绑定， 就需要找到这个函数的直接调用位置。 找到之后 就可以顺序应用下面这四条规则来判断 <code>this</code> 的绑定对象。<br></p><ol><li><code>new</code> 调用：绑定到新创建的对象，注意：显示<code>return</code>函数或对象，返回值不是新创建的对象，而是显式返回的函数或对象。<br></li><li><code>call</code> 或者 <code>apply</code>（ 或者 <code>bind</code>） 调用：严格模式下，绑定到指定的第一个参数。非严格模式下，<code>null</code>和<code>undefined</code>，指向全局对象（浏览器中是<code>window</code>），其余值指向被<code>new Object()</code>包装的对象。<br></li><li>对象上的函数调用：绑定到那个对象。<br></li><li>普通函数调用： 在严格模式下绑定到 <code>undefined</code>，否则绑定到全局对象。<br></li></ol><p><code>ES6</code> 中的箭头函数：不会使用上文的四条标准的绑定规则， 而是根据当前的词法作用域来决定<code>this</code>， 具体来说， 箭头函数会继承外层函数，调用的 this 绑定（ 无论 this 绑定到什么），没有外层函数，则是绑定到全局对象（浏览器中是<code>window</code>）。 这其实和 <code>ES6</code> 之前代码中的 <code>self = this</code> 机制一样。</p><p><code>DOM</code>事件函数：一般指向绑定事件的<code>DOM</code>元素，但有些情况绑定到全局对象（比如<code>IE6~IE8</code>的<code>attachEvent</code>）。</p><p>一定要注意，有些调用可能在无意中使用普通函数绑定规则。 如果想“ 更安全” 地忽略 <code>this</code> 绑 定， 你可以使用一个对象， 比如<code>ø = Object.create(null)</code>， 以保护全局对象。</p><p>面试官考察<code>this</code>指向就可以考察<code>new、call、apply、bind</code>，箭头函数等用法。从而扩展到作用域、闭包、原型链、继承、严格模式等。这就是面试官乐此不疲的原因。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ jQuery 源码整体架构 ]]>
                </title>
                <description>
                    <![CDATA[ 虽然现在基本不怎么使用 jQuery 了，但 jQuery 流行 10 多年的 JS 库，还是有必要学习它的源码的。也可以学着打造属于自己的 JS 类库，求职面试时可以增色不少。 本文章学习的是v3.4.1版本。 unpkg.com源码地址：unpkg.com/jquery@3.4.… [https://unpkg.com/jquery@3.4.1/dist/jquery.js] jQuery github仓库 [https://github.com/jquery/jquery] 自执行匿名函数 (function(global, factory){ })(typeof window !== "underfined" ? window: this, function(window, noGlobal){ }); 外界访问不到里面的变量和函数，里面可以访问到外界的变量，但里面定义了自己的变量，则不会访问外界的变量。 匿名函数将代码包裹在里面，防止与其他代码冲突和污染全局环境。 关于自执行函数不是很了解的读者可以参看这篇文章。 [译] JavaScript：立即执行函数表达式（ ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/learn-the-overall-architecture-of-jquery-source-code/</link>
                <guid isPermaLink="false">5df7087bca1efa04e196a9b2</guid>
                
                <dc:creator>
                    <![CDATA[ 若川 ]]>
                </dc:creator>
                <pubDate>Fri, 24 Jul 2020 08:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/photo-1576289392914-0169e9abebf1.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>虽然现在基本不怎么使用 jQuery 了，但 jQuery 流行 10 多年的 JS 库，还是有必要学习它的源码的。也可以学着打造属于自己的 JS 类库，求职面试时可以增色不少。</p><p>本文章学习的是<code>v3.4.1</code>版本。 <code>unpkg.com</code>源码地址：<a href="https://unpkg.com/jquery@3.4.1/dist/jquery.js" rel="nofollow noopener noreferrer">unpkg.com/jquery@3.4.…</a></p><p><a href="https://github.com/jquery/jquery" rel="nofollow noopener noreferrer"><code>jQuery</code> <code>github</code>仓库</a></p><h2 id="-">自执行匿名函数</h2><pre><code>(function(global, factory){

})(typeof window !== "underfined" ? window: this, function(window, noGlobal){

});</code></pre><p>外界访问不到里面的变量和函数，里面可以访问到外界的变量，但里面定义了自己的变量，则不会访问外界的变量。 匿名函数将代码包裹在里面，防止与其他代码冲突和污染全局环境。 关于自执行函数不是很了解的读者可以参看这篇文章。 <a href="https://segmentfault.com/a/1190000003985390" rel="nofollow noopener noreferrer">[译] JavaScript：立即执行函数表达式（IIFE）</a></p><p>浏览器环境下，最后把<code>$</code> 和 <code>jQuery</code>函数挂载到<code>window</code>上，所以在外界就可以访问到<code>$</code>和<code>jQuery</code>了。</p><pre><code>if ( !noGlobal ) {
	window.jQuery = window.$ = jQuery;
}
// 其中`noGlobal`参数只有在这里用到。</code></pre><h2 id="-commonjs-amd-">支持多种环境下使用 比如 commonjs、amd规范</h2><h3 id="commonjs-">commonjs 规范支持</h3><p><code>commonjs</code>实现 主要代表 <code>nodejs</code></p><pre><code>// global是全局变量，factory 是函数
( function( global, factory ) {

	//  使用严格模式
	"use strict";
	// Commonjs 或者 CommonJS-like  环境
	if ( typeof module === "object" &amp;&amp; typeof module.exports === "object" ) {
		// 如果存在global.document 则返回factory(global, true);
		module.exports = global.document ?
			factory( global, true ) :
			function( w ) {
				if ( !w.document ) {
					throw new Error( "jQuery requires a window with a document" );
				}
				return factory( w );
			};
	} else {
		factory( global );
	}

// Pass this if window is not defined yet
// 第一个参数判断window，存在返回window，不存在返回this
} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {});</code></pre><h3 id="amd-requirejs">amd 规范 主要代表 requirejs</h3><pre><code>if ( typeof define === "function" &amp;&amp; define.amd ) {
	define( "jquery", [], function() {
		return jQuery;
	} );
}</code></pre><h3 id="cmd-seajs">cmd 规范 主要代表 seajs</h3><p>很遗憾，<code>jQuery</code>源码里没有暴露对<code>seajs</code>的支持。但网上也有一些方案。这里就不具体提了。毕竟现在基本不用<code>seajs</code>了。</p><h2 id="-new-">无 new 构造</h2><p>实际上也是可以 <code>new</code>的，因为<code>jQuery</code>是函数。而且和不用<code>new</code>效果是一样的。 new显示返回对象，所以和直接调用<code>jQuery</code>函数作用效果是一样的。 如果对<code>new</code>操作符具体做了什么不明白。可以参看我之前写的文章。</p><p><a href="https://juejin.im/post/5bde7c926fb9a049f66b8b52" rel="">面试官问：能否模拟实现JS的new操作符</a></p><p>源码：</p><pre><code> var
	version = "3.4.1",

	// Define a local copy of jQuery
	jQuery = function( selector, context ) {
		// 返回new之后的对象
		return new jQuery.fn.init( selector, context );
	};
jQuery.fn = jQuery.prototype = {
	// jQuery当前版本
	jquery: version,
	// 修正构造器为jQuery
	constructor: jQuery,
	length: 0,
};
init = jQuery.fn.init = function( selector, context, root ) {
	// ...
	if ( !selector ) {
		return this;
	}
	// ...
};
init.prototype = jQuery.fn;</code></pre><pre><code>jQuery.fn === jQuery.prototype; 	// true
init = jQuery.fn.init;
init.prototype = jQuery.fn;
// 也就是
jQuery.fn.init.prototype === jQuery.fn;	 // true
jQuery.fn.init.prototype === jQuery.prototype; 	// true</code></pre><p>关于这个笔者画了一张<code>jQuery</code>原型关系图，所谓一图胜千言。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/12/image-18.png" class="kg-image" alt="image-18" width="600" height="400" loading="lazy"></figure><pre><code>&lt;sciprt src="https://unpkg.com/jquery@3.4.1/dist/jquery.js"&gt;
&lt;/script&gt;
console.log({jQuery});
// 在谷歌浏览器控制台，可以看到jQuery函数下挂载了很多静态属性和方法，在jQuery.fn 上也挂着很多属性和方法。</code></pre><p><code>Vue</code>源码中，也跟<code>jQuery</code>类似，执行的是<code>Vue.prototype._init</code>方法。</p><pre><code>function Vue (options) {
	if (!(this instanceof Vue)
	) {
		warn('Vue is a constructor and should be called with the `new` keyword');
	}
	this._init(options);
}
initMixin(Vue);
function initMixin (Vue) {
	Vue.prototype._init = function (options) {};
};</code></pre><h2 id="-extend">核心函数之一 extend</h2><p>用法：</p><pre><code>jQuery.extend( target [, object1 ] [, objectN ] )        Returns: Object

jQuery.extend( [deep ], target, object1 [, objectN ] )</code></pre><p><a href="https://api.jquery.com/jQuery.extend/" rel="nofollow noopener noreferrer">jQuery.extend APIjQuery.fn.extend API</a></p><p>看几个例子： （例子可以我放到在线编辑代码的<a href="https://codepen.io/lxchuan12/pen/QeGdqj" rel="nofollow noopener noreferrer">jQuery.extend例子codepen</a>了，可以直接运行）。</p><pre><code>// 1. jQuery.extend( target)
var result1 = $.extend({
	job: '前端开发工程师',
});

console.log(result1, 'result1', result1.job); // $函数 加了一个属性 job  // 前端开发工程师

// 2. jQuery.extend( target, object1)
var result2 = $.extend({
	name: '若川',
},
{
	job: '前端开发工程师',
});

console.log(result2, 'result2'); // { name: '若川', job: '前端开发工程师' }

// deep 深拷贝
// 3. jQuery.extend( [deep ], target, object1 [, objectN ] )
var result3 = $.extend(true,  {
	name: '若川',
	other: {
		mac: 0,
		ubuntu: 1,
		windows: 1,
	},
}, {
	job: '前端开发工程师',
	other: {
		mac: 1,
		linux: 1,
		windows: 0,
	}
});
console.log(result3, 'result3');
// deep true
// {
//     "name": "若川",
//     "other": {
//         "mac": 1,
//         "ubuntu": 1,
//         "windows": 0,
//         "linux": 1
//     },
//     "job": "前端开发工程师"
// }
// deep false
// {
//     "name": "若川",
//     "other": {
//         "mac": 1,
//         "linux": 1,
//         "windows": 0
//     },
//     "job": "前端开发工程师"
// }</code></pre><p>结论：<code>extend</code>函数既可以实现给<code>jQuery</code>函数可以实现浅拷贝、也可以实现深拷贝。可以给jQuery上添加静态方法和属性，也可以像<code>jQuery.fn</code>(也就是<code>jQuery.prototype</code>)上添加属性和方法，这个功能归功于<code>this</code>，<code>jQuery.extend</code>调用时<code>this</code>指向是<code>jQuery</code>，<code>jQuery.fn.extend</code>调用时<code>this</code>指向则是<code>jQuery.fn</code>。</p><h3 id="--1">浅拷贝实现</h3><p>知道这些，其实实现浅拷贝还是比较容易的：</p><pre><code>// 浅拷贝实现
jQuery.extend = function(){
	// options 是扩展的对象object1，object2...
	var options,
	// object对象上的键
	name,
	// copy object对象上的值，也就是是需要拷贝的值
	copy,
	// 扩展目标对象，可能不是对象，所以或空对象
	target = arguments[0] || {},
	// 定义i为1
	i = 1,
	// 定义实参个数length
	length = arguments.length;
	// 只有一个参数时
	if(i === length){
		target = this;
		i--;
	}
	for(; i &lt; length; i++){
		// 不是underfined 也不是null
		if((options = arguments[i]) !=  null){
			for(name in options){
				copy = options[name];
				// 防止死循环，continue 跳出当前此次循环
				if ( name === "__proto__" || target === copy ) {
					continue;
				}
				if ( copy !== undefined ) {
					target[ name ] = copy;
				}
			}
		}

	}
	// 最后返回目标对象
	return target;
}</code></pre><p>深拷贝则主要是在以下这段代码做判断。可能是数组和对象引用类型的值，做判断。</p><pre><code>if ( copy !== undefined ) {
	target[ name ] = copy;
}</code></pre><p>为了方便读者调试，代码同样放在<a href="https://codepen.io/lxchuan12/pen/VoPPmQ" rel="nofollow noopener noreferrer">jQuery.extend浅拷贝代码实现codepen</a>，可在线运行。</p><h3 id="--2">深拷贝实现</h3><pre><code>$.extend = function(){
	// options 是扩展的对象object1，object2...
	var options,
	// object对象上的键
	name,
	// copy object对象上的值，也就是是需要拷贝的值
	copy,
	// 深拷贝新增的四个变量 deep、src、copyIsArray、clone
	deep = false,
	// 源目标，需要往上面赋值的
	src,
	// 需要拷贝的值的类型是函数
	copyIsArray,
	//
	clone,
	// 扩展目标对象，可能不是对象，所以或空对象
	target = arguments[0] || {},
	// 定义i为1
	i = 1,
	// 定义实参个数length
	length = arguments.length;

	// 处理深拷贝情况
	if ( typeof target === "boolean" ) {
		deep = target;

		// Skip the boolean and the target
		// target目标对象开始后移
		target = arguments[ i ] || {};
		i++;
	}

	// Handle case when target is a string or something (possible in deep copy)
	// target不等于对象，且target不是函数的情况下，强制将其赋值为空对象。
	if ( typeof target !== "object" &amp;&amp; !isFunction( target ) ) {
		target = {};
	}

	// 只有一个参数时
	if(i === length){
		target = this;
		i--;
	}
	for(; i &lt; length; i++){
		// 不是underfined 也不是null
		if((options = arguments[i]) !=  null){
			for(name in options){
				copy = options[name];
				// 防止死循环，continue 跳出当前此次循环
				if ( name === "__proto__" || target === copy ) {
					continue;
				}

				// Recurse if we're merging plain objects or arrays
				// 这里deep为true，并且需要拷贝的值有值，并且是纯粹的对象
				// 或者需拷贝的值是数组
				if ( deep &amp;&amp; copy &amp;&amp; ( jQuery.isPlainObject( copy ) ||
					( copyIsArray = Array.isArray( copy ) ) ) ) {

					// 源目标，需要往上面赋值的
					src = target[ name ];

					// Ensure proper type for the source value
					// 拷贝的值，并且src不是数组，clone对象改为空数组。
					if ( copyIsArray &amp;&amp; !Array.isArray( src ) ) {
						clone = [];
						// 拷贝的值不是数组，对象不是纯粹的对象。
					} else if ( !copyIsArray &amp;&amp; !jQuery.isPlainObject( src ) ) {
						// clone 赋值为空对象
						clone = {};
					} else {
						// 否则 clone = src
						clone = src;
					}
					// 把下一次循环时，copyIsArray 需要重新赋值为false
					copyIsArray = false;

					// Never move original objects, clone them
					// 递归调用自己
					target[ name ] = jQuery.extend( deep, clone, copy );

				// Don't bring in undefined values
				}
				else if ( copy !== undefined ) {
					target[ name ] = copy;
				}
			}
		}

	}
	// 最后返回目标对象
	return target;
};</code></pre><p>为了方便读者调试，这段代码同样放在<a href="https://codepen.io/lxchuan12/pen/jgyyyN" rel="nofollow noopener noreferrer">jQuery.extend深拷贝代码实现codepen</a>，可在线运行。</p><h3 id="-isfunction">深拷贝衍生的函数 isFunction</h3><p>判断参数是否是函数。</p><pre><code>var isFunction = function isFunction( obj ) {

	// Support: Chrome &lt;=57, Firefox &lt;=52
	// In some browsers, typeof returns "function" for HTML &lt;object&gt; elements
	// (i.e., `typeof document.createElement( "object" ) === "function"`).
	// We don't want to classify *any* DOM node as a function.
	return typeof obj === "function" &amp;&amp; typeof obj.nodeType !== "number";
};</code></pre><h3 id="-jquery-isplainobject">深拷贝衍生的函数 jQuery.isPlainObject</h3><p><code>jQuery.isPlainObject(obj)</code>测试对象是否是纯粹的对象（通过 "{}" 或者 "new Object" 创建的）。</p><pre><code>jQuery.isPlainObject({}) // true
jQuery.isPlainObject("test") // false</code></pre><pre><code>var getProto = Object.getPrototypeOf;
var class2type = {};
var toString = class2type.toString;
var hasOwn = class2type.hasOwnProperty;
var fnToString = hasOwn.toString;
var ObjectFunctionString = fnToString.call( Object );

jQuery.extend( {
	isPlainObject: function( obj ) {
		var proto, Ctor;

		// Detect obvious negatives
		// Use toString instead of jQuery.type to catch host objects
		// !obj 为true或者 不为[object Object]
		// 直接返回false
		if ( !obj || toString.call( obj ) !== "[object Object]" ) {
			return false;
		}

		proto = getProto( obj );

		// Objects with no prototype (e.g., `Object.create( null )`) are plain
		// 原型不存在 比如 Object.create(null) 直接返回 true;
		if ( !proto ) {
			return true;
		}

		// Objects with prototype are plain iff they were constructed by a global Object function
		Ctor = hasOwn.call( proto, "constructor" ) &amp;&amp; proto.constructor;
		// 构造器是函数，并且 fnToString.call( Ctor )  === fnToString.call( Object );
		return typeof Ctor === "function" &amp;&amp; fnToString.call( Ctor ) === ObjectFunctionString;
	},
});</code></pre><p><code>extend</code>函数，也可以自己删掉写一写，算是<code>jQuery</code>中一个比较核心的函数了。而且用途广泛，可以内部使用也可以，外部使用扩展 插件等。</p><h2 id="--3">链式调用</h2><p><code>jQuery</code>能够链式调用是因为一些函数执行结束后 <code>return this</code>。 比如 <code>jQuery</code> 源码中的<code>addClass</code>、<code>removeClass</code>、<code>toggleClass</code>。</p><pre><code>jQuery.fn.extend({
	addClass: function(){
		// ...
		return this;
	},
	removeClass: function(){
		// ...
		return this;
	},
	toggleClass: function(){
		// ...
		return this;
	},
});</code></pre><h2 id="jquery-noconflict-js-"><code>jQuery.noConflict</code> 很多<code>js</code>库都会有的防冲突函数</h2><p><a href="https://api.jquery.com/jQuery.noConflict/" rel="nofollow noopener noreferrer">jQuery.noConflict API</a></p><p>用法：</p><pre><code> &lt;script&gt;
	var $ = '我是其他的$，jQuery不要覆盖我';
&lt;/script&gt;
&lt;script src="./jquery-3.4.1.js"&gt;
&lt;/script&gt;
&lt;script&gt;
	$.noConflict();
	console.log($); // 我是其他的$，jQuery不要覆盖我
&lt;/script&gt;</code></pre><p>jQuery.noConflict 源码</p><pre><code>var

	// Map over jQuery in case of overwrite
	_jQuery = window.jQuery,

	// Map over the $ in case of overwrite
	_$ = window.$;

jQuery.noConflict = function( deep ) {
	// 如果已经存在$ === jQuery;
	// 把已存在的_$赋值给window.$;
	if ( window.$ === jQuery ) {
		window.$ = _$;
	}

	// 如果deep为 true, 并且已经存在jQuery === jQuery;
	// 把已存在的_jQuery赋值给window.jQuery;
	if ( deep &amp;&amp; window.jQuery === jQuery ) {
		window.jQuery = _jQuery;
	}

	// 最后返回jQuery
	return jQuery;
};</code></pre><h2 id="--4">总结</h2><p>全文主要浅析了<code>jQuery</code>整体结构，自执行匿名函数、无<code>new</code>构造、支持多种规范（如commonjs、amd规范）、核心函数之<code>extend</code>、链式调用、<code>jQuery.noConflict</code>等方面。</p><p>重新梳理下文中学习的源码结构。</p><pre><code>// 源码结构
( function( global, factory )
	"use strict";
	if ( typeof module === "object" &amp;&amp; typeof module.exports === "object" ) {
		module.exports = global.document ?
			factory( global, true ) :
			function( w ) {
				if ( !w.document ) {
					throw new Error( "jQuery requires a window with a document" );
				}
				return factory( w );
			};
	} else {
		factory( global );
	}

} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
	var	version = "3.4.1",

		// Define a local copy of jQuery
		jQuery = function( selector, context ) {
			return new jQuery.fn.init( selector, context );
		};

	jQuery.fn = jQuery.prototype = {
		jquery: version,
		constructor: jQuery,
		length: 0,
		// ...
	};

	jQuery.extend = jQuery.fn.extend = function() {};

	jQuery.extend( {
		// ...
		isPlainObject: function( obj ) {},
		// ...
	});

	init = jQuery.fn.init = function( selector, context, root ) {};

	init.prototype = jQuery.fn;

	if ( typeof define === "function" &amp;&amp; define.amd ) {
		define( "jquery", [], function() {
			return jQuery;
		} );
	}
	jQuery.noConflict = function( deep ) {};

	if ( !noGlobal ) {
		window.jQuery = window.$ = jQuery;
	}

	return jQuery;
});</code></pre><p>可以学习到<code>jQuery</code>巧妙的设计和架构，为自己所用，打造属于自己的<code>js</code>类库。 相关代码和资源放置在<a href="https://github.com/lxchuan12/blog" rel="nofollow noopener noreferrer">github blog</a>中，需要的读者可以自取。</p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
