JavaScript

Table of Contents

1. JavaScript 简介

JavaScript is a high-level, dynamic, untyped, and interpreted programming language. It has been standardized in the ECMAScript language specification. Alongside HTML and CSS, it is one of the three essential technologies of World Wide Web content production.

JavaScript 最常见的运行环境是浏览器,也可以用于其它环境中,如 pdf 文件中,Server-side JavaScript 等等。

参考:
本文主要摘自《JavaScript 权威指南(原书第 6 版)》
JavaScript: The Good Parts(非常棒的书)
JavaScript Guide: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide
JavaScript reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference
JavaScript 参考文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference
ECMAScript® 2015 Language Specification: https://www.ecma-international.org/ecma-262/6.0/
ECMAScript 6 入门,阮一峰著:http://es6.ruanyifeng.com/
JavaScript and HTML DOM Reference: http://www.w3schools.com/jsref/default.asp
JavaScript syntax: https://en.wikipedia.org/wiki/JavaScript_syntax

1.1. 标准化

ECMAScript 的正式名称为 ECMA-262 和 ISO/IEC 16262。
JavaScript 是 ECMAScript 规范的一种实现。

Table 1: ECMAScript 标准化
Edition 日期 说明
v1 June 1997 第一个标准,与 JavaScript 1.3 的实现基本一致
v2 June 1998 维护版本,无新特性
v3 December 1999 增加了正则表达式,try/catch 异常处理,与 JavaScript 1.5 的实现基本一致
v4 被放弃的版本 新功能太多,浏览器厂商不愿支持。标准被放弃。
v5 December 2009 对 Object 新增了很多属性和方法,对 Array 新增了方法,提供一个内置的全局 JSON 对象,增加了"use strict"等等
v5.1 June 2011 无新特性。与国际标准 ISO/IEC 16262:2011 的第三版本一致。
v6 June 2015 增加了 class,模块,for...of 循环,let 关键字,字符串模板,Map/Set/WeekMap/WeakMap 等等
v7 June 2016 增加了 exponentiation operator 和 Array.prototype.includes
v8 June 2017 增加了 await/async

参考:
ECMAScript 标准的新特性及浏览器的支持情况:http://kangax.github.io/compat-table/es6/
Standard ECMA-262 5.1 Edition: http://www.ecma-international.org/ecma-262/5.1/
Standard ECMA-262 6th Edition: http://www.ecma-international.org/ecma-262/6.0/
Standard ECMA-262 7th Edition: http://www.ecma-international.org/ecma-262/7.0/
Standard ECMA-262 8th Edition: http://www.ecma-international.org/ecma-262/8.0/

1.2. 浏览器中的 JavaScript

Javascript 可控制 html 文档的内容。如:

<html>
<head><title>Factorials</title></head>
<body>
<h2>Table of Factorials</h2>
<script>
var fact = 1;
for(i = 1; i < 10; i++) {
    fact = fact*i;
    document.write(i + "! = " + fact + "<br>");
}
</script>
</body>
</html>

用浏览器直接打开上面 html 文件,会显示下面内容:

js_browser_factorials.png

Figure 1: 浏览器中的 JavaScript 实例——显示阶乘

1.3. Server-side JavaScript

JavaScript 也可运行时服务器端。

Node.js 是一个 Javascript 运行环境,它是对 Google V8 引擎进行了封装。V8 引擎执行 Javascript 的速度非常快,性能非常好。Node.js 对一些特殊用例进行了优化,提供了替代的 API,使得 V8 在非浏览器环境下运行得更好。

Node.js 版本的 Hello World 服务器:

// file test.js
var http = require('http');
server = http.createServer(function (req, res) {
    res.writeHeader(200, {"Content-Type": "text/plain"});
    res.end("Hello World\n");
    });
server.listen(8000);
console.log("httpd start @8000");

启动

$ node test.js

用浏览器打开 127.0.0.1:8000,即可看到网页显示 Hello World。

2. 基本语法

JavaScript 语法简单,熟悉 C、Java 背景的话可很快地掌握它。
JavaScript 的一些基础概念如下:
一、区分大小写(注:HTML 并不区分大小写);
二、注释和 Java、C 的注释相同:单行注释用 // ,多行注释用 /* */
三、行结尾的分号在无歧义时可省略,如下面写法都是合法的:

var test1 = "a";       // 有分号
var test2 = "b"        // 没有分号

2.1. 标识符

JavaScript 标识符必须以字母、下划线(_)或美元符号($)开始,后续字符可以是字母、数字、下划线或美元符号。

注 1:标识符不能以数字开头(这样就容易把标识符和数字分开了)。
注 2: 作为标识符的一部分,美元符号($)在 JavsScript 中没有什么特殊含义,把它看作普通字符即可。 比如,在框架 prototype.js 中,有一个用来查找对象的函数,函数名字为 $ ,可以认为它是这么实现的:

// 真正的实现可能比这个复杂
function $(id) {
  return document.getElementById(id);
}

比如有<div id='s1'></div>,则 obj=$('s1')就是引用 id='s1'这个对象。

2.2. 变量

JavaScript 中用 var 定义变量,它是弱类型语言,无需指定变量的类型。使用变量时,直接引用即可。如:

// file test_var.js
var test1 = "hi";
var test2 = test1 + "abc", test3 = 100;  // 一行可以定义多个变量(用逗号分开)
var _test4 = 123, $test5 = 456;          // 变量名可以是下划线(_)或美元符号($)开头。但不推荐这么做!
var test6;                               // 没有赋值的变量是undefined的。
test7 = "xyz"                            // 变量可不必声明,直接使用。但不推荐这么做!
test1 = 666;                             // 变量本身没有类型!之前定义时把字符串赋给它,现在可以把数值赋给它。

console.log(test1);
console.log(test2);
console.log(test3);
console.log(_test4);
console.log($test5);
console.log(test6);
console.log(test7);

在 node.js 中测试如下:

$ node test_var.js
666
hiabc
100
123
456
undefined
xyz

2.2.1. 变量作用域

全局变量拥有全局作用域,它在 JavaScript 代码中的任何地方都是有定义的。函数内声明的变量只在函数体内有定义,它们是局部变量,作用域是局部性的。函数参数也是局部变量,它们只在函数体内定义。

在函数体内,局部变量的优先级高于同名的全局变量。如果函数体内有一个和全局变量重名的局部变量,那么全局变量被局部变量所遮盖。如:

var scope = "global";        // Declare a global variable

function checkscope( ) {
  var scope = "local";       // 全局变量scope被局部变量遮盖了
  console.log(scope);        // Use the local variable, not the global one
}
checkscope();                // Prints "local"
2.2.1.1. 省略 var 定义的变量为全局变量(应避免这样使用)

全局作用域编写代码时可以不写 var 语句,但声明局部变量时则必须使用 var 语句。

下面代码中,函数 checkscope2 中变量 scope 和 myscope 都是全局变量:

scope = "global";          // Declare a global variable, even without var.
function checkscope2() {
  scope = "local";          // Oops! We just changed the global variable.
  myscope = "local";        // This implicitly declares a new global variable.
  return [scope, myscope];  // Return two values.
}
checkscope2()               // => ["local", "local"]: has side effects!
scope                       // => "local": global variable has changed.
myscope                     // => "local": global namespace cluttered up.
2.2.1.2. 变量的“函数作用域”和“声明提前”

在 C、Java 等语言中,花括号内的每一段代码都具有各自的作用域,而且变量在声明它们的代码段之外是不可见的,这称为“块作用域”。但,JavaScript 中没有“块作用域”, JavaScript 中使用的是“函数作用域”(function scope):函数内声明的所有变量在函数体内始终是可见的。 这意味着变量在声明之前甚至已经可用。

function test(o) {
    var i = 0;                      // i is defined throughout function
    if (o == 1) {
        var j = 0;                  // j is defined everywhere, not just block
        for(var k=0; k < 3; k++) {  // k is defined everywhere, not just loop
            console.log(k);
        }
        console.log(k);             // k is still defined: prints 3
    }
    console.log(j);                 // j is defined, but may not be initialized
}

test(1);

上面程序将输出:

0
1
2
3
0

JavaScript 的函数作用域还意味着变量在声明之前已经可用。这个特性被称为声明提前(hoisting)。 请看下面实例:

var scope = "global";        // Declare a global variable

function f() {
  console.log(scope);        // Prints "undefined", rather than "global"
  var scope = "local";       // 尽管在这里赋初始值,但变量本身在函数体内任何地方均是可访问的。
  console.log(scope);        // Prints "local"
}

f();

2.2.2. let(ES6 中增加)

let 语句声明一个“块级作用域”的本地变量,并且可选的将其初始化为一个值。

下面例子演示了 varlet 的区别。

function varTest() {
  var x = 1;
  if (true) {
    var x = 2;  // 同样的变量!
    console.log(x);  // 2
  }
  console.log(x);  // 2
}

