了不起的node.js学习笔记

chapter 2 JavaScript概览

介绍

js是基于原型,面向对象, 弱类型的的动态脚本语言。

Javascript基础

  • 类型:
    • 基本类型包括number, boolean, string, null 及 undefined
    • 复杂类型包括array, function 及 object
var a = 5;
var b = 5;
b = a;
a; // => 5
b; //=> 6
var a = ['hello', 'world', 1]; //array
var b = a;
b[0] = 'bye';
a[0]; //=> 'bye'
b[0]; //= >'bye'
-  类型的困惑(在js中判断变量值的类型并非易事)
你可以用两种方式来创建字符串
```JavaScript
  var a = 'woot';
  var b = new String('woot')
  a + b; //=> 'woot woot'
  //要对这两个变量使用typeof和instanceof操作符,things go fun.
  typeof a; //'string'
  typeof b; //'object'
  a instanceof String; //false
  b instanceof String; //true
  //事实上, 这两个变量值绝对都是字符串
  a.substr == b.substr; //true
  a == b; //true
  a === b; //false 考虑类型是否相同了
  //考虑有此差异,建议通过直观方式进行定义,避免使用new
  //特定值会被判定为false: null, undefined, ' ', 0
  var a = 0;
  if(a){
  //will not execute
  }
  a == false; //true
  a === false; //false
  //值得注意的是:
  typeof null == 'object'; //很不幸,结果为true
  //数组也不例外
  typeof [] == 'object'; // true;
```
  • 函数
    javascript 中函数最重要。它们都属于一等函数:可以作为引用存储在变量中,随后可以像其他对象一样,进行传递:
var a = function(){}
console.log(a); //将函数作为参数传递

Javascript 中所有的函数都可以进行命名。有一点很重要,就是要区分函数名和变量名。

var a = function a () {
  'function' == typeof a; //true
};
  • THIS, FUNCTION#CALL, FUNCTION#APPLY
    下面代码中函数被调用时, this的值是全局对象。在浏览器中,就是window对象。
function a() {
window == this; //true
};
a();

调用以下函数时,使用.call 和.apply 方法可以改变this值:

function a() {
  this.a == 'b'; // true
}
a.call({a: 'b'});

call 和 apply 区别在于 call 接受参数列表, 而apply接受一个参数数组

function a(b, c){
  b == 'first'; //true;
  c == 'second'; //true
};
a.call({a: 'b'}, 'first', 'second');
a.apply({a: 'b'}, ['first', 'second']);
  • 函数的参数数量
    函数有一个很有意思的属性---参数数量, 该属性指明函数声明时可接受的参数数量。在javascript中,该属性名叫length。
  var a = function(a, b, c);
  a.length == 3; //true 

尽管者在浏览器端很少使用, 但是, 他对我们非常重要, 因为一些流行的node框架通过此属性来根据不同的参数个数提供不同功能的。

  • 闭包
    在javascript中, 每次函数调用时, 新的作用域就会产生。
    在某个作用域中定义的变量只能在该作用域或其内部的作用域(该作用域中定义的作用域)中才能访问到:
var a = 5;
function woot(){
  a == 5; //false;
  var a = 6;
  function test(){
    a == 6; //true
  };
  test();
};
woot();

自执行函数是一种机制, 通过者中机制声明和调用一个匿名函数, 能够达到仅定义一个新作用域的作用。

var a = 3;
(function(){var a = 5;})();
a == 3; //true;

自执行函数对声明私有变量很有用的, 这样可以让私有变量不被其他代码所访问。


  • javascript中没有class关键词, 类只能通过函数来定义:
 function Animal(){}

要给所有的Animal的实例定义函数, 可以通过prototype属性来完成:

Animal.prototype.eat = function(food){
  //eat method
};

这里值得一提的时, 在prototype的函数内部, this并非像普通函数那样指向global对象, 而是指向通过该创建的实例对象:

function Animal(name) {
  this.name = name;
}
Animal.prototype.getName(){
  return this.name;
};
var animal = new Animal('tobi');
a.getName == 'tobi'; //true
  • 继承
    javascript有基于原型的继承的特定。通常,你可以通过以下方式来模拟类型继承。
    定义一个要继承自Animal的构造器。
function Ferret(){};
//要定义继承链, 首先创建一个Animal对象, 然后将其赋值给ferret.prototype.
//实现继承
Ferret.prototype = new Animal();
//随后可以为子类定义属性和方法:
Ferret.prototype.type = 'domestic';
//还可以通过prototype来重写和调用父类函数
Ferret.prototype.eat = function(food){
  Animal.prototype.eat.call(this, foot);//调用父类函数
  //ferrte特有的逻辑写在这里
}

