《Node.js開發指南》知識整理


Node.js簡介

Node是一個可以讓JavaScript運行在服務器端的平台,拋棄了傳統平台依靠多線程來實現高並發的設計思路,而采用單線程、異步式I/O、事件驅動式的程序設計模型。

安裝和配置Node.js

安裝配置簡單,無需多說。

Node.js快速入門

異步式I/O與事件式編程

回調函數

用異步的方式讀取一個文本內容為“test content”的文件,代碼如下:

var fs = require("fs");

fs.readFile("test.txt", "utf-8", function(err, data) {
	console.log(data);
});

console.log("end");

運行的結果:

end
test content

使用同步的方式讀取文件:

var fs = require("fs");

console.log(fs.readFileSync("test.txt", "utf-8"));
console.log("end");

運行結果:

test content
end

同步的方式是阻塞線程等待文件讀取完畢后,將文件內容輸出,然后才繼續執行下一步代碼,因此控制台先打印“test content”,然后再打印“end”。

而異步式I/O是通過回調函數來實現的,通過將控制台輸出文件內容的函數作為參數傳給fs.readFile函數,fs.readFile調用時只是將異步式I/O請求發送給操作系統,然后立即返回並執行后面的語句,當fs接收到I/O請求完成的事件時,才會調用回調函數。因此我們先看到“end”,再看到“test.txt”的內容。

事件

Node.js所有的異步I/O操作在完成時都會發送一個事件到事件隊列。事件由EventEmitter提供,前面提到的fs.readFile的回調函數就是通過EventEmitter來實現。

模塊和包

Node提供了require函數來調用其他模塊。node中的模塊和包並沒有本質的區別,包可以理解為某個功能模塊的集合。

什么是模塊

模塊是node應用程序的基本組成部分,文件和模塊是一一對應的。一個node.js文件就是一個模塊,這個文件可能是JavaScript代碼、JSON或者編譯過的C/C++擴展。

創建及加載模塊

node提供了exports和require兩個對象,其中exports是模塊的公開接口,require用於從外部獲取一個模塊的接口。

新建一個myModule.js文件,輸入如下代碼:

var text;

function setText(myText){
	text = myText;
}

function printText(){
	console.log(text);
}

exports.setText = setText;
exports.printText = printText;

再新建一個test.js文件:

var myModule = require("./myModule");

myModule.setText("Hello world!");
myModule.printText();

運行test.js,結果為:

Hello world!

單次加載

模塊的調用與創建一個對象不同,require不會重復加載模塊。

修改test.js:

var myModule1 = require("./myModule");
myModule1.setText("Hello world!");

var myModule2 = require("./myModule");
myModule2.setText("Hi baby!");

myModule1.printText();
myModule2.printText();

運行結果:

Hi bayby!
Hi bayby!

這是因為模塊不會重復加載,myModule1和myModule2指向的都是同一個實例,因此myModule1的text值就被myModule2覆蓋了。

創建包

包是模塊基礎上更深一步的抽象,類似於C/C++的函數庫或者java/.net的類庫。

node包是一個目錄,其中包括一個JSON格式的包說明文件package.json。node對包的要求並不嚴格,但還是建議制作包時遵循CommonJS規范。

建立一個名為mypackage的文件夾,在文件夾中新建index.js:

exports.hello = function(){
	console.log("Hello world");
};

然后在mypackage之外建立test.js:

var mypackage = require("./mypackage");

mypackage.hello();

這就是一個簡單的包了,通過定制package.json,我們可以創建更復雜的包。

定制package.json:

  1. 在mypackage文件夾下創建一個package.json文件和一個lib子文件夾。

  2. 將index.js重命名為interface.js並放入lib文件夾。

  3. 打開package.json並輸入如下json文本:

    {
    "main": "./lib/interface.js"
    }

運行test.js,依然可以看到控制台輸出“Hello world”。

node調用某個包時,會首先檢查package.json文件的main字段,將其作為包的接口模塊,如果package.json或main字段不存在,就會去尋找index.js或index.node作為包的接口。

node.js包管理器

本地模式和全局模式

npm默認為本地模式,會從http://npmjs.org搜索或下載包 ,將包安裝到當前目錄的node_modules
子目錄下。

也可以使用全局安裝,它會將包安裝在系統目錄:

“npm [install/i] -g [package_name]”

本地模式可以直接通過require使用,但是不會注冊環境變量。
全局模式不能直接通過require使用,但是它會注冊環境變量。

通過npm link命令可以在本地包和全局包之間創建符號鏈接,使全局包可以通過require使用,例如要require全局模式安裝的express,可以在工程目錄下運行:

