# 2 JavaScript 那些事

### 2 JavaScript那些事

二十世纪 90 年代初，Java 开始投入市场，并取得巨大成功。作为 Java 的维护者的 Sun 公司，也趁热打铁，开发出了 Applet，其实这是一个合成词，可以拆分成 Application 和 little 两个单词，意译的话就是“小程序”。

同时网景（Netscape）公司想开发一门让网页制作者就能学会的脚本语言，于是他们就委派了 Brendan Eich（布兰登·艾克）这位大神。由于时间紧迫，他用了10天便开发出了一个最初版本，开始的时候这个项目的代号还叫 Mocha（好吧，听起来很熟悉吧），并在第一版发布的时候给这门语言起名 LiveScript。不过，很快网景公司和 Sun 公司开展合作，两者达成协议，将这门语言改名为 Javascript。

从此 Sun 公司的 Applet 可以运行在浏览器上，只需要一个浏览器你的应用就可以随处运行了，而 Javascript 也借着 Java 的名声炒作了一把（虽然其语言本身跟 Java 没有多大关系，不过他成功迷惑了好多语言入门者，就好像我小时候搞不清楚雷锋和雷峰塔一样）。不过后来的结果大家大概都猜到了，小程序活不久，而 js 却被大量使用，直到今天依然无可替代。

#### 2.1 数据类型

JavaScript 由于当初设计之初时间紧迫，所以有好多历史遗留问题，所以对于其有些语法知识点，大家可以完全以吐槽的心态来学习，完全不用去深究。

**2.1.1 数字**

一般编程语言都有像整数、浮点数之类的数据类型，但 js 将整数按照浮点数来存储，所以 `1 === 1.0`。不过 js 本身提供了一些浮点数转成整数（保证在显示的时候不带小数点）的函数，比如说 [Math.ceil](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math/ceil)（向上取整）、[Math.floor](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math/floor)（向下取整）、[parseInt](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/parseInt)（将字符串转化成整数，如果参数不是字符串，会先转化为字符串）。

**2.1.2 布尔**

布尔，也就我们常说的 `true` `false`，本来布尔就是用来做逻辑判断的，但是我们在做逻辑判断的时候也可以这个样子

```javascript
if (undefined) {
    console.log('undefined is true');
} 
if (null) {
    console.log('null is true');
} 
if ('') {
    console.log('\'\' is true');
} 
if (0) {
    console.log('0 is true');
}
if ('0') {
    console.log('\'0\' is true');
}
if (NaN) {
    console.log('NaN is true');
}
```

**代码 2.1.2.1 逻辑判断语句**

你会发现 只有 `'0' is true` 被打印出来，其他的在做逻辑判断的时候都为假。本来这一节是讲布尔型数据类型的，但是我发现实在没啥可讲的，所以我就讲些逻辑判断相关的知识。其中 `undefined` 在 js 里面表示变量未定义；`null` 代表当前变量是一个对象，但是没有初始化；`''` 代表当前是一个字符串，但是字符串中没有任何字符；`'0'` 表示当前是一个字符串，且字符串就只有一个0字符；NaN 代表当前变量不是数字，这个数据类型在调用 parseInt 的时候会返回，比如说 `parseInt('a')` 就返回 `NaN`。

js 判断相等有两种方式 `==` 和 `===`，两者的区别是前者在做判断前，会将等号两边的数据类型转化成一致的；而后者在做判断的时候，如果检测到等号两边数据类型不一致，直接返回false。例如 `0 == '0'` 为 true，而 `0==='0'` 为 false。最好需要注意，判断一个变量是否是NaN，不要使用等号来判断，而需要使用函数 [isNaN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/isNaN)。

最后讲述一个小技巧，使用下面方式判断一个变量是否是“不正常”的：

```
if (!x) {
    console.log('x is dirty');
}
```

**2.1.3 字符串**

js 中字符串可以使用''或者""来包裹，'a' === "a"，有一些语言（Java或者C）中用 'a' 来表示 `字符` 类型，在 js 中是没有 `字符` 类型的。

js 中提供了N多关于字符串的函数，比如说字符串截取函数 [substring](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/substring)、替换函数 [replace](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/replace)、查找函数 [indexOf](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf)。

**2.1.4 数组**

[数组](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array) 是 js 中一项重要的数据结构，我们可以通过如下方式声明一个数组：

var array = \['a','bb','cc'];

js 提供了一系列函数来对数组进行增删改查。

首先是查，你可以通过下标来访问数组元素，下标从0开始， `array[0]` 返回 `'a'` 。同时数组还有一个 `length` 属性，通过这个属性我们可以写一个遍历这个数组的 for 循环：