慕课网


92C8DD3E-A2FC-418C-9DDB-C8ED9C8C4022.png
var animal = new Animal();
animal instanceof Animal //true
animal instanceof Ferret; // false
var ferret = new Ferret();
ferret instanceof Animal; //true
ferret instanceof Ferret; //true

这项技术是同类方案中最好的, 不会改变instanceof操作符的结果。

它的不足: 声明继承的时候创建的对象总要进行初始化(Ferret.prototype=new Animal),这种方式不好。一种解决办法就是在构造器中添加判断条件:

function Animal(a){
  if(false !== a) return;
  //初始化
}
Ferret.prototype = new Animal(false);

另外一个办法就是在定义一个新的空的构造器, 并重写它的原型:

function Animal(){
 //constructor stuff
};
function f() {};
f.prototype = Animal.prototype;
Ferret.prototype = new f;

v8 提供了更为简洁的解决方案

  • TRY{} CATCH{}
    try/catch 允许进行异常捕获。下述代码会抛出异常

var a = 5;
a[]
当函数抛出错误时, 代码就停止执行:

function () {
  throw new Error('hi');
  console.log('hi'); //这里永远不会被执行到
}

若使用try/catch则可以进行错误处理, 并让代码继续执行下去:

function () {
  var a = 5; 
  try {
    a[];
}catch(e){
    e instanceof Error; //true
}
console.log('you get here!')
}
  • v8中的javascript

    -OBJECT#KEYS
    要想获取下述对象的键值(a and c):

var a = {a: 'b', c: 'd'};

通常会使用如下迭代方式:

for(var i in a) {}

通过对键值进行迭代, 可以将它们收集到一个数组中。不过, 如果采用如下方式对prototype进行扩展:

 object.prototype.c = 'd';

为了避免迭代中获取c可以用hasOwnProperty来进行检查

for(var i in a){
    if(a.hasOwnProperty(i)){}
}

在v8中, 要获取对象的自由键, 还有更简单的方法:

  var a = {a: 'b', c: 'd'};
  Object.keys(a);
  • ARRAY#ISARRAY
    对数组使用typeof操作符会返回object, 然而大部分情况下, 我们要检查数组是否真的是数组。
Array.isArray(new Array) //true
Array.isArray([]) //true
Array.isArray(null) //false
Array.isArray(arguments) //false
  • 数组方法
//要遍历数组, 可以使用forEach($.each(jquery中))
[1, 2, 3].forEach(function(v){
  console.log(v);
})
//要过滤数组中的元素
[1, 2, 3].filter(function(v){
  return v < 3;
});
//要改变数组中每个元素的值,可以使用map
[1, 2,3 ].map(function(v){
  return v * 2;
}); //[2, 4, 6]
  • 字符串方法
    要移除字符串首末的空格, 可以使用:
 '  hello '.trim();
  • JSON
    v8 提供了JSON.stringfy 和JSON.parse的方法来对JSON数据进行解码和编码
var obj = JSON.parse('{"a": "b"}');
obj.a == 'b'; //true
  • FUNCTION#BIND
    .bind 允许改变this的引用
function a(){
  this.hello == "world"; //true
};
var b = a.bind({hello, "world"});
b();
  • FUNCTION#NAME
    V8还支持非标准的函数属性名:
var a = function woot() {};
a.name == "woot"; //true
//该属性用于v8内部的堆栈追踪。当错误抛出时, v8会显示一个堆栈追踪的信息, 会告诉你是哪个函数用导致了错误的发生。
var woot = function() {throw new Error();};
woot();
//Error:
  // at [object context]: 1:32
//v8无法为函数的引用指派名字。然而,如果对函数进行了命名,v8就能在堆栈追踪中将函数名显示出来:
   var woot = function buggy(){throw new Error();}
  woot();
//Error
  //at buggy ([object context]: 1: 34)
  • PROTO(继承)
    "proto"使得定义继承链变得更加容易
function Animal() {}
function Ferret() {}
Ferret.prototype.__proto__ = Animal.prototype;
//这是一个非常有用的特性:
 //免去如下工作: 1.借助中间构造器, 2, 借助OOP工具类库。
  • 存取器
    你可以通过调用方法来定义属性, 访问属性就使用defineGetter, 设置属性就使用defineSetter.
    比如, 为Date对象定义一个ago属性, 返回以自然语言描述的日期间隔。
    很多时候, 特别在软件中, 想用自然语言来描述日期距离某个特定时间点的时间间隔。比如: “某事件发生在三秒前”, 这种表达,要远比“某件事情发生在x年x月x日”, 这种表达更容易理解。