npm link express

使用npm link命令還可以將本地包鏈接到全局,但是npm link命令不支持windows

調試

命令行調試

例如要調試test.js,則在命令行中輸入:

node debug test.js

以下為基本的調試命令:

run: 執行腳本,在第一行暫停
restart: 重新執行腳本
cont, c: 繼續執行,直到遇到下一個斷點
next, n: 單步執行
step, s: 單步執行並進入函數
out, o: 從函數中步出
setBreakpoint(), sb(): 在當前行設置斷點
setBreakpoint(‘f()’), sb(...): 在函數f的第一行設置斷點
setBreakpoint(‘script.js’, 20), sb(...): 在 script.js 的第20行設置斷點
clearBreakpoint, cb(...): 清除所有斷點
backtrace, bt: 顯示當前的調用棧
list(5): 顯示當前執行到的前后5行代碼
watch(expr): 把表達式 expr 加入監視列表
unwatch(expr): 把表達式 expr 從監視列表移除
watchers: 顯示監視列表中所有的表達式和值
repl: 在當前上下文打開即時求值環境
kill: 終止當前執行的腳本
scripts: 顯示當前已加載的所有腳本
version: 顯示 V8 的版本

遠程調試

使用以下語句之一可以打開調試服務器:

node --debug[=port] test.js	//腳本正常執行,調試客戶端可以連接到調試服務器。
node --debug-brk[=port] test.js	//腳本暫停執行等待客戶端連接。

如不指定端口,則默認為5858。

客戶端:

node debug 127.0.0.1:5858

node-inspector

安裝node-inspector:

npm install -g node-inspector

在終端連接調試服務器:

node --debug-brk=5858 test.js

啟動node-inspector:

node-inspector

在瀏覽器中打開http://127.0.0.1:8080/debug?port=5858。

Node.js核心模塊

全局對象

在瀏覽器javascript中,通常window就是全局對象,而在node中全局對象是global,它是全局變量的宿主,全局變量是它的屬性。

process

用於描述當前node進程狀態,提供一個與操作系統的簡單接口。

常用成員方法

process.argv:
命令行參數數組,第一個元素是node,第二個元素是腳本文件名,從第三個元素開始每個元素都是一個運行參數。
process.stdout:
標准輸出流,通常我們使用的console.log()向標准輸出打印字符,而process.stdout.write()函數提供更底層的接口。
process.stdin:
標准輸入流。

其他成員方法

process.platform; process.pid; process.execPath; process.memoryUsage()

console

用於提供控制台標准輸出。

常用成員方法

console.log():
向標准流打印字符並以換行符結束,可以使用類似C語言printf()命令的格式輸出。
console.error():
向標准錯誤流輸出。
console.trace():
向標准錯誤流輸出當前的調用棧。

常用工具util

用於提供常用函數集合。

util.inherits(constructor, superConstructor)

實現對象間原型繼承的函數。

var util = require("util");

function Base() {
	this.name = "base";
	this.base = 1991;
	
	this.sayHello = function(){
		console.log("hello " + this.name);
	};
}

Base.prototype.showName = function(){
	console.log(this.name);
};

function Sub(){
	this.name = "sub";
}

util.inherits(Sub, Base);

var objBase = new Base();
objBase.showName();
objBase.sayHello();
console.log(objBase);

var objSub = new Sub();
objSub.showName();
console.log(objSub);

輸出結果:

base
Hello base
{ name: 'base', base: 1991, sayHello: [Function] }
sub
{ name: 'sub' }

Sub只繼承了Base在原型中定義的函數,而函數內部的base屬性和sayHello函數都沒有被繼承。而且,在原型中定義的屬性不會被console.log()作為對象的屬性輸出。

util.inspect(object, [showHidden], [depth], [colors])

將任意對象轉換為字符串,通常用於調試和錯誤輸出。
object:要轉換的對象。
showHidden:如果為true,將會輸出更多隱藏信息。
depth:表示最大遞歸的層數,默認會遞歸2層,指定為null表示不限制最大遞歸數完整遍歷對象。
color:如果為true,輸出格式將會以ANSI顏色編碼。
util.inspect不會簡單的直接將對象轉換成字符成,即便是對象定義了toString方法也不會調用。

其他函數

util.isArray(); util.isRegExp(); util.isDate(); util.isError(); util.format(); util.debug();

事件驅動events

events是node最重要的模塊,因為node本身架構就是事件式的,而它提供了唯一的接口。

事件發射器

