方便大家学习的Node.js教程(一):理解Node.js
理解Node.js
为了理解Node.js是如何工作的,首先你需要理解一些使得Javascript适用于服务器端开发的关键特性。Javascript是一门简单而又灵活的语言,这种灵活性让它能够经受住时间的考验。函数、闭包等特性使Javascript成为一门适合Web开发的理想语言。
有一种偏见认为Javascript是不可靠的,然而事实并非如此。人们对Javascript的偏见来源于DOM,DOM是浏览器厂商提供的用于Javascript与浏览器交互的API,不同浏览器厂商实现的DOM存在差异。然而,Javascript本身是一门定义清晰的语言,可以在不同的浏览器及Node.js中运行。本节,我会先介绍一些Javascript的基础以及Node.js是如何使用Javascript提供了一个性能优异的Web开发平台。
变量
Javascript使用var关键字定义变量。例如下面的代码创建了一个名为foo的变量,并在命令行中输出。(可以通过node variable.js在命令行中执行下面的代码文件。)
代码文件 variable.js
var foo = 123;
console.log(foo); // 123
Javascript运行环境(浏览器或者Node.js)通常会定义一些我们可以使用的全局变量,例如console对象,console对象包含一个成员函数log,log函数能够接受任意数量的参数并输出它们。我们接下来会遇到更多的全局对象,你将会发现,Javascript具有一个优秀的编程语言应该包含的大部分特性。
数值
Javascript支持常见的算数操作符(+,-,*,/,%)。例如下列代码:
var foo = 3;
var bar = 5;
console.log(foo+1); //4
console.log(foo / bar); //0.6
console.log(foo * bar); //15
console.log(foo - bar); //-2
console.log(foo % 2); //取余:1
布尔值
布尔值包括true和false。你可以给变量赋值为true或false,并对其进行布尔操作。例如下列代码:
var foo = true;
console.log(foo); //true
//常见的布尔操作符号: &&,||, !
console.log(true && true); //true
console.log(true && false); /false
console.log(true || false); //true
console.log(false || false); //false
console.log(!true); //false
console.log(!false); //true
数组
在Javascript中,我们可以通过[]创建数组。数组对象包含很多有用的函数,例如下列代码所示:
var foo = [];
foo.push(1); //添加到数组末尾
console.log(foo); // [1]
foo.unshift(2); //添加到数组头部
console.log(foo); // [2, 1]
//数组起始位置从0开始
console.log(foo[0]); // 2
对象字面量
Javascript中通常使用对象字面量{}创建对象,例如下列代码所示:
var foo = {};
console.log(foo); // {}
foo.bar = 123;
console.log(foo); // {bar: 123}
上面的代码在运行时添加对象属性,我们也可以在创建对象时定义对象属性:
var foo = {
bar: 123
};
console.log(foo); // {bar: 123}
对象字面量中可以嵌套其它对象字面量,例如下列代码所示:
var foo = {
bar: 123,
bas: {
bas1: 'some string',
bas2: 345
}
};
console.log(foo);
当然,对象字面量中也可以包含数组:
var foo = {
bar: 123,
bas: [1,2,3]
};
console.log(foo);
数组当中也可以包含对象字面量:
var foo = {
bar: 123,
bas: [{
qux: 1
},
{
qux: 2
},
{
qux: 3
}]
};
console.log(foo.bar); //123
console.log(foo.bas[0].qux); // 1
console.log(foo.bas[2].qux); // 2
函数
Javascript的函数非常强大,我们接下来将通过一系列的例子来逐渐了解它。
通常情况下的Javascript函数结构如下所示:
function functionName(){
//函数体
}
Javascript的所有函数都有返回值。在没有显式声明返回语句的情况下,函数会返回undefined。例如下面代码所示:
function foo(){return 123;}
console.log(foo); // 123
function bar(){ }
console.log(bar()); // undefined
立即执行函数
我们在定义函数以后立即执行它,通过括号()包裹并调用函数。如下列代码所示:
(function foo(){
console.log('foo was executed!');
})();
出现立即执行函数的原因是为了创建新的变量作用域。if、else、while不会创建新的变量作用域,如下列代码所示:
var foo = 123;
if(true){
var foo = 456;
}
console.log(foo); // 456
在Javascrit中,我们通过函数创建新的变量作用域,例如使用立即执行函数:
var foo = 123;
if(true){
(function(){
var foo = 456;
})();
}
console.log(foo); // 123
在上面的代码中,我们没有给函数命名,这被称为匿名函数。
匿名函数
没有名字的函数被称为匿名函数。在Javascript中,我们可以把函数赋值给变量,如果准备将函数当作变量使用,就不需要给函数命名。下面给出了两种等价的写法:
var foo1 = function nameFunction(){
console.log('foo1');
}
foo1(); // foo1
var foo2 = function(){
console.log('foo2');
}
foo2(); // foo2f
据说如果一门编程语言能够把函数当作变量来对待,它就是一门优秀的编程语言,Javascript做到了这一点。
高阶函数
由于Javascript允许我们将函数赋值给变量,所以我们可以将函数作为参数传递给其它函数。将函数作为参数的函数被称为高阶函数。setTimeout就是常见的高阶函数。
setTimeout(function(){
console.log('2000 milliseconds have passed since this demo started');
}, 2000);
如果在Node.js中运行上面的代码,会看到命令窗口2秒钟后输出信息。在上面的代码中,我们传递了一个匿名函数作为setTimeout的第一个参数。我们也可以传递一个普通的函数:
function foo(){
console.log('2000 milliseconds have passed since this demo started');
}
setTimeout(foo, 200);
现在,我们已经了解了对象字面量和函数,接下来我们会了解闭包的概念。
闭包
闭包是能够访问其它函数内部变量的函数。如果在函数内部定义另一个函数,内部函数能够访问外部函数的变量,这就是闭包的常见形式。我们会通过一些例子来解释。
在下面的代码中,你可以看到内部函数能够访问外部函数的变量:
function outerFunction(arg){
var variableInOuterFunction = arg;
function bar(){
console.log(variableInOuterFunction);
}
bar();
}
outerFunction('hello closure!'); // hello closure!
令人惊喜的是:内部函数在外部函数返回之后依然可以访问外部函数作用域中的变量。这是因为,变量仍然被绑定于内部函数,不依赖于外部函数。例如:
function outerFunction(arg){
var variableInOuterFunction = arg;
return function(){
console.log(variableInOuterFunction);
}
}
var innerFunction = outerFunction('hello closure!');
innerFunction(); // hello closure!
现在,我们已经了解了闭包,接下来,我们会探究一下使Javascript成为一门适合服务器端编程的语言的原因。
Node.js性能
Node.js致力于开发高性能应用程序。接下来的部分,我们会介绍大规模I/O问题,并分别展示传统方式及Node.js是如何解决这个问题的。
大规模I/O问题
大多数Web应用通过硬盘或者网络(例如查询另一台机器的数据库)获取数据,从硬盘或网络获取数据的速度远远慢于CPU的处理周期。当收到一个HTTP请求以后,我们需要从数据库获取数据,请求会一直等待直到获取数据完成。这些创建的连接和还未结束的请求会消耗服务器的资源(内存和CPU)。为了使同一台Web服务器能够处理大规模请求,我们需要解决大规模I/O问题。
每一个请求创建一个进程
传统的Web服务器为每一个请求创建一个新的进程,这是一种对内存和CPU开销都很昂贵的操作。PHP最开始就是采用的这种方法。在等待响应期间,进程仍然会消耗资源,并且进程的创建更慢。所以现代Web应用大多使用线程池的方法。
线程池
现代Web服务器使用线程池来处理每个请求。线程和进程相比,更加轻量级。在创建线程池以后,我们就不再需要为开始或结束进程而付出额外代价。当收到一个请求,我们为它分配一个线程。然而,线程池仍然会浪费一些资源。
单线程模式
我们知道为请求分别创建进程或者线程会导致系统资源浪费。与之相对,Node.js采取了单线程来处理请求。单线程服务器的性能优于线程池服务器的理念并不是Node.js首创,Nginx也是基于这种理念。Nginx是一种单线程服务器,能够处理极大数量的并发请求。
Javascript是单线程的,如果你有一个耗时操作(例如网络请求),就必须使用回调。下面的代码使用setTimeout模拟了一个耗时操作,可以用Node.js执行。
function longRunningOperation(callback){
setTimeout(callback, 3000);
}
function UserClicked(){
console.log('starting a long operation');
longRunningOperation(function(){
console.log('ending a long operation');
})
}
UserClicked();
让我们模拟一下Web请求:
function longRunningOperation(callback){
setTimeout(callback, 3000);
}
function webRequest(request){
console.log('starting a long operation for request:', request.id);
longRunningOperation(function(){
console.log('ending a long operation for request:', request.id);
});
}
webRequest({id: 1});
webRequest({id: 2});
//输出
//starting a long operation for request: 1
//starting a long operation for request: 2
//ending a long operation for request: 1
//ending a long operation for request: 2
更多的Node.js细节
Node.js的核心是一个event loop。event loop使得任何用户图形界面应用程序可以在任何操作系统中工作。当事件被触发时(例如:用户点击鼠标),操作系统调用程序的某个函数,程序执行函数中的代码。之后,程序准备响应已经在队列中的事件或尚未出现的事件。
线程饥饿
通常,在GUI程序中,当由一个事件调用的函数执行期间,其它事件不会被处理。因此,当你在相关函数中执行耗时操作时,GUI会变得无响应。这种CPU资源的短缺被成为饥饿。
Node.js基于和GUI应用程序相同的event loop原则。因此,它也会面临饥饿的问题。为了帮助更好的理解,我们通过几个例子来说明:
console.time('timer');
setTimeout(function(){
console.timeEnd('timer'); //timer: 1002.615ms
}, 1000)
运行这段代码,与我们期望的相同,终端显示的数字在1000ms左右。
接下来我们想写一段耗时更长的代码,例如一个未经优化的计算Fibonacci数列的方法:
console.time('timeit');
function fibonacci(n){
if(n<2){
return 1;
}else{
return fibonacci(n-2) + fibonacci(n-1);
}
}
fibonacci(44);
console.timeEnd('timeit'); //我的电脑耗时 11863.331ms,每台电脑会有差异
现在我们可以模拟Node.js的线程饥饿。setTimeout用于在指定的时间以后调用函数,如果我们在函数调用以前,执行一个耗时方法,由于耗时方法占用CPU和Javascript线程,setTimeout指定的函数无法被及时调用,只能等待耗时方法运行结束以后被调用。例如下面的代码:
function fibonacci(n){
if(n<2){
return 1;
}else{
return fibonacci(n-2) + fibonacci(n-1);
}
}
console.time('timer');
setTimeout(function(){
console.timeEnd('timer'); // 输出时间会大于 1000ms
}, 1000)
fibonacci(44);
所以,如果你面临CPU密集型场景,Node.js并不是最佳选择,但也很难找到其它合适的平台。但是Node.js非常适用于I/O密集型场景。
数据密集型应用
Node.js适用于I/O密集型。单线程机制意味着Node.js作为Web服务器会占用更少的内存,能够支持更多的请求。与执行代码相比,从数据库获取数据需要花费更多的时间。下图展示了传统的线程池模型的服务器是如何处理用户请求的:

Node.js服务器处理请求的方式如下图。因为所有的工作都在单线程内完成,所以消耗更少的内存,同时因为不需要切换线程,所以CPU负载更小。

V8 Javascript引擎
Node.js中的所有Javascript通过V8 Javascript引擎执行。V8产生于谷歌Chrome项目,V8在Chrome中用于运行Javascript。V8不仅速度更快,而且很容易被集成到其它项目。
更多的Javascript
精通Javascript使得Node.js开发者不仅能够写出更加容易维护的项目,而且能够利用到Javascript生态链的优势。
默认值
Javascript变量的默认值是undefined。如下列代码所示:
var foo;
console.log(foo); //undefined
变量不存在的属性也会返回undefined
var foo = {bar: 123};
console.log(foo.bar); // 123
console.log(foo.bas); // undefined
全等
需要注意Javascript当中 ==与===的区别。==会对变量进行类型转换,===不会。推荐的用法是总是使用===。
console.log(5 == '5'); // true
console.log(5 === '5'); // false
null
null是一个特殊的Javascript对象,用于表示空对象。而undefined用于表示变量不存在或未初始化。我们不需要给变量赋值为undefined,因为undefined是变量的默认值。
透露模块模式
透露模块模式的关键在于Javascript对闭包的支持以及能够返回任意对象的能力。如下列代码所示:
function printableMessage(){
var message = 'hello';
function setMessage(newMessage){
if(!newMessage) throw new Error('cannot set empty message');
message = newMessage;
}
function getMessage(){
return message;
}
function printMessage(){
console.log(message);
}
return {
setMessage: setMessage,
getMessage: getMessage,
printMessage: printMessage
};
}
var awesome1 = printableMessage();
awesome1.printMessage(); //hello
var awesome2 = printableMessage();
awesome2.setMessage('hi');
awesome2.printMessage(); // hi
awesome1.printMessage(); //hello
理解this
this总是指向调用函数的对象。例如:
var foo = {
bar: 123,
bas: function(){
console.log('inside this.bar is: ', this.bar);
}
}
console.log('foo.bar is:', foo.bar); //foo.bar is: 123
foo.bas(); //inside this.bar is: 123
由于函数bas被foo对象调用,所以this指向foo。如果是纯粹的函数调用,则this指向全局变量。例如:
function foo(){
console.log('is this called from globals? : ', this === global); //true
}
foo();
如果我们在浏览器中执行上面的代码,全局变量global会变为window。
如果函数的调用对象改变,this的指向也会改变:
var foo = {
bar: 123
};
function bas(){
if(this === global){
console.log('called from global');
}
if(this === foo){
console.log('called from foo');
}
}
//指向global
bas(); //called from global
//指向foo
foo.bas = bas;
foo.bas(); //called from foo
如果通过new操作符调用函数,函数内的this会指向由new创建的对象。
function foo(){
this.foo = 123;
console.log('Is this global? : ', this == global);
}
foo(); // Is this global? : true
console.log(global.foo); //123
var newFoo = new foo(); //Is this glocal ? : false
console.log(newFoo.foo); //123
通过上面代码,我们可以看到,在通过new调用函数时,函数内的this指向发生改变。
理解原型
Javascript通过new操作符及原型属性可以模仿面向对象的语言。每个Javascript对象都有一个被称为原型的内部链接指向其他对象。
当我们调用一个对象的属性,例如:foo.bar,Javascript会检查foo对象是否存在bar属性,如果不存在,Javascript会检查bar属性是否存在于foo._proto_,以此类推,直到对象不存在_proto_。如果在任何层级发现属性的值,则立即返回,否则,返回undefined。
var foo ={};
foo._proto_.bar = 123;
console.log(foo.bar); //123
当我们通过new操作符创建对象时,对象的_proto_会被赋值为函数的prototype属性,例如:
function foo(){};
foo.prototype.bar = 123;
var bas = new foo();
console.log(bas._proto_ === foo.prototype); //true
console.log(bas.bar);
函数的所有实例共享相同的prototype
function foo(){};
foo.prototype.bar = 123;
var bas = new foo();
var qux = new foo();
console.log(bas.bar); //123
console.log(qux.bar); //123
foo.prototype.bar = 456;
console.log(bas.bar); //456
console.log(qux.bar); //456
只有当属性不存在时,才会访问原型,如果属性存在,则不会访问原型。
function foo(){};
foo.prototype.bar = 123;
var bas = new foo();
var qux = new foo();
bas.bar = 456;
console.log(bas.bar);//456
console.log(qux.bar); //123
上面的代码表明,如果修改了bas.bar, bas._proto_.bar就不再被访问。
错误处理
Javascript的异常处理机制类似其它语言,通过throw关键字抛出异常,通过catch关键字捕获异常。例如:
try{
console.log('About to throw an error');
throw new Error('Error thrown');
}
catch(e){
console.log('I will only execute if an error is thrown');
console.log('Error caught: ', e.message);
}
finally{
console.log('I will execute irrespective of an error thrown');
}
总结
本章,我们介绍了一些Node.js及Javascript的重要概念,知道了Node.js适用于开发数据密集型应用程序。下章我们将开始介绍如何使用Node.js开发应用程序。
学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入学习交流群
343599877,我们一起学前端!
方便大家学习的Node.js教程(一):理解Node.js的更多相关文章
- Node.js 教程 01 - 简介、安装及配置
系列目录: Node.js 教程 01 - 简介.安装及配置 Node.js 教程 02 - 经典的Hello World Node.js 教程 03 - 创建HTTP服务器 Node.js 教程 0 ...
- node.js零基础详细教程(4):node.js事件机制、node异步IO操作
第四章 建议学习时间3小时 课程共10章 学习方式:详细阅读,并手动实现相关代码 学习目标:此教程将教会大家 安装Node.搭建服务器.express.mysql.mongodb.编写后台业务逻辑. ...
- node.js零基础详细教程(7):node.js操作mongodb,及操作方法的封装
第七章 建议学习时间4小时 课程共10章 学习方式:详细阅读,并手动实现相关代码 学习目标:此教程将教会大家 安装Node.搭建服务器.express.mysql.mongodb.编写后台业务逻辑. ...
- Node.js 教程
简单的说 Node.js 就是运行在服务端的 JavaScript. Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台. Node.js是一个事件驱动I/O服务端Ja ...
- [转载]Node入门 » 一本全面的Node.js教程
http://www.nodebeginner.org/index-zh-cn.html 作者: Manuel Kiessling 翻译: goddyzhao & GrayZhang & ...
- 【Todo】React & Nodejs学习 &事件驱动,非阻塞IO & JS知识栈:Node为主,JQuery为辅,Bootstrap & React为辅辅,其他如Angular了解用途即可
JS知识栈:Node为主,JQuery为辅,Bootstrap & React为辅辅,其他如Angular了解用途即可 今天在学习ReactJS和NodeJS,看到关于ReactJS的这篇文章 ...
- 10+ 最佳的 Node.js 教程和实例
如果你正在找Node.js的学习资料及指南,那么请继续(阅读),我们的教程将会覆盖即时聊天应用.API服务编写.投票问卷应用.人物投票APP.社交授权. Node.js on Raspberry Pi ...
- Node.js 教程 04 - 模块系统
前言: Node.js的模块系统类似于C/C++的文件引用,可以声明对象,也可以定义类 创建对象. 大家这么理解,就简单了. 定义: 为了让Node.js的文件可以相互调用,Node.js提供了一个简 ...
- Node.js教程系列~目录
Node.js这个东西在近几年火起来了,而且会一直火下去,无论在infoq还是在cnblogs,csdn上,都可以到处看到它的样子,它主推的应该就是异步式I/O 吧,是的,设计的很完美,很吸引人,虽然 ...
随机推荐
- Git无法删除文件问题:fatal: pathspec 'readme.txt' did not match any files
在使用Git时,不小心创建了一个不需要的文件,想要删除一个文件时,出现了错误: fatal: pathspec 'readme.txt' did not match any files 原因是新建的这 ...
- 【刷题】洛谷 P3901 数列找不同
题目描述 现有数列 \(A_1,A_2,\cdots,A_N\) ,Q 个询问 \((L_i,R_i)\) , \(A_{Li} ,A_{Li+1},\cdots,A_{Ri}\) 是否互不相同 输入 ...
- [AHOI2009]中国象棋 DP,递推,组合数
DP,递推,组合数 其实相当于就是一个递推推式子,然后要用到一点组合数的知识 一道很妙的题,因为不能互相攻击,所以任意行列不能有超过两个炮 首先令f[i][j][k]代表前i行,有j列为一个炮,有k列 ...
- 【BZOJ3240】【NOI2013】矩阵游戏(数论)
[BZOJ3240][NOI2013]矩阵游戏(数论) 题面 BZOJ 题解 搞什么矩阵十进制快速幂加卡常? 直接数学推导不好吗? 首先观察如何从每一行的第一个推到最后一个 \(f[i]=a·f[i- ...
- HDU.1575 Tr A ( 矩阵快速幂)
HDU.1575 Tr A ( 矩阵快速幂) 点我挑战题目 题意分析 直接求矩阵A^K的结果,然后计算正对角线,即左上到右下对角线的和,结果模9973后输出即可. 由于此题矩阵直接给出的,题目比较裸. ...
- 如何通过反射来创建对象?getConstructor()和getDeclaredConstructor()区别?
1. 通过类对象调用newInstance()方法,适用于无参构造方法: 例如:String.class.newInstance() public class Solution { public st ...
- stout代码分析之十:c++11之move和forward
stout中大量使用了c++11的特性,而c++11中move和forward大概是最神奇的特性了. 左值和右值的区别 ; // a是左值,0是右值 int b = rand(); // b是左值,r ...
- uboot 的命令体系
1.代码位置 (1)uboot命令体系的实现代码在uboot/common/cmd_xxx.c中.有若干个.c文件和命令体系有关.(还有command.c main.c也是和命令有关的) 2.传参方 ...
- dp,状压dp等 一些总结
也就作业几题而已,分析一下提醒 最重要的就是,记住,没用的状态无论怎么转移最后都会是没用的状态,所以每次转移以后的有值的状态都是有用的状态. 几种思考方向: 第一种:枚举当前的状态,转移成另外一个状态 ...
- [洛谷P4768] [NOI2018]归程 (kruskal重构树模板讲解)
洛谷题目链接:[NOI2018]归程 因为题面复制过来有点炸格式,所以要看题目就点一下链接吧\(qwq\) 题意: 在一张无向图上,每一条边都有一个长度和海拔高度,小\(Y\)的家在\(1\)节点,并 ...