理解Node.js 為了理解Node.js是如何工作的,首先你需要理解一些使得Javascript適用於伺服器端開發的關鍵特性。Javascript是一門簡單而又靈活的語言,這種靈活性讓它能夠經受住時間的考驗。函數、閉包等特性使Javascript成為一門適合Web開發的理想語言。 有一種偏見認為J ...
理解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,我們一起學前端!