events.EventEmitter的核心就是事件發射與事件監聽器功能的封裝。

var events = require("events");

var emitter = new events.EventEmitter();

emitter.on("testEvent", function( arg1, arg2){
	console.log(arg1, arg2);
});

emitter.emit("testEvent", "test", 234);

以上為EventEmitter基本用法。

常用API:
EventEmitter.on(event, listener):
為指定事件注冊一個監聽器,接收一個字符串event和一個回調函數listener。
EventEmitter.emit(event, [arg1], [arg2], [...]):
發射event事件。
EventEmitter.once(event, listener):
為指定事件注冊一個單次監聽器,監聽器觸發后立刻解除。
EventEmitter.removeListener(event, listener):
移除指定事件的某個監聽器。
EventEmitter.removeAllListener([event]):
移除所有事件的所有監聽器,如果指定event,則移除指定事件的所有監聽器。

error事件

遇到異常時通常會發射error事件,當error被發射時,如果沒有響應的監聽器,Node會把它當做異常,退出程序並打印調用棧。

繼承EventEmitter

大多數時候不要直接使用EventEmitter,而是在對象中繼承它。這樣做使某個實體功能的對象實現事件符合語義,而且javascript的對象機制是基於原型的,支持部分多重繼承。

文件系統

fs模塊是文件操作的封裝,fs模塊所有操作都提供了異步和同步兩個版本。
fs.readFile(filename, [encoding], [callback(err, data)]):
讀取文件,默認以二進制模式讀取。
fs.readFileSync(filename, [encoding]):
以同步方式讀取文件,讀取的文件內容以返回值的形式返回。
fs.open(path, flags, [mode], [callback(err, fd)]):
其中flags可以是以下值:

  • r:以讀取模式打開文件。
  • r+:讀寫。
  • w:寫入,如果文件不存在則創建。
  • w+:讀寫,如果文件不存在則創建。
  • a:追加,如果文件不存在則創建。
  • a+:讀取追加,如果文件不存在則創建。

mode參數用於創建文件時給文件指定權限,默認為0666。回調函數將會傳遞一個文件描述符fd。
fs.read(fd, buffer, offset, length, postion, [callback(err, bytesRead)]):
從指定的文件描述符fd中讀取數據並寫入buffer指向的緩沖區對象。offset是buffer的寫入偏移量。length是要從文件中讀取的字節數。position是文件讀取的起始位置,如果為null,則從當前文件指針的位置讀取。回調函數傳遞bytesRead和buffer,分別表示讀取的字節數和緩沖區對象。

HTTP服務器和客戶端

node提供了http模塊。http.server是一個基於事件的HTTP服務器,http.request則是一個HTTP客戶端工具。

HTTP服務器

http.server是HTTP服務器對象。
http.createServer(callback(request, response)):
創建一個http.server的實例,將一個函數作為HTTP請求處理函數。

http.sever的事件

request:
當客戶端請求到來時,該事件被觸發,提供兩個參數http.ServerRequest和http.ServerResponse的實例。事實上http.createServer的顯示實現方式就是request事件:

var http = require('http');
var server = new http.Server();
server.on('request', function(req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.write('<h1>Node.js</h1>');
res.end('<p>Hello World</p>');
});
server.listen(3000);

connection:
當TCP連接建立時,該事件被觸發,提供一個socket參數,是net.Socket的實例。客戶端在Keep-Alive模式下可能會在同一個連接內發送多次請求。
close:
當服務器關閉時,該事件觸發。

http.ServerRequest

是HTTP請求的信息。一般由http.server的request事件發送,作為第一個參數傳遞。

HTTP請求一般可以分為兩部分:請求頭(Reqeust Header)和請求體(Request Body)。http.ServerRequest提供了以下3個事件用於控制Request Body傳輸:

  • data:當請求體數據到來時,該事件觸發。事件提供一個參數chunk,表示接收到的數據。該事件可能被調用多次。
  • end:當請求體數據傳輸完成時,該事件觸發。
  • close:用戶當前請求結束時,該事件觸發。當用戶強制終止傳輸,也還是調用close。

ServerRequest的屬性:

  • complete:客戶端請求是否已經完成。
  • httpVersion:HTTP協議版本。
  • method:HTTP請求方法,如GET、POST、PUT等。
  • url:原始的請求路徑。
  • headers:HTTP請求頭。
  • trailers:HTTP請求尾。
  • connection:當前HTTP連接套接字,為net.Socket的實例。
  • socket:connection屬性的別名。
  • client:client屬性的別名。

http.ServerResponse

