流程控制

JavaScript 流程控制资料索引

循环

for

forEach

for-in

for-of

do-while

流程控制

运算符

基本运算符

加法运算符

在 JavaScript 中,加法运算符可以同时完成两种运算,加法以及字符连接,所以在使用加法运算符时往往也涉及到运算类型的确定与数据类型的转换。

  • 运算子之中存在字符串

两个运算子之中,只要有一个是字符串,则另一个不管是什么类型,都会被自动转为字符串,然后执行字符串连接运算。前面的《自动转换为字符串》一节,已经举了很多例子。

  • 两个运算子都为数值或布尔值

这种情况下,执行加法运算,布尔值转为数值(true 为 1,false 为 0)。

true + 5 // 6

true + true // 2
  • 运算子之中存在对象

运算子之中存在对象(或者准确地说,存在非原始类型的值),则先调用该对象的 valueOf 方法。如果返回结果为原始类型的值,则运用上面两条规则;否则继续调用该对象的 toString 方法,对其返回值运用上面两条规则。

1 + [1,2]
// "11,2"

上面代码的运行顺序是,先调用[1,2].valueOf(),结果还是数组[1,2]本身,则继续调用[1,2].toString(),结果字符串 “1,2”,所以最终结果为字符串 “11,2”。

1 + {a:1}
// "1[object Object]"

对象 {a:1} 的 valueOf 方法,返回的就是这个对象的本身,因此接着对它调用 toString 方法。({a:1}).toString() 默认返回字符串 “[object Object]",所以最终结果就是字符串 “1[object Object]”

有趣的是,如果更换上面代码的运算次序,就会得到不同的值。

{a:1} + 1
// 1

原来此时,JavaScript 引擎不将 {a:1} 视为对象,而是视为一个代码块,这个代码块没有返回值,所以被忽略。因此上面的代码,实际上等同于 {a:1};+1,所以最终结果就是 1。为了避免这种情况,需要对 {a:1} 加上括号。

({a:1})+1
"[object Object]1"

将 {a:1} 放置在括号之中,由于 JavaScript 引擎预期括号之中是一个值,所以不把它当作代码块处理,而是当作对象处理,所以最终结果为 “[object Object]1”。

1 + {valueOf:function(){return 2;}}
// 3

上面代码的 valueOf 方法返回数值 2,所以最终结果为 3。

1 + {valueOf:function(){return {};}}
// "1[object Object]"

上面代码的 valueOf 方法返回一个空对象,则继续调用 toString 方法,所以最终结果是 “1[object Object]”。

1 + {valueOf:function(){return {};}, toString:function(){return 2;}}
// 3

上面代码的 toString 方法返回数值 2(不是字符串),则最终结果就是数值 3。

1 + {valueOf:function(){return {};}, toString:function(){return {};}}
// TypeError: Cannot convert object to primitive value

上面代码的 toString 方法返回一个空对象,JavaScript 就会报错,表示无法获得原始类型的值。

( 1)空数组 + 空数组

[] + []
// ""

首先,对空数组调用 valueOf 方法,返回的是数组本身;因此再对空数组调用 toString 方法,生成空字符串;所以,最终结果就是空字符串。

( 2)空数组 + 空对象

[] + {}
// "[object Object]"

这等同于空字符串与字符串 “[object Object]” 相加。因此,结果就是 “[object Object]”。

( 3)空对象 + 空数组

{} + []
// 0

JavaScript 引擎将空对象视为一个空的代码块,加以忽略。因此,整个表达式就变成 “+ []”,等于对空数组求正值,因此结果就是 0。转化过程如下:

+ []
// Number([])
// Number([].toString())
// Number("")
// 0

如果 JavaScript 不把前面的空对象视为代码块,则结果为字符串 “[object Object]”。

({}) + []
// "[object Object]"

( 4)空对象 + 空对象

{} + {}
// NaN

JavaScript 同样将第一个空对象视为一个空代码块,整个表达式就变成 “+ {}”。这时,后一个空对象的 ValueOf 方法得到本身,再调用 toSting 方法,得到字符串 “[object Object]”,然后再将这个字符串转成数值,得到 NaN。所以,最后的结果就是 NaN。转化过程如下:

+ {}
// Number({})
// Number({}.toString())
// Number("[object Object]")

逻辑运算符

等于运算符

JavaScript 中存在着两个判断是否等同的运算符,即 == 与 ===:

==,两边值类型不同的时候,要先进行类型转换,再比较。