```javascript
for (var i=0,len=array.length;i<len;i++) {
    console.log(array[i]);
}
```

**代码 2.1.4.1 for循环遍历数组**

我们还可以通过 [indexOf](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf) 函数来查找某一个元素是否在当前数组中。

然后是改， 直接举个栗子，设置 0 号元素为 `'11'`，则使用 `array[0] = '11'`，即可。

接着是增加，通过 [shift](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/shift) 函数删除数组最开头的元素， `var first = array.shift()` 调用完之后，变量 `array` 的值为 `['bb','cc']`，同时 `first` 被赋值 `'a'`。

通过 [unshift](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/unshift) 函数可以往数组头部添加元素，例如 `array.unshift('123')`，那么 array 变量的值就变成了 `['123','a','bb','cc']`。

通过 [pop](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/pop) 可以删除数组最末尾的一个元素，例如 `var last = array.pop()` 调用完之后， array 就变成了 `['a','bb']` 同时变量 `last` 被赋值 `'cc'`。

通过 [push](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/push) 函数可以在数组末尾添加元素，例如 `array.push('333')`，则 array 变成 `['a','bb','cc','333']`。

**2.1.5 函数**

作为一门编程语言，我们免不了在使用的时候要把某些功能封装成一个模块，函数作为模块的载体在任何程序语言中都是必不可少的， JavaScript 也不例外。在 js 中定义一个函数很简单：

```javascript
function doAdd(a,b) {
    return a + b;
}
```

**代码 2.1.5.1 函数定义**\
虽然函数体只有一行，但是这个函数却将函数三要素都澄清了：函数名 `doAdd`，参数 `a` 和 `b`，返回值 `a + b`。当然三要素并不是必不可缺的：

```javascript
var doEcho = function() {
    console.log('你好');
}
```

**代码 2.1.5.2 匿名函数**

这里其实是定义了一个匿名函数，只不过我们在定义完之后将它赋值给了变量 `doEcho`，同时这个函数在运行的时候可以不用传任何参数，同时函数内部没有任何 `return` 语句，其实这种情况跟 `return undefined` 是等价的。匿名函数一般作用是作为函数参数使用，例如下面这个栗子：

```javascript
function addLater(a,b,callback) {
    setTimeout(function() {
        var sum = a+b;
        callback(sum);
    },1000);
}

addLater(1,2,function(result) {
    console.log('the result:',result);
});
```

**代码 2.1.5.3 匿名函数使用示例**

上面这个栗子中，addLater函数在调用的时候，第三个参数在使用的时候是一个函数，而且它是匿名的。

#### 2.2 对象

其实对象也是一种数据类型，只不过由于它太特殊，所以这里单独拿出来讲。在 ES6 之前 javascript 还是一门**基于对象**的编程语言，为啥叫基于呢，因为 ES5 和之前版本的 javascript 中原生语法中没有类（class）这个关键词，你只能拿原型（prototype）来模拟一个类的行为。