返回給客戶端的信息,由http.Server的request事件發送,作為第二個參數傳遞。

成員函數:
** response.writeHead(statusCode, [headers]):**
向請求的客戶端發送響應頭。statusCode是HTTP狀態碼。該函數在一次請求中最多只能調用一次。
response.write(data, [encoding]):
向請求的客戶端發送響應內容。data是一個buffer或字符串,表示要發送的內容。如果data是字符串,需要指定encoding來說明它的編碼方式,默認為utf-8。
response.end([data],[encoding]):
結束響應,當所有要返回的內容發送完畢的時候,該函數必須被調用一次,否則客戶端永遠處於等待狀態。

HTTP客戶端

http.request(options, callback):
發起HTTP請求,返回一個http.ClientRequest的實例。callback是請求的回調函數,傳遞一個參數為http.ClientResponse的實例。option常用的參數如下:

  • host:請求網站的域名或IP地址。
  • port:請求網站的端口,默認80。
  • method:請求方法,默認GET。
  • path:請求相對於根的路徑,默認是“/”,包含QueryString,例:“/search?s=sb”。
  • headers:請求頭對象。

http.get(options, callback):
http.request的簡化版,用於處理GET請求,同時不需要手動調用req.end()。
http.clientRequest:
表示一個已經產生而且正在進行的HTTP請求。提供一個response事件。http.clientRequest函數:

  • request.abort():終止正在發送的請求。
  • request.setTimeout(timeout, [callback]):設置請求超時事件,超時后callback將被調用。

http.clientResponse:
與http.ServerResponse類似,提供了三個事件data、end和close。http.clientResponse特殊函數:

  • response.setEncoding([encoding]):設置編碼方式,默認null,以buffer形式存儲。
  • response.pause():暫停接收數據和發送事件。
  • response.resume():從暫停狀態恢復。

使用Node.js進行Web開發

Express

安裝

運行命令:

npm install -g express

如果npm安裝慢的話可以指定中國鏡像(更新比官方慢,發布包時需要切回來):

npm config set registry https://registry.npm.taobao.org 
npm info underscore  

windows下可能還需要安裝express-generator:

npm install -g express-generator

建立工程

進入工程目錄,建立工程:

express -e projectName

控制台輸出命令提示還要進入工程目錄,執行npm install,按照提示執行后,node根據package.json文件自動安裝了ejs。無參數的 npm install 的功能就是檢查當前目錄下的 package.json,並自動安裝所有指定的依賴。

啟動服務器。express 4.x已經不能用之前的node app.js來啟動了,而應該使用:

npm start

要使用supervisor監控文件更改自動重啟服務器,可以使用:

supervisor ./bin/www

工程結構

app.js

app.js是工程的入口,用var app = express()創建一個應用實例,app.set(key, value)是Express的參數設置工具,可用參數如下:

  • basepath:基礎地址。
  • views:視圖文件的目錄,存放模板文件。
  • view engine:視圖模板引擎。
  • view options:全局視圖參數對象。
  • view cache:啟用視圖緩存。
  • case sensitive routes:路徑區分大小寫。
  • strict routing:嚴格控制,啟用后不會忽略路徑末尾的“/”。
  • jsonp callback:開啟透明的JSONP支持。

Express依賴於connect,提供大量中間件,可以通過app.use啟用。

routes/index.js

路由文件,相當於MVC中的控制器。

index.ejs

模板文件。

路由控制

路徑匹配

可以給路由配置規則,如:

router.get('/:username', function(req, res, next) {
  res.send(req.params.username);
});

路徑規則還支持JavaScript正則表達式,如:app.get(/user/([^/]+)/?, callback),但是這種方式參數是匿名的,因此需要通過req.params[0]這樣的方式獲取。

REST風格的路由規則

HTTP協議定義了以下8種標准的方法:

  • GET:請求獲取指定資源。
  • HEAD:請求指定資源的響應頭。
  • POST:向指定資源提交數據。
  • PUT:請求服務器存儲一個資源。
  • DELETE:請求服務器刪除指定資源。
  • TRACE:回顯服務器收到的請求,主要用於測試或診斷。
  • CONNECT:HTTP/1.1 協議中預留給能夠將連接改為管道方式的代理服務器。
  • OPTIONS:返回服務器支持的HTTP請求方法。

我們常用到的是四種,根據REST設計模式,這四種分別用於以下功能:

  • GET:獲取
  • POST:新增
  • PUT:更新
  • DELETE:刪除