function letTest() {
  let x = 1;
  if (true) {
    let x = 2;  // 不同的变量
    console.log(x);  // 2
  }
  console.log(x);  // 1
}

参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/let

3. 类型

ECMAScript 中有七种内置类型:

  • Undefined
  • Null
  • Boolean
  • String
  • Number
  • Symbol (这是 ES6 中增加的)
  • Object

其中前六种 Undefined, Null, Boolean, String, Number, Symbol 称为原始类型(primitive type),除了原始类型外就是对象。这里将重点介绍原始类型。

参考:
http://www.ecma-international.org/ecma-262/6.0/#sec-ecmascript-language-types

3.1. null 和 undefinded

null 是 JavaScript 中的关键字,常用来描述“空值”。
undefined 是 JavaScript 中的预定义的全局变量,这是变量的一种取值,表示变量没有初始化。

尽管 null 和 undefined 不同,但它们都表示“值的空缺”,两者往往可以互换。用相等运算符“==”测试两者会返回 true,用严格相等运算符“===”测试两者会返回 false。

可以认为 undefined 表示系统级的、出乎意料的或类似错误的值的空缺;而 null 是表示程序级的、正常的或在意料之中的值的空缺。 如果你想将它们赋值给变量或者属性,或将它们作为参数传入函数,最佳选择是使用 null。

3.2. 数字

和 C 和 Java 等不同,JavaScript 中不区分整数值和浮点数值。 JavaScript 中的所有数字均用浮点数值(IEEE 754 标准)表示。

3.2.1. 全局变量 Infinity 和 NaN

JavsScript 中预定义了全局变量 Infinity 和 NaN,用来表示正穷大和非数字值。

JavaScript 中算术运算结果超过了 JavaScript 所能表示的数字上限(或下限)时,不会报错,其结果为一个特殊的值:无穷大 Infinity (或负无穷大)。

JavaScript 中对于“0/0”,“Infinity/Infinity”还有对负数开方等情况,也不会报错,会返回结果 NaN

JavaScript 中 NaN 有一点特殊:它和任何值(包括它自己)都不相等。也就是说,没有办法通过 x==NaN 来判断变量 x 是否为 NaN,我们应当使用 x!=x 来判断,因为当且仅当 x 为 NaN 时,这个表达式才为 true。

Infinity 和 NaN 测试如下:

// Test Infinity
console.log(Number.MAX_VALUE);      // 1.7976931348623157e+308
console.log(Number.MAX_VALUE * 2);  // Infinity
console.log(1/0);                   // Infinity
console.log(-1/0);                  // -Infinity

// Test NaN
console.log(0/0);                   // NaN
console.log(Infinity/Infinity);     // NaN
console.log(Math.sqrt(-1));         // NaN
console.log(parseInt("abcd"));      // NaN

3.2.2. 全局函数 isFinite()和 isNaN()

JavaScript 中有全局函数 isFinite()isNaN() 可以用来进行相关测试。

对于变量 a,isFinite(a)在以下几种情况下为 true:
(1) a 为 number,但不为 NaN 或正负 Infinity;
(2) a 为字符串,但该字符串的内容为非 NaN、非正负 Infinity 的数字;
(3) a 为 boolean 值;
(4) a 为 null。

对于变量 a,isNaN(a)在以下几种情况下为 true:
(1) a 为 NaN;
(2) a 为字符串,且该字符串不是数字;
(3) a 为对象;
(4) a 为 undefined。
你可以认为 isNaN 是这样实现的:

isNaN = function(value) {
    Number.isNaN(Number(value));
}

isFinite 和 isNaN 的相关测试:

// Test isFinite
console.log(isFinite(5));          // true
console.log(isFinite("5"));        // true

console.log(isFinite("abcd"));     // false

console.log(isFinite(Infinity));   // false
console.log(isFinite(NaN));        // false

console.log(isFinite(true));       // true
console.log(isFinite(false));      // true

console.log(isFinite(null));       // true
console.log(isFinite(undefined));  // false


// Test isNaN
console.log(isNaN(NaN));       // true
console.log(isNaN(undefined)); // true
console.log(isNaN({}));        // true

console.log(isNaN(true));      // false
console.log(isNaN(null));      // false
console.log(isNaN(37));        // false

// strings
console.log(isNaN("37"));      // false: "37" is converted to the number 37 which is not NaN
console.log(isNaN("37.37"));   // false: "37.37" is converted to the number 37.37 which is not NaN
console.log(isNaN("123ABC"));  // true:  parseInt("123ABC") is 123 but Number("123ABC") is NaN
console.log(isNaN(""));        // false: the empty string is converted to 0 which is not NaN
console.log(isNaN(" "));       // false: a string with spaces is converted to 0 which is not NaN

// dates
console.log(isNaN(new Date()));                // false
console.log(isNaN(new Date().toString()));     // true

// This is a false positive and the reason why isNaN is not entirely reliable
console.log(isNaN("blabla"))   // true: "blabla" is converted to a number.
                               // Parsing this as a number fails and returns NaN

3.2.3. 数字转换为字符串

JavaScript 中数字可以自动转换为字符串。如可以直接把数字和字符串拼接在一起:

var n = 100;
var n_as_string = n + "";    // "100"
var s = 100 + " times";      // "100 times"

下面是一些函数,可以用来把数字以特定的格式转换为字符串。

Table 2: 数字转换为字符串相关函数
相关函数 说明
toExponential 转换数字为指数表示形式的字符串
toFixed 转换数字为字符串,只保留指定小数位
toPrecision 转换数字为字符串,保留指定精度
toString 以指定进制表示形式转换数字为字符串

注:这些函数都是返回另一个副本,不会直接修改原变量,除非对原变量显式地赋值。

// Test toExponential
var num = 18.2345;
num.toExponential();    // "1.82345e+1"
num.toExponential(2);   // "1.82e+1"
num.toExponential(3);   // "1.823e+1"

// Test toFixed
var num = 18.2345;
num.toFixed();      // "18"
num.toFixed(2);     // "18.23"
num.toFixed(3);     // "18.234"

// Test toPrecision
var num = 18.2345;
num.toPrecision();   // "18.2345"
num.toPrecision(1);  // "2e+1"
num.toPrecision(2);  // "18"
num.toPrecision(3);  // "18.2"
num.toPrecision(4);  // "18.23"

// Test toString
var num = 15;
num.toString();     // "15"
num.toString(2);    // "1111"
num.toString(8);    // "17"
num.toString(16);   // "f"

3.3. 字符串

