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特有的逻辑写在这里
}
慕课网
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如何实现高并发,使得一台笔记本能每秒处理上千个请求?
搞清问题:
- 明白调用堆栈的概念:
当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
需求
我们从定义需求开始:
- 程序需要在命令行中运行,然后通过终端提供交互给用户进行输入,输出。
- 程序启动后,需要显示当前目录列表
- 选择某个文件时, 程序需要显示该文件内容
- 选择一个目录是,程序需要显示该目录下的信息
- 运行结束后程序退出
根据上述信息,你可以将此项目分到如下几个步调:
- 创建模块
- 决定采用同步fs还是异步fs
- 理解什么是流
- 实现输入和输出
- 重构
- 使用fs进行文件交互
- 完成
编写首个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);
})
})
}
}