Express支持所有HTTP請求的綁定函數,如:app.get(path, callback)、app.post(path, callback)等。
其中還包括一個app.all(path, callback)函數,它支持吧所有的請求方式綁定到同一個響應函數。

Express還支持同一路徑綁定多個路由響應函數,例:

var express = require('express');
var router = express.Router();

/* GET users listing. */
router.all('/:username', function(req, res, next) {
  res.send("all");
});

router.get('/:username', function(req, res, next) {
  res.send(req.params.username);
});

module.exports = router;

如果訪問路徑同時匹配多個規則時,會優先匹配先定義的路由規則,請求總會被前一條路由規則捕獲,后一條被忽略,因此上面代碼將輸出“all”。
但是也可以調用next,會將路由優先給后面的規則:

var express = require('express');
var router = express.Router();

/* GET users listing. */
router.all('/:username', function(req, res, next) {
  console.log("all");
  next();
  res.send("all");      
});

router.get('/:username', function(req, res, next) {
  res.send(req.params.username);
});

module.exports = router;

這時回先在控制台打印“all”,然后被next()函數轉移,接着被第二條規則捕獲,向瀏覽器發送信息。
這一點非常有用,可以讓我們非常輕松實現中間件,提高代碼的復用,如:

var users = {
    'byvoid': {
        name: 'Carbo',
    }
};
app.all('/user/:username', function (req, res, next) {
    // 檢查用戶是否存在
    if (users[req.params.username]) {
        next();
    } else {
        next(new Error(req.params.username + ' does not exist.'));
    }
});
app.get('/user/:username', function (req, res) {
    // 用戶一定存在,直接展示
    res.send(JSON.stringify(users[req.params.username]));
});
app.put('/user/:username', function (req, res) {
    // 修改用戶信息
    res.send('Done');
});

模板引擎

這里使用的ejs引擎,設置視圖的路徑及模板引擎的類型:

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

在routers/index.js下使用render調用:

  res.render('index', { title: 'Index' });

ejs只有3種標簽:

  • <% code %>:JavaScript 代碼。
  • <%= code %>:顯示替換過 HTML 特殊字符的內容。
  • <%- code %>:顯示原始 HTML 內容。

MongoDB

開源的NoSQL數據庫,適合數據規模大、事務性不強的場合。
NoSQL是Not Only SQL的簡稱,主要指非關系型、分布式、不提供ACID的數據庫系統。

MongoDB是一個對象數據庫,沒有表、行等概念,也沒有固定的模式和結構,所有數據以文檔的形式存儲。所謂文檔就是一個關聯數組式的對象,它的內部由屬性組成,一個屬性對應的值可能是一個數、字符串、日期、數組,甚至是一個嵌套的文檔。例:

{
	"_id": ObjectId( "4f7fe8432b4a1077a7c551e8" ),
	"uid": 2004,
	"username": "byvoid",
	"net9": {
		"nickname": "BYVoid",
		"surname": "Kuo",
		"givenname": "Carbo",
		"fullname": "Carbo Kuo",
		"emails": [
			"byvoid@byvoid.com",
			"byvoid.kcp@gmail.com"
		],
		"website": "http://www.byvoid.com",
		"address": "Zijing 2#, Tsinghua University"
	}
}

Node.js進階話題

模塊加載機制

控制流

Node.js應用部署

Node.js不適合的場景

計算密集型的程序

單線程特性。

單用戶多任務型應用

如本地的命令行工具或圖形界面。
發揮不出高並發的優勢,不擅長處理多進程相互協作。

邏輯十分復雜的事務

Node.js控制流不是線性的,它被一個個事件拆散。Node更擅長處理邏輯簡單但訪問頻繁的任務。

Unicode與國際化

不支持完整的Unicode。

JavaScript的高級特性

作用域

函數作用域

不同於大多類C語言,由一對花括號封閉的代碼塊就是一個作用域,javascript的作用域是通過函數來定義的,在一個函數中定義的變量只對這個函數內部可見。在函數引用一個變量時,javascript會先搜索當前函數作用域,如果沒有則搜索其上層作用域,一直到全局作用域。例:

var scope = 'global';
var f = function () {
	console.log(scope); // 輸出 undefined
	var scope = 'f';
}
f();

最后輸出結果不是global,而是undefined。在console.log函數訪問變量scope時,javascript會先搜索函數f的作用域,結果找到了var scope = 'f',所以就不會再往上層去找,但執行到console.log時,scope還沒有被定義,所以是undefined。

全局作用域

全局作用域中的變量不論在什么函數中都可以被直接引用,而不必通過全局對象。滿足以下條件的變量屬於全局作用域:

  • 在最外層定義的變量
  • 全局對象的屬性
  • 任何地方隱式定義的變量(未定義直接賦值的變量)