字符串字面量以一对单引号(')或者双引号(")包围。可以用反斜线(\)进行转义,用加号(+)拼接字符串。

用字符串的 length 属性可以确定字符串的长度。一些字符串相关函数如下所示:

Table 3: 字符串相关函数
字符串相关函数 描述
charAt() Returns the character at the specified index (position)
charCodeAt() Returns the Unicode of the character at the specified index
concat() Joins two or more strings, and returns a new joined strings
endsWith() Checks whether a string ends with specified string/characters
fromCharCode() Converts Unicode values to characters
includes() Checks whether a string contains the specified string/characters
indexOf() Returns the position of the first found occurrence of a specified value in a string
lastIndexOf() Returns the position of the last found occurrence of a specified value in a string
localeCompare() Compares two strings in the current locale
match() Searches a string for a match against a regular expression, and returns the matches
repeat() Returns a new string with a specified number of copies of an existing string
replace() Searches a string for a specified value, or a regular expression, and returns a new string where the specified values are replaced
search() Searches a string for a specified value, or regular expression, and returns the position of the match
slice() Extracts a part of a string and returns a new string
split() Splits a string into an array of substrings
startsWith() Checks whether a string begins with specified characters
substr() Extracts the characters from a string, beginning at a specified start position, and through the specified number of character
substring() Extracts the characters from a string, between two specified indices
toLocaleLowerCase() Converts a string to lowercase letters, according to the host's locale
toLocaleUpperCase() Converts a string to uppercase letters, according to the host's locale
toLowerCase() Converts a string to lowercase letters
toString() Returns the value of a String object
toUpperCase() Converts a string to uppercase letters
trim() Removes whitespace from both ends of a string
valueOf() Returns the primitive value of a String object

字符串相关测试如下:

var s = 'hello world'

console.log(s.length);         // 11 (不要用length(),因为length是属性,不是函数)

console.log(s.charAt(0));      // "h"
console.log(s.toUpperCase());  // "HELLO WORLD"

3.3.1. 字符串转换为数字

JavaScript 中,字符串可以自动转换为数字。

var n = "12" * "2";              // n is the number 24.

var string_value = "100";
var number = string_value - 0;   // 变量number为数字 100
                                 // 注意:只能是减0,不能用‘+ 0’,否则number会为"1000"

函数 parseInt 和 parseFloat 可以用来做一些更复杂的字符串到数字的转换。如:

parseInt("3 blind mice");    // Returns 3
parseFloat("3.14 meters");   // Returns 3.14
parseInt("12.34");           // Returns 12
parseInt("0xFF");            // Returns 255

parseInt("11", 2);           // Returns 3 (1*2 + 1)
parseInt("ff", 16);          // Returns 255 (15*16 + 15)
parseInt("zz", 36);          // Returns 1295 (35*36 + 35)
parseInt("077", 8);          // Returns 63 (7*8 + 7)
parseInt("077", 10);         // Returns 77 (7*10 + 7)

parseInt("eleven");          // Returns NaN
parseFloat("abc72.47");      // Returns NaN

3.3.2. 模板字符串(ES6 中增加)

ES6 中增加了模板字符串。模板字符串使用反引号 `` 来代替普通字符串中的用双引号和单引号。模板字符串可以包含特定语法(${expression})的占位符。占位符中的表达式和周围的文本会一起传递给一个默认函数,该函数负责将所有的部分连接起来。

模板字符串的简单例子:

var msg1="a";
var msg2="b";

console.log("msg1 is " + msg1 + ", msg2 is " + msg2);
console.log(`msg1 is ${msg1}, msg2 is ${msg2}`);            // 和上一行输出相同

模板字符串中的表达式在输出前会进行计算,如:

var a = 5;
var b = 10;
console.log("Fifteen is " + (a + b) + " and not " + (2 * a + b) + ".");   // 输出 Fifteen is 15 and not 20.
console.log(`Fifteen is ${a + b} and not ${2 * a + b}.`);                 // 和上一行输出相同

参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/template_strings

3.4. Boolean 值

布尔值用来指代真或假、开或关、是或否。这个类型只有两个值,即保留字 true 和 false。

JavaScript 中任何类型的值都可以转换为布尔值。转换规则是:下面 6 个值转换为 false

undefined
null
0                // Number 0
-0               // Number -0
NaN              // Number NaN
""               // empty String

其它值都会转换为 true。

注:字符串“false”会转换为 true。

3.5. 全局对象

全局对象(global objects,也称为 standard built-in objects)是 JavaScript 解释器在启动时(或 Web 浏览器加载新页面时)创建的,在 JavaScript 程序中可以直接使用。比如它包含:

  • 全局属性,如 undefined,Infinity 和 NaN;
  • 全局函数,如 isNaN(),parseInt()等;
  • 构造函数,如 Date(),RegExp(),String(),Object()和 Array();
  • 全局对象,如 Math 和 JSON。

3.6. 原始类型值不可改变

JavaScript 中的原始值(undefined,null,布尔值,字符串,数字,符号)和对象(包括数组和函数)有着根本的区别。原始值是不可以更改的,任何方法都无法改变一个原始值。 如字符串函数 replace 等都是返回另外一个字符串。

4. 运算符

JavaScript 中运算符总结如下。
注:表从上到下按优先级从高到低的顺序排列,列 A 代表结合性,列 N 代表操作符数量。

Table 4: JavaScript operators
Operator Operation A N Types
++ Pre- or post-increment R 1 lval→num
-- Pre- or post-decrement R 1 lval→num
- Negate number R 1 num→num
+ Convert to number R 1 num→num
~ Invert bits R 1 int→int
! Invert boolean value R 1 bool→bool
delete Remove a property R 1 lval→bool
typeof Determine type of operand R 1 any→str
void Return undefined value R 1 any→undef
*, /, % Multiply, divide, remainder L 2 num,num→num
+, - Add, subtract L 2 num,num→num
+ Concatenate strings L 2 str,str→str
<< Shift left L 2 int,int→int
>> Shift right with sign extension L 2 int,int→int
>>> Shift right with zero extension L 2 int,int→int
<, <=,>, >= Compare in numeric order L 2 num,num→bool
<, <=,>, >= Compare in alphabetic order L 2 str,str→bool
instanceof Test object class L 2 obj,func→bool
in Test whether property exists L 2 str,obj→bool
== Test for equality L 2 any,any→bool
!= Test for inequality L 2 any,any→bool
=== Test for strict equality L 2 any,any→bool
!== Test for strict inequality L 2 any,any→bool
& Compute bitwise AND L 2 int,int→int
^ Compute bitwise XOR L 2 int,int→int
| Compute bitwise OR L 2 int,int→int
&& Compute logical AND L 2 any,any→any
| | Compute logical OR L 2 any,any→any
?: Choose 2nd or 3rd operand R 3 bool,any,any→any
= Assign to a variable or property R 2 lval,any→any
*=, /=, %=, +=, -, &, ^=, |=, <<=, >>=, >>>= Operate and assign R 2 lval,any→any
, Discard 1st operand, return second L 2 any,any->any

参考:
Javascript - The Definitive Guide, 6th, 4.7 Operator Overview

5. 语句

JavaScript 中语句总结如下:

Table 5: JavaScript statement syntax
Statement Syntax Purpose
break break [label]; Exit from the innermost loop or switch or from named enclosing statement
case case expression: Label a statement within a switch
continue continue [label]; Begin next iteration of the innermost loop or the named loop
debugger debugger; Debugger breakpoint
default default: Label the default statement within a switch
do/while do statement while (expression); An alternative to the while loop
empty ; Do nothing
for for(init; test; incr) statement An easy-to-use loop
for/in for (var in object) statement Enumerate the properties of object
function function name([param[,...]]) { body } Declare a function named name
if/else if (expr) statement1 [else statement2] Execute statement1 or statement2
label label: statement Give statement the name label
return return [expression]; Return a value from a function
switch switch (expression) { statements } Multiway branch to case or default: labels
throw throw expression; Throw an exception
try try { statements } [catch {statements}] [finally {statements}] Handle exceptions
use strict "use strict"; Apply strict mode restrictions to script or function
var var name [ = expr] [ ,... ]; Declare and initialize one or more variables
while while (expression) statement A basic loop construct
with with (object) statement Extend the scope chain (forbidden in strict mode)

参考:
Javascript - The Definitive Guide, 6th, 5.8 Summary of JavaScript Statements
http://www.ecma-international.org/ecma-262/5.1/#sec-12

5.1. for...of(ES6 中增加)

for...of 语句在可迭代对象(包括 Array, Map, Set, String, TypedArray,arguments 对象等等)上创建一个迭代循环。

下面是遍历 Array 的例子:

let iterable = [10, 20, 30];

for (let value of iterable) {
  console.log(value);
}
// 10
// 20
// 30

如果你不修改语句块中的变量,也可以使用 const 代替 let

let iterable = [10, 20, 30];

for (const value of iterable) {
  console.log(value);
}
// 10
// 20
// 30

参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/for...of

5.2. for...in

for...in 语句以任意顺序遍历一个对象的可枚举属性。对于每个不同的属性,语句都会被执行。

下面是 for...in 的一个例子:

var obj = {a:1, b:2, c:3};

for (var prop in obj) {
  console.log("obj." + prop + " = " + obj[prop]);
}

// Output:
// "obj.a = 1"
// "obj.b = 2"
// "obj.c = 3"

参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/for...in

5.3. Handle exceptions

5.3.1. throw

使用 throw 语句可以产生异常。如:

throw 'Error2'; // generates an exception with a string value
throw 42;       // generates an exception with the value 42
throw true;     // generates an exception with the value true

下面是 throw 语句的一个完整例子:

function UserException(message) {
   this.message = message;
   this.name = 'UserException';
}
function getMonthName(mo) {
   mo = mo - 1; // Adjust month number for array index (1 = Jan, 12 = Dec)
   var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul',
      'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
   if (months[mo] !== undefined) {
      return months[mo];
   } else {
      throw new UserException('InvalidMonthNo');
   }
}

try {
   // statements to try
   var myMonth = 15; // 15 is out of bound to raise the exception
   var monthName = getMonthName(myMonth);
} catch (e) {
   monthName = 'unknown';
   console.log(e.message, e.name); // pass exception object to err handler
}

5.3.2. try catch finally

try ... catch 语句有下面三种形式:

try...catch
try...finally
try...catch...finally
5.3.2.1. finally 语句中的 return

If the finally block returns a value, this value becomes the return value of the entire try-catch-finally production, regardless of any return statements in the try and catch blocks:

function f() {
  try {
    console.log(0);
    throw 'bogus';
  } catch(e) {
    console.log(1);
    return true; // this return statement is suspended
                 // until finally block has completed
    console.log(2); // not reachable
  } finally {
    console.log(3);
    return false; // overwrites the previous "return"
    console.log(4); // not reachable
  }
  // "return false" is executed now
  console.log(5); // not reachable
}
f(); // console 0, 1, 3; returns false

6. 对象

对象是 JavaScript 的基本数据类型。 对象是属性的无序集合,每个属性都是一个“名/值”对。 属性名是字符串,因此我们可以把对象看成是从字符串到值的映射。这种基本结构还有其它叫法,如“哈希表”、“字典”、“关联数组”。 JavaScript 中对象有一个非常重要的特性:可以从一个称为原型(prototpe)的对象继承属性,这个特性被称为原型式继承(prototypal inheritance)。

除了字符串、数字、布尔值,null 和 undefined 外,JavaScript 中的值都是对象。尽管字符串、数字和布尔值不是对象,但由于它们都有相应的“包装对象”,使得它们的行为和对象非常类似。

6.1. 创建对象

可以通过对象直接量、关键安 new 和 Object.create()函数来创建对象。

6.1.1. 对象直接量(用{}表示)

对象直接量(或称字面量)是由若干“名/值”对组成的映射表,“名/值”对中间用冒号分隔,“名/值”对之间用逗号分隔,整个映射表用花括号括起来。

用对象直接量创建对象的例子如下:

var empty = {};                          // An object with no properties
var point = { x:0, y:0 };                // Two properties, same as { "x":0, "y":0 };
var point2 = { x:point.x, y:point.y+1 }; // More complex values
var book = {
  "main title": "JavaScript",            // Property names include spaces,
  'sub-title': "The Definitive Guide",   // and hyphens, so use string literals
  "for": "all audiences",                // for is a reserved word, so quote
  author: {                              // The value of this property is
    firstname: "David",                  // itself an object. Note that
    surname: "Flanagan"                  // these property names are unquoted.
  }
};

6.1.2. 通过 new 创建对象(使用构造函数)

new 运算符可以创建并初始化一个新对象。 关键字 new 后跟随一个函数调用,这个函数称为 构造函数(constructor)。

var o = new Object();       // Create an empty object: same as {}.
var a = new Array();        // Create an empty array: same as [].
var d = new Date();         // Create a Date object representing the current time
var r = new RegExp("js");   // Create a RegExp object for pattern matching.

下面是自己定义对象构造函数的例子:

function Person(first, last, age, eye) {   # 按约定(不是语法要求),构造函数的首字母一般用大写
    this.firstName = first;
    this.lastName = last;
    this.age = age;
    this.eyeColor = eye;
}

var myFather = new Person("John", "Doe", 50, "blue");

6.1.3. 对象原型(JavaScript 核心概念之一)

每一个 JavaScript 对象(null 除外)都和另一个对象相关联,“另一个”对象就是原型(prototpe),几乎每一个对象都从原型继承属性。

所有通过对象直接量创建的对象都具有同一个原型对象:Object.prototype。
通过关键字 new 和构造函数创建的对象使用的原型是构造函数的 prototype 属性的值。如通过 new Array() 创建的对象的原型是 Array.prototype ,通过 new Date() 创建的对象的原型是 Date.prototype

所有的内置构造函数都具有一个继承自 Object.prototype 的原型。如 Date.prototype 的属性继承自 Object.prototype ,因此由 new Date() 创建的 Date 对象的属性同时继承自 Date.prototypeObject.prototype ,这一系列链接的原型对象就是所谓的 “原型链(prototype chain) ”

6.2. 查询和创建属性(点号或方括号)

可以用点(.)或方括号([])运算来获取对象属性的值。对于点(.)来说,右侧必须是一个以属性名称命名的简单标识符;对于方括号([])来说,方括号内必须是一个计算结果为字符串的表达式,这个字符串就是属性的名字。

也就是说,对象的属性可以以下面两种形式访问:

object.property
object["property"]

要改变或创建对象的属性,可以直接把点(.)或方括号([])放到赋值语句左边。如:

var book = {
  "main title": "JavaScript",            // Property names include spaces,
  'sub-title': "The Definitive Guide",   // and hyphens, so use string literals
  "for": "all audiences",                // for is a reserved word, so quote
  author: {                              // The value of this property is
    firstname: "David",                  // itself an object. Note that
    surname: "Flanagan"                  // these property names are unquoted.
  }
};

var author = book.author;          // Get the "author" property of the book.
var name = author.surname          // Get the "surname" property of the author.
var title = book["main title"]     // Get the "main title" property of the book.

book.edition = 6;                   // Create an "edition" property of book.
book["main title"] = "ECMAScript";  // Set the "main title" property.

6.3. 删除属性(delete 运算符)

delete 运算符可以删除对象的属性。delete 运算符只能删除自有属性,不能删除继承属性(要删除继承属性必须从定义这个属性的原型对象上删除它,而且这会影响到所有继承自这个原型的对象)。

delete book.author;               // The book object now has no author property.
delete book["main title"];        // Now it doesn't have "main title", either.

6.4. 检测属性(in 运算符、hasOwnProperty、propertyIsEnumerable)

JavaScript 中可以使用 in 运算符,hasOwnProperty()方法和 propertyIsEnumerable()方法来判断某个属性是否存在于某个对象中。

in 运算符的左侧是属性名,右侧是对象。如果对象的自有属性或继承属性中包含这个属性则返回 true。如:

var o = { x: 1 }
"x" in o;                // true: o has an own property "x"
"y" in o;                // false: o doesn't have a property "y"
"toString" in o;         // true: o inherits a toString property

对象的 hasOwnProperty()方法用来检测给定的名字是否是对象的自有属性。对于继承属性它将返回 false。如:

var o = { x: 1 }
o.hasOwnProperty("x");           // true: o has an own property x
o.hasOwnProperty("y");           // false: o doesn't have a property y
o.hasOwnProperty("toString");    // false: toString is an inherited property

propertyIsEnumerable()是 hasOwnProperty()的增强版,只有检测到是自有属性且这个属性是可枚举的时它才返回 true。通常由 JavaScript 代码创建的属性都是“可枚举的”,某些内置属性是不可枚举的。

var o = { x: 1 }
o.propertyIsEnumerable("toString");                   // false: not enumerable
Object.prototype.propertyIsEnumerable("toString");    // false: not enumerable

说明:用“!==”判断一个属性是否存在有时是不可靠的。如:

var o = { x: undefined }   // Property is explicitly set to undefined
o.x !== undefined          // false: property exists but is undefined
o.y !== undefined          // false: property doesn't even exist
"x" in o                   // true: the property exists
"y" in o                   // false: the property doesn't exists

6.5. 遍历属性(for/in 循环)

用 for/in 循环可以在循环体内遍历对象中所有可枚举的属性。如:

var o = {x:1, y:2, z:3};             // Three enumerable own properties
o.propertyIsEnumerable("toString")   // => false: not enumerable
for(p in o) {                        // Loop through the properties
  console.log(p);                    // Prints x, y, and z, but not toString
}

6.6. 引用

对象是通过引用来传递的,它们不会被拷贝。

7. 数组

数组是值的有序集合。每个值叫做一个元素,而每个元素在数组中有一个位置,以数字表示,称为索引。JavaScript 数组是无类型的:数组元素可以是任意类型,并且同一个数字中的不同元素也可能有不同的类型。JavaScript 数组的第一个元素的索引为 0。

JavaScript 数组是 JavaScript 对象的特殊形式,数组索引实际上和碰巧是整数的属性名差不多。

7.1. 创建数组

7.1.1. 数组直接量(用[]表示)

使用数组直接量是创建数组最简单的方法,在方括号中将数组元素用逗号隔开即可。 例如:

var empty = [];                    // An array with no elements
var primes = [2, 3, 5, 7, 11];     // An array with 5 numeric elements
var misc = [ 1.1, true, "a", ];    // 3 elements of various types

如果省略数组直接量中的某个值,省略的元素将被赋予 undefined 值,如:

var count = [1,,3];    // An array with 3 elements, the middle one undefined.
var undefs = [,,];     // 数组直接量允许有“可选的结尾逗号”,所以这个数组是2个元素(都为undefined),而不是3个元素

7.1.2. 通过 new 创建数组

调用构造函数 Array()是创建数组的另一种方法。可以用三种方式调用构造函数:
方式一:调用时不指定参数:

var a = new Array();          // 创建一个没有元素的空数组,等同于数组直接量[]

方式二:调用时有一个数值参数,它指定了数组的长度:

var a = new Array(10);        // 仅是预先分配数组空间,数组中没有存储值,甚至数组的索引属性还未定义。

方式三:显式指定两个或多个数组元素或者数组的一个非数值元素:

var a = new Array(5, 4, "test1", "test2");
var a = new Array("abc");

7.2. 数组长度

每个数组有一个 length 属性,其值比数组中最大的索引大 1。值得注意的是,设置 length 属性为一个小于当前长度的非负整数 n 时,当前数组中那些索引值大于或等于 n 的元素将从中删除。

a = [1,2,3,8,9];
var len = a.length;        // len 会为 5

a.length = 3;              // 现在a为[1,2,3]
a.length = 0;              // 删除了所有的元素,a为[]
a.length = 5;              // 长度为5,但是没有元素,就像new Array(5)

7.3. 数组元素的读写

使用 [] 操作符可以访问数组中的一个元素。数组的引用位于方括号的左边,方括号中一个返回非负整数值的任意表达式。使用该语法既可以读也可以写数组的一个元素。

如:

var misc = [ 1.1, true, "a", ];
var abc = misc[0];    // abc = 1.1
misc[1] = false;      // 修改数组索引为1的元素为false

数组是对象的特殊形式。如果使用负数或者非整数来索引数组,这种情况下,数值转换为字符串,字符串作为属性名来用。JavaScript 数组不会出现“越界”的错误,访问不存在属性,会得到 undefined 值。

var misc = [ 1.1, true, "a", ];
misc[-1.23] = "tes1";    // 创建了一个名为"-1.23"的属性
misc["abcd"] = "test2";  // 创建了一个名为"abcd"的属性
misc["2"];               // 和misc[2]相同

7.4. 数组元素的增加和删除

增加数组元素最简单的方法是:为新索引赋值。如:

a = [];           // a是空数组
a[0] = "zero";    // 向数组a中增加了一个元素
a[1] = "one";     // 向数组a中增加了另外一个元素

可以像删除对象属性一样使用 delete 运算符来删除数组元素,delete 操作并不影响数组的长度属性 length。如:

a = [1,2,3];
delete a[2];   // a在索引2的位置不再有元素
2 in a;        // => false,数组索引2并未在数组中定义
a.length       // => 2,delete操作并不影响数组长度

还有其他方法可以增加和删除数组元素,如 push()/unshift()/pop()/shift()等。

7.5. 常用的数组方法

Table 6: 一些常用的数组方法
Method Description
concat() Joins two or more arrays, and returns a copy of the joined arrays
copyWithin() Copies array elements within the array, to and from specified positions
every() Checks if every element in an array pass a test
fill() Fill the elements in an array with a static value
filter() Creates a new array with every element in an array that pass a test
find() Returns the value of the first element in an array that pass a test
findIndex() Returns the index of the first element in an array that pass a test
forEach() Calls a function for each array element
indexOf() Search the array for an element and returns its position
isArray() Checks whether an object is an array
join() Joins all elements of an array into a string
lastIndexOf() Search the array for an element, starting at the end, and returns its position
map() Creates a new array with the result of calling a function for each array element
pop() Removes the last element of an array, and returns that element
push() Adds new elements to the end of an array, and returns the new length
reduce() Reduce the values of an array to a single value (going left-to-right)
reduceRight() Reduce the values of an array to a single value (going right-to-left)
reverse() Reverses the order of the elements in an array
shift() Removes the first element of an array, and returns that element
slice() Selects a part of an array, and returns the new array
some() Checks if any of the elements in an array pass a test
sort() Sorts the elements of an array
splice() Adds/Removes elements from an array
toString() Converts an array to a string, and returns the result
unshift() Adds new elements to the beginning of an array, and returns the new length
valueOf() Returns the primitive value of an array

参考:http://www.w3schools.com/jsref/jsref_obj_array.asp

8. 函数

函数是这样一段 JavaScript 代码,它只定义一次,但可能被执行或调用任意次。

JavaScript 中函数的基本语法为:

function functionName(arg0, arg1, ... argN) {
  statements
}

JavsScript 中无需指定函数返回值的类型。除实参外,每次调用时函数还会拥有另一个值:本次调用的上下文,可以在函数体内通过 this 关键字来引用它。

如果函数挂载在一个对象上,作为对象的一个属性,那么这个函数也称为“对象的方法”。当通过这个对象来调用函数时,该对象就是此次调用的上下文,即该函数的 this 值。

在 JavaScript 中,函数也是对象。可以把函数赋值给变量,或者作为参数传递给其它函数。因为函数是对象,也可以给它们设置属性,甚至调用它们的方法。

函数的简单例子:

function sum(iNum1, iNum2) {
    return iNum1 + iNum2;
}

8.1. 函数对象

JavsScript 中的函数是一等公民。参见:First-class function

在 JavaScript 中,函数也是对象,例如可直接把函数赋值给变量。

var sum = function() {
    var i, x = 0;
    for (i = 0; i < arguments.length; ++i) { // arguments是特殊对象,保存着函数参数
        x += arguments[i];
    }
    return x;
}

var result = sum(1, 2, 3);
console.log(result);        // 输出 6

8.2. 函数调用方式(影响 this 含义)


一般地,有 4 种方法来调用 JavaScript 函数:

  • 作为函数;
  • 作为对象的方法;
  • 作为构造函数;
  • 通过它们的 call()和 apply()方法来间接调用。

其中,作为函数调用,最简单。如:

function myFun1(a, b) {
    return a * b;
}
myFun1(10, 2);           // myFun1(10, 2) will return 20

后方将介绍其它 3 种调用方法。

不同调用方式中 this 关键字的含义不同。

8.2.1. 函数作为“对象方法”调用

保存在对象的属性里的 JavaScript 函数,又称为方法。

假设 o 是对象,f是函数。

o.m = f;      // 给对象o定义方法m
o.m();        // 调用对象方法(相当于调用函数f)

函数作为“对象方法”被调用时,this 值指向调用它的对象。
先看一个简单的例子:

var o1 = {
    prop: 37,
    f1: function() {
        return this.prop;
    }
};
console.log(o1.f1());         // 输出 37

再看一个复杂些的例子:

var o = {prop: 37};

function independent() {
    return this.prop;
}

o.f = independent;

console.log(o.f());           // 输出 37
o.b = {g: independent, prop: 42};
console.log(o.b.g());         // 输出 42

注意:上面例子中 console.log(o.b.g()); 不会输出 37。

参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this

8.2.2. 函数作为“构造函数”调用

如果函数或方法调用之前带有关键字 new ,它就构成“构造函数调用”。

// This is a function constructor:
function myFunction(arg1, arg2) {
    this.firstName = arg1;
    this.lastName  = arg2;
}

// This creates a new object
var x = new myFunction("John","Doe");
console.log(x.firstName);               // 输出 "John"

没有形参的构造函数调用可以省略圆括号。 如,下面两行代码等价:

var o = new Object();
var o = new Object;      // 和同一行等价

构造函数调用创建一个新的空对象,构造函数中可以用 this 关键字引用这个新创建的对象。如,表达式 new o.m() 中,调用上下文并不是 o。

8.2.3. 通过 call()和 apply()调用函数

使用 call()和 apply()可以间接地调用函数,这两个方法都允许 显式指定调用所需的 this 值(调用上下文)。

call()和 apply()的第 1 个参数用来指定调用上下文,在函数体内通过 this 可以获得对它的引用。如,以对象 o 的方法来调用函数 f1(),可以这样:

f1.call(o);    // f1中,this为o
f1.apply(o);   // 同上

call()和 apply()的唯一不同是传递待调用函数参数的方式。call()是一个一个分开传递参数,而 apply()是通过数组传递参数。 如:

function fun1(a, b) {
    return a + b;
}

var o = new Object;
o = fun1.call(o, 10, 2);       // 12
o = fun1.apply(o, [10, 2]);    // 12

8.3. 函数的参数

JavaScript 参数传递方式为值传递(“pass by value”)。

(1) Javascript is always pass by value, but when a variable refers to an object (including arrays), the "value" is a reference to the object.
(2) Changing the value of a variable never changes the underlying primitive or object, it just points the variable to a new primitive or object.
(3) However, changing a property of an object referenced by a variable does change the underlying object.

摘自:http://stackoverflow.com/questions/6605640/javascript-by-reference-vs-by-value

JavaScript 参数传递方式测试例子:

function changeStuff(a, b, c)
{
  a = a * 10;               // 并不会影响形参
  b.item = "changed";       // 通过“对象引用的属性”可以改变形参对象的属性
  c = {item: "changed"};    // 并不会影响形参
}

var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};