> ES ( [ECMAScript](https://zh.wikipedia.org/wiki/ECMAScript) )，可以理解为 javascript 的语法标准， 2015年6月发布的 ES6 （又称 ES2015） 版本增加了N多语言特性，其中就包括类和继承的实现。由于 ES6 规避了之前版本中 javascript 中的一些糟粕设计，并且提升了开发效率，所以产生了学习 ES6 的大量前端开发人员，但是现行浏览器对于 ES6 语法的支持能力参差不齐，所以 [babeljs](https://babeljs.io) 应运而生，它提供了 ES6 转 ES5的功能，一时间产生了大量的拥趸。同时国内大神阮一峰也写了一本 [ECMAScript 6 入门](https://es6.ruanyifeng.com/) 开源图书，我想使用 ES6 语法的程序员，没有一位没有浏览过这本书的。另外 node 从4.x开始逐渐引入 ES6 语法，具体各个版本的实现情况可以参见[Node.js ES2015 Support](https://node.green/)。

我们这里先讲一下 ES5 中怎样模拟一个类，答案是使用原型：

```javascript
function PersonES5(p) {
    this.age = p.age;
    this.name = p.name;
    this.sex = p.sex;
}

PersonES5.prototype.showInfo = function() {
    console.log(this);
};

var person = new PersonES5({
    age:18,
    name:'tom',
    sex:'boy'
});

person.showInfo();
```

**代码 2.2.1 person\_es5.js**

而在 ES6 中由于直接有类的概念，所以代码语法上还是有差别的：

```javascript
class PersonES6 {
    constructor(p) {
        this.age = p.age;
        this.name = p.name;
        this.sex = p.sex;
    }
    showInfo() {
        console.log(this);
    }
}


var person = new PersonES6({
    age:18,
    name:'tom',
    sex:'boy'
});

person.showInfo();
```

**代码 2.2.2 person\_es6.js**

由于 javascript 长期函数式编程思想盛行，因为我们一般不会在一个网页中呈现过多的 UI 组件，所以它的代码处理流程一般都是线性的。比如说我们在前端使用 javascript 的流程是这样的：加载网页->请求数据->渲染 UI 组件->触发事件监听，后端的流程是这样的：接收请求->数据库操作->返回处理结果。当然你会说，不对，我们处理的流程可比这复杂多了，当然随着单页应用（SPA,Single Page Application）的兴起，前端 js 的处理逻辑会越来越复杂。比如说有一天，你的经理可能会给你分配一个在线 photoshop 的需求，这时候面向对象就派上用场了，你可能需要一个抽象类来描述组件的基本属性和功能，同时派生出若干继承自这个抽象类的具体组件类，比如说矩形类、三角形类、圆形类。我想面对这么复杂需求的时候，开发者肯定会选择 ES6 来实现，更不用说如今流行 mvvm 框架都是采用 ES6 来开发。

ES6 的类语法和其他很多语言是相似的，和原型链操作比起来可读性也更好，所以现在原型链操作已经很少被使用了。

### 2.3 闭包

很多编程语言都有闭包的特性，有的是天生就有闭包特性（Python Ruby Go 等），有的是随着语言自身的发展吸纳了闭包这个特性（PHP Java 等），JavaScript 属于前者。

JavaScript 中对于函数的返回值，其类型可以是前面提到的基本数据类型、对象，也可以是一个函数。

```javascript
function add2Total() {
	var total = 0;
	return function(num) {
		total += num;
		return total;
	}
}
```

**代码 2.3.1**

下面通过一个例子来讲述闭包的原理，首先看代码：

```javascript
var addFun = add2Total();
console.log(addFun(3));
console.log(addFun(1));
var addFun2 = add2Total()
console.log(addFun(3));
```

**代码 2.3.2**

上述代码中一共有三行打印，打印的结果将会是：

```
3
4
3
```

每次调用 `add2Total` 时将会创建一个闭包，`total` 变量位于闭包内部，它的内存随着闭包创建而被生成， 而我们调用 `add2Total` 得到的 `addFun` 变量就是这个闭包中的内部函数，它保持了对 total 变量的引用。在 `add2Total` 执行结束后，`total` 变量也并不会被销毁，而是被 `addFun` “闭住”了。

所以我们才会看到上述输出结果，对其调用步骤进行输出的话，会是如下流程：

1. 第一次执行 `add2Total()` 时，创建了一个新的作用域，其中定义了变量 `total = 0`。
2. 返回的函数 `function(num)` 是一个闭包，它可以访问 `add2Total` 中的变量 `total`。
3. 当我们执行 `addFun(3)` 时：
   * 访问的是第一个闭包内的 `total`（初始为 0），
   * 计算 `total += 3`，结果为 `3`，返回 `3`。
4. 接下来执行 `addFun(1)`：
   * 仍然在原来的闭包作用域中，`total` 已经是 `3`，
   * 执行 `total += 1`，结果为 `4`，返回 `4`。
5. 然后我们再次执行 `add2Total()`，创建了一个**全新的闭包**，也就是说，这时的 `total` 又从 `0` 开始。
6. `addFun2(3)` 执行时，对应的是新的作用域中的 `total`，所以返回值是 `3`

#### 2.4 异步回调

由于在 javascript 中存在大量的异步操作，函数调用完成之后，不能立马拿到执行结果，必须在回调函数中得到执行结果，如果你在一个函数中要接连做好几次这样的异步处理，是不是画面应该是这样的：

![代码深层次嵌套](/files/-M0waM4cb9UC0xnHfpvd)

**图 2.4.1 代码深层次嵌套的即视感**

#### 2.4.1 旧时代的异步

正是由于考虑到这种问题，所以 ES6 在设计的时候增加 Promise 类，不过这东西在批量处理异步回调时候依然让人不爽，大家可以参考 [A quick guide to JavaScript Promises](https://www.twilio.com/blog/2016/10/guide-to-javascript-promises.html)。在 ES5 语法时代，一版使用第三方库来进行异步回调管理，使用最频繁的就是回调流程控制库 [async](https://caolan.github.io/async/docs.html) (这的 `async` 是库的名字)。

我们在处理异步任务的时候，大体上分为两种情况，一种是串行操作，即处理完一个任务之后才能接着处理下一个任务；一种是并行操作，即各个任务都是独立运行的，大家一起运行，没有前后依赖关系。

对于串行运行在 async 中，可以是这样的：

```javascript
var async = require('async');

async.waterfall([
    function(callback) {
        setTimeout(function() {
            callback(null, 2+3);
        },100);
    },
    function(sum,callback) {
        setTimeout(function() {
            callback(null, sum-1);
        },100);
    },
    function(left,callback) {
        setTimeout(function() {
            callback(null, left * 2);
        },100);
    }
],function(err,result) {
    console.log(err,result);
});
```

**代码 2.4.1.1 async waterfall方法示例**

[waterfall](https://caolan.github.io/async/docs.html#waterfall) 函数接受两个参数，第一个参数是 Array 类型，用来指明各个需要异步执行的任务，数组的第一个元素为：

```javascript
function(callback) {
  setTimeout(function() {
    callback(null, 2+3);
  },100);
}
```

注意`callback(null, 2+3);`这一句，调用完这一句，它就参数 2+3 这个值传递到下一个任务中去了，然后数组的第二个元素：

```javascript
function(sum,callback) {
  setTimeout(function() {
    callback(null, sum-1);
  },100);
}
```

其中里面的`sum`正是刚才我们在第一个函数中传递过来的 `3+2`，同理可得我们最终将 `4`作为参数 `left` 传递到了第三方函数中。 `waterfall` 的第二个参数是一个回调函数：

```javascript
function(err,result) {
    console.log(err, result);
}
```

其第一个参数 `err` 代表错误信息，假设我们在处理任何一个异步任务的回调时写了一个 `callback(errorInfo);`，整个 `waterfall` 函数会提前结束，并且将这个 `errorInfo` 传递到第一个参数 `err` 上；第二个参数 `result` 代表最终处理得到的结果，具体到上面那个栗子，最终的结构就应该是 `4*2` 得 `8`。

接着将并行处理，也就是 [parallel](https://caolan.github.io/async/docs.html#parallel) ，我们再举个栗子:

```javascript
var async = require('async');

async.parallel([
    function(callback) {
        setTimeout(function() {
            callback(null, 1);
        }, 200);
    },
    function(callback) {
        setTimeout(function() {
            callback(null, 2);
        }, 100);
    }
],
function(err, results) {
  	console.log(err,results);
    //最终打印结果：null [1,2] 
});
```

**代码 2.4.1.2 async parallel 函数示例**

和 `waterfall` 类似，只要其中有一个任务在 callback 的时候传递了一个 error 对象，就会导致整个 parallel 函数立马结束。

> 最后大家可能留意到我们的第一行使用了 `require('async')` ，这个require 函数用来加载第三方包，我们需要在代码文件所在目录运行 `npm install async --save` 来安装这个第三方包。更多关于 npm 的知识可以参见本书第4章。
>
> 本章部分代码：<https://github.com/yunnysunny/nodebook-sample/tree/master/chapter2>

#### 2.4.2 新时代的异步

2017 年 6 月 **ECMAScript 2017（ES8）** 规范正式发布，正式将 async/await 语法引入了 JavaScript，Node 在 7.6 版本中也正式对其进行支持。由于它简洁的语法，更高的可读性，async/await 迅速成为 JavaScript 异步编程的主流方案，至今仍是处理异步操作（如网络请求、文件 I/O 等）的首选语法。

对于 **代码 2.4.1.1** 可以简化为下述代码：

```javascript
const {setTimeout} = require('timers/promises');

async function main() {
    let num = 0;
    await setTimeout(100);
    num = 2+3;
    await setTimeout(100);
    num -= 1;
    await setTimeout(100);
    num = num * 2;
    console.log(num);
}
main()
```

**代码 2.4.2.1 async/await 串行操作示例**

上述代码中，我们使用 `await` 关键字来等待异步操作完成，`await` 关键字会"暂停"当前函数的执行，直到异步操作完成。 这里的暂停并不是说当前函数被暂停了，而是一个语法糖，它背后会等待 await 后面的 Promise 对象 resolve 之后再继续执行后面的代码。

对于**代码 2.4.1.2**， 可以使用下面代码进行简化：

```javascript
const { setTimeout } = require('timers/promises');

async function doTask(num) {
    await setTimeout(100);
    return num;
}
async function main() {
    const result = await Promise.all([doTask(1), doTask(2)]);
    console.log(result);
}
main();
```

**代码 2.4.2.2 async/await 并行操作示例**

#### 2.5 参考文献

* <https://www.twilio.com/blog/2016/10/guide-to-javascript-promises.html>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://nodebook.whyun.com/02_node_javascript.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