需要格外注意的是第三點,在任何地方隱式定義的變量都會定義在全局作用域中,即不通過 var 聲明直接賦值的變量。這一點經常被人遺忘,而模塊化編程的一個重要原則就是避免使用全局變量,所以我們在任何地方都不應該隱式定義變量。

閉包

閉包是由函數(環境)及其封閉的自由變量組成的集合體。

javascript中每一個函數都是一個閉包,但通常意義上嵌套的函數更能體現閉包的特性。

var generateClosure = function () {
	var count = 0;
	var get = function () {
		count++;
		return count;
	};
	return get;
};
var counter1 = generateClosure();
var counter2 = generateClosure();
console.log(counter1()); // 輸出 1
console.log(counter2()); // 輸出 1
console.log(counter1()); // 輸出 2
console.log(counter1()); // 輸出 3
console.log(counter2()); // 輸出 2

閉包的特性,當一個函數返回它內部定義的一個函數時,就產生了一個閉包,閉包不但包括被返回的函數,還包括這個函數的定義環境。counter1和counter2分別調用了generateClosure()函數,生成兩個閉包實例,它們內部引用的count變量分別屬於各自的運行環境。我們可以理解成,在generateClosure()返回get函數時,私下將get可能引用到的generateClosure()函數的內部變量(也就是count變量)也返回了,並在內存中生成了一個副本。

閉包的用途

閉包兩個主要用途,一是實現嵌套的回調函數,二是隱藏對象的細節。

嵌套的回調回函

exports.add_user = function (user_info, callback) {
	var uid = parseInt(user_info['uid']);
	mongodb.open(function (err, db) {
		if (err) { callback(err); return; }
		db.collection('users', function (err, collection) {
			if (err) { callback(err); return; }
			collection.ensureIndex("uid", function (err) {
				if (err) { callback(err); return; }
				collection.ensureIndex("username", function (err) {
					if (err) { callback(err); return; }
					collection.findOne({ uid: uid }, function (err) {
						if (err) { callback(err); return; }
						if (doc) {
							callback('occupied');
						} else {
							var user = {
								uid: uid,
								user: user_info,
							};
							collection.insert(user, function (err) {
								callback(err);
							});
						}
					});
				});
			});
		});
	});
};

每一層的嵌套都是一個回調函數,回調函數不會立即執行,而是等待相應請求處理完后由請求的函數回調。我們可以看到在嵌套的每一層都有對callback的引用,而最里層還用到了外層定義的uid變量。由於閉包的存在,即使外層函數已經執行完畢,其作用域內申請的變量也不會釋放。

實現私有成員

javascript對象沒有私有屬性,只能通過閉包實現。

var generateClosure = function () {
	var count = 0;
	var get = function () {
		count++;
		return count;
	};
	return get;
};
var counter1 = generateClosure();
var counter2 = generateClosure();
console.log(counter1()); // 輸出 1
console.log(counter2()); // 輸出 1
console.log(counter1()); // 輸出 2
console.log(counter1()); // 輸出 3
console.log(counter2()); // 輸出 2

只有調用counter()才能訪問到閉包內的count變量。

對象

javascript中的對象不是基於類的實例的,而是基於原型。

創建和訪問

javascript對象實際上就是一個由屬性組成的關聯數組,屬性由名稱和值組成,值的類型可以是任何數據類型,或者函數和其他對象。javascript具有函數式編程的特性,所以函數也是一種變量。

javascript中可以用以下方法創建一個簡單的對象:

var foo = {};
foo.prop_1 = 'bar';
foo.prop_2 = false;
foo.prop_3 = function(){
	return 'hello world';
}
console.log(foo.prop_3());

使用關聯數組訪問對象成員

var foo = {};
foo['prop_1'] = 'bar';
foo['prop_2'] = false;
foo['prop_3'] = function(){
	return 'hello world';
}
console.log(foo['prop_3']);

使用對象初始化器創建對象

var foo = {
	prop1: 'bar',
	prop2: 'false',
	prop3: function (){
			return 'hello world';
		}
	};

構造函數

function User(name, uri) {
	this.name = name;
	this.uri = uri;
	this.display = function() {
			console.log(this.name);
		}
	}

使用new來創建對象:

var user1 = new User('name', 'www.google.com');

上下文對象

javascript中,上下文對象就是this指針,即被調用函數所處的環境。上下文對象的作用是在一個函數內部引用調用它的對象本事。

