Node.js相关知识

一、你不知道的JavaScript

1、作用域

作用域 LHS RHS

  • RHS查询与简单地查找某个变量的值别无二致,而LHS查询则是试图找到变量的容器本身,从而可以对其赋值。从这个角度说,RHS并不是真正意义上的“赋值操作的右侧”,更准确地说是“非左侧”。RHS理解成retrieve his source value(取到它的源值),这意味着“得到某某的值”。
  • 非严格模式下,当引擎执行LHS查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎。严格模式下则会抛出异常未声明ReferenceError,另外还有typeError
  • 提升: 引擎会在解释JavaScript代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。当你看到var a = 2;时,可能会认为这是一个声明。但JavaScript实际上会将其看成两个声明:var a;a = 2;。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。函数声明的提升高于变量声明。
  • 匿名函数 (function(args){})(args)
  • varlet,let关键字可以将变量绑定到所在的任意作用域中(通常是{ .. }内部)。换句话说,let为其声明的变量隐式地劫持了所在的块作用域。let只能运行在严格模式下。
    var foo = true;
    
    if (foo) {
        let bar = foo * 2;
        bar = something( bar ); 
        console.log( bar );
    }
    
    console.log( bar ); 
    // 因为let , 对于用let声明的bar,if(){}变成一个块作用域,所以在外部引用bar 时, 未声明,不存在变量   ReferenceError
    
    
    var foo = true;
    if (foo) {
        var a = 2;
        const b = 3; // 包含在if中的块作用域常量
    
        a = 3; // 正常!
        b = 4; // 错误!
    }
    console.log( a ); // 3
    console.log( b ); // ReferenceError!
     //const同样会劫持块作用域,b在外部引用就是未声明,var声明的a则不存在劫持块作用域,const定义变量值不可改变
    

2、闭包

  • 把内部函数baz传递给bar,当调用这个内部函数时(现在叫作fn),它涵盖的foo()内部作用域的闭包就可以观察到了,因为它能够访问a。

    function foo() {
        var a = 2;
    
        function baz() {
            console.log( a ); // 2
        }
    
        bar( baz );
    }
    
    function bar(fn) {
        fn(); // 妈妈快看呀,这就是闭包!
    }
    
    //间接传递函数
    var fn;
    function foo() {
       var a = 2;
    
       function baz() {
            console.log( a );
       }
    
      fn = baz; // 将baz分配给全局变量
    }
    
    function bar() {
        fn(); // 妈妈快看呀,这就是闭包!
    }
    foo();
    bar(); // 2
    
    //  jquery example
    function setupBot(name, selector) {
      $( selector ).click( function activator() {
          console.log( "Activating:" + name );
      } );
    }
    
    setupBot( "Closure Bot 1", "#bot_1" );
    setupBot( "Closure Bot 2", "#bot_2" );
    
  • 虽然以下段代码可以正常工作,但严格来讲它并不是闭包。为什么?因为函数(示例代码中的IIFE)并不是在它本身的词法作用域以外执行的。它在定义时所在的作用域中执行(而外部作用域,也就是全局作用域也持有a)。a是通过普通的词法作用域查找而非闭包被发现的。

    var a = 2;
    
    (function IIFE() {
        console.log( a );
    })();
    
  • 循环中的闭包

    for (var i=1; i<=5; i++) {
        setTimeout( function timer() {
            console.log( i );
        }, i*1000 );
    }
    //运行时会以每秒一次的频率输出五次6
    // for循环每次迭代并没有创建一个新的作用域
    
    
    for (var i=1; i<=5; i++) {
        (function() {
            setTimeout( function timer() {
                console.log( i );
            }, i*1000 );
        })();
    }
    // 不行,虽然每次迭代立即执行函数创建新的作用域,但是一个空作用域,不包含i
    
    for (var i=1; i<=5; i++) {
      (function(j) {
          // 或则  var j = i;
          setTimeout( function timer() {
              console.log( j );
          }, j*1000 );
      })( i );
    }
    // 每秒一次,输出1~5 , 每次迭代,变量i当前值赋予函数的参数 j 并被封装到函数作用域中。
    
    
    // let劫持块作用域,这样for每次迭代对let声明的i 都是一个新的块作用域
    for (let i=1; i<=5; i++) {
      setTimeout( function timer() {
          console.log( i );
      }, i*1000 );
    }
    
  • 模块,如下代码:CoolModule()只是一个函数,必须要通过调用它来创建一个模块实例。如果不执行外部函数,内部作用域和闭包都无法被创建。CoolModule()返回一个用对象字面量语法{ key: value, ... }来表示的对象。这个返回的对象中含有对内部函数而不是内部数据变量的引用。我们保持内部数据变量是隐藏且私有的状态。可以将这个对象类型的返回值看作本质上是模块的公共API。这个对象类型的返回值最终被赋值给外部的变量foo,然后就可以通过它来访问API中的属性方法,比如foo.doSomething()。

    function CoolModule() {
        var something = "cool"; 
        var another = [1, 2, 3];
    
        function doSomething() { 
            console.log( something );
        }
    
        function doAnother() {
            console.log( another.join( " ! " ) );
        }
    
        return {
            doSomething: doSomething, 
            doAnother: doAnother
        };
    }
    
    var foo = CoolModule(); 
    
    foo.doSomething(); // cool
    foo.doAnother(); // 1 ! 2 ! 3
    
     //我们将模块函数转换成了IIFE(参见第3章),立即调用这个函数并将返回值直接赋值给单例的模块实例标识符foo。
    var foo = (function CoolModule() { 
      var something = "cool";
      var another = [1, 2, 3];
    
        function doSomething() { 
           console.log( something );
        }
    
        function doAnother() {
            console.log( another.join( " ! " ) );
        }
    
        return {
            doSomething: doSomething, 
            doAnother: doAnother
        };
    })();
    
    foo.doSomething(); // cool 
    foo.doAnother(); // 1 ! 2 ! 3
    
    //模块也是普通的函数,因此可以接受参数
    function CoolModule(id) {
        function identify() {
            console.log( id );
        }
    
        return {
            identify: identify
        };
    }
    var foo1 = CoolModule( "foo 1" ); 
    var foo2 = CoolModule( "foo 2" );
    
    foo1.identify(); // "foo 1"
    foo2.identify(); // "foo 2"
    
    //模块模式另一个简单但强大的用法是命名将要作为公共API返回的对象
    var foo = (function CoolModule(id) {
        function change() {
            // 修改公共API
            publicAPI.identify = identify2;
        }
    
        function identify1() { 
            console.log( id );
        }
    
        function identify2() {
            console.log( id.toUpperCase() );
        }
    
    
        var publicAPI = { 
            change: change,
            identify: identify1
        };
    
        return publicAPI;
    })( "foo module" );
    
    foo.identify(); // foo module
    foo.change();
    foo.identify(); // FOO MODULE
    
  • 现代的模块机制

    var MyModules = (function Manager() {
       var modules = {};
    
       function define(name, deps, impl) {
           for (var i=0; i<deps.length; i++) {
               deps[i] = modules[deps[i]];
           }
           modules[name] = impl.apply( impl, deps );
           // apply函数 继承,deps作为参数传入,此处为函数依赖的其他函数
       }
       function get(name) {
           return modules[name];
       }
       return {
           define: define,
           get: get
       };
    })();
    //这段代码的核心是modules[name] = impl.apply(impl, deps)。
    //为了模块的定义引入了包装函数(可以传入任何依赖),并且将返回值,也就是模块的API,储存在一个根据名字来管理的模块列表中
    
    
    MyModules.define( "bar", [], function() {
       function hello(who) {
           return "Let me introduce: " + who;
       }
    
       return {
           hello: hello
       };
    } );
    
    MyModules.define( "foo", ["bar"], function(bar) {
       var hungry = "hippo";
    
       function awesome() {
           console.log( bar.hello( hungry ).toUpperCase() );
       }
    
       return {
           awesome: awesome
       };
    } );
    
    var bar = MyModules.get( "bar" );
    var foo = MyModules.get( "foo" );
    
    console.log(
       bar.hello( "hippo" )
    ); // <i>Let me introduce: hippo</i>
    
    foo.awesome(); // LET ME INTRODUCE: HIPPO
    
  • ES6模块机制 , ES6的模块没有“行内”格式,必须被定义在独立的文件中(一个文件一个模块)。浏览器或引擎有一个默认的“模块加载器”(可以被重载,但这远超出了我们的讨论范围)可以在导入模块时同步地加载模块文件。

    //bar.js
     function hello(who) {
        return "Let me introduce: " + who;
     }
    
    export hello;
    
    //foo.js
    // 仅从"bar"模块导入hello()
    import hello from "bar";
    var hungry = "hippo";
    function awesome() {
        console.log(
            hello( hungry ).toUpperCase()
        );
    }
    export awesome;
    

    import可以将一个模块中的一个或多个API导入到当前作用域中,并分别绑定在一个变量上(在我
    们的例子里是hello)。module会将整个模块的API导入并绑定到一个变量上(在我们的例子里是
    foo和bar)。export会将当前模块的一个标识符(变量、函数)导出为公共API。

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。

3、 动态作用域

  • 事实上JavaScript并不具有动态作用域。它只有词法作用域,简单明了。但是this机制某种程度上很像动态作用域。
     function foo() {
        console.log( a ); // 2
    }
    
    function bar() {
        var a = 3;
        foo();
    }
    
    var a = 2;
    
    bar();
    // foo中的a 会通过RHS查询到全局的a,基于词法作用域查找
    // 词法作用域最重要的特征是它的定义过程发生在代码的书写阶段
    // 如果基于动态作用域,作用域链是基于调用栈的,即foo向上查找bar中的a
    
    主要区别:词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。(this也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用

4、 this

对this的认识,this既不指向函数自身也不指向函数的词法作用域,抛开以前错误的假设和理解。this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
1、 this并不像我们所想的那样指向函数本身
2、this在任何情况下都不指向函数的词法作用域。

function foo() {
    var a = 2;
    this.bar();
}

function bar() {
    console.log( this.a );
}

foo(); // ReferenceError: a is not defined

this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

  • 绑定规则
    • 1、默认绑定,当调用foo()时,this.a被解析成了全局变量a。为什么?因为在本例中,函数调用时应用了this的默认绑定,因此this指向全局对象。foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。如果使用严格模式(strict mode),则不能将全局对象用于默认绑定,因此this会绑定到undefined
      function foo() { 
          // "use strict"; 严格模式
          console.log( this.a );
      }
      
      var a = 2;
      
      foo(); // 2
      
    • 2、隐式绑定,如下,调用位置会使用obj上下文来引用函数,因此你可以说函数被调用时obj对象“拥有”或者“包含”它。当foo()被调用时,它的前面确实加上了对obj的引用。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。对象属性引用链中只有上一层或者说最后一层在调用位置中起作用,如obj1.obj2.foo();foo()this指向obj2隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上。
      function foo() { 
          console.log( this.a );
      }
      
      var obj = { 
          a: 2,
          foo: foo 
      };
      
      obj.foo(); // 2
      
      如下代码,虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
      function foo() { 
          console.log( this.a );
      }
      
      var obj = { 
          a: 2,
          foo: foo 
      };
      
      var bar = obj.foo; // 函数别名!
      var a = "oops, global"; // a是全局对象的属性
      
      bar(); // "oops, global
      
      参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,fn = obj.foo
      function foo() { 
          console.log( this.a );
      }
      
      function doFoo(fn) {
          // fn其实引用的是foo
      
          fn(); // <-- 调用位置!
      }
      
      var obj = { 
          a: 2,
          foo: foo 
      };
      
      var a = "oops, global"; // a是全局对象的属性
      
      doFoo( obj.foo ); // "oops, global"
      
      //js的内置函数一样,将函数传递给另一函数的形参,其中包含了隐式赋值
      function foo() { 
          console.log( this.a );
      }
      
      var obj = { 
          a: 2,
          foo: foo 
      };
      
      var a = "oops, global"; // a是全局对象的属性
      
      setTimeout( obj.foo, 100 ); // "oops, global"
      
      
      //JavaScript环境中内置的setTimeout()函数实现和下面的伪代码类似
      function setTimeout(fn,delay) {
          // 等待delay毫秒
          fn(); // <-- 调用位置!
      }
      
    • 显示绑定,通过call()apply()函数实现,它们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。
      function foo() { 
          console.log( this.a );
      }
      
      var obj = { 
          a:2
      };
      //显示指定foo函数中的this并执行该函数
      foo.call( obj ); // 2
      
      
      //硬绑定,是一种非常常用的模式,所以ES5提供了内置的方法Function.prototype.bind,它的用法如下
      function foo(something) { 
          console.log( this.a, something ); 
          return this.a + something;
      }
      
      var obj = { 
          a:2
      };
      
      var bar = foo.bind( obj );//将foo函数的this绑定到obj,返回一个新的foo函数实例bar
      
      var b = bar( 3 ); // 2 3 
      console.log( b ); // 5
      
      
      //第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,
      //通常被称为“上下文”(context),其作用和bind(..)一样,确保你的回调函数使用指定的this。
      function foo(el) { 
          console.log( el, this.id );
      }
      
      var obj = {
          id: "awesome"
      };
      
      // 调用foo(..)时把this绑定到obj
      [1, 2, 3].forEach( foo, obj );
      // 1 awesome 2 awesome 3 awesome
      //这些函数实际上就是通过call(..)或者apply(..)实现了显式绑定,这样你可以少写一些代码。
      
    • new 绑定
      function foo(a) { 
          this.a = a;
      } 
      
      var bar = new foo(2);
      //new foo(2)执行时 foo的this指向new新建的对象foo
      //若不用new直接执行foo(),其this指向全局作用域,同默认规则 
      
      console.log( bar.a ); // 2
      
      // 使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作
      //1. 创建(或者说构造)一个全新的对象。
      //2. 这个新对象会被执行[[Prototype]]连接。
      //3. 这个新对象会绑定到函数调用的this。
      //4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
      
      //new foo(2)创建一个全新的foo对象,this指向的就是foo对象,返回foo
      
  • 胖箭头=>,箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。箭头函数的绑定无法被修改。
    function foo() { 
        setTimeout(() => {
            // 这里的this在词法上继承自foo()
            console.log( this.a ); 
        },100);
    }
    
    var obj = { 
        a:2
    };
    
    foo.call( obj ); // 2
    

5、对象

  • 语法,声明

    var myObj = {
        key: value
        // ...
    };
    
    var myObj = new Object();
    myObj.key = value;
    
  • 对象类型

    // 对象是JavaScript的基础。在JavaScript中一共有六种主要类型,基本类型(术语是“语言类型”):
    string
    number
    boolean
    null
    undefined
    object
    //基本类型按照值传递
    
    //内置对象
    String
    Number
    Boolean
    Object
    Function
    Array
    Date
    RegExp
    Error
    
  • 值传递,基本类型按照值传递,对象其实也是按值传递。js的基础类型原始值存储在栈中,对象存储在堆中,堆地址不能直接访问,所有栈中存储它的地址,引用值是存储栈中的指向堆的地址。参考https://www.zhihu.com/question/27114726/answer/35481766

    var obj = {x : 1};
    function foo(o) {
        o = 100;
    }
    foo(obj);
    console.log(obj.x); // 仍然是1, obj并未被修改为100.
    
    
    //
    var obj1 = {
      value:'111'
    };
    var obj2 = {
      value:'222'
    };
    function changeStuff(obj){
      obj.value = '333';
      obj = obj2;
      return obj.value;
    }
    
    var foo = changeStuff(obj1);
    
    console.log(foo);// '222' 参数obj指向了新的对象obj2
    console.log(obj1.value);//'333'
    
    
  • ES6增加了可计算属性名,可以将对象的key用变量表示,即对变量加上[]

    var prefix = "foo";
    var a="bar"
    
    var myObject = {
        [a]: "hello", 
        ["baz"]: "world"
      };
    
    console.log(myObject["bar"]); // hello
    console.log(myObject["baz"]); // world
    
  • ES6定义了Object.assign(..)方法来实现浅复制,方法的第一个参数是目标对象,之后还可以跟一个或多个源对象。

    var newObj = Object.assign( {}, myObject );
    
    newObj.a; // 2
    newObj.b === anotherObject; // true
    newObj.c === anotherArray; // true
    newObj.d === anotherFunction; // true
    

6、类

二、Node.js Interview

1、内存释放

  • 引用类型是在没有引用之后, 通过 v8 的 GC 自动回收, 值类型如果是处于闭包的情况下, 要等闭包没有引用才会被 GC 回收, 非闭包的情况下等待 v8 的新生代 (new space) 切换的时候回收.
  • 内存泄漏几种情况
    • 全局变量

      a = 10;
      //未声明对象。
      global.b = 11;
      //全局变量引用
      //全局变量直接挂在 root 对象上,不会被清除掉。
      
      ////非严格模式下bar会被定义到全局变量,页面中的全局变量只有在页面关闭后才会被销毁
      function foo(arg) {
          bar = "some text";
      }
      
      ////
      function foo() {
        this.var1 = "potential accidental global";
      }
      // Foo 被调用时, this 指向全局变量(window),意外的创建了全局变量.
      foo();
      
    • 闭包,闭包会引用到父级函数中的变量,如果闭包未释放,就会导致内存泄漏。如下代码是 inner 直接挂在了 root 上,从而导致内存泄漏(bigData 不会释放)。

      function out() {
        const bigData = new Buffer(100);
        inner = function () {
          void bigData;
        }
      }
      
    • 未销毁的定时器和全局函数,如下代码:如果后续 renderer 元素被移除, 整个定时器实际上没有任何作用. 但如果你没有回收定时器, 整个定时器依然有效, 不但定时器无法被内存回收, 定时器函数中的依赖也无法回收. 在这个案例中的 serverData 也无法被回收.

      var serverData = loadData();
      setInterval(function() {
          var renderer = document.getElementById('renderer');
          if(renderer) {
              renderer.innerHTML = JSON.stringify(serverData);
          }
      }, 5000); // 每 5 秒调用一次
      
    • 事件监听

  • Js中的内存管理,参考https://zhuanlan.zhihu.com/p/30552148
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 第一章 编译原理 js是一门编译语言 传统编译语言流程: 分词/词法分析:把字符串分解成有意义的代码块 解析/语法...
    冥冥2017阅读 541评论 0 0
  • 一、理解js作用域 1、作用域:作用域是一套规则,用于确定在何处以及如何查找变量(标字符)。2、LHS查询:查找的...
    怀念不能阅读 446评论 0 1
  • 工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。简单...
    舟渔行舟阅读 7,621评论 2 17
  • 深入响应式 追踪变化: 把普通js对象传给Vue实例的data选项,Vue将使用Object.defineProp...
    冥冥2017阅读 4,788评论 6 16
  • 个人入门学习用笔记、不过多作为参考依据。如有错误欢迎斧正 目录 简书好像不支持锚点、复制搜索(反正也是写给我自己看...
    kirito_song阅读 2,419评论 1 37