//基于John的prettyDate
Date.prototype.__defineGetter___('ago', function(){
  var diff = (new Date()).getTime() - this.getTime()) / 1000)
  var day_diff = Math.floor(diff / 86400);
  return day_diff == 0 && (diff < 60 && "just now" || 
diff < 120 && "1 minute ago" || 
diff < 3600 && Math.floor(diff/60) + "minutes ago" || 
diff<7200 && "1 hour ago"|| 
diff < 86400 && Math.floor((diff/3600) + "hours ago") || 
day_diff == 1 && "Yesterday"||
day_diff < 7 && "day_diff" + " days ago"||
Math.ceil(day_diff/ 7 + "weeks ago";
});

然后简单的访问ago属性即可

var a = new Date('09/18/1991');
a.ago;

单线程的世界

Node是单线程,但它内置了child_process模块,允许创建子进程。

var start = Date.now();
setTimeout(function(){
  console.log(Date.now() - start);

for (var i=0; i<100000000; i ++){}
}, 1000)

setTimeout(function(){
  console.log(Date.now() - start);
}, 2000);

//node timeout.js
//1000
//3783

为什么会这样? 是事件轮询被javascript代码阻塞了。当第一个事件分发时,会执行javascript的回调函数。由于回调函数要执行一段时间,所以下一个事件轮询执行的事件就远远超过了两秒。

HTTP?
NODE IO 非阻塞

既然执行时只有一个线程, 也就是说,当一个函数执行时,同一时间不可能有第二个也在执行。那么node如何实现高并发,使得一台笔记本能每秒处理上千个请求?

搞清问题:

  1. 明白调用堆栈的概念:
    当v8首次调用一个函数时,会创建一个众所周知调用堆栈,或者称为执行堆栈。
    如果该函数调用又去调用另外一个函数的话, v8就会把它添加到调用堆栈上。考虑如下列子:
function a(){
    b();
};
function b(){};
//针对上述例子, 调用堆栈是‘a’ 后面跟着‘b’。当‘b'执行完, v8就不在执行任何代码了。

回到HTTP服务器的例子:

http.createServer(function(){
  a();
});
function a(){b();};
function b(){}

在上述例子中, 一旦HTTP请求到达服务器,Node就会分发一个通知。最终,回调函数被执行,并且调用堆栈变为 “a” > "b".
由于Node是运行在单线程环境中,所以,调用堆栈展开时,node就无法处理其他的客户端或者http 请求。
node最大的并发量不就是1了,是的, node不提供真正的并行操作, 因为那样需要引入更多的并行执行线程。
关键在于, 在调用堆栈执行非常快的情况下,同一时刻你无须处理多个请求。这也是为何说v8搭配非阻塞IO是最好的组合,v8执行javascript速度非常快,非阻塞IO确保了单个线程执行时,不会因为有数据库访问或者硬盘访问等操作被挂起。

真实世界运用非阻塞IO的例子是云。在绝大多数如AWS这样的云部署系统中,操作系统都是虚拟出来的,硬件也是由租用者之间互相共享的。也就是说,假设硬盘正在为另外租用者搜索文件,而你也要进行文件搜索,那么延迟就会变长。由于硬盘IO的效率难以预测,所以,读文件时,如果把执行线程阻塞住,那么程序运行起来会非常不稳定,而且很慢。

在我们的应用中,常见的IO的例子就是从数据库中获取数据,假设我们需要为了某个请求响应数据库获取的数据。

http.createServer(funciton(req, res){
  database.getInformation(funcion(data){
        res.writeHead(200);
        res.end(data);
});
})

在上述列子中,当请求到达是,调用堆栈中只有数据库调用。由于调用是非阻塞的,当数据库IO完成时,就完全取决于事件轮询何时在初始化新的调用堆栈。不过,在告诉node“当你获取数据库响应时记得通知我”之后,Node就可以继续处理其他事情了。也就是说,node可以处理更多的请求了。

错误处理

首先,很重要的一点,node应用依托在一个拥有大量共享状态的大进程中。
举例来说,当一个http请求中,如果某个回调函数发生了错误,整个进程都会遭殃。

var http = require('http');
http.createServer(function(){
  throw new Error("错误不会被捕获")
}).listen(3000);

因为错误未被捕获,若访问web服务器,进程就会崩溃。