var someuser = {
	name: 'byvoid',
	display: function () {
		console.log(this.name);
	}
};
someuser.display(); // 輸出 byvoid
var foo = {
	bar: someuser.display,
	name: 'foobar'
};
foo.bar(); // 輸出 foobar

this指針不屬於某個函數,而是函數調用時所屬的對象。

javascript的函數式編程特性使得函數可以像一般的變量一樣賦值、傳遞和計算。

var someuser = {
	name: 'byvoid',
	func: function () {
		console.log(this.name);
	}
};
var foo = {
	name: 'foobar'
};
someuser.func(); // 輸出 byvoid
foo.func = someuser.func;
foo.func(); // 輸出 foobar
name = 'global';
func = someuser.func;
func(); // 輸出 global

使用不同的引用來調用同一個函數,this指針永遠是這個引用所屬的對象。

call和apply

call和apply的功能是以不同的對象作為上下文來調用某個函數。簡而言之,就是允許一個對象去調用另一個對象的成員函數。

call和apply的功能是一致的,兩者細微的差別在於call以參數表來接受被調用函數的參數,而apply以數組來接受被調用函數的參數。

func.call(thisArg[, arg1[, arg2[, ...]]])
func.apply(thisArg[, argsArray])

看一個call的例子:

var someuser = {
	name: 'byvoid',
	display: function (words) {
		console.log(this.name + ' says ' + words);
	}
};
var foo = {
	name: 'foobar'
};
someuser.display.call(foo, 'hello'); // 輸出 foobar says hello

someuser.display是被調用的函數,它通過call將上下文改變為foo對象,因此函數體內訪問this.name時,實際上訪問的是foo.name,因而輸出foobar。

bind

使用call和apply方法可以改變被調用函數的上下文,而使用bind可以永久地綁定函數的上下文,使其無論被誰調用,上下文都是固定的。

func.bind(thisArg[, arg1[, arg2[, ...]]])

例:

var someuser = {
	name: 'byvoid',
	func: function() {
		console.log(this.name);
	}
};
var foo = {
	name: 'foobar'
};
	foo.func = someuser.func;
	foo.func(); // 輸出 foobar
	foo.func1 = someuser.func.bind(someuser);
	foo.func1(); // 輸出 byvoid
	func = someuser.func.bind(foo);
	func(); // 輸出 foobar
	func2 = func;
	func2(); // 輸出 foobar

bind綁定參數

bind還有一個重要功能:綁定參數。

var person = {
	name: 'byvoid',
	says: function (act, obj) {
		console.log(this.name + ' ' + act + ' ' + obj);
	}
};
person.says('loves', 'diovyb'); // 輸出 byvoid loves diovyb
byvoidLoves = person.says.bind(person, 'loves');
byvoidLoves('you'); // 輸出 byvoid loves you

byvoidLoves將this指針綁定到了person,並將第一個參數綁定到loves,之后再調用byvoidLoves時,只需要傳入obj參數。這樣可以在代碼多出調用時省略重復輸入相同的參數。

原型

function Person() {
}
Person.prototype.name = 'BYVoid';
Person.prototype.showName = function () {
	console.log(this.name);
};
var person = new Person();
person.showName();

上面代碼使用原型初始化對象,這樣與直接在構造函數內定義屬性的區別:

  • 繼承方式不同,子對象需要顯示調用調用父對象才能繼承構造函數內定義的屬性。

  • 構造函數內定義的任何屬性,包括函數在內都會被重新創建,同一個構造函數產生兩個不共享的實例。

  • 構造函數內定義的函數有運行時閉包的開銷。

    function Foo() {
    var innerVar = 'hello';
    this.prop1 = 'BYVoid';
    this.func1 = function () {
    innerVar = '';
    };
    }
    Foo.prototype.prop2 = 'Carbo';
    Foo.prototype.func2 = function () {
    console.log(this.prop2);
    };
    var foo1 = new Foo();
    var foo2 = new Foo();
    console.log(foo1.func1 == foo2.func1); // 輸出 false
    console.log(foo1.func2 == foo2.func2); // 輸出 true

原型和構造函數適用場景:

  • 除非必須用構造函數閉包,否則盡量用原型定義成員函數,因為這樣可以減少開銷。
  • 盡量在構造函數內定義一般成員,尤其是對象或數組,因為用原型定義的成員是多個實例共享的。

原型鏈

javascript中有兩個特殊對象:Object和Function,它們都是構造函數,用於生產對象。Object.prototype是所有對象的祖先,Function.prototype是所有函數的原型,包括構造函數。

