流程控制

JavaScript流程控制资料索引

循环

for

forEach

for-in

for-of

do-while

流程控制

运算符

基本运算符

加法运算符

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

  • 运算子之中存在字符串

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

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

这种情况下,执行加法运算,布尔值转为数值(true1false0)

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

多个ifelse连在一起使用的时候,可以转为使用更方便的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语句返回到外层函数。并且需要注意的是,forEachArray的原型方法,因此不能用于普通的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);
}

它同样支持MapSet对象遍历。举个例子,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,以取号机为例会比较形象:你可以通过取一张票来向机器请求一个号码。你接收了你的号码,但是机器不会自动为你提供下一个。换句话说,取票机 “ 暂停 ” 直到有人请求另一个号码,此时它才会向后运行。

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

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

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

next方法不仅返回值,它返回的对象具有两个属性:donevaluevalue是你获得的值,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:错误的堆栈(非标准属性,但是大多数平台支持)

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

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种错误,也就是说,存在Error6个派生对象。

  • 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

URIErrorURI相关函数的参数不正确时抛出的错误,主要涉及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一般用于抛出异常,而如果需要对于异常进行处理,需要使用trycatch结构。

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();
}
下一页