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 规范的一种实现。
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 |
v9 | June 2018 | Async iterators, Object rest/spread properties, Promise prototype finally |
v10 | June 2019 | Array.prototype.{flat, flatmap}, Object.fromEntries, String.prototype.{trimStart,trimEnd}, Symbol.prototype.description |
v11 | June 2020 | Bigint, Dynamic import, Exporting modules, Optional Chaining, Nullish coalescing operator, Promise.AllSettled |
v12 | June 2021 | Numberic separators, String.protype.replaceAll, Logical assignment operator, Promise.any, WeakRef |
参考:
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/
Standard ECMA-262 9th Edition: https://262.ecma-international.org/9.0/
Standard ECMA-262 10th Edition: https://262.ecma-international.org/10.0/
Standard ECMA-262 11th Edition: https://262.ecma-international.org/11.0/
Standard ECMA-262 12th Edition: https://262.ecma-international.org/12.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 文件,会显示下面内容:
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
语句声明一个“块级作用域”的本地变量,并且可选的将其初始化为一个值。
下面例子演示了 var
和 let
的区别。
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"
下面是一些函数,可以用来把数字以特定的格式转换为字符串。
相关函数 | 说明 |
---|---|
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 属性可以确定字符串的长度。一些字符串相关函数如下所示:
字符串相关函数 | 描述 |
---|---|
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 代表操作符数量。
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
4.1. Comma operator
The comma (,) operator evaluates each of its operands (from left to right) and returns the value of the last operand.
let x = 1; x = (x++, x); // the last operand is returned console.log(x); // Expected output: 2 x = (2, 3, 4); // the last operand is returned console.log(x); // Expected output: 4
参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comma_operator
5. 语句
JavaScript 中语句总结如下:
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.prototype
和 Object.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. 常用的数组方法
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 |
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 含义)
- 作为函数;
- 作为对象的方法;
- 作为构造函数;
- 通过它们的 call() 和 apply() 方法来间接调用。
其中,作为函数调用,最简单。如:
function myFun1(a, b) { return a * b; } myFun1(10, 2); // myFun1(10, 2) will return 20
后方将介绍其它 3 种调用方法。
不同调用方式中 this 关键字的含义不同。
8.2.1. 函数作为“对象方法”调用
保存在对象的属性里的 JavaScript 函数,又称为方法(Method)。
假设 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
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 中增加)
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 中下面这些语句需要使用 ;
来表示结束:
- 空语句
- 以
let
、const
、import
、export
开头的声明语句; - 以
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
的属性 a
和 b
分别赋值到变量 newName1
和 newName2
中,即相当于:
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中," 应该为 " } else if ( a < b) { // 在xhtml中,< 应该为 < 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
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