• 《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函数。

  • 相关阅读:
    使用transfor让图片旋转
    block与inline,inline和inline-block,块级和行内元素,行内替换和行内非替换元素
    使用classList来实现两个按钮样式的切换
    关于css透明度的问题
    级动(两级联动)思路分享
    js图片预览(一张图片预览)
    js关于密码框强弱度的提示
    javascript对象与实例
    javascript 逻辑运算符
    Raphael画圆弧
  • 原文地址:https://www.cnblogs.com/Shoring/p/4739302.html
Copyright © 2020-2023  润新知