changeStuff(num, obj1, obj2);

console.log(num);              // 输出 10
console.log(obj1.item);        // 输出 changed
console.log(obj2.item);        // 输出 unchanged

参考:http://stackoverflow.com/questions/518000/is-javascript-a-pass-by-reference-or-pass-by-value-language

JavaScript 函数调用不检查传入参数的个数。
如果调用函数时传入的实参比函数声明时指定的形参个数要少,剩下的形参将设置为 undefined 值。 常常用这个特性来实现“可选参数”。如:

function copyPropertyNamesToArray(o, /* optional */ a) {
    if (a === undefined) a = [];  // If undefined or null, use a blank array
    for(var property in o) a.push(property);
    return a;
}

copyPropertyNamesToArray(o);  // 调用时实参数少于形参数。

如果调用函数时传入的实参比函数声明时指定的形参个数要多时,可以通过特殊对象 arguments 来获得所有实参。

function sumAll() {
    var i, sum = 0;
    for (i = 0; i < arguments.length; i++) {
        sum += arguments[i];
    }
    return sum;
}
x = sumAll(1, 123, 500, 115, 44, 88);   // 调用时实参数多于形参数。

8.4. 函数的返回值

A function always returns a value. If the return value is not specified, then undefined is returned.
If the function was invoked with the new prefix, then this (the new object) is returned.

8.5. 自调用匿名函数(立即执行函数表达式)

下面代码是“定义匿名函数,并马上调用它”,函数运行结果显然是输出 2 个 7:

(function (a1,a2) { console.log(a1+a2); })(3,4); // 定义匿名函数,并马上调用它
(function (a1,a2) { console.log(a1+a2); }(3,4)); // 同上,另一种风格

这种方式被称为“Self-Executing Anonymous Function”或者“Immediately-Invoked Function Expression”。

在 JS 框架中,常常用这种方式来解决命名空间问题,确保 JS 框架中的变量不会污染到全局命名空间。

(function() {
    // 框架所有代码都写在这个匿名函数内,框架中的变量不会污染到全局命名空间
    var var1, var2, var3;
    function fun1() { }
    function fun2() { }
    ......
}())

参考:https://en.wikipedia.org/wiki/Immediately-invoked_function_expression

8.6. 嵌套函数

在 JavaScript 中,函数可以嵌套在其他函数里。

值得注意的是, 嵌套函数可以读写它外层函数的参数和变量。 如:

function add() {
    var counter = 0;
    function plus() {counter += 1;}  // plus可以外层函数add中的变量counter
    plus();

    return counter;
}

8.7. 闭包

直接看例子:

var displayClosure = function() {
    var count = 0;
    return function () {
        return ++count;     // 嵌套函数(内部函数)可以读写外部函数的参数和变量。
    };
}
var inc = displayClosure();
inc();                      // 第1次调用
inc();                      // 第2次调用
inc();                      // 第3次调用
var result = inc();         // 第4次调用
console.log(result);        // 会输出 4

9. Classes

ES6 中增加了 class(类)这个概念。

在 ES6 之前,可以用构造函数实现相同的目的。如:

function Person(first, last, age, eye) {
    this.firstName = first;
    this.lastName = last;
    this.age = age;
    this.eyeColor = eye;
}

Person.prototype.toString = function () {
  return '(' + this.firstName + ', ' + this.lastName + ', ' + this.age + ', ' + this.eyeColor + ')';
};

var myFather = new Person("John", "Doe", 50, "blue");

如果把上面代码改写为 ES6 中 class 的形式,则为:

// ES6中定义类的方式

class Person {
    constructor(first, last, age, eye) {
        this.firstName = first;
        this.lastName = last;
        this.age = age;
        this.eyeColor = eye;
    }

    toString() {       // 不需要加上function这个关键字
        return '(' + this.firstName + ', ' + this.lastName + ', ' + this.age + ', ' + this.eyeColor + ')';
    }
}

var myFather = new Person("John", "Doe", 50, "blue");

ES6 中的类,完全可以看作“其构造函数的另一种写法”。请看例子:

class Person {
    ......
}

console.log(typeof Person);                              // 输出 function
console.log(Person === Person.prototype.constructor);    // 输出 true

上面代码的输出表明,类的数据类型就是“函数”,类本身指向其构造函数。

参考:https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Classes

9.1. 静态方法(static)

使用 static 关键字可以定义静态方法。Static methods are called without instantiating their class and cannot be called through a class instance.

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  static distance(a, b) {
    const dx = a.x - b.x;
    const dy = a.y - b.y;

    return Math.hypot(dx, dy);
  }
}

const p1 = new Point(5, 5);
const p2 = new Point(10, 10);

console.log(Point.distance(p1, p2));      // 在类名上直接调用static方法
//console.log(p1.distance(p1, p2));       // 这行会报错,因为不能在对象上调用static方法!

9.2. 继承(extends)

用关键字 extends 可以实现继承。如:

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Dog extends Animal {
  speak() {
    console.log(this.name + ' barks.');
  }
}

var d = new Dog('Mitzie');
d.speak();                    // Mitzie barks.

9.2.1. 调用父对象中的方法(supper)

通过 supper 关键字可以调用父对象中的方法。如:

class Cat {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Lion extends Cat {
  speak() {
    super.speak();
    console.log(this.name + ' roars.');
  }
}

var l = new Lion('Fuzzy');
l.speak();

上面代码会输出:

Fuzzy makes a noise.
Fuzzy roars.

10. 模块

Node.js 默认采用 CommonJS 模块机制,导入模块时使用 require ,导出时使用 module.exports

在 ES6 中,引入了自己的模块机制,使用 import 语句来导入 ES6 模块,导出时使用 export

Node.js 也支持了 ES6 的模块机制,不过需要使用下面任意一种方式来启用它:1. 源文件使用 .mjs 后缀,2. 在 package.json 中定义 "type": "module" 。关于 Node.js 如何确定模块机制可参考:https://nodejs.org/api/packages.html#determining-module-system

10.1. import(ES6 中增加)

7import 的几种用法。

Table 7: Import Forms
Import Statement Form Module Request Import Name Local Name
import v from "mod"; "mod" "default" "v"
import {x} from "mod"; "mod" "x" "x"
import {x as v} from "mod"; "mod" "x" "v"

下面是导入 CommonJS 模块的例子:

// CommonJS的写法
const moduleA = require('moduleA');
const func1 = moduleA.func1;
const func2 = moduleA.func2;

相应地,如果是导入 ES6 模块,则应该写为:

// ES6的写法
import { func1, func2 } from 'moduleA';

10.2. export(ES6 中增加)

下面是 export 的例子:

// module "my-module.js"
function cube(x) {
  return x * x * x;
}
const foo = Math.PI + Math.SQRT2;
var graph = {
    options:{
        color:'white',
        thickness:'2px'
    },
    draw: function(){
        console.log('From graph draw function');
    }
}
export { cube, foo, graph };

11. 高级主题

11.1. Automatic Semicolon Insertion (ASI)

在 JavaScript 中下面这些语句需要使用 ; 来表示结束:

  • 空语句
  • letconstimportexport 开头的声明语句;
  • var 开头的变量声明语句;
  • 表达式语句;
  • debugger 语句;
  • continue 语句;
  • break 语句;
  • return 语句;
  • throw 语句。

不过,由于有 Automatic Semicolon Insertion(自动分号插入)机制,在书写代码时,上面这些语句结束处的 ; 有时可以省略。如:

var var1=1              // 省略语句结束处分号
var var2=2              // 省略语句结束处分号
console.log(var1)       // 省略语句结束处分号

需要说明的是,下面这些语句本身就不需要用 ; 来表示结束:

  • 块语句
  • if 语句
  • try 语句

你若在上面这三类语句的结束处加上 ; ,那只是画蛇添足地多增加了一个空语句而已。 比如:

function cube(n) {
    return n*n*n;
}     // 不需要在函数定义语句(块语句)结束处插入分号,如果插入了分号,只是把它当前一个多余的空语句。

if (i % 15 === 0) {
    console.log("Foooo");
}     // 不需要在if语句结束处插入分号,如果插入了分号,只是把它当前一个多余的空语句。

11.1.1. ASI 有时和你的意图不一样

一般情况下,ASI 工作得很好,使你可以少写很多分号。

不过,ASI 有时和你的意图不一样。请看下面例子:

           JavaScript 代码                              代码输出                      
 var name = {"str1": [1, 2], "str2": [4, 5]}    
 ["str1", "str2"].forEach(function(value) {     
     console.log(value);                        
 })                                             
 console.log(name)                              
 4                                   
 5                                   
 undefined                           
                                     
                                     
 var name = {"str1": [1, 2], "str2": [4, 5]};   
 ["str1", "str2"].forEach(function(value) {     
     console.log(value);                        
 })                                             
 console.log(name)                              
 str1                                
 str2                                
 { str1: [ 1, 2 ], str2: [ 4, 5 ] }  
                                     
                                     

上面两份代码的区别仅仅在于 var 语句结束处有没有分号,得到的结束却截然不同。在第 1 份代码中,编译器把 {"str1": [1, 2], "str2": [4, 5]}["str1", "str2"] 当作一个整体来分析了,因为它也是合法的代码,这个代码相当于 {"str1": [1, 2], "str2": [4, 5]}["str2"] ,也就是 [4, 5]

下面是另外一个 ASI 和你的意图不一样的例子:

 /* this */          
 a = b + c           
 (d + e).print()     
   /* "Understood" as */     
   a = b + c(d + e).print(); 
                             

总结: ASI 有时和你的意图不一样,如果你的语句开头是 [ 或者 ( ,则很可能发生这种情况。此时,你可以在它前面加上 ; 来避免误解。

11.1.2. Restricted Production(不能跨行书写)

在 JavaScript 中,下面几种特殊语句是不允许行结束符存在的(即不能跨行书写):

PostfixExpression :
    LeftHandSideExpression [no LineTerminator here] ++
    LeftHandSideExpression [no LineTerminator here] --

ContinueStatement :
    continue;
    continue [no LineTerminator here] LabelIdentifier ;

BreakStatement :
    break ;
    break [no LineTerminator here] LabelIdentifier ;

ReturnStatement :
    return [no LineTerminator here] Expression ;
    return [no LineTerminator here] Expression ;

ThrowStatement :
    throw [no LineTerminator here] Expression ;

ArrowFunction :
    ArrowParameters [no LineTerminator here] => ConciseBody

YieldExpression :
    yield [no LineTerminator here] * AssignmentExpression
    yield [no LineTerminator here] AssignmentExpression

如果你把上面这些语句跨行书写(你不应该这么做!),那么很可能它和你的真正意图不一样。比如:

+------------------------+------------------------+
| Code (bad code)        | "Understood" as        |
+------------------------+------------------------+
| a                      | a;                     |
| ++                     | ++b;                   |
| b                      |                        |
+------------------------+------------------------+
| return                 | return;                |
| 2*a + 1;               | 2*a + 1;               |
+------------------------+------------------------+
| function getObject() { | function getObject() { |
|   return               |   return;              |
|   {                    |   {                    |
|     // some lines      |     // some lines      |
|   };                   |   };                   |
| }                      | }                      |
+------------------------+------------------------+

参考:https://www.ecma-international.org/ecma-262/6.0/#sec-rules-of-automatic-semicolon-insertion

11.2. Built-in object: Promise

The Promise object is used for asynchronous computations. A Promise represents a single asynchronous operation that hasn't completed yet, but is expected in the future.

11.2.1. 什么是 Promise(未来值的占位符)

Promise 是一种异步编程设施(ES6 中引入), Promise 对象中保存着未来才会结束的操作的结果,它是未来值的占位符。

设想一下这样一个场景:我走到快餐店的柜台,点了一个芝士汉堡。我交给收银员 1.47 美元。通过下订单并付款,我已经发出了一个对某个值(就是那个汉堡)的请求。我已经启动了一次交易。
但是,通常我不能马上就得到这个汉堡。收银员会交给我某个东西来代替汉堡:一张带有订单号(假设为 113)的收据。订单号就是一个承诺(promise),保证了最终我会得到我的汉堡。所以我得好好保留带有订单号的收据。在等待的过程中,我可以做点其他的事情,比如给朋友发个短信:“嗨,要来和我一起吃午饭吗?我正要吃芝士汉堡。”我已经在想着未来的芝士汉堡了,尽管现在我还没有拿到手。我的大脑之所以可以这么做,是因为它已经把订单号当作芝士汉堡的占位符了。从本质上讲, 这个占位符使得这个值不再依赖时间。这是一个未来值。

终于,我听到服务员在喊“订单 113”,然后愉快地拿着收据走到柜台,把收据交给收银员,换来了我的芝士汉堡。换句话说,一旦我需要的值准备好了,我就用我的承诺值(value-promise)换取这个值本身。

但是,还可能有另一种结果。他们叫到了我的订单号,但当我过去拿芝士汉堡的时候,收银员满是歉意地告诉我:“不好意思,芝士汉堡卖完了。”除了作为顾客对这种情况感到愤怒之外,我们还可以看到 未来值的一个重要特性:它可能成功,也可能失败。

Promise 类似于上面场景中“带有订单号的收据”,它是未来值的占位符。

11.2.2. Promise 的三种状态

Promise 对象有三种状态:Pending(进行中)、Fulfilled (已完成,或称为 Resolved )和 Rejected(已失败)。Promise 对象的状态改变,只有两种可能:“从 Pending 变为 Fulfilled”和“从 Pending 变为 Rejected”。Promise 对象的状态一旦改变,就不会再变化,这就是 Promise 名字的由来。

下面是创建 Promise 对象的常见形式:

var p = new Promise(
    /* executor */
    function(resolve, reject) {

    // Do an async task and then...

    if(/* good condition */) {
        resolve('Success!');     // 将Promise对象的状态从Pending变为Fulfilled(Resolved)
    }
    else {
        reject('Failure!');      // 将Promise对象的状态从Pending变为Rejected
    }
});

executor 函数(就是 Promise 参数)在 Promise 构造函数执行时同步执行。Promise 对象生成以后,可以用 then 方法的两个参数分别指定当 Promise 对象的状态变为 Fulfilled 和 Reject 时的回调函数。 如:

p.then(function(value) {    // 如果Promise实例p的状态是Fulfilled,则会调用这个函数(第一个函数)
  // success
}, function(error) {        // 如果Promise实例p的状态是Rejected,则会调用这个函数(第二个函数,可选的)
  // failure
});

下面是一个 Promise 简单例子(没有什么意义,仅是演示 Promise 功能):

var myPromise = new Promise(function(resolve, reject) {
    console.log("Run promise");

    // 为模拟可能成功或失败,这里随机地调用resolve或者reject
    var randomNumber = Math.floor((Math.random() * 10) + 1)
    if (randomNumber <= 5) {
      resolve(randomNumber)    // randomNumber会传给then中指定的第一个回调函数
    } else {
      reject(randomNumber)     // randomNumber会传给then中指定的第二个回调函数
    }
});

console.log("Test");

myPromise.then(function(value) {
    console.log("OK, return " + value)
}, function(value) {
    console.log("FAILURE, return " + value)
});

myPromise.then(function(value) {    // 由于Promise对象状态一旦改变就不会再变化,所以第二次(或更多次)在同一个Promise对象上调用then的输出一定和第一次相同
    console.log("OK, return " + value)
}, function(value) {
    console.log("FAILURE, return " + value)
});

上面代码的可能输出:

Run promise
Test
OK, return 3
OK, return 3         # 注:这一行内容一定会和上一行相同

当然,还可以是其它输出,如:

Run promise
Test
FAILURE, return 9
FAILURE, return 9    # 注:这一行内容一定会和上一行相同

11.2.3. Promise.prototype.then()

前面已经介绍了 Promise 对象中 then 方法的基本用法( then 方法的第一个参数是 Resolved 状态的回调函数,第二个参数是 Rejected 状态的回调函数,且第二个参数可以省略)。这里对 then 方法再作一些介绍。

如果 then 的参数(即两个回调函数)返回 Promise 对象,则这个 Promise 对象会作为 then 方法的返回值。即: then 方法会返回 then 的参数(即两个回调函数)返回的 Promise 对象。 如:

function getMyPromise(value) {
    return new Promise(function(resolve, reject) {
        resolve(value)
    });
}

getMyPromise("aa").then(function(value) {  // 省略了Rejected状态的回调函数
    console.log(value);
    return getMyPromise("bb");    // Resolved状态的回调函数返回了另外一个Promise对象
});

// 上面代码会输出:
// aa

这样,我们可以在 then 的返回对象(另外一个 Promise 对象)上接着调用 then ,这就是 then 的链式调用。 比如:

function getMyPromise(value) {
    return new Promise(function(resolve, reject) {
        resolve(value)
    });
}

getMyPromise("aa").then(function(value) {
    console.log(value);
    return getMyPromise("bb");
}).then(function(value) {       // 再次调用 then
    console.log(value);         // 有需要的话,还可以返回Promise,后面再调用then
});

// 上面代码会输出:
// aa
// bb
11.2.3.1. 返回值自动包装为 Promise

我们知道, then 方法会返回 then 的参数(即两个回调函数)返回的 Promise 对象。

then 的参数(即两个回调函数)返回的是普通的数字或字符串等时,会通过 Promise.resolve 把它自动包装为 Promise 对象。如:

var p2 = new Promise(function(resolve, reject) {
  resolve(1);
});

p2.then(function(value) {
  console.log(value);       // 输出 1
  return value + 1;         // 尽管不是返回Promise,但相当于 return Promise.resolve(value + 1);
}).then(function(value) {
  console.log(value);       // 输出 2
});

then 的参数(即两个回调函数)抛出异常时,会通过 Promise.reject 把它自动包装为 Promise 对象。如:

var p2 = new Promise(function(resolve, reject) {
  resolve(1);
});

p2.then(function(value) {
  console.log(value);       // 输出 1
  throw value + 1;          // 相当于 return Promise.reject(value + 1);
}).then(function(value) {
  console.log(value);
}, function(value) {
  console.log("error " + value);  // 输出 error 2
});

11.2.4. Promise.prototype.catch()

catch 只处理 Rejected 状态的 Promise。 catch(onRejected) 作用和 then(undefined, onRejected) 类似。

比如:

// 方式1(不推荐)
promise.then(function(value) {   // 同时指定Fulfilled状态和Rejected状态的回调函数
    // success
    console.log(value);
  }, function(err) {
    // fail
    console.log(err);
  });

上面代码相当于:

// 方式2(推荐)
promise.then(function(value) {   // 只指定Fulfilled状态的回调函数
    // success                   // 和方式1的不同之处:在这个函数里抛出的异常,也能被后面的catch处理。
    console.log(value);
  })
  .catch(function(err) {         // 相当于指定Rejected状态的回调函数
    // fail
    console.log(err);
  });

不过,如上面代码的注释中说明的那样,方式 1 和方式 2 并不完全相同。

总结: 不推荐使用两个参数的 then;如果有需要,推荐使用 catch。

11.2.5. 使用 Arrow function 简化代码

使用 ES6 中的 Arrow function => 可以使代码更加简洁。

假如,有代码:

var myPromise = new Promise(function(resolve, reject) {
    console.log("Run promise");

    var randomNumber = Math.floor((Math.random() * 10) + 1)
    if (randomNumber <= 5) {
      resolve(randomNumber)
    } else {
      reject(randomNumber)
    }
});

myPromise.then(function(value) {
    console.log("OK, return " + value)
}, function(value) {
    console.log("FAILURE, return " + value)
});

使用 => 时,上面代码也可以写为:

var myPromise = new Promise((resolve, reject) => {
    console.log("Run promise");

    var randomNumber = Math.floor((Math.random() * 10) + 1)
    if (randomNumber <= 5) {
      resolve(randomNumber)
    } else {
      reject(randomNumber)
    }
});

myPromise.then(
    value => console.log("OK, return " + value),
    value => console.log("FAILURE, return " + value)
);

11.2.6. reject 或 resolve 后要不要加 return 语句

考虑下面代码:

 1: function divide(numerator, denominator) {
 2:   return new Promise((resolve, reject) => {
 3:     if (denominator === 0) {
 4:       reject("Cannot divide by 0");
 5:       // return;  // 需要这个return语句吗?
 6:     }
 7: 
 8:     resolve(numerator / denominator);
 9:   });
10: }
11: 
12: divide(5, 0)
13:   .then((result) => console.log('result: ', result))
14:   .catch((error) => console.log('error: ', error));

我们需要第 5 行中的 return 语句吗?不需要,但在上面场景中最好加上。

首先,我们要明确 reject 或者 resolve 并不会改变代码执行流程。如下面代码:

 1: function divide(numerator, denominator) {
 2:   return new Promise((resolve, reject) => {
 3:     if (denominator === 0) {
 4:       reject("Cannot divide by 0");
 5:     }
 6: 
 7:     console.log('operation succeeded');
 8:     resolve(numerator / denominator);
 9:   });
10: }
11: 
12: divide(5, 0)
13:   .then((result) => console.log('result: ', result))
14:   .catch((error) => console.log('error: ', error));

会输出:

operation succeeded
error:  Cannot divide by 0

仍然会输出“operation succeeded”,因为 reject 并不隐含着 return 语句,所以第 7 行和第 8 行代码仍然会执行(第 8 行代码没有效果,因为 Promise 对象状态一旦改变就不会再变化,reject 后再 resolve,resolve 会没有效果)。

这个输出是令人困惑的,有下面三种方案解决这个问题。
方案一:reject 后马上加个 return 语句。

 1: function divide(numerator, denominator) {
 2:   return new Promise((resolve, reject) => {
 3:     if (denominator === 0) {
 4:       reject("Cannot divide by 0");
 5:       return;
 6:     }
 7: 
 8:     console.log('operation succeeded');
 9:     resolve(numerator / denominator);
10:   });
11: }

方案二(不推荐):把方案一的 return 和 reject 合并为一行。

 1: function divide(numerator, denominator) {
 2:   return new Promise((resolve, reject) => {
 3:     if (denominator === 0) {
 4:       return reject("Cannot divide by 0");
 5:     }
 6: 
 7:     console.log('operation succeeded');
 8:     resolve(numerator / denominator);
 9:   });
10: }

Promise 中,callback 函数的返回值是被直接忽略的,所以这种方案只是方案一的一个小变种。不推荐这种方案,它使程序看起来更复杂。

方案三:使用 if/else 确保 reject 或 resolve 是最后一条语句。

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) {
      reject("Cannot divide by 0");
    } else {
      console.log('operation succeeded');
      resolve(numerator / denominator);
    }
  });
}

参考:https://stackoverflow.com/questions/32536049/do-i-need-to-return-after-early-resolve-reject

12. ES6 新特性

12.1. 解构赋值(Destructuring assignment)

ES6 提供了一种新语法,它可以从数组和对象中提取值,对变量进行赋值,这被称为“解构”。

参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

12.1.1. 数组解构

下面是数组解构的简单实例:

var a, b, rest;
[a, b] = [10, 20];   // 这是解构,从数组 [10, 20] 中提取值,赋值给变量 a 和 b

console.log(a);      // 10
console.log(b);      // 20

[a, b, ...rest] = [10, 20, 30, 40, 50];  // 最后一个变量前加三点,会把数组剩余元素组成的数组赋值给它

console.log(rest);   // [30,40,50]

12.1.2. 对象解构

下面是对象解构的简单实例:

let { foo, bar } = { bar: 'bbb', foo: 'aaa' };
console.log(foo);    // aaa
console.log(bar);    // bbb

对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

在对象解构中,如果想把变量名设置为和属性名不一样,则需要采用下面的语法:

let { a: newName1, b: newName2 } = o;

上面的语法有点奇怪,它表示把对象 o 的属性 ab 分别赋值到变量 newName1newName2 中,即相当于:

let newName1 = o.a;
let newName2 = o.b;

下面是解构对象时,属性重命名的例子:

var {p: foo, q: bar} = {p: 42, q: true};  // 对象解构,属性p重命名为了foo,而属性q重命名为了bar

console.log(foo);      // 42
console.log(bar);      // true

12.2. 展开(Spread)

数组变量前加三个点表示把它“展开”,如:

let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];   // first和second被展开,bothPlus相当于 [0, 1, 2, 3, 4, 5]

下面是数组展开的另一个例子:

function sum(x, y, z) {
  return x + y + z;
}

const numbers = [1, 2, 3];

console.log(sum(...numbers));            // 输出 6
console.log(sum.apply(null, numbers));   // 同上,输出 6

对象也可以展开,如:

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };

上面例子中, search 相当于 { food: "rich", price: "$$", ambiance: "noisy" } 。注意, 展开对象后面的属性会覆盖前面的属性, 所以对象 search 中属性 food 会为 "rich" ,而不是 "spicy"

再看一个例子:

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search1 = { food: "rich", ...defaults };

这个例子中,根据原则“展开对象后面的属性会覆盖前面的属性”, 所以对象 search1 中属性 food 仍然是 "spicy"

参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax

13. Tips

13.1. “//<![CDATA[”的作用

有时会看到类似下面的代码,其中“//<![CDATA[”和“//]]>”有什么用呢?

<script type="text/javascript">
//<![CDATA[
...code...
//]]>
</script>

“//<![CDATA[”和“//]]>”在 html 文件只是注释,没有任何意义。
加入它们的目的是 确保这个 html 被解析为 xhtml(xml)时,也是合法的。 因为在 xhtml 中小于号(<)、大于号(>)、和号(&)和双引号(")不是合法的 xml 元素,所以下面代码作为 xhtml 分析时会报错误:

<script type="text/javascript">
function compare(a, b) {
  if (a > b) {                       // 在xhtml中, > 应该为 $gt
    alert("a is greater than b");    // 在xhtml中," 应该为 &quot
  } else if ( a < b) {               // 在xhtml中,< 应该为 &lt
    alert("a is less than b");
  } else {
    alert("a is equal to b");
  }
}
</script>

解决办法是把代码放到“<![CDATA[”和“]]>”之间,这样 xhtml 分析器就不会分析这段代码了。

The term CDATA is used about text data that should not be parsed by the XML parser.
Characters like "<" and "&" are illegal in XML elements.
A CDATA section starts with "<![CDATA[" and ends with "]]>"

而“<![CDATA[”和“]]>”在 html 中并不识别,所以把它们放入 html 的注释中。即下面代码在 html 和 xhtml 中都是合法的:

<script type="text/javascript">
//<![CDATA[
function compare(a, b) {
  if (a > b) {
    alert("a is greater than b");
  } else if ( a < b) {
    alert("a is less than b");
  } else {
    alert("a is equal to b");
  }
}
//]]>
</script>

说明:把上面的函数 compare 写到一个单独的 js 文件中,就无需使用“//<![CDATA[”和“//]]>”了。

参考:http://stackoverflow.com/questions/7092236/what-is-cdata-in-html

13.2. !! 的作用

JavaScript 中在表达式前面使用两个取反运算 !! 可以把表达式值变为 true 或者 false,比如:

var isIE8 = !! navigator.userAgent.match(/MSIE 8.0/);
console.log(isIE8);             // returns true or false

var isIE8 = navigator.userAgent.match(/MSIE 8.0/);
console.log(isIE8);             // returns either an Array or null

参考:https://medium.com/@chirag.viradiya_30404/whats-the-double-exclamation-sign-for-in-javascript-d93ed5ad8491

13.3. this keyword

大多数情况下,函数中 this 关键的含义由该函数被调用的方式决定,具体细节参见节 8.2 。在 ES5 中,函数对象上引入了 bind 方法可以明确设置 this 的含义(这样 this 和函数被调用的方式无关了)。在 ES2015 中,引用了 arrow functions ,它没有自己的 this 绑定(在 arrow functions 中使用 this 时,是其外层函数的 this 绑定)。

参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this

Author: cig01

Created: <2015-10-07 Wed>

Last updated: <2022-03-16 Wed>

Creator: Emacs 27.1 (Org mode 9.4)