node之所以这样处理是因为,在发生未被捕获的错误时,进程的状态就不确定了。之后就可可能无法正常工作了,并且如果错误始终不处理的话,就会一直抛出意料之外的错误。很难调试。。。

如果添加了uncatchExceptionc处理器,就不一样了。这个时候,进程就不会退出,并且之后的事情都在你的掌握中。

process.on('uncaughtException', function(err){
  console.error(err);
  process.exit(1);
});

事件

node.js 中基础api之一就是eventEmiiter。无论是在node中还是在浏览器中,大量代码都依赖于监听或者分发的事件。

window.addEventListenner('load', function(){
  alert("窗口已加载");
});

浏览器中负责处理事件相关的DOM API 主要包括addEventListenner,removeEventListener,dispatchEvent。他们还用一系列从window 到XMLHTTPRequest等的其他对象上。
下面一个列子发起一个ajax请求。并通过监听stateChange事件来获取何时到达。

var ajax = new XMLHTTPRequest();
ajax.addEventListenner('stateChange', function(){
  if(ajax.readyState == 4 && ajax.responseText) {
    alert("we got some data: " + ajax.responseText);
  }
});
ajax.open('GET', './my-page');
ajax.send(null);

在node中,你也希望可以随处进行事件的监听和分发。为此,node暴露的event, eventapi, 该api定义了on, emit, removeListenner的方法。 它 以process.EventEmitter形式暴露出来。

eventemitter/index.js

var EventEmitter = require('events').EventEmitter;
var a = new EventEmitter;
a.on('event', function(){
    console.log("event called");
});
a.emit('event');

这个api相比dom中更简洁,node内部在使用,你也可以很容易的将其添加到自己的类中。

var EventEmitter = require("events").EventEmitter
var myClass = function(){};
myClass.prototype.__proto__ = EventEmitter.prototype;
//这样所有myclass的实例都具备了事件的功能
var a = new myClass;
a.on("某一件事情", function(){
  //做些什么事情。
});

事件是node非阻塞设计的重要体现。node通常不会直接返回数据 (因为这样可能会在等待某个资源的时候发生线程阻塞), 而是采用分发事件来传递数据方式。

我们在以http服务器为例, 当请求到达的时候,node就会调用一个回调函数,这个时候数据可能不会一下子到达。post请求(用户提交一个表达)就是这样一个例子。
当用户提交表单的时候,你通常会监听请求的data和end事件。

http.Server(function(req, res){
  var buf = '';
  req.on('data', function(data){
    buf += data;  
  });
  req.end('end', function(){
    console.log("数据接收完毕");  
  });
})

这是node中常见的例子:将请求数据内容进行缓冲(data事件),等到所有数据都接收完毕后(end事件)在对数据进行处理。

不管是否“所有数据都到达”, node为了让你能够尽快知道请求到达了服务器,都需要分发事件出来。在node中,事件机制就是一个很好的机制,能够通知你尚未发生的但即将要发生的事情。

事件是否会触发取决于实现它的api。比如,你知道了ServerRequest继承自EventEmitter。现在你也知道了它会分发data和end事件。

有写api会分发error事件,该事件也许根本不会发生。有些事件只会触发一次(如end事件), 而有些事件会触发多次(data事件), 有些api只会在特定情况下触发某件事 , 又比如在某件事触发后,某些事就不会发生。在上述http例中,你肯定不希望在end事件后,还触发data事件。否则,你的应用就会发生故障。

同样的,有的时候,会有这样的需求: 不管某个事件在将来会被触发多少次,我都希望调用一次回调函数。node为这个类需求提供了一个名字的简洁方法:

  a.once("某个事件", funtion(){
    //尽管事件会被触发多次,但此方法只会执行一次
  });

buffer

对二进制数据的处理
buffer是一个表示固定内存分配的全局对象。

buffers/index.js

var mybuffer = new Buffer('==ii1j2i3h11i23h', 'base64');
console.log(mybuffer);
require('fs').writeFile('logo.png', mybuffer);

命令行工具以及fs api

需求

我们从定义需求开始:

  • 程序需要在命令行中运行,然后通过终端提供交互给用户进行输入,输出。
  • 程序启动后,需要显示当前目录列表
  • 选择某个文件时, 程序需要显示该文件内容
  • 选择一个目录是,程序需要显示该目录下的信息
  • 运行结束后程序退出

根据上述信息,你可以将此项目分到如下几个步调:

  1. 创建模块
  2. 决定采用同步fs还是异步fs
  3. 理解什么是流
  4. 实现输入和输出
  5. 重构
  6. 使用fs进行文件交互
  7. 完成

编写首个node应用

创建模块

  • 创建一个目录 (file-explore)
  • 创建package.json

package.json

   {
       "name": "file-explore",
       //...
   }

同步还是异步

我们从声明依赖关系开始。由于stdioAPI是全局process对象的一部分,所以我们程序唯一依赖的就是fs模块。

index.js

//module dependencies
var fs = require('fs');
  • fs 是唯一同时提供同步和异步的API模块。

console.log(require('fs').readdirSync(__dirname));
他会立即返回内容,或者抛出异常

下面是异步版本

function async (err, files) {console.log(files);};
require('fs').readdir('.', async);

var fs = require('fs');
//callback 首个参数是一个err对象(如果没有错误发生,该对象为null)另外一个参数为files数组。
fs.readdir(__dirname, function(err, files){
  console.log(files);
});

理解什么是流

console.log是输出到控制台。事实上:它在指定的字符串后加上\n,并将其写到stdout流中。

console.log('hello, world')
process.stdout.write('hello, world');

process全局对象包含了3个流对象。
stdin
stdout
stderr
text terminal

key board --> stdin

display <---stderr program
<---stdout

stdin 默认的状态是暂停的,通常,执行一个程序,程序会作出一些处理然后退出。不过有些时候,就像本章的应用一样,程序需要一直处在运行的状态来接收用户输入的数据。
当恢复那个流的时候,node会观察对相应的文件描述符(unix 0),随后保持事件循环的运行,同时保持程序不退出,等待事件触发。除非有IO等待,否则node.js 总是会自动退出。
流的另外一个属性是他默认的编码,如果在流上设置了编码,那么会得到编码后的字符串。而不是原始的buffer作为事件的参数。

简而言之,当涉及持续不断的对数据进行读写时候,流就出行了

s输入和输出

index.js

  var fs = require('fs');
  fs.readdir(process.cwd(), function(err, files){
  if(!files.length) {
        console.log("no files to show!!!")
    }
  console.log("select which file or directory you want to see!!!!\n")
  function file(i){
    var filename = files[i];

    fs.stat(__dirname +"/" + filename, function(err, stat){
      if(stat.isDirectory()){
      console.log("     "  + i +  filename +"  directory ");
  }else{
      console.log(i +filename);
    }
    i ++;
  if(i == files.length){
      console.log('');
      process.stdout.write("enter your choice: ");
      process.stdin.resume();
    }else{
        file(i);
    }
  });
  }
    file(0);
});

重构

 var fs = require('fs'),
stdin = process.stdin,
stdout = process.stdout;
//called for each file walked in the directory
function file(i){
  var filename = files[i];
  fs.stat(__dirname+'/'+filename, function(err,stat){
      if(stat.isDirectory()){
            console.log(i +filename);      
      }else{
          console.log(i +filename)      
    }   
    if(++i == files.length){
          read();
    }else{file(i)}
    });
}
//read user input when files are shown
function read(){
    console.log();
    stdout.write('enter your choice:');
    stdin.resume();
    stdin.setEncoding('utf8');
    stdin.on("data", option);
}
function option(data) {
  var filename = files[Number(data)]
  if(!filename){
      console.log('enter your choice: ')  
  }else{
  stdin.pause();
   fs.readFile(__dirname+'/'+ filename, 'utf8', function(err, data){
      console.log();
      consoel.log(data);
    }) 
  };
}
var stat = [];
function file(i){
var filename = files[i];
fs.stat(.., function(err, stat){
    stat[i] = stat;
//...
});
if(stat[Number(data)].isDirectory()){
    fs.readdir(..., function(err, files){
      console.log();
      console.log(files.length )
      files.forEach(function(file){
        console.log('- ' + file);
    })
    })
}
}  

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

推荐阅读更多精彩内容

  • https://nodejs.org/api/documentation.html 工具模块 Assert 测试 ...
    KeKeMars阅读 6,313评论 0 6
  • topics: 1.The Node.js philosophy 2.The reactor pattern 3....
    宫若石阅读 1,064评论 0 1
  • Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。 众所周知,在Netscape设计出JavaScri...
    w_zhuan阅读 3,610评论 2 41
  • Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。 众所周知,在Netscape设计出JavaScri...
    Myselfyan阅读 4,066评论 2 58
  • 【宝树杂谈】:接上一篇微信,真实故事之二。犹记得多年前的一次饭局上,一位喝了酒的朋友和我发牢骚,和一起学习画画,相...
    杨宝树阅读 291评论 0 2