任何對象都有一個_proto_屬性,它指向該對象的原型,從任何對象沿着它開始遍歷都可以追溯到Object.prototype。

構造函數對象的prototype屬性,指向一個原型對象。原型對象有constructor屬性,指向它的構造函數。

function Foo() {
}
Object.prototype.name = 'My Object';
Foo.prototype.name = 'Bar';
var obj = new Object();
var foo = new Foo();
console.log(obj.name); // 輸出 My Object
console.log(foo.name); // 輸出 Bar
console.log(foo.__proto__.name); // 輸出 Bar
console.log(foo.__proto__.__proto__.name); // 輸出 My Object
console.log(foo.__proto__.constructor.prototype.name); // 輸出 Bar

對象的復制

javascript沒有C語言中的指針,所有對象類型的變量都是指向對象的引用,兩個變量之間的賦值是傳遞引用。如果需要完整地復制一個對象,就需要手動實現這樣的函數,一個簡單的做法就是復制對象的所有屬性。

Object.prototype.clone = function() {
var newObj = {};
for (var i in this) {
newObj[i] = this[i];
}
return newObj;
}
var obj = {
name: 'byvoid',
likes: ['node']
};
var newObj = obj.clone();
obj.likes.push('python');
console.log(obj.likes); // 輸出 [ 'node', 'python' ]
console.log(newObj.likes); // 輸出 [ 'node', 'python' ]

上面代碼是一個對象的淺拷貝,只復制基本類型的屬性。實現深拷貝需要使用遞歸的方式來實現:

Object.prototype.clone = function () {
	var newObj = {};
	for (var i in this) {
		if (typeof (this[i]) == 'object' || typeof (this[i]) == 'function') {
			newObj[i] = this[i].clone();
		} else {
			newObj[i] = this[i];
		}
	}
	return newObj;
};
Array.prototype.clone = function () {
	var newArray = [];
	for (var i = 0; i < this.length; i++) {
		if (typeof (this[i]) == 'object' || typeof (this[i]) == 'function') {
			newArray[i] = this[i].clone();
		} else {
			newArray[i] = this[i];
		}
	}
	return newArray;
};
Function.prototype.clone = function () {
	var that = this;
	var newFunc = function () {
		return that.apply(this, arguments);
	};
	for (var i in this) {
		newFunc[i] = this[i];
	}
	return newFunc;
};
var obj = {
	name: 'byvoid',
	likes: ['node'],
	display: function () {
		console.log(this.name);
	},
};
var newObj = obj.clone();
newObj.likes.push('python');
console.log(obj.likes); // 輸出 [ 'node' ]
console.log(newObj.likes); // 輸出 [ 'node', 'python' ]
console.log(newObj.display == obj.display); // 輸出 false

上面方法在大多數情況下都沒有問題,但是遇到相互引用的對象時就會進入死循環,如:

var obj1 = {
	ref: null
};
var obj2 = {
	ref: obj1
};
obj1.ref = obj2;

遇到這個問題必須設計一套圖論算法,分析對象之間的依賴關系,然后分別一次復制每個頂點,並重新建立它們之間的關系。

Node.js編程規范

縮進

因為node.js中很容易寫出深層的函數嵌套,因此選擇兩空格縮進。

行寬

80字符。

語句分隔符

使用分號。

變量定義

永遠使用var定義變量。
通過賦值隱式變量總是全局變量,會造成命名空間污染。

變量名和屬性名

小駝峰式命名法。

函數

一般的函數使用小駝峰式命名法。但對於對象的構造函數名稱,使用大駝峰式命名法。

引號

使用單引號,因為JSON、XML都規定了必須是雙引號,這樣便於無轉義地直接引用。

關聯數組的初始化

除非鍵名之中有空格或非法字符,否則一律不使用引號。

等號

盡量使用=而不是,因為==包含了隱式類型轉換,很多時候可能與預期不同。

var num = 9;
var literal = '9';
if (num === literal) {
	console.log('9==="9"');
}
var num = 9;
var literal = '9';
if (num == literal) {
	console.log('9=="9"');
}

輸出

9=="9"

命名函數

盡量給構造函數和回調函數命名,這樣調試時可以看見更清晰的調用棧。
對於回調函數,第一個參數是錯誤對象err,如果沒有錯誤發生,其值為undefined。

對象定義

盡量將所有的成員函數通過原型定義,將屬性在構造函數內定義,然后對構造函數使用new關鍵字創建對象。

繼承

避免使用復雜的繼承,盡量使用Node.js的util模塊中提供的inherits函數。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM