本文系统梳理 Node.js 基于 JavaScript 的基础语法,涵盖变量声明、循环语句、运算符特性(如 + 号与 == 区别)、typeof 类型判断及 null/undefined/NaN 特殊值。深入讲解数组与 JSON 对象操作、类(函数)的声明与实例化、成员变量与函数的定义方式,以及匿名函数与回调机制的应用,并附带深拷贝实现示例。
Node.js 基础语法与类型详解
前言
本文旨在梳理 Node.js 中涉及的数据类型知识。参考自网络快照文章,若原作者发现内容有误,请随时联系。
以下内容主要涉及 JavaScript 基础,适合初学者阅读,熟悉者可直接跳过。作者因 JavaScript 基础薄弱,在开发 Node.js 时遇到困惑,故整理此文以巩固知识。
正文
Node.js 的核心基于 JavaScript 脚本语言。与大多数脚本语言一样,JavaScript 具有“弱类型”特征。
与 PHP 不同,PHP 声明新变量无需显式声明类型,而 JavaScript 必须使用 var 关键字。var 在此涵盖了 C++ 中 int、string、char 乃至 function 等所有类型的含义。
本文及后续内容的代码均在 Linux 或 Cygwin 环境下使用 vim 编辑,并在命令行中运行查看结果。其他环境请自行调整操作方式。
基本语法与变量声明
基本语法
变量声明
在 C/C++ 中,变量声明通常如下所示:
```C++
代码如下:
void foo() {}
int a = 0;
char b = 'a';
float c = 1.0f;
void (*d)() = foo; ///< 函数指针,写法可能略有不同
而在 Node.js 中,声明方式如下: ```javascript
代码如下:
function foo() {}
var a = 0;
var b = 'a';
var c = 1.0;
var d = foo;
由此可见,无论变量类型如何,在 Node.js 中均通过 var 关键字进行声明。
循环语句与运算符特性
循环语句
for…i
该循环结构与 C/C++ 类似,基本形式为:
```C++
代码如下:
for(int i = 0; i < foo; i++)
{
//...
}
鉴于 Node.js 的弱类型特性,只需: ```javascript
代码如下:
for(var i = 0; i < foo; i++) {
//...
}
for…in
这是一种后验型循环语句,类似于 PHP 的 foreach。
例如,我们有一个 JSON 对象如下:
javascript
代码如下:
var foo = {
"hello" : "world",
"node" : "js",
"blahblah" : "bar"
};
此时,我们可以使用 for...in 进行遍历:
javascript
代码如下:
for(var key in foo) {
console.log(key + ": " + foo[key]);
}
如果在命令行执行以下命令:
代码如下:
$ node foo.js
屏幕将显示如下内容:
代码如下:
hello
: world
node: js
blahblah: bar
提示:由上可知,for...in 用于遍历 JSON 对象、数组或对象的键名,不提供直接遍历键值的方式。若需获取键值,必须通过 foo[<当前键名>] 的形式。这与 PHP 的 foreach 存在一定差异。
while…do, do…whill
此处不再赘述,与其他语言无异,仅需注意变量声明时使用 var 即可。
运算符
+, -, *, /
主要运算符包括 +、-、*、/。需特别注意 + 号,它既可用于字符串拼接,也可用于数值运算。弱类型语言中,数字有时以字符串形态出现,字符串有时以数值形态出现,但在必要时仍需明确类型。可通过以下代码观察结果:
代码如下:
var a = "1";
var b = 2;
console.log(a + b);
console.log(parseInt(a) + b);
这里的 parseInt 是 Node.js 的内置函数,用于将字符串解析为 int 类型变量。
上述代码执行结果为:
代码如下:
12
3
注:第一个 console.log 结果为 12,因 a 为字符串,b 亦被转换为字符串进行拼接,结果为字符串连接。第二个 console.log 结果为 3,因将 a 转换为 int 类型后,两 int 变量相加为数值加法,结果为 3。
==, ===, !=, !==
需解释的是,当逻辑运算符长度为 2 时(==, !=),仅判断外在值是否相等,不判断类型。如
代码如下:
var a = 1, b = "1";
console.log(a == b);
其输出结果为 true。若在判断中增加一个等号(===),则为严格判断,需类型和值均相等时才返回 true,否则为 false。即
代码如下:
var a = 1, b = "1";
console.log(a === b);
时,返回结果为 false,因为 a 为 int 型,而 b 为字符串。
顺带提及条件语句,if 与其他语言无异,主要区别在于逻辑运算符中两个等号与三个等号的用法,此处不再赘述。
类型判断与特殊值处理
typeof
此处姑且将其视为运算符而非函数。
该运算符用于判断变量类型,返回类型名字符串。具体执行以下代码即可知晓:
代码如下:
function foo() {}
var a = 0;
var b = '嘘~蛋花汤在睡觉。';
var c = 1.0;
var d = foo;
var e = { "a" : a };
var f = [ 1, 2, 3 ];
var g = null;
var h = undefined;
console.log(typeof a);
console.log(typeof b);
console.log(typeof c);
console.log(typeof d);
console.log(typeof e);
console.log(typeof f);
console.log(typeof g);
console.log(typeof h);
执行结果如下:
代码如下:
number
string
number
function
object
object
object
undefined
null, undefined, NaN
在 JavaScript 中,有三个特殊值,如标题所示。其中第一个大家可能较熟悉,C/C++ 中也有,但为大写,其本质即为
```C++
define NULL 0
而在 JavaScript 中,这三个值的含义各不相同。
### null ###
null 是一种特殊的 object,大致意为空。例如:
var a = null; 大家都能看懂,此处不再赘述。但与 C/C++ 不同的是,此处的 null 与 0 不相等。
### undefined ###
该值表示变量未声明。为更好区分 null,样例代码如下:
```javascript
代码如下:
var a = {
"foo" : null
};
console.log(a["foo"]);
console.log(a["bar"]);
上述代码中,a["foo"] 的值为空,即 null。而 a["bar"] 压根未声明,它连空都不是。输出结果大致可猜:
代码如下:
null
undefined
NaN
这是一个空的数值,是一种特殊的 number。其全称为 Not a Number。略显奇怪,可理解为“不是数字形态”或“数值出错的 number 类型变量”。
多出现在浮点型数值运算错误(如被 0 除)的情况下,甚至可以是用户自行让变量等于 NaN 以返回错误值,提示函数运算出错。
小杂碎
其余语句与其他语言类似,如 break、switch、continue 等,此处不再赘述。
变量类型与数组操作
变量类型
本节主要讲解 JavaScript 对象,其他类型略过。
基础类型
Node.js 包含的基础类型大致如下:
number string boolean array 其中前三种类型可直接赋值,而 array 的赋值仅为引用赋值。在新变量中修改某个值时,旧变量的值也会改变。可尝试以下代码:
javascript var foo = [ 1, 2, 3 ]; var bar = foo; bar[0] = 3; console.log(foo); 其结果为:
javascript [ 3, 2, 3 ] 即 array 若要复制出新数组,不能直接赋值,而必须进行“深拷贝”。
此处有必要讲解 array 的三种创建方法。
第一种:
javascript
代码如下:
var dog = new Array();
dog[0] = "嘘~";
dog[1] = "蛋花汤";
dog[2] = "在睡觉";
第二种:
javascript
代码如下:
var dog = new Array( "嘘~", "蛋花汤", "在睡觉" );第四种:
javascript
代码如下:
var dog = [
"嘘~",
"蛋花汤",
"在睡觉"
];
个人偏好第三种写法,较为简洁。
JSON对象与类的基础
JSON对象
此处将 JSON 对象单独列出,而非归类为 JavaScript 对象。若觉有误,可直接跳过本节。
本人对 JSON 对象与 JavaScript 对象的区分在于:是否仅用于存储数据,而非类的实例化。JSON 的本质即为 JavaScript Object Notation。
更多有关 JSON 的信息请自行查阅百科。
在 Node.js 中声明一个 JSON 对象非常简单:
javascript
代码如下:
var dog = {
"pre" : "嘘~",
"sub" : {
"name" : "蛋花汤",
"act" : "在睡觉",
"time" : 12
},
"suf" : [ "我说了", "它在睡觉", "就是在睡觉" ]
};
有两种方式能得到 JSON 对象中某键名的键值,第一种是用点连接,第二种是用中括号:
javascript
代码如下:
dog
.pre;
dog["pre"];
注意:上面在用点的时候,后面直接跟的是 JSON 中的 key。若把 key 当成变量去访问,只能用 dog[key] 试试看。现在自己动手试试,用 for...in 形式遍历上述 JSON 对象。别忘了使用 typeof 喵~
类(对象)的基础
严格意义上,Node.js 的类不能算是类,它只是一个函数集合体,加上一些成员变量。其本质是一个函数。
为通俗讲解,后续内容均称其为“类”,实例化后的称为“对象”。
因类具有许多函数特性,或本质即为函数,故此处顺带讲解函数基础。
类的声明、实例化与成员变量
类的声明和实例化
声明一个类非常简单,请勿嘲笑:
javascript function foo() { //... } 如此,我们便写好了一个 foo 类。
真的假的?!真的。
不信?请继续输入代码验证:
javascript var bar = new foo(); 尽管它是函数,但以这种形式(new)写出,即为该类的实例化。
此处的 foo() 即为该 foo() 类的构造函数。
成员变量
成员变量有两种声明方法。
第一种是在类的构造函数或任何构造函数中使用 this.<变量名>。可在任何时候声明成员变量,即使未声明时调用,也会有 undefined 支撑。即第一种方法:
代码如下:
javascript
function foo() {
this.hello = "world";
}
注意:仅当加上 this 时,才是调用类的成员变量,否则仅为函数内的局部变量。需分清有无 this 时变量的作用范围。
第二种方法是在构造函数或任何成员函数外部声明,格式为 <类名>.prototype.<变量名>:
javascript
代码如下:
function foo() {
//...
}
foo.prototype.hello = "world";
无论上述哪种方法,均为对成员变量的声明。可查看效果:
javascript
代码如下:
var bar = new foo();
console.log(bar.hello);
甚至可如此修改该类:
javascript
代码如下:
function foo() {
this.hello = "world";
}
foo.prototype.hello = "蛋花汤";
再使用上述代码输出。
思考为何输出仍为 world 而非 蛋花汤。
构造函数与成员函数声明
构造函数
前文已述,foo() 实为构造函数。显然可给构造函数传参,故有如下代码:
javascript
代码如下:
// 代码2.1
function foo(hello) {
if(hello === undefined) {
this.hello = "world";
} else {
this.hello = hello;
}
}
上述代码中有一个看似奇葩的判断 if(hello === undefined),其作用为何?第一种可能,开发者特意传入 undefined,无可厚非。
另一种情况。前文已述,JavaScript 是弱类型语言,传参亦不严谨。可多传或少传(只要保证程序或逻辑不出错),原则上均允许。多传的参数会被自动忽略,少传的参数则以 undefined 补足。
看以下代码即可明白:
javascript
代码如下:
// 上接代码2.1
var bar1 = new foo();
var bar2 = new foo("蛋花汤");
请自行输出两个 bar 的 hello 变量,会发现一个为 world,一个为 蛋花汤。显而易见,第一个 bar1 在声明时,被 Node.js 自动视为:
javascript
代码如下:
var bar1 = new foo(undefined);
故有 world 一说。
此外,构造函数中传入的参数名为 hello,而类中已有成员变量 this.hello。前文已述,有 this 和无 this 时作用域不同,参数仅作用于构造函数中,而加 this 的则为成员变量。用一个 this 即可区分,故即使同名亦无妨。
成员函数
成员函数声明
成员函数声明与成员变量第二种声明方法类似,即 <类名>.prototype.<函数名> = <函数>;。
javascript
代码如下:
// 上接代码2.1
function setHello(hello) {
this.hello = hello;
}
foo.prototype.setHello = setHello;
bar1.setHello("鸡蛋饼");
上述代码显而易见,实现了 foo 类的 setHello 函数,可通过它修改 foo.hello 的值。
但如此写是否麻烦?接下来要讲 JavaScript 函数的一个重要特性。
匿名函数与回调机制
★ 匿名函数 ★
很多时候,某些函数仅在一个地方被引用或调用,为此函数起名不值,故可临时写好该函数,直接让引用者引用,调用者调用。因此函数可省略函数名,如:
javascript
代码如下:
function(hello) {
this.hello = hello;
}
如何引用或调用呢?若需上述类引用,则写法如下:
javascript
代码如下:
foo.prototype.setHello = function(hello) {
this.hello = hello;
}
此写法与成员函数声明效果相同,且节省大量代码。实际上,基本上的类成员函数声明均采用这种匿名函数方式。
至于如何让匿名函数被调用?这通常用于传入仅被某个函数调用的函数时这样写。
例如,我们有一个函数原型为:
javascript
代码如下:
/**
* 我们将传入a,b两个变量,
* 在算出a+b的值后,交由func(num)
* 去进行输出
*/
function sumab(a, b, func) {
var c = a + b;
func(a, b, c);
}
例如,我们有两个版本的输出函数,一个是中文输出,一个是英文输出。若不使用匿名函数,写法如下:
javascript
代码如下:
function zh(a, b, sum) {
console.log(a + " + " + b + " 的值是:" + sum);
}
function en(a, b, sum) {
console.log(a + " plus " + b + " is " + sum);
}
sumab(1, 2, zh);
sumab(3, 4, en);
执行这段代码,输出结果将为:
1 + 2 的值是:3 3 plus 4 is 7 若采用匿名函数形式,则写法如下:
javascript
代码如下:
sumab(1, 2, function(a, b, sum) {
console.log(a + " + " + b + " 的值是:" + sum);
});
sumab(3, 4, function(a, b, sum) {
console.log(a + " plus " + b + " is " + sum);
});
这种形式通常用于回调函数。回调机制算是 Node.js 或 JavaScript 的精髓,将在后续篇章介绍。
成员函数声明的匿名函数声明方式
虽上一节已讲,此处再复述一遍。
通常声明类成员函数时,均使用匿名函数声明,因该函数仅是类的一个成员函数,不会在其他地方单独引用或调用,故有如下代码:
javascript
代码如下:
// 上接代码2.1
foo.prototype.setHello = function(hello) {
this.hello = hello;
}
如此,便使 foo 类拥有了 setHello 函数。
类的随意性与深拷贝实现
2.3.4. 类的随意性
此为胡扯。所谓类的随意性,即 JavaScript 中可在任何地方修改类,这与 Ruby 有一定相似之处。
例如,string 本身也是一个类,拥有 length 等成员变量,以及 indexOf、substr 等成员函数。若觉得 string 某处不完善,想添加自己的方法,可在所需处为其增加函数,例如:
javascript
代码如下:
String.prototype.sb = function() {
var newstr = "";
for(var i = 0; i < this.length; i++) {
if(i % 2 === 0) newstr += "s";
else newstr += "b";
}
return newstr;
};
该函数意为填充字符串,使其变成 sb 的化身。
我们来测试一下:
代码如下:
var str = "嘘~蛋花汤在睡觉。";
console.log(str.sb());
你将得到如下结果:
sbsbsbsbs 你跟你的电脑说“嘘~蛋花汤在睡觉。”,你的电脑会骂你四次半傻逼。(赶快砸了它)
3. 附
3.1. 深拷贝
所谓深拷贝,即新建一个数组或对象,将源数组或对象中的基础类型变量值逐个手动拷贝过去,而非仅获取源数组或对象的引用。这涉及递归调用等。
以下是我实现的一个深拷贝函数,大家可以写一个自己的,并加入 Node.js 知识库中。
javascript
代码如下:
function cloneObject(src) {
var dest = {};
for(var key in src) {
if(typeof src === "object") dest[key] = cloneObject(src[key]);
else dest[key] = src[key];
}
return dest;
}