===,不做类型转换,类型不同的一定不等。即严格等于。

举例而言:

"1" == true

类型不等,true 会先转换成数值 1,现在变成 “1” == 1,再把 “1” 转换成 1,比较 1 == 1,相等。再譬如:

const a = 3;
const b = "3";

a==b 返回 true
a===b 返回 false

条件

JavaScript 提供 if 结构和 switch 结构,完成条件判断。

if

if 结构先判断一个表达式的布尔值,然后根据布尔值的真伪,执行不同的语句。

if (expression)
  statement

if 代码块后面,还可以跟一个 else 代码块,表示括号中的表示式为 false 时,所要执行的代码。

if (m === 3) {
  // then
} else {
  // else
}

switch

多个 if…else 连在一起使用的时候,可以转为使用更方便的 switch 结构。

switch (fruit) {
  case "banana":
    // ...
    break;
  case "apple":
    // ...
    break;
  default:
  // ...
}

上面代码根据变量 fruit 的值,选择执行相应的 case。如果所有 case 都不符合,则执行最后的 default 部分。需要注意的是,每个 case 代码块内部的 break 语句不能少,否则会接下去执行下一个 case 代码块,而不是跳出 switch 结构。

switch 或者 case 后面也可以跟上一个表达式。

循环

for

最原始的方式即是用 for 循环遍历一个数组,

for (const index = 0; index < myArray.length; index++) {
  console.log(myArray[index]);
}

forEach: 数组原型方法

自 ES5 正式发布后,你可以使用内建的 forEach 方法来遍历数组:

myArray.forEach(function (value) {
  console.log(value);
});
//或者带下标的方式遍历
myArray.forEach(function (value, index) {
  console.log(value);
});

这段代码看起来更加简洁,但这种方法也有一个小缺陷:你不能使用 break 语句中断循环,也不能使用 return 语句返回到外层函数。并且需要注意的是,forEach 是 Array 的原型方法,因此不能用于普通的 Object

for-in: 适用于对于对象的遍历 ( 会遍历对象的原型 )

for-in循环可以用于数组与对象的遍历,但是不建议用于数组的遍历中,如下是个简单的示例:

for (const index in myArray) {
  // 千万别这样做
  console.log(myArray[index]);
}

在这段代码中,赋给 index 的值不是实际的数字,而是字符串 “0”、“1”、“2”,此时很可能在无意之间进行字符串算数计算,例如:“2” + 1 == “21”,这给编码过程带来极大的不便。另外,作用于数组的 for-in 循环体除了遍历数组元素外,还会遍历自定义属性与原型链属性。举个例子,如果你的数组中有一个可枚举属性 myArray.name,循环将额外执行一次,遍历到名为 “name” 的索引。就连数组原型链上的属性都能被访问到。最让人震惊的是,在某些情况下,这段代码可能按照随机顺序遍历数组元素。简而言之,for-in 是为普通对象设计的,你可以遍历得到字符串类型的键,因此不适用于数组遍历。

for-of: 适用于对于数组以及集合类型的遍历

//遍历数组,直接返回值
for (const value of myArray) {
  console.log(value);
}
//遍历map,返回的是一个Array;也可以直接调用解构。
//注意,for-of不支持普通的Object
for (const data of myMap) {
  console.log(data); //Array
}

for-of 循环不仅支持数组,还支持大多数类数组对象,例如 DOM NodeList 对象

for-of 循环也支持字符串遍历,它将字符串视为一系列的 Unicode 字符来进行遍历:

for (const chr of "") {
  alert(chr);
}

它同样支持 Map 和 Set 对象遍历。举个例子,Set 对象可以自动排除重复项:

// 基于单词数组创建一个set对象
const uniqueWords = new Set(words);

生成 Set 对象后,你可以轻松遍历它所包含的内容:

for (const word of uniqueWords) {
  console.log(word);
}

Map 对象稍有不同:内含的数据由键值对组成,所以你需要使用解构(destructuring )来将键值对拆解为两个独立的变量:

for (const [key, value] of phoneBookMap) {
  console.log(key + "'s phone number is: " + value);
}

现在,你只需记住:未来的 JS 可以使用一些新型的集合类,甚至会有更多的类型陆续诞生,而 for-of 就是为遍历所有这些集合特别设计的循环语句。for-of 循环不支持普通对象,但如果你想迭代一个对象的属性,你可以用 for-in 循环(这也是它的本职工作)或内建的 Object.keys() 方法:

// 向控制台输出对象的可枚举属性
for (const key of Object.keys(someObject)) {
  console.log(key + ": " + someObject[key]);
}

while

While 语句包括一个循环条件,只要该条件为真,就不断循环。

while (expression)
statement

while 语句的循环条件是一个表达式(express ),必须放在圆括号中。语句(statement )部分默认只能写一条语句,如果需要包括多条语句,必须添加大括号。

while (expression){
    statement
}

do…while 循环

do…while 循环与 while 循环类似,唯一的区别就是先运行一次循环体,然后判断循环条件。

do
statement
while(expression);

// 或者

do {
    statement
} while(expression);

不管条件是否为真,do..while 循环至少运行一次,这是这种结构最大的特点。另外,while 语句后面的分号不能省略。

控制

break & continue

break 语句和 continue 语句都具有跳转作用,可以让代码不按既有的顺序执行。

break 语句用于跳出代码块或循环。

const i = 0;

while (i < 100) {
  console.log("i当前为:" + i);
  i++;
  if (i === 10) break;
}

上面代码只会执行 10 次循环,一旦 i 等于 10,就会跳出循环。

continue 语句用于立即终止本次循环,返回循环结构的头部,开始下一次循环。

const i = 0;

while (i < 100) {
  i++;
  if (i % 2 === 0) continue;
  console.log("i当前为:" + i);
}

上面代码只有在 i 为奇数时,才会输出 i 的值。如果 i 为偶数,则直接进入下一轮循环。

如果存在多重循环,不带参数的 break 语句和 continue 语句都只针对最内层循环。

标签

JavaScript 语言允许,语句的前面有标签(label )。标签通常与 break 语句和 continue 语句配合使用,跳出特定的循环。

top: for (const i = 0; i < 3; i++) {
  for (const j = 0; j < 3; j++) {
    if (i === 1 && j === 1) break top;
    console.log("i=" + i + ",j=" + j);
  }
}
// i=0,j=0
// i=0,j=1
// i=0,j=2
// i=1,j=0

上面代码为一个双重循环区块,加上了 top 标签(注意,top 不用加引号)。当满足一定条件时,使用 break 语句加上标签名,直接跳出双层循环。如果 break 语句后面不使用标签,则只能跳出内层循环,进入下一次的外层循环。

continue 语句也可以与标签配合使用。

top: for (const i = 0; i < 3; i++) {
  for (const j = 0; j < 3; j++) {
    if (i === 1 && j === 1) continue top;
    console.log("i=" + i + ",j=" + j);
  }
}
// i=0,j=0
// i=0,j=1
// i=0,j=2
// i=1,j=0
// i=2,j=0
// i=2,j=1
// i=2,j=2

上面代码在满足一定条件时,使用 continue 语句加上标签名,直接进入下一轮外层循环。如果 continue 语句后面不使用标签,则只能进入下一轮的内层循环。

迭代器

生成器是 ES6 中最具有吸引力的特性之一,笔者第一次接触到生成器的概念是在 Python 中,Generator 很像是一个函数,但是你可以暂停它的执行。你可以向它请求一个值,于是它为你提供了一个值,但是余下的函数不会自动向下执行直到你再次向它请求一个值。关于 Generator,以取号机为例会比较形象:你可以通过取一张票来向机器请求一个号码。你接收了你的号码,但是机器不会自动为你提供下一个。换句话说,取票机 “ 暂停 ” 直到有人请求另一个号码,此时它才会向后运行。

Generator 在 ES6 中像一个函数一样被声明,除了在之前有一个星号的差别外:

function* ticketGenerator(){}
function* ticketGenerator(){
    yield 1;
    yield 2;
    yield 3;
}

当你调用一个 generator 时,它将返回一个迭代器对象。这个迭代器对象拥有一个叫做 next 的方法来帮助你重启 generator 函数并得到下一个值。

next 方法不仅返回值,它返回的对象具有两个属性:done 和 value。value 是你获得的值,done 用来表明你的 generator 是否已经停止提供值。

const takeANumber = ticketGenerator();

takeANumber.next();

//>{value: 1, done: false}

takeANumber.next();
//>{value: 2, done: false}

takeANumber.next();
//>{value: 3, done: false}

takeANumber.next();
//>{value: undefined, done: true}

值得特别一提的是,生成器不是线程,在支持线程的语言中,多段代码可以同时运行,通通常导致竞态条件和非确定性,不过同时也带来不错的性能。生成器则完全不同。当生成器运行时,它和调用者处于同一线程中,拥有确定的连续执行顺序,永不并发。与系统线程不同的是,生成器只有在其函数体内标记为 yield 的点才会暂停。在其他语言中,生成器作为迭代器最直观的例子就是作为斐波那契数列的构造者:

function* fab(max) {
  const count = 0,
    last = 0,
    current = 1;

  while (max > count++) {
    yield current;
    const tmp = current;
    current += last;
    last = tmp;
  }
}

const o = fab(10),
  ret,
  result = [];

while (!(ret = o.next()).done) {
  result.push(ret.value);
}

console.log(result); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Modules( 模块 )

ES6 终于在官方引入了基于组件定义的模块化方式,类似于 Node、RequireJs、SeaJs 这些支持 CMD 规范与 AMD 规范的第三方组件库。

Export

export 关键字类似于 NPM 中,可以用于为某个模块设置对外的接口。一个最基本的模型如下所示:

export function generateRandom() {
  return Math.random();
}

export function sum(a, b) {
  return a + b;
}

//或者在底部使用
export { generateRandom, sum };

在 app.js 中,引用这些模块的方式如下:

import { generateRandom, sum as s } from "utility"; //这里默认使用了解构赋值

console.log(generateRandom()); //logs a random number
console.log(s(1, 2)); //3

如果只需要引入某个单独的模块,可以利用:

import { sum } from "utility";

同时,也可以将整个模块引入当做一个 object,然后调用其内部属性:

import 'utility' as utils;

console.log(utils.generateRandom()); //logs a random number
console.log(utils.sum(1, 2)); //3

Default Exports and Re-exporting

utility.js

const utils = {
  generateRandom: function () {
    return Math.random();
  },
  sum: function (a, b) {
    return a + b;
  },
};

export default utils;

app.js

import utils from "utility"; //这里需要注意下,如果export时不是用的{},那么这里也就不用添加{}

console.log(utils.generateRandom()); //logs a random number
console.log(utils.sum(1, 2)); //3
export default utils; //exports the imported value

总结而言,全部的 import 语法为:

import name from "module-name";
import * as name from "module-name";
import { member } from "module-name";
import { member as alias } from "module-name";
import { member1, member2 } from "module-name";
import { member1, member2 as alias2, [...] } from "module-name";
import defaultMember, { member [, [...] ] } from "module-name";
import defaultMember, * as alias from "module-name";
import defaultMember from "module-name";
import "module-name";

Loaders

异常处理

Error( 异常定义与类型 )

Error 对象

一旦代码解析或运行时发生错误,JavaScript 引擎就会自动产生并抛出一个 Error 对象的实例,然后整个程序就中断在发生错误的地方。Error 对象的实例有三个最基本的属性:

  • name:错误名称
  • message:错误提示信息
  • stack:错误的堆栈(非标准属性,但是大多数平台支持)

一般来说,利用 name 和 message 这两个属性,可以对发生什么错误有一个大概的了解:

if (error.name) {
  console.log(error.name + ": " + error.message);
}

上面代码表示,显示错误的名称以及出错提示信息。stack 属性用来查看错误发生时的堆栈。

function throwit() {
  throw new Error("");
}

function catchit() {
  try {
    throwit();
  } catch (e) {
    console.log(e.stack); // print stack trace
  }
}

catchit();
// Error
//    at throwit (~/examples/throwcatch.js:9:11)
//    at catchit (~/examples/throwcatch.js:3:9)
//    at repl:1:5

错误类型

Error 对象是最一般的错误类型,在它的基础上,JavaScript 还定义了其他 6 种错误,也就是说,存在 Error 的 6 个派生对象。

  • SyntaxError
SyntaxError是解析代码时发生的语法错误。
// 变量名错误
const 1a;

// 缺少括号
console.log 'hello');
  • ReferenceError

ReferenceError 是引用一个不存在的变量时发生的错误。

unknownVariable;
// ReferenceError: unknownVariable is not defined

另一种触发场景是,将一个值分配给无法分配的对象,比如对函数的运行结果或者 this 赋值。

console.log() = 1;
// ReferenceError: Invalid left-hand side in assignment

this = 1;
// ReferenceError: Invalid left-hand side in assignment

上面代码对函数 console.log 的运行结果和 this 赋值,结果都引发了 ReferenceError 错误。

  • RangeError

RangeError 是当一个值超出有效范围时发生的错误。主要有几种情况,一是数组长度为负数,二是 Number 对象的方法参数超出范围,以及函数堆栈超过最大值。

new Array(-1)(
  // RangeError: Invalid array length

  1234
).toExponential(21);
// RangeError: toExponential() argument must be between 0 and 20
  • TypeError

TypeError 是变量或参数不是预期类型时发生的错误。比如,对字符串、布尔值、数值等原始类型的值使用 new 命令,就会抛出这种错误,因为 new 命令的参数应该是一个构造函数。

new 123();
//TypeError: number is not a func

const obj = {};
obj.unknownMethod();
// TypeError: undefined is not a function

上面代码的第二种情况,调用对象不存在的方法,会抛出 TypeError 错误。

  • URIError

URIError 是 URI 相关函数的参数不正确时抛出的错误,主要涉及 encodeURI()、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape() 和 unescape() 这六个函数。

decodeURI("%2");
// URIError: URI malformed
  • EvalError

eval 函数没有被正确执行时,会抛出 EvalError 错误。该错误类型已经不再在 ES5 中出现了,只是为了保证与以前代码兼容,才继续保留。

以上这 6 种派生错误,连同原始的 Error 对象,都是构造函数。开发者可以使用它们,人为生成错误对象的实例。

new Error("出错了!");
new RangeError("出错了,变量超出有效范围!");
new TypeError("出错了,变量类型无效!");

上面代码表示新建错误对象的实例,实质就是手动抛出错误。可以看到,错误对象的构造函数接受一个参数,代表错误提示信息(message )。

自定义错误 (ES6 Based)

ES6 中引入了类的概念,因此对于自定义错误也可以使用继承自 Error 错误类的方式,只是需要重定义 name 以及 message 的信息。

class ExtendableError extends Error {
  constructor(message) {
    super(message);
    this.name = this.constructor.name;
    this.message = message;
    Error.captureStackTrace(this, this.constructor.name);
  }
}

class MyError extends ExtendableError {
  constructor(m) {
    super(m);
  }
}

const myerror = new MyError("ll");
console.log(myerror.message);
console.log(myerror instanceof Error);
console.log(myerror.name);
console.log(myerror.stack);

Throw

throw 语句的作用是中断程序执行,抛出一个意外或错误。它接受一个表达式作为参数。

throw "Error!";
throw 42;
throw true;
throw {
  toString: function () {
    return "Error!";
  },
};

上面代码表示,throw 可以接受各种值作为参数。JavaScript 引擎一旦遇到 throw 语句,就会停止执行后面的语句,并将 throw 语句的参数值,返回给用户。如果只是简单的错误,返回一条出错信息就可以了,但是如果遇到复杂的情况,就需要在出错以后进一步处理。这时最好的做法是使用 throw 语句手动抛出一个 Error 对象。

throw new Error("出错了!");

Try-Catch-Finally

throw 一般用于抛出异常,而如果需要对于异常进行处理,需要使用 try…catch 结构。

try {
  throw new Error("出错了!");
} catch (e) {
  console.log(e.name + ": " + e.message); // Error: 出错了!
  console.log(e.stack); // 不是标准属性,但是浏览器支持
}
// Error: 出错了!
// Error: 出错了!
//   at <anonymous>:3:9
//   at Object.InjectedScript._evaluateOn (<anonymous>:895:140)
//   at Object.InjectedScript._evaluateAndWrap (<anonymous>:828:34)
//   at Object.InjectedScript.evaluate (<anonymous>:694:21)

上面代码中,try 代码块抛出的错误(包括用 throw 语句抛出错误),可以被 catch 代码块捕获。catch 接受一个参数,表示 try 代码块传入的错误对象。

function throwIt(exception) {
  try {
    throw exception;
  } catch (e) {
    console.log("Caught: " + e);
  }
}

throwIt(3);
// Caught: 3
throwIt("hello");
// Caught: hello
throwIt(new Error("An error happened"));
// Caught: Error: An error happened

为了捕捉不同类型的错误,catch 代码块之中可以加入判断语句。

try {
  foo.bar();
} catch (e) {
  if (e instanceof EvalError) {
    console.log(e.name + ": " + e.message);
  } else if (e instanceof RangeError) {
    console.log(e.name + ": " + e.message);
  }
  // ...
}

同时为了更好地执行逻辑顺序,在 JavaScript 中引入了 finally 关键字,表示不管是否出现错误,都必需在最后运行的语句。

openFile();

try {
  writeFile(Data);
} catch (e) {
  handleError(e);
} finally {
  closeFile();
}
下一页