[关闭]
@Otokaze 2018-03-29T00:18:08.000000Z 字数 80075 阅读 494

JavaScript 笔记

html

Hello world

在 HTML 文档中使用 javascript 脚本有两种方式,都是通过 <script> 元素实现的。

  1. <!-- 直接内嵌 -->
  2. <script>
  3. console.log('hello, world!');
  4. </script>
  5. <!-- 外部文件 -->
  6. <script src="hello.js"></script>
  7. <!-- 'hello,js'文件内容 -->
  8. console.log('hello, world!');

如果是少量的 js,可以使用内嵌方式,这种方式的缺点是不易维护,代码不可重用。
如果是大量的 js,建议使用外部文件,这种方式的优点是容易维护,代码可以重用。

javascript 的严格模式'use strict';,因为 javascript 有很多设计缺陷,经常导致一些诡异的 bug,很难发现,为了尽早的发现这些错误,强烈建议使用严格模式

开启严格模式很简单,只需在脚本的头部函数的头部添加'use strict';语句,不支持严格模式的浏览器会直接忽略它,因为这是一个普通的语句,没有任何副作用。本文中的所有代码均以严格模式为标准。不考虑宽松模式(正常模式)。

  1. 'use strict';
  2. // TODO
  1. function func() {
  2. 'use strict';
  3. // TODO
  4. }

javascript 允许不在句末写分号,因为它会自动的在句末添加;号。但是强烈建议自己写上分号,不要去省略。我们来看一下自动加分号的隐藏 bug:

  1. 'use strict';
  2. function func() {
  3. return
  4. 'hello, world!';
  5. }
  6. var str = func();
  7. console.log(str); // undefined

因为自动加分号的机制,它实际上等价于:return; 'hello, world!';,而不带返回值的 return 语句实际上就是return undefined;

数据类型

注释// 单行注释/* 多行注释 */

javascript 标识符
必须以字母_$开头,其后可跟字母数字_$

定义变量
var
这是 ES6(ES2015)之前的唯一方式,var 不能定义局部作用域的变量,使用 var 定义的变量只有两种作用域:全局作用域函数作用域。另外,var 定义的全局变量其实是全局对象(在浏览器中为 window)的一个属性,也即,对于var a;,可以通过awindow.a来访问,没有区别。但是,它又与直接定义的 window 属性不同,使用 var 定义的全局变量不可以使用 delete 删除。对于同一个用 var 定义的变量,可以再次使用 var 进行定义(应该叫做重新声明),不会有影响。除此之外,var 声明语句会被默认提升至当前作用域的开头(脚本的头部、函数的头部),这种行为称为变量提升(hoisting)。除了 var 语句会被提升外,函数的定义也会被提升(非函数表达式),具体的表现如下:

  1. 'use strict';
  2. console.log(a); // undefined
  3. console.log(f()); // 'hello, world!'
  4. var a = 100;
  5. function f() { return 'hello, world!'; }

let
ES6 新增的语法,用来定义一个变量。与 var 不同的是,let 定义的变量有三种作用域:全局作用域函数作用域块级作用域。并且,let 定义的全局变量不再是 window 对象的属性,更不能使用 delete 语句删除它。除此之外,不允许使用 let 重复声明同一个变量,let 语句也没有所谓的变量提升。因此,如果可以,请尽量使用 let 定义变量!

const
ES6 新增的语法,用来定义一个常量。因此必须在声明的同时赋初值(即初始化),并且,常量被定义后,不允许被修改(能修改还能叫做常量吗)。除此之外,它和 let 具有一样的特性。

变量初始值
在 C/C++ 中,一个变量如果在定义的同时没有赋初值(即没有初始化),那么它也是有初始值的,对于 static 变量,初始值为 0,对于其他的变量(栈、堆),初始值为垃圾值。

同样的,在 javascript 中,如果在定义变量的同时没有赋初值(如var a; let b;),那么,该变量的初始值为undefinedundefined是一个值,它的类型是undefined(非对象)。

除了 undefined 外,还有一个特殊的值:null,也就是空指针/空引用,null 是一个值,它的类型是Object(对象)。

undefined、null 的相关属性:

  1. 'use strict';
  2. console.log(undefined); // 'undefined'
  3. console.log(null); // 'null'
  4. console.log(typeof undefined); // 'undefined' 基本类型
  5. console.log(typeof null); // 'object' 引用类型
  6. console.log(null instanceof Object); // false,因为 null 没有 __proto__ 属性,它只是空指针!

instanceof判断一个对象(哈希表)是否属于某个类型(构造函数)的依据是:obj.__proto__ === Constructor.prototype,因此,null instanceof Object的结果为 false 就不奇怪了。

undefinednull的异同
1. undefined是基本类型,null是引用类型
2. undefined表示变量未初始化,null表示对象的值为空(指针)
3. undefinednull在布尔环境中均被转换为 false
4. undefined在数值环境中被转换为 NaN,null 被转换为 0

数据类型
与 java 一样,javascript 中也分为基本类型(原始类型)引用类型,同时基本类型也有对应的包装类型(引用类型)。

基本类型(6 种)
boolean:布尔类型。字面值有truefalse
number:数值类型。64位双精度,无穷大+/-Infinity、非数值NaN
string:字符串类型。使用单/双引号表示字符串,如'hello'"hello"
symbol:ES6 新增类型。不过我暂时不知道它有什么卵用。
undefined:未定义类型。字面值只有undefined
null:空指针类型。字面值只有null(个人认为 null 是值而不是类型)。

基本类型的值是不可变的。

引用类型(对象)
Object:对象。本质是 key-value 键值对,与 Java 中的 HashMap 类似。其中,key 为字符串,value 为任意类型。Object 是所有引用类型的基类(包括 Function、Array)。

包装类型(存在自动装箱)
Boolean,布尔类型 boolean 的包装类;
Number,数值类型 number 的包装类;
String,字符串类型 string 的包装类;
Symbol,符号类型 symbol 的包装类。

包装类型的通用方法:valueOf(),用于获取所包装的基本类型值。
包装类型内部的基本类型值是不可变的,但是包装类型自己是可变的。

判断数值
isNaN(testValue):检测传入的值是否为 NaN(非数值)
isFinite(testValue):检测传入的值是否为有效值(非无穷大)

小结

  1. 'use strict';
  2. /* 变量的类型可在运行时改变 */
  3. let foo; // type: undefined
  4. console.log(typeof foo);
  5. foo = false; // type: boolean
  6. console.log(typeof foo);
  7. foo = 10; // type: number
  8. console.log(typeof foo);
  9. foo = 'hello'; // type: string
  10. console.log(typeof foo);
  11. /* 基本类型与对应的包装类型 */
  12. let bool1 = true; // 基本类型
  13. let bool2 = Boolean(true); // 基本类型(普通函数调用)
  14. let bool3 = new Boolean(true) // 引用类型(构造函数)
  15. console.log(bool1);
  16. console.log(bool2);
  17. console.log(bool3);
  18. console.log(bool1 === bool2); // true
  19. console.log(bool2 === bool3); // false,引用类型比较的是内存地址

一般情况下,不建议使用包装类型,浪费内存,给自己找麻烦。

数据类型转换

一般无需手动将其他类型转换为布尔值,if/while/for 等测试语句会自动转换。
false 值undefinednull-0+0NaN''(空串)。
true 值:除了上面给定的值外,其余的值均被自动转换为 true。

字符串转换为数值类型:

String() 会调用 toString() 方法;拼接字符串可使用 + 操作符(调用 toString())

  1. 'use strict';
  2. console.log(true + '-string'); // 'true-string'
  3. console.log(100 + '-string'); // '100-string'
  4. console.log('str' + '-string'); // 'str-string'
  5. console.log(null + '-string'); // 'null-string'
  6. console.log(undefined + '-string'); // 'undefined-string'
  7. console.log(new Boolean(true) + '-string'); // 'true-string'
  8. console.log(new Number(100) + '-string'); // '100-string'
  9. console.log(new String('str') + '-string'); // 'str-string'

布尔字面量
布尔类型有两种字面量:truefalse

整型字面量
二进制:0b0B开头
八进制:0o0O开头(非严格模式可用0开头)
十进制:没有特定前缀
十六进制:0x0X开头

浮点型字面量
语法:[(+|-)][digits][.digits][(E|e)[(+|-)]digits]
浮点数字面量至少有一位数字,而且必须带小数点或者e/E(10 的 N 次方)

字符串字面量
字符串字面量是由单引号'或双引号"括起来的零个或多个字符。使用单引号和双引号没有区别,但是它们必须成对。单引号中如果需要表示单引号本身,则需要使用反斜杠转义,双引号同理。字符串也有一个 length 属性,它表示 UTF-16 code-unit 码元的数量(这点和 Java 是一样的),因此一个字符串就算只有一个字符,它的 length 也不一定为 1,也有可能为 2(代理对)。

模板字符串
ES6 中的模版字符串提供了一些语法糖来帮助你构造字符串(字符串拼接),模版字符串前后使用反引号`括住,其中可以使用${expression}来插入 js 表达式。同时,使用模版字符串可以很容易的输入一个多行字符串。

字符转义序列

转移序列 相关解释
\0 NULL符
\b 退格符
\r 回车符
\n 换行符
\f 换页符
\t 水平制表
\v 垂直制表
\' 单引号
\" 双引号
\\ 反斜杠
\nnn 0~377八进制转义字符(Latin-1)严格模式不允许
\xhh 00~FF十六进制转义字符(Latin-1)
\uhhhh 0000~FFFF十六进制转义字符(BMP Unicode/surrogate pair)
\u{hhhhhh} 0000~10FFFF十六进制转义字符(Unicode code point)

对象字面量
对象字面值使用一对花括号表示,其中可以有零个或多个 key-value 键值对,{k1: v1, k2: v2, k3: v3, ...}。注意,不能在一行的开头使用对象字面量,因为此时的花括号会被当作语句块的开始!其中 key 为字符串类型(暂不考虑 Symbol 类型),value 为 js 任意数据类型。如果 key 并非合法的 js 标识符,则必须使用单引号或双引号括住,并且在访问时也不能通过.来访问,只能使用['key']来访问!因此,强烈建议使用合法 js 标识符来表示 key,避免不必要的麻烦。

数组字面量
数组字面值使用一对方括号表示,其中可以有零个或多个 value 值,[v1, v2, v3, ...]。key 被隐式的声明为 [0, 1, 2, 3, ...] 下标(字符串类型),不过通常使用数字来访问,因此,数组也是特殊的对象。除此之外,数组还有 length 属性,它总是比最大下标大 1,因此它并非表示实际的数组元素个数!

在后面,我们会经常接触到一个词 - 类数组对象,类数组其实就是拥有 length 属性,以及存在 [0, 1, 2, 3, ...] 非负整数的下标 key 的对象,比如函数内部的 arguments 对象就是典型的类数组对象。

数组字面量中的多余,逗号:不建议在数组尾部添加多余的逗号,不过在数组中间,是可以存在多个逗号的,这些下标的值都是undefined,不过,还是建议自己写上 undefined,这样的可读性更好,毕竟很多时候代码都是写给人看的。

正则字面量
正则表达式字面量使用一对正斜杠表示,即/pattern/flags,其中 flags 是正则标志,如是否启用全局匹配、是否忽略大小写、是否启用多行模式等,flags 部分是可选的。

流程控制

语句块:和 Java 一样,语句块也是使用花括号括住。基本语法为:

  1. {
  2. statement_1;
  3. statement_2;
  4. statement_3;
  5. ...
  6. statement_N;
  7. }

条件判断
if...else if...else:与 Java 一样,如果只有一条语句,可以省略花括号。
switch...case:语法与 Java 一样,不过可以使用任意类型,使用===判等。

for 循环

  1. for ([initialExpression]; [condition]; [incrementExpression]) {
  2. statement_1;
  3. statement_2;
  4. ...
  5. statement_N;
  6. }

while 循环

  1. while (condition) {
  2. // TODO
  3. }

do-while 循环

  1. do {
  2. // TODO
  3. } while (condition);

break/continue 跳出循环
break:跳出当前循环体,执行循环后面的语句;
continue:结束此轮循环,直接开始下一轮循环。
和 Java 一样,break/continue 后面可以接一个 label 标签,表示用于跳出哪个循环,如果省略,默认跳出 break/continue 语句所在的循环体。具体的演示如下:

  1. 'use strict';
  2. outer: for (let i = 0; i < 10; i++) {
  3. inner: for (let j = 0; j < 10; j++) {
  4. if (i === j)
  5. continue outer;
  6. console.log('i = ' + i + ', j = ' + j);
  7. }
  8. }

console 输出如下:

  1. i = 1, j = 0
  2. i = 2, j = 0
  3. i = 2, j = 1
  4. i = 3, j = 0
  5. i = 3, j = 1
  6. i = 3, j = 2
  7. i = 4, j = 0
  8. i = 4, j = 1
  9. i = 4, j = 2
  10. i = 4, j = 3
  11. i = 5, j = 0
  12. i = 5, j = 1
  13. i = 5, j = 2
  14. i = 5, j = 3
  15. i = 5, j = 4
  16. i = 6, j = 0
  17. i = 6, j = 1
  18. i = 6, j = 2
  19. i = 6, j = 3
  20. i = 6, j = 4
  21. i = 6, j = 5
  22. i = 7, j = 0
  23. i = 7, j = 1
  24. i = 7, j = 2
  25. i = 7, j = 3
  26. i = 7, j = 4
  27. i = 7, j = 5
  28. i = 7, j = 6
  29. i = 8, j = 0
  30. i = 8, j = 1
  31. i = 8, j = 2
  32. i = 8, j = 3
  33. i = 8, j = 4
  34. i = 8, j = 5
  35. i = 8, j = 6
  36. i = 8, j = 7
  37. i = 9, j = 0
  38. i = 9, j = 1
  39. i = 9, j = 2
  40. i = 9, j = 3
  41. i = 9, j = 4
  42. i = 9, j = 5
  43. i = 9, j = 6
  44. i = 9, j = 7
  45. i = 9, j = 8

for...in 遍历对象的key

  1. let obj = {
  2. k1: 'v1',
  3. k2: 'v2',
  4. k3: 'v3'
  5. };
  6. for (let key in obj)
  7. console.log(obj[key]); // 不能使用 obj.key!

for...of 遍历可迭代对象
(ES6) 内置可迭代对象:ArrayStringMapSetTypedArrayarguments

  1. 'use strict';
  2. let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
  3. for (let i of arr)
  4. console.log(i);

异常处理

js 的异常处理机制与 java 很相似,使用trycatchfinallythrow四个关键字。js 中的 throw 语句可以抛出任意数据类型,不过强烈建议使用内置的 Error 类型(个人认为使用 Expection 异常更恰当,可惜没有)。还有一点要注意,因为 js 的变量类型是运行时动态确定的,因此 catch 语句最多只有一个。catch 中的 e 变量只能在 catch 块中使用,在其他地方无法使用!

  1. try {
  2. // 可能发生异常的语句
  3. } catch (e) {
  4. // 用来处理异常的语句
  5. } finally {
  6. // 不管是否发生异常,finally 块总是被执行
  7. }

函数

函数定义(声明)

  1. function square(number) {
  2. return number * number;
  3. }

函数表达式

  1. let func = function(number) {
  2. return number * number;
  3. };

除了传统的 function func(param...) { ... } 方式来定义函数外,我们还可以直接将函数对象赋给一个变量,因为函数也是对象嘛,是对象就可以赋值给变量。因为这是一个完整的赋值语句,所以函数右花括号后必须加上分号。如果需要在函数内部调用自身(或者引用自身),可以使用函数表达式的变量名 func,除此之外,还可以为函数字面量定义一个私有名称,这个函数变量只能在函数内部使用,在外部是无法使用的,调用会提示标识符未定义。比如下面这个例子,用来计算阶乘的函数:

  1. 'use strict';
  2. let factorial = function(n) {
  3. return n < 2 ? 1 : n * factorial(n - 1);
  4. };
  5. console.log(factorial(0));
  6. console.log(factorial(1));
  7. console.log(factorial(2));
  8. console.log(factorial(3));
  9. console.log(factorial(4));
  10. console.log(factorial(5));
  11. console.log(factorial(6));
  12. console.log(factorial(7));
  13. console.log(factorial(8));
  14. console.log(factorial(9));
  15. console.log(factorial(10));
  1. 'use strict';
  2. let factorial = function f(n) {
  3. return n < 2 ? 1 : n * f(n - 1);
  4. };
  5. console.log(factorial(0));
  6. console.log(factorial(1));
  7. console.log(factorial(2));
  8. console.log(factorial(3));
  9. console.log(factorial(4));
  10. console.log(factorial(5));
  11. console.log(factorial(6));
  12. console.log(factorial(7));
  13. console.log(factorial(8));
  14. console.log(factorial(9));
  15. console.log(factorial(10));
  16. console.log(f(20)); // ReferenceError: f is not defined

普通函数函数表达式 的区别:普通函数的声明和定义都会被提升(var 变量是声明被提升,但是定义(赋值)不会),函数表达式不存在所谓的提升,如果使用 var 存储函数表达式,则声明被提升,如果使用 let、const 存储函数表达式,则不存在提升。如下:

  1. 'use strict';
  2. console.log(factorial(5)); // 此时 factorial 为 undefined
  3. var factorial = function(n) {
  4. return n < 2 ? 1 : n * factorial(n - 1);
  5. };

嵌套函数和闭包
JS 的函数与 Java 的函数有一点不同,JS 的函数中可以嵌套定义函数,只要你愿意,你可以嵌套定义任意多层函数。内层函数可以访问外层函数能访问的变量和函数,但是外层函数却不能访问内层函数中的变量和函数。内层函数只能在外层函数中访问,当然,你也可以通过函数返回值的形式将内层函数返回给调用者,这样调用者也可以间接的访问内层函数。定义内层函数通常是为了实现一些非通用的逻辑功能,这个很像 Java 中的私有成员函数,仅限内部使用。

嵌套函数的例子,利用内部函数实现非通用逻辑:

  1. 'use strict';
  2. function addSquares(a, b) {
  3. function square(x) {
  4. return x * x;
  5. }
  6. return square(a) + square(b);
  7. }
  8. console.log(addSquares(2, 3)); // 4 + 9 = 13
  9. console.log(addSquares(3, 4)); // 9 + 16 = 25
  10. console.log(addSquares(4, 5)); // 16 + 25 = 41

返回内部函数,内部函数中引用了外部函数的变量:

  1. 'use strict';
  2. function add(x) {
  3. function f(y) {
  4. return x + y;
  5. }
  6. return f;
  7. }
  8. let fn1 = add(5);
  9. let fn2 = add(6);
  10. console.log(fn1(5)); // 5 + 5 = 10
  11. console.log(fn2(5)); // 6 + 5 = 11
  12. console.log(fn1(8)); // 5 + 8 = 13
  13. console.log(fn2(8)); // 6 + 8 = 14

是不是很奇怪,在 C/C++、Java 中,每次调用函数时,函数局部变量的内存被分配,当该函数返回后,函数局部变量的内存被回收。也就是说,函数的每次调用,其局部变量的内存都是不一样的。回到上面的例子中,函数 add() 共被调用了两次:分别是 add(5)add(6),前面说了,函数返回后,局部变量会被自动回收,那么当我们调用 fn1/fn2 时应该是访问不到外部函数中的局部变量的呀,可是实际的运行结果却相反,完全正常访问,这是为什么呢?又是什么原理呢?

本节开头我们就说了,在 js 中,函数也是对象,还记得 C++ 中的仿函数吗?仿函数其实是一个普通的对象,之所以称之为仿函数,是因为我们可以直接在该对象后面加上 (param...) 来调用它,和函数调用很相似。那么它的实现原理是什么呢?非常简单,就是运算符重载,只要一个类重载了 operator() 运算符,当给它的对象加上圆括号调用时,其实就是调用了这个成员函数。好吧,有点扯远了,回到 js 中,来看一下这个例子:

  1. 'use strict';
  2. function newObj(value) {
  3. return {
  4. getValue: function() {
  5. return value;
  6. },
  7. increment: function() {
  8. return ++value;
  9. }
  10. };
  11. }
  12. let obj = newObj(10);
  13. console.log(obj.getValue()); // 10
  14. console.log(obj.increment()); // 11
  15. console.log(obj.getValue()); // 11

不仅内部函数可以访问外部函数中的局部变量,内部对象也可以访问外部函数中的局部变量。每调用一次函数,内存中都会产生一个新的 调用栈对象(意淫的东西,只是为了加深理解),调用栈对象中存储着传入的实参,函数局部变量,以及其他一些东西。如果函数返回后,没有引用可以访问该调用栈对象的数据,那么该调用栈对象就会被 GC 回收;相反,如果函数返回后,在外部仍然能够访问调用栈中的数据,那么该调用栈对象就不会被 GC 回收。说到这里,你应该能够理解上面两个例子了吧。因为函数返回后,调用者依旧持有其调用栈对象的引用,导致本该立即被 GC 回收的对象没有被回收,直到这个引用被断开为止(比如显式得给函数返回值赋予 null 值,帮助 GC 回收内存)。因此,如果希望回收这些内存,建议赋予 null 值,否则在某些环境中可能导致内存泄漏(比如旧版 IE 浏览器)。

Closure 闭包
闭包是指在 JavaScript 中,内部函数总是可以访问其所在的外部函数中声明的变量、接收的实参,即使在其外部函数被返回了之后。

来自 MDN 的描述:

闭包是 JavaScript 中最强大的特性之一。JavaScript 允许函数嵌套,并且内部函数可以访问定义在外部函数中的所有变量和函数,以及外部函数能访问的所有变量和函数。但是,外部函数却不能够访问定义在内部函数中的变量和函数。这给内部函数的变量提供了一定的安全性。此外,由于内部函数可以访问外部函数的作用域,因此当内部函数生存周期大于外部函数时,外部函数中定义的变量和函数的生存周期也和内部函数的存活时间一样长。当内部函数以某一种方式被任何一个外部函数作用域访问时(比如将其作为函数返回值返回给调用者),一个闭包就产生了。

变量命名冲突
如果内层函数与外层函数中存在同名变量,那么在内层函数中,内层函数中的同名变量优先级更高,反正只要记住一句话,谁离得近就用谁

默认参数
ES6 中,允许我们为函数参数设置一个默认值。在这之前,函数参数的默认值是 undefined,这时候通常使用下面的方法来设置默认值:

  1. 'use strict';
  2. function func(a, b, c) {
  3. a = a || 1;
  4. b = b || 2;
  5. c = c || 3;
  6. console.log('a = ' + a);
  7. console.log('b = ' + b);
  8. console.log('c = ' + c);
  9. }
  10. func(); // a=1, b=2, c=3
  11. func(11); // a=11, b=2, c=3
  12. func(11, 22); // a=11, b=22, c=3
  13. func(11, 22, 33); // a=11, b=22, c=33

这里简单解释一下 a = a || 1 的意思(其它两个类似),在 js 中,逻辑连接符有三个:&&逻辑与、||逻辑或、!逻辑非。其中 &&|| 都是 二元运算符,与 Java 强类型不同的是,js 中的 &&|| 两边的操作数可以是任意类型,而不是仅限于布尔值。expr1 && expr2 的意思是,如果操作数 expr1 和expr2 都为 true,则整个表达式的结果为 true;expr1 || expr2 的意思是,只要有一个操作数为 true,则整个表达式的结果为 true。而在 js 中,undefinednull0NaN''为 false,其它值均为 true。也就是说,当 expr1、expr2 不是布尔值时,对于 expr1 && expr2,如果 expr1 能转换为 true,则返回 expr2,否则返回 expr1;对于 expr1 || expr2,如果 expr1 能转换为 false,则返回 expr2,否则返回 expr1。很容易想到,&& 常用于测试左操作数是否为空,|| 常用于为左操作数设置默认值

不过到了 ES6,我们有了更加优雅的方式为函数参数设置默认值,请看:

  1. 'use strict';
  2. function func(a = 1, b = 2, c = 3) {
  3. // a = a || 1;
  4. // b = b || 2;
  5. // c = c || 3;
  6. console.log('a = ' + a);
  7. console.log('b = ' + b);
  8. console.log('c = ' + c);
  9. }
  10. func();
  11. func(11);
  12. func(11, 22);
  13. func(11, 22, 33);
  14. func(undefined, undefined, undefined); // 等价于 func()

在 C++ 中,默认参数必须位于参数列表的后头,如果为一个参数设置了默认值,那么它后面的所有参数都必须设置默认值!不过,在 js 中没有这样的硬性规定,但是,强烈建议将默认参数放在参数列表的尾部,这样才更能够体现出默认参数原本的意义。

剩余参数
这个类似于 Java 中的可变参数,如 func(String... args),其中 args 是用于接收可变参数的容器,本质上,它只是一个数组,也就是说,这是一颗语法糖,在调用的时候,我可以传入多个参数,也可以直接传入一个数组。回到 js 中,ES6 也提供了这样一颗语法糖,它的语法很相似,如 func(...args),其中,args 本质上也是一个数组(Array),不过,你只能传入多个参数,不能直接传入一个数组,因为直接传入一个数组会被当作一个参数处理,不过也有办法传入数组,那就是在数组前面加上三个 . 号。具体的细节如下:

  1. 'use strict';
  2. function concat(sep, ...arr) {
  3. let result = '';
  4. for (let i = 0; i < arr.length - 1; i++)
  5. result += arr[i] + sep;
  6. result += arr[arr.length - 1];
  7. return result;
  8. }
  9. console.log(concat('; ', 1, 2, 3)); // '1; 2; 3'
  10. console.log(concat('; ', [1, 2, 3])); // '1,2,3' Array.toString()
  11. console.log(concat('; ', ...[1, 2, 3])); // '1; 2; 3'

在 Java 中,剩余参数必须位于参数列表的尾部,因为这样才好确定有多少参数,在 JS 中这个限制同样适用。

箭头函数
所谓的箭头函数就是 Lambda 表达式,在 Java 中,使用 -> 箭头,在 JS 中,使用 => 箭头。Lambda 表达式也就是所谓的匿名函数,通常用于编写简短的函数体,比如作为参数传递给被调用函数的函数。除了箭头不一样外,其它的语法和 Java 的 Lambda 表达式颇为相似。

  1. 'use strict';
  2. function fillArray(arr, func) {
  3. for (let i = 0; i < arr.length; i++)
  4. arr[i] = func(i);
  5. return arr;
  6. }
  7. let arr = [];
  8. arr.length = 10;
  9. console.log(fillArray(arr, i => i));
  10. console.log(fillArray(arr, i => i * i));
  11. // Output:
  12. (10) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  13. (10) [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

使用箭头函数的好处除了代码简洁外,还因为箭头函数会自动捕获外层函数的 this 指针!在严格模式下,每个函数的 this 指针默认为 undefined,如果是以 "对象方法" 调用,则 this 指针指向被调用的对象。例如:func()直接调用,那么 func 函数内部的 this 指向 undefinedobj.func() 对象方法,那么 func 函数内部的 this 指向 obj。(注:在全局作用域中,this 指向 window 全局对象)。以面向对象的编程风格,这样着实有些恼人。一个简单的办法是,在外部函数中暂存 this 指针,然后在内部函数中使用这个暂存的指针,而不是 this。不过,ES6 的箭头函数已经帮我们做了,我们直接使用 this 即可正确引用外部函数的 this 指针指向。

  1. 'use strict';
  2. let obj = {
  3. func: function() {
  4. console.log(this); // obj
  5. (function() {
  6. console.log(this); // undefined
  7. })();
  8. (() => console.log(this))(); // obj
  9. }
  10. };
  11. obj.func();

eval 全局函数
eval 和 shell 中的 eval 作用相似,用来执行字符串中的代码。因为 eval 是解释运行的,会调用 js 解释器,因此效率比直接写的代码低。因此,非必要情况下,尽量不要使用 eval,不仅效率低,还可能存在潜在的风险。函数原型:eval(string),返回执行后的结果。

Function 构造函数
new Function([arg1[, arg2[, ...argN]], ]functionBody):每个函数(函数定义、函数表达式)其实都是 Function 的一个实例,arg 是函数的形参,functionBody 是函数的主体。它们均为字符串类型,因为在运行时才进行编译解析,因此效率很低,不建议使用。

Function 实例属性
funcObj.length:函数需要的形参数目,形参的数量不包括剩余参数个数仅包括第一个具有默认值之前的参数个数。

Function 实例方法
funcObj.call(thisArg, arg1, ..., argN):手动设置 this 值,并以参数列表的形式传入所需参数,调用当前函数。
funcObj.apply(thisArg, argsArray):手动设置 this 值,并以数组形式传入所需参数,调用当前函数。
funcObj.bind(thisArg[, arg1, ..., argN]):创建一个新函数(偏函数),为其指定 this 值,可选的为原函数设置默认值。
funcObj.toString():返回当前函数源代码的字符串(内置函数除外)。

运算符

JS 中有 一元运算符二元运算符三元运算符,一元运算符需要一个操作数,二元运算符需要两个操作数,三个运算符需要三个操作数。

赋值运算符
=+=-=*=/=%=**=(乘方)
<<=>>=>>>=(无符号右移)
&=按位与、|=按位或、^=按位异或
例如 a += b,等价于 a = a + b

解构赋值(ES6)
解构赋值说得简单点就是:批量赋值,一一赋值。比如我有一个数组,该数组有三个元素,我现在要使用三个变量来取出这三个元素,不使用解构的情况下,只能这么做:

  1. 'use strict';
  2. let arr = [1, 2, 3];
  3. let e0 = arr[0],
  4. e1 = arr[1],
  5. e2 = arr[2];
  6. console.log(e0);
  7. console.log(e1);
  8. console.log(e2);

数量少还好,数量多就比较麻烦了。使用解构就不一样了:

  1. 'use strict';
  2. let arr = [1, 2, 3];
  3. let [e0, e1, e2] = arr;
  4. console.log(e0);
  5. console.log(e1);
  6. console.log(e2);

有了解构,交换两个变量就非常容易了,不再需要显式定义一个临时变量:

  1. 'use strict';
  2. let a = 10,
  3. b = 20;
  4. console.log('a = ' + a + ', b = ' + b);
  5. [a, b] = [b, a];
  6. console.log('a = ' + a + ', b = ' + b);

解构赋值表达式需要两个操作数,操作数的类型必须为数组或对象(Array、Object),并且,通常情况下,两个操作数的类型需要一致,当然,也允许左操作数是对象(key 必须为非负整数的字符串形式),而右操作数为数组的情况,如:let {'1': e1, '0': e0, '2': e2} = [1, 2, 3];,执行后,e0 = 1, e1 = 2, e2 = 3。不过一般情况下两个操作数的类型都是一样的,不要给自己制造麻烦。

解构赋值的执行过程也很容易理解,因为数组本质上是对象的简写形式(key 隐式的为数组下标),因此这里只讨论对象之间的解构赋值。

对于 let {'k1': v1, 'k2': v2, 'k3': v3} = {'k1': 1, 'k2': 2, 'k3': 3};

因为 k1、k2、k3 是合法的 javascript 标识符,因此可以省略引号,即 let {k1: v1, k2: v2, k3: v3} = {k1: 1, k2: 2, k3: 3};,和上面的表达式是完全一致的,没有区别,要注意的是,左操作数的 k1k2k3 只是字符串(省略了引号),并不是声明的变量,因此不能在后面引用它们!

当然,如果左操作数的 key 和 value 同名,即 let {k1: k1, k2: k2, k3: k3} = {k1: 1, k2: 2, k3: 3};,显得有些冗余,我们完全可以写作:let {k1, k2, k3} = {k1: 1, k2: 2, k3: 3};

如果左操作数中的某些 key 在右操作数中不存在,则对应的 value 为 undefined。有些时候我们希望为那些不存在的 key 设置默认值,只需要像函数默认参数那样直接写就可以了(如果对应 key 本身的值为 undefined,那么也会被设置默认值,这个和函数默认值是一样的)。如 let {k1 = 1, k2 = 2, k3 = 3} = {};

除了可以应用函数的默认参数外,还可以使用剩余参数语法。和函数的剩余参数一样,解构赋值中的剩余参数必须位于左操作数的尾部。如 let {k1, ...rest} = {k1: 1, k2: 2, k3: 3};,k1 为 1,rest 是一个对象,有两个 key-value 对,即 rest = {k2: 2, k3: 3}

当操作数是数组时,有时候,我们不需要捕获某些值,希望跳过它们,可以使用逗号 , 占位,和数组的空位语法是一样的。如 let [e0, , e2] = [1, 2, 3];

考虑这么一种情况,左操作数的 key 名称是一个变量,该怎么获取它对应的 value 呢?比如:

  1. 'use strict';
  2. let key = 'k';
  3. let {key: value} = {k: 'v'};
  4. console.log(value); // undefined

这时,我们可以给左操作数的 key 加上中括号,表示 key 是一个变量而非字符串:

  1. 'use strict';
  2. let key = 'k';
  3. let {[key]: value} = {k: 'v'};
  4. console.log(value); // 'v'

比较运算符
比较运算符的返回值是一个布尔值,如果为 true 则表示该条件成立,如果为 false 则表示该条件不成立。比较运算符需要两个操作数,它们的类型可以不一致,判等的比较有两个:==忽略数据类型,单纯的比较变量的"值";===如果数据类型不一致,直接返回 false,如果数据类型一致,才会比较变量的"值"(!=!==同理)。因此,为了避免某些诡异的情况,强烈建议使用 ===!== 进行变量的比较操作。特别的,如果操作数是字符串,则依次比较对应字符的 Unicode 码点值。例子(注:true 在内存中的值为 1,false 的值为 0):

  1. 'use strict';
  2. console.log(1 == true); // true
  3. console.log(1 === true); // false
  4. console.log(1 != true); // false
  5. console.log(1 !== true); // true

算术运算符
算术运算符通常需要两个操作数,并且通常两个操作数都是 number 类型。特别注意,除零不会导致异常,而是返回 Infinity 无限大,对于单目运算符 +正数、-负数,如果操作数不是 number 而是 string,那么会尝试将 string 解析为 number,如果无法解析,则返回 NaN;或者操作数是只有一个元素的数组,则返回(或者解析它)这个元素(-运算符同理)。

  1. 'use strict';
  2. console.log(+1 / 0); // Infinity
  3. console.log(-1 / 0); // -Infinity
  4. console.log(+true); // 1
  5. console.log(+false); // 0
  6. console.log(+10); // 10
  7. console.log(-10); // -10
  8. console.log(+'10'); // 10
  9. console.log(-'10'); // -10
  10. console.log(-'-10'); // 10
  11. console.log(+'10a'); // NaN
  12. console.log(+' 10'); // 10
  13. console.log(+'10 '); // 10
  14. console.log(+' 10 '); // 10
  15. console.log(+'\t10\t'); // 10
  16. console.log(+null); // 0
  17. console.log(+undefined); // NaN
  18. console.log(+{}); // NaN
  19. console.log(+[]); // 0
  20. console.log(+[10]) // 10
  21. console.log(+[10, 20]); // NaN

位运算符
<<左移、>>右移(补符号位)、>>>无符号右移(补 0)。
位运算符会将操作数转换为 32-bit 长度的有符号整数(通常位运算符要求操作数为整数而非浮点数,如果为浮点数,则小数部分被舍弃,如果整数的长度大于 32,则被截断)。和 Java 一样,如果位移的步长大于等于 32 或小于 0,则会先对步长取模,然后再进行位移。

逻辑运算符
常规用法(常规理解):

变种用法(变种理解):

因此,&&常用于检测左操作数是否为空,||常用于给左操作数设置默认值

字符串运算符
+/+=:字符串拼接,如果操作数为对象,则调用其 toString() 方法。

  1. 'use strict';
  2. console.log('array: ' + [1, 2, 3, 4, 5]); // array: 1,2,3,4,5
  3. Array.prototype.toString = function() {
  4. let result = '[';
  5. for (let i = 0; i < this.length - 1; i++)
  6. result += this[i] + ', ';
  7. return result += this[this.length - 1] + ']';
  8. };
  9. console.log('array: ' + [1, 2, 3, 4, 5]); // array: [1, 2, 3, 4, 5]

条件运算符
条件运算符是 js 中唯一的三元运算符,语法:cond ? expr1 : expr2。如果条件 cond 为真,则整个表达式的值为 expr1,如果条件 cond 为假,则整个表达式的值为 expr2。

一元运算符
delete:删除 对象的键数组元素(本质都是删除给定的 key),只能删除自身的键,继承链上的不影响。删除数组的元素后,数组的 length 不会改变,但是这个元素确实不存在了(使用 key in obj 可判断),这与直接给这个元素赋 undefined 值是不一样的。在严格模式下,delete 表达式总是返回 true,如果给定 key 是不可配置属性(Non-configurable),会导致语法错误;在非严格模式下则返回 false。

  1. 'use strict';
  2. let sup = {key: 'sup::value'};
  3. let sub = {key: 'sub::value'};
  4. sub.__proto__ = sup;
  5. console.log(sup.key); // 'sup::value'
  6. console.log(sub.key); // 'sub::value'
  7. console.log(delete sub.key); // true
  8. console.log(sup.key); // 'sup::value'
  9. console.log(sub.key); // 'sup::value'
  10. console.log(delete sub.key); // true
  11. console.log(sup.key); // 'sup::value'
  12. console.log(sub.key); // 'sup::value'
  13. console.log(delete sup.key); // true
  14. console.log(sup.key); // 'undefined'
  15. console.log(sub.key); // 'undefined'

delete arr[ind]arr[ind] = undefined 的区别:

  1. 'use strict';
  2. let arr = [0, 1, 2];
  3. console.log(arr); // [0, 1, 2]
  4. console.log(arr.length); // 3
  5. delete arr[0]; // always return true
  6. console.log(arr); // [empty, 1, 2]
  7. console.log(arr.length); // 3
  8. console.log(0 in arr); // false
  9. arr[2] = undefined;
  10. console.log(arr); // [empty, 1, undefined]
  11. console.log(arr.length); // 3
  12. console.log(2 in arr); // true

void:对给定的表达式进行求值,然后返回 undefined。目前唯一的用途(不一定,个人觉得这个运算符好多余),就是用来执行"立即执行的函数",如果不使用 void,则必须在函数两边加上圆括号。

  1. void function() {
  2. 'use strict';
  3. console.log('hello, world!');
  4. }();

typeof:返回给定操作数的数据类型(字符串),可能的值有:
1. boolean:返回字符串 'boolean'
2. number:返回字符串 'number'
3. string:返回字符串 'string'
4. symbol:返回字符串 'symbol'
5. undefined:返回字符串 'undefined'
6. null:返回字符串 'object'
7. 对象:返回字符串 'object'
8. 函数:返回字符串 'function'

关系运算符
key in obj:如果给定 key(字符串或数组下标)在给定 obj 中存在,则返回 true。

obj instanceof constructor:原理很简单,就是检查 obj.__proto__[.__proto__[.__proto__[...]]] 是否与 constructor.prototype 相等,instanceof 会沿着原型链一级一级往上查询,如果在某个节点上匹配,则返回 true,如果到了 null(顶级)还未匹配,则返回 false。

  1. 'use strict';
  2. function Pet() {
  3. // TODO
  4. }
  5. let pet = new Pet();
  6. console.log(pet instanceof Pet); // true
  7. console.log(pet.__proto__ === Pet.prototype); // true

表达式

表达式是一组可以计算出一个数值的有效的代码的集合。

每一个合法的表达式都能计算成某个值,但从概念上讲,有两种类型的表达式:有副作用的(比如赋值)和单纯计算求值的,前者会对内存中的数据产生影响,后者则不会。

基本表达式
this 指针:this 的本意是指代当前正在被调用的对象,具体的(严格模式下):

...array定义剩余参数、原地展开数组:举个栗子,现有一个数组,我想在创建一个新数组,将现有的数组作为它的一部分,就可以使用此方法,将该现有数组展开:

  1. 'use strict';
  2. let arrOld = [4, 5, 6];
  3. let arrNew = [1, 2, 3, ...arrOld, 7, 8, 9];
  4. console.log(arrNew); // 1,2,3,4,5,6,7,8,9

数字和日期

数值字面值 number
JS 中不区分整数和浮点数,统统使用 64-bit 双精度浮点类型表示,因此一个数字的范围只能在 -(253-1) ~ 253-1 之间。除了具体的数值外,JS 中还有三个特殊的数值:+Infinity正无穷、-Infinity负无穷、NaNnot-a-number 非数字。

在 js 中可以使用 4 种数字进制(二、八、十、十六):

对于大数字,还可以使用科学记数法(指数形式):a * 10n,其中 a 必须是单位数字,可以有正负,n 是一个整数,可以有正负,在编程语言中,通常表示为 aEn,e 可以大写也可以小写。比如一百万可以表示为 1E6,千分之一可以表示为 1E-3

数值包装类 Number
Number 的属性(静态字段):

Number 的方法(静态方法):

Number 原型上的方法(实例方法):

数学对象 Math
Math 对象的属性:

Math 对象的方法:

Math.random() 详解
random() 返回一个范围在 [0, 1)伪随机浮点数(左闭右开区间)。

[min, max) 浮点数
Math.random() * (max - min) + min

[min, max) 整数

  1. function getRandomInt(min, max) {
  2. min = Math.ceil(min); // 向上取整
  3. max = Math.floor(max); // 向下取整
  4. return Math.floor(Math.random() * (max - min)) + min;
  5. }

[min, max] 整数

  1. function getRandomIntInclusive(min, max) {
  2. min = Math.ceil(min); // 向上取整
  3. max = Math.floor(max); // 向下取整
  4. return Math.floor(Math.random() * (max - min + 1)) + min;
  5. }

日期对象(构造函数)
Date 的静态属性:
Date.length:Date 构造函数可接受的参数个数(7 个)。

Date 的静态方法:
Date.now():返回自 1970-1-1 00:00:00 UTC 至今所经过的毫秒数。
Date.parse(dateString):dateString 是符合 ISO8601 日期格式的字符串,返回自 1970-01-01T00:00:00Z 至 dateString 经过的毫秒数,如果无法解析则返回 NaN。
Date.UTC(year, month[, date[, hrs[, min[, sec[, ms]]]]]):返回自 1970-01-01T00:00:00Z 至给定日期所经过的毫秒数。

Date 的构造函数:
Date():普通方法调用,返回当前时间的字符串形式
new Date():当前时间
new Date(value):自 1970-01-01T00:00:00Z 经过的毫秒数
new Date(dateString):符合 ISO8601 日期时间格式的字符串
new Date(year, month[, day[, hour[, minutes[, seconds[, milliseconds]]]]])年(四位整数)、月(0-11)、日(1-31)、时(0-23)、分(0-59)、秒(0-59)、毫秒(0-999)。

Date 的实例方法:
本地时间
Date.prototype.getFullYear():年
Date.prototype.getMonth():月(0-11)
Date.prototype.getDate():日(1-31)
Date.prototype.getDay():星期(0-6)
Date.prototype.getHours():时(0-23)
Date.prototype.getMinutes():分(0-59)
Date.prototype.getSeconds():秒(0-59)
Date.prototype.getMilliseconds():毫秒(0-999)
Date.prototype.getTimezoneOffset():时区偏移量(单位:分钟)
Date.prototype.getTime():相对于 1970-01-01T00:00:00Z 的偏移量(毫秒)
UTC 时间
Date.prototype.getUTCFullYear():年
Date.prototype.getUTCMonth():月(0-11)
Date.prototype.getUTCDate():日(1-31)
Date.prototype.getUTCDay():星期(0-6)
Date.prototype.getUTCHours():时(0-23)
Date.prototype.getUTCMinutes():分(0-59)
Date.prototype.getUTCSeconds():秒(0-59)
Date.prototype.getUTCMilliseconds():毫秒(0-999)

本地时间
Date.prototype.setFullYear():年
Date.prototype.setMonth():月
Date.prototype.setDate():日
Date.prototype.setHours():时
Date.prototype.setMinutes():分
Date.prototype.setSeconds():秒
Date.prototype.setMilliseconds():毫秒
Date.prototype.setTime():相对于 1970-01-01T00:00:00Z 的偏移量(毫秒)
UTC 时间
Date.prototype.setUTCFullYear():年
Date.prototype.setUTCMonth():月
Date.prototype.setUTCDate():日
Date.prototype.setUTCHours():时
Date.prototype.setUTCMinutes():分
Date.prototype.setUTCSeconds():秒
Date.prototype.setUTCMilliseconds():毫秒

其它方法
Date.prototype.toDateString():(易读)日期
Date.prototype.toTimeString():(易读)时间
Date.prototype.toString():(易读)日期时间
Date.prototype.toLocaleDateString():(本地化)日期
Date.prototype.toLocaleTimeString():(本地化)时间
Date.prototype.toLocaleString():(本地化)日期时间
Date.prototype.toUTCString():UTC 日期时间
Date.prototype.toISOString():ISO 日期时间格式(UTC)
Date.prototype.toJSON():JSON 日期时间(同 toISOString)
Date.prototype.valueOf():自 1970-01-01T00:00:00Z 起的毫秒数

文本格式化

字符串字面值
JavaScript 中的 string 类型用于表示文本型的数据,它是由无符号整数值(16-bit)作为元素而组成的集合。字符串中的每个元素在字符串中占据一个位置,第一个元素的 index 值是 0,下一个元素的 index 值是 1,以此类推。字符串的长度就是字符串中所含的元素个数。你可以通过 string 字面值或者 String 对象两种方式创建一个字符串。

JS 中的字符串使用 UTF-16 编码,因此 string 的 length 并不代表该字符串的字符个数,length 的意义仅仅是 UTF-16 码元(code-unit)的数量,除了 BMP 基本多文种平面的字符可以使用一个 code-unit(即 16-bit)表示外,其他的辅助平面均需要使用两个 code-unit(即 32-bit)表示(称之为代理对)。在 Unicode 字符集中,共计有 17 个平面,其中第 0 号平面是基本平面(简称 BMP),其余 16 个平面都是辅助平面,而每个平面都有 216(可表示 65536 个字符)个码点(code-point)。因此,BMP 的码点值范围是 0x0000 ~ 0xFFFF,辅助平面的码点值范围是 0xN0000 ~ 0xNFFFF,其中 N 的范围是 [1, 10](十六进制)。

早期 JS 可能采用的是 UCS-2 编码(这和 Java 很相似),UCS-2 编码是 UTF-16 编码的子集,UCS-2 是没有辅助平面之前的主流编码方式,也就是说,UCS-2 只能表示 BMP 字符,辅助平面的字符则无能为力。

字符串字面值可以使用 单引号 或者 双引号 表示,使用单、双引号没有区别,在 JS 中建议使用单引号,因为经常需要与 HTML 元素打交道,双引号有些麻烦。

字符串包装类
实例方法
string[index]:返回给定索引处的字符(char),返回值类型为字符串
charAt(index):返回给定索引处的字符(char),返回值类型为字符串
charCodeAt(index):返回给定索引处的码元(code-unit),返回值类型为数值
codePointAt(index):返回给定索引处的码点(code-point),返回值类型为数值

indexOf(searchValue[, fromIndex]):查询给定子串的第一次匹配位置
lastIndexOf(searchValue[, fromIndex]):查询给定子串的最后一次匹配位置

startsWith(searchString[, position]):测试当前字符串是否以给定子串开头
endsWith(searchString[, position]):测试当前字符串是否以给定子串结尾
includes(searchString[, position]):测试当前字符串是否包含给定子串

concat(string2, string3[, ..., stringN]):连接多个字符串,建议使用++=
split([separator[, limit]]):sep 为字符串或正则,limit 是结果数组的长度

slice(beginSlice[, endSlice]):提取 [beg, end) 子串并返回
substring(indexStart[, indexEnd]):提取 [beg, end) 子串并返回
substr(start[, length]):从给定索引处开始,提取 len 个字符并返回

match(regex):使用正则匹配当前字符串(不建议使用 g 标志),匹配成功则返回一个数组,第 0 项是 group0,接下来是子捕获组,除此之外,还有两个额外的属性,input 指向原字符串,index 是匹配结果在原串中的起始索引;匹配失败则返回 null。如果使用了 g 标志,则只会返回所有匹配的 group0,不包含子捕获组,并且也不存在 input、index 属性。

replace(regex|substr, replace|function):返回一个由替换值替换一些或所有匹配的模式后的新字符串。模式可以是一个字符串或者一个正则表达式,替换值可以是一个字符串或者一个每次匹配都要调用的函数

search(regex):尝试将当前字符串与正则表达式相匹配,匹配成功则返回捕获组 0 相对于原字符串的起始索引,匹配失败则返回 -1。

toLowerCase():将当前字符串转换为小写形式(不改变原串)
toUpperCase():将当前字符串转换为大写形式(不改变原串)

repeat(count):将原串的 count(非负整数)个副本拼接在一起,并返回它
trim():删除字符串两端(不改变原串)的空白符(空格,制表,回车,换行等)

静态方法
fromCharCode(num1, ..., numN):从给定 code-unit 创建字符串 string
fromCodePoint(num1[, ...[, numN]]):从给定 code-point 创建字符串 string

模版字符串
ES6 模版字符串相比传统的字符串字面量,有以下两个不同之处:
1. 模版字符串中允许换行,而不需要转义
2. 模版字符串中可以插入 JS 表达式,${expression}
模版字符串使用反引号 ` 表示,而非单引号、双引号

正则表达式
创建正则表达式有三种方法:

  1. /pattern/flags:字面量,编译期间进行正则表达式编译
  2. new RegExp(pattern[, flags]):构造函数,运行期间编译
  3. RegExp(pattern[, flags]):普通方法调用,运行期间编译

建议不要使用第三种方式,语义不明确;如果正则表达式是确定的,不变的,则选择字面量形式,如果正则表达式是不定的,未知的,则选择构造函数形式。

对于 /pattern/flags,flags 是可选的,flags 可以是以下值的任意组合:

uys 三个 flag 都是 ES6、ES2018 新增的,使用时请注意浏览器兼容性。

对于 new RegExp(pattern[, flags]),在 ES6 之前,pattern 和 flags 必须是字符串类型,从 ES6 开始,pattern 可以是正则字面量。要注意的是,在字符串中的正则表达式需要转移反斜杠字符,如 /\w+/ 需要表示为 '\\w+'

RegExp 实例属性
RegExp.prototype.global:布尔属性,指示是否启用了全局匹配标志
RegExp.prototype.ignoreCase:布尔属性,指示是否启用了忽略大小写标志
RegExp.prototype.multiline:布尔属性,指示是否启用了多行模式标志
RegExp.prototype.unicode:布尔属性,指示是否启用了 Unicode 码点转义
RegExp.prototype.sticky:布尔属性,指示是否启用了粘性匹配标志
RegExp.prototype.source:字符串属性,存储正则表达式对象的源模式文本
RegExp.prototype.lastIndex:数值属性,存储最后一次成功匹配的结束位置 + 1

唯一可更改的属性就是 lastIndex,其它属性都是只读属性。

RegExp 实例方法
RegExp.prototype.exec(input):执行一次搜索匹配,返回一个结果数组或 null。如果启用了 gy 标志位,则每次调用 exec() 都会更新 regex 对象的 lastIndex 属性。需要注意的是,exec() 方法只会执行一次匹配,如需进行全局匹配粘性匹配,需要采取循环方式,即 while ((result = regex.exec(input))),当 result 为 null 时循环就会停止。返回的 Array 结果中,第 0 项是捕获组 0,第 1 项是捕获组 1,以此类推,除此之外,还有 input 属性(指向原字符串),index 属性(lastIndex 值)。

RegExp.prototype.test(input):测试正则表达式是否可以与输入字符串匹配,如果可以则返回 true,如果不可以则返回 false。当你仅仅想测试是否匹配时,使用 test() 比 exec() 快的多。除了返回值类型不同外,test() 基本与 exec() 行为相似。

RegExp.prototype.toString():返回正则表达式的字符串描述(/pattern/flags)。

String 与 RegExp 相关的方法
String.prototype.match(regex):如果没有启用 g 标志,则返回值与 regex.exec(input) 一样;如果启用了 g 标志,则返回所有匹配的结果(捕获组 0)的数组,并且没有子捕获组信息,也没有 input、index 额外属性;如果启用了 y 标志,则会修改 regex 对象的 lastIndex 属性(g 标志下不会修改该属性)。

String.prototype.replace(regex|string, replace|function):执行正则替换。

String.prototype.search(regex):和 regex.test() 方法差不多,只不过该方法会返回成功匹配的起始索引值,如果匹配失败则返回 -1。

String.prototype.split(separator[, limit]):使用正则表达式分割字符串,返回分割后的结果数组。

JS 正则表达式
JS 的正则和 Perl、Java 的很相似,可以理解为 Java 正则 的子集。因此这里只简单的说明 JS 正则与 Java 正则的显著区别:

索引集合类

数组对象
数组是一个有序的数据集合,我们可以通过数组名称和索引进行访问。例如,我们定义了一个数组 emp,数组中的每个元素包含了一个雇员的名字以及其作为索引的员工号。那么 emp[1] 将会代表 1 号员工,emp[2] 将会代表 2 号员工,以此类推。

JavaScript 中没有明确的数组数据类型。但是,我们可以通过使用内置Array 对象和它的方法对数组进行操作。Array 对象有很多操作数组的方法,比如合并、反转、排序等。数组对象有一个决定数组长度和使用正则表达式操作其他属性的属性。

创建数组
1. [elem0, elem1, ..., elemN]:数组字面量(数组初始化器)
2. new Array(elem0, elem1, ..., elemN):构造函数(不推荐)
3. Array(elem0, elem1, ..., elemN):普通函数调用(更不推荐)
对于后两者,如果只有一个参数,该参数将被当作数组长度,因此须为非负整数。

数组对象有一个特殊属性 length,表示数组的长度,该属性的值取决于数组最大索引值,它总是等于最大索引值 + 1,因此,length 属性并不总是等于数组元素的个数。我们不仅可以读取 length 属性的值,还可以更改它的值,如果新的长度比原来的长度小,则数组会被截断,多余的元素会被清空。该属性会自动递增,如 arr[10] = value(假设当前数组长度为 10),执行后,数组长度会自动变为 11,但是这种自动递增也是有条件的,你的属性名(即 key 名称)必须是 非负整数,否则它将被当作一个普通的属性,而不是数组的元素,比如 arr[1.5] = value 不会改变 length 属性,只是增加了一个名叫 '1.5' 的属性而已。

遍历数组
1. for (let i = 0; i < arr.length; i++):原始方法
2. Array.prototype.forEach(callback[, thisObj])callback(curVal, curInd, curArr),ES5.1,使用箭头函数更简洁,在数组定义时省略的元素不会在 forEach 遍历时被列出,但是手动赋值为 undefined 的元素是会被列出。

Array 静态方法
Array.isArray(obj):判断 obj 是否是 Array 的实例,如果是则返回 true,否则返回 false。
Array.of(element0[, element1[, ...[, elementN]]]):从传入的参数列表中构建数组。
Array.from(arrayLike[, mapFn[, thisArg]]):从类数组对象可迭代对象中创建数组对象,如果使用了 mapFn、thisArg 参数,实际上等价于 Array.from(arrayLike).map(mapFn[, thisArg])

类数组对象:拥有一个 length 属性和若干索引属性的任意对象。

Array 实例方法
修改方法

访问方法

迭代方法
在下面的众多遍历方法中,有很多方法都需要指定一个回调函数作为参数。在每一个数组元素都分别执行完回调函数之前,数组的 length 属性会被缓存在某个地方,所以,如果你在回调函数中为当前数组添加了新的元素,那么那些新添加的元素是不会被遍历到的。此外,如果在回调函数中对当前数组进行了其它修改,比如改变某个元素的值或者删掉某个元素,那么随后的遍历操作可能会受到未预期的影响。总之,不要尝试在遍历过程中对原数组进行任何修改,虽然规范对这样的操作进行了详细的定义,但为了可读性和可维护性,请不要这样做。

面向对象编程

在 JS 中,没有所谓的"类",一切皆对象。那么只有对象是怎么实现"面向对象"的呢?与 Java、C++ 的面向对象风格不同,JS 的继承是 原型继承,每个对象都有一个隐藏属性 [[Prototype]],在大多数浏览器中可以使用 __proto__ 来访问(读写),不过在 ES5 之后,建议使用规范 API:Object.getPrototypeOf(obj)读取、Object.setPrototypeOf(obj, newProto)写入。不过,修改对象的 [[Prototype]] 是一个 非常慢且影响性能的操作。无论是什么方式,都不建议在运行时修改对象的原型,要在创建对象的设置原型,建议使用 Object.create(proto) 方法来创建一个新对象,并使用指定的原型。

那么这个原型对象是谁创建的呢?不可能是自己生成的吧?这里只讨论典型情况:对象 obj 是调用所属构造函数创建的,即 let obj = new Foo()。语法和 Java、C++ 相似,没什么好讲的,对象 obj 是构造函数 Foo 的一个实例,这时候你查看 obj.__proto__ 会发现,它不是 undefined,说明在 new 操作过程中,为其设置了一个原型,那么这个原型是谁呢?Foo.prototype,没错,就是构造函数 Foo 的 prototype 属性,每个函数都有一个 prototype 属性,prototype 默认是只有一个属性(不计算隐藏属性)的对象,这个唯一的属性就是 constructor 构造器,它指向当前 prototype 对象所属的构造函数,在这里就是 Foo 了。

如何分清 prototype[[Prototype]]

别急,我们来看这个例子:

  1. 'use strict';
  2. let obj = {};
  3. console.log(obj.__proto__ === Object.prototype); // true
  4. console.log(obj.__proto__.__proto__ === null); // true
  5. function func() {}
  6. console.log(func.__proto__ === Function.prototype); // true
  7. console.log(func.__proto__.__proto__ === Object.prototype); // true
  8. console.log(func.__proto__.__proto__.__proto__ === null); // true

先来解释一下对象 obj:
obj 的原型是 Object.prototype 没啥好解释的,Object.prototype 的原型是 null 前面也说了。

再来解释一下函数 func:
函数也是对象,这在本文开头就说了,因此,函数对象的原型是 Function.prototype;而 Function.prototype 是一个普通对象,因此它的原型是 Object.prototype,而 Object.prototype 是 null。

清楚了没有?再来看这个例子:

  1. 'use strict';
  2. console.log(typeof Object); // function
  3. console.log(typeof Function); // function
  4. console.log(Object.__proto__ === Function.prototype); // true
  5. console.log(Object.__proto__.__proto__ === Object.prototype); // true
  6. console.log(Object.__proto__.__proto__.__proto__ === null); // true
  7. console.log(Function.__proto__ === Function.prototype); // true
  8. console.log(Function.__proto__.__proto__ === Object.prototype); // true
  9. console.log(Function.__proto__.__proto__.__proto__ === null); // true

Object、Function 都是函数,因此它们的原型是 Function.prototype;Function.prototype 是普通对象,因此它的原型是 Object.prototype,Object.prototype 的原型是 null。

Object.prototype 是几乎所有对象的原型(尽头),它的原型有点特殊,是 null。
为什么说是几乎,因为 JS 运行我们自由的修改对象的原型指向,如使用 let obj = Object.create(null) 创建了一个新对象 obj,该对象没有原型,因此它是一个干净的对象,没有命名污染。当然也可以直接使用 obj.__proto__ = null 来实现。注意,表面上我们将它的原型设为了 null,实际上是将它的 __proto__ 属性 delete 了,因此 obj.__proto__ === null 是 false 的,因为 obj 中不存在 __proto__ 属性了,尝试获取它的值也是得到 undefined。这时候如果你使用 obj instanceof Object 会发现,结果是 false!难道这个 obj 不是对象?!不是的,obj 是对象,因为 instanceof 的判断方法就是直接判断 obj.__proto__ === Object.prototype!这下知道了 instanceof 的原理了吧,不过我们可以使用 typeof 来获取 obj 的类型,结果是 'object'

现在,我们来说一下 JS 的面向对象编程(基本准则):

静态成员的放置没有什么疑问,关键是实例方法为什么要放在构造函数的 prototype 中呢?放在构造函数的函数体中不也可以吗,如 this.xxx = function() {},为什么不这么做呢?因为这样会浪费不必要的内存呀!每创建一个新对象,在内存中就会有一个多余的函数对象生成,这个完全没有必要!因此实例方法说是说"实例"方法,实际上是一个额外接收 this 指针的全局函数,和静态函数没有区别,当然整个 this 参数是隐式传递的。那既然是全局函数,为什么不放在 F 构造函数中呢,作为它的一个方法?这是不行的,因为你访问对象的方法时,是使用 obj.method(),而不是 F.method()!你只能将实例方法放在 prototype 中才会被顺着原型链找到该方法,正确的获取 this 指向。这也是 prototype 的主要作用了。

面向对象的例子
A -> B -> C 三个类,继承关系从左往右(父类 -> 子类)

  1. 'use strict';
  2. function A() {
  3. /*
  4. * this.prop = value;
  5. * ...
  6. */
  7. }
  8. function B() {
  9. A.call(this);
  10. // TODO
  11. }
  12. B.prototype = Object.create(A.prototype);
  13. B.prototype.constructor = B;
  14. function C() {
  15. B.call(this);
  16. // TODO
  17. }
  18. C.prototype = Object.create(B.prototype);
  19. C.prototype.constructor = C;
  20. let objA = new A(),
  21. objB = new B(),
  22. objC = new C();
  23. console.log(objA instanceof Object); // true
  24. console.log(objA instanceof A); // true
  25. console.log(objB instanceof Object); // true
  26. console.log(objB instanceof A); // true
  27. console.log(objB instanceof B); // true
  28. console.log(objC instanceof Object); // true
  29. console.log(objC instanceof A); // true
  30. console.log(objC instanceof B); // true
  31. console.log(objC instanceof C); // true

获取对象的所有属性

  1. for...in:依次访问一个对象及其原型链中所有可枚举的属性;
  2. Object.keys(o):访问一个对象中所有可枚举的属性(返回数组);
  3. Object.getOwnPropertyNames(o):访问一个对象中的所有属性(返回数组)。

Object 构造函数
new Object([value]):如果没有参数,则返回一个没有属性的空对象;如果存在参数 value(基本类型),则返回参数 value 的包装类对象,如果 value 为 undefined 或 null,则返回一个没有属性的空对象。其中 new 可以省略,即当作普通函数来调用(可以看作是类型转换函数)。

Object 实例方法

Object 静态方法

除了使用 Object.defineProperty() 方法来定义 getter-setter 方法外,还可以直接使用字面量方法,具体的语法如下(后一种是 ES6 新增的语法,允许在运行时计算属性的名称,其中方括号中的是一个 JS 表达式,其值应为字符串类型,该语法适用于对象的全部属性,而不仅是 getter-setter 伪属性):

  1. // getter/setter for 'key'
  2. let obj = {
  3. get key() {
  4. // TODO
  5. return someValue;
  6. },
  7. set key(newValue) {
  8. // TODO
  9. }
  10. };
  11. // getter/setter for 'key'
  12. let key = 'keyName';
  13. let obj = {
  14. get [key]() {
  15. // TODO
  16. return someValue;
  17. },
  18. set [key](newValue) {
  19. // TODO
  20. }
  21. };

JSON

JSON(JavaScript Object Notation,JS 对象表示法),是一种由 道格拉斯·克罗克福特 构想设计、轻量级的数据交换格式,以文本为基础,且易于让人阅读。尽管 JSON 是 Javascript 的一个子集,但 JSON 是独立于语言的文本格式,并且采用了类似于 C 语言家族的一些习惯。

JSON 数据格式与语言无关,脱胎于 JavaScript,但目前很多编程语言都支持 JSON 格式数据的生成和解析。JSON 的官方 MIME 类型是 application/json,文件扩展名是 .json

JSON 建构于两种结构:

这两种结构分别对应 JavaScript 中的 对象数组。注意,JSON 只是一个字符串!是一个纯文本!

值(即对象中的 value、数组中的 element)可以是以下类型:

number 只支持十进制的整数、浮点数。其中浮点数支持科学记数法,即 1.3E4 表示 13000(E 大小写不敏感)。

string 必须使用双引号包围,包括 object 中的 key,这是为了适应 C/C++、Java 中的"单引号为字符,双引号为字符串"语法。此外,还支持一些转义序列:

JS 内置的 JSON 全局对象包含两个方法,用于序列化和反序列化 JSON 数据:

DOM

什么是 DOM
文档对象模型(DOM)是 HTML 和 XML 文档的编程接口。它提供了对文档的结构化表述,并定义了一种方式使得程序可以对该结构进行访问,从而改变文档的结构,样式和内容。DOM 将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合。简言之,它会将 web 页面和脚本或程序语言连接起来。

一个 web 页面是一个文档。这个文档可以在浏览器窗口或作为 HTML 源码显示出来。但上述两个情况中都是同一份文档。文档对象模型(DOM)提供了对同一份文档的另一种表现,存储和操作的方式。DOM 是 web 页面的完全的面向对象表述,它能够使用如 JavaScript 等脚本语言进行修改。

W3C DOM 和 WHATWG DOM 标准在绝大多数现代浏览器中都有对 DOM 的基本实现。许多浏览器提供了对 W3C 标准的扩展,所以在使用时必须注意,文档可能会在多种浏览器上使用不同的 DOM 来访问。

如何访问 DOM
在 JS 中,一般通过 documentwindow 两个全局对象提供的 API 来操作 DOM。

DOM 其实就是文档树(Tree),既然是一棵树,那么肯定就有节点(Node)了。文档树就是由若干个节点组成的,如图所示:
DOM 图片示例

Node 节点的类型

Node 是一个接口,上述的节点类型都实现了 Node 接口,因此它们具有一些共同的属性和方法。Node 有一个 nodeType 属性,表示 Node 的类型,它是一个整数,其对应的数值如上所示。

这些 Node 类型中,我们最常用的就是 element,text,attribute,comment,document,document_fragment这几种类型。

Element 类型
Element 提供了对元素标签名,子节点和特性的访问,我们常用 HTML 元素比如 div,span,a 等标签就是 element 中的一种。Element 有下面几条特性:

Text 类型
Text 表示文本节点,它包含的是纯文本内容,不能包含 html 代码,但可以包含转义后的 html 代码。Text 有下面的特性:

Attr 类型
Attr 类型表示元素的属性,相当于元素的 attributes 属性中的节点,它有下面的特性:

Comment 类型
Comment 表示 HTML 文档中的注释,它有下面的几种特征:

Document 类型
Document 表示文档,在浏览器中,document 对象是 HTMLDocument 的一个实例,表示整个页面,它同时也是 window 对象的一个属性。Document 有下面的特性:

DocumentFragment 类型
DocumentFragment 是所有节点中唯一一个没有对应标记的类型,它表示一种轻量级的文档,可能当作一个临时的仓库用来保存可能会添加到文档中的节点。DocumentFragment 有下面的特性:

节点创建型 API
createElement
createElement 通过传入指定的一个标签名来创建一个元素,如果传入的标签名是一个未知的,则会创建一个自定义的标签,注意:IE8 以下浏览器不支持自定义标签。
var div = document.createElement("div");
使用 createElement 要注意:通过 createElement 创建的元素并不属于 html 文档,它只是创建出来,并未添加到 html 文档中,要调用 appendChild 或 insertBefore 等方法将其添加到 HTML 文档树中。

createTextNode
createTextNode 用来创建一个文本节点,用法如下:
var textNode = document.createTextNode("一个 textNode");
createTextNode 接收一个参数,这个参数就是文本节点中的文本,和 createElement 一样,创建后的文本节点也只是独立的一个节点,同样需要 appendChild 将其添加到 HTML 文档树中。

cloneNode
cloneNode 会返回当前节点的一个副本,它接收一个 bool 参数,用来表示是否复制子元素,使用如下:

  1. var parent = document.getElementById("parentElement");
  2. var parent2 = parent.cloneNode(true);// 传入true
  3. parent2.id = "parent2";

这段代码通过 cloneNode 复制了一份 parent 元素,其中 cloneNode 的参数为 true,表示 parent 的子节点也被复制,如果传入 false,则表示只复制了 parent 节点。要注意的几点:
1)和 createElement 一样,cloneNode 创建的节点只是游离于 html 文档外的节点,要调用 appendChild 方法才能添加到文档树中;
2)如果复制的元素有 id,则其副本同样会包含该 id,由于 id 具有唯一性,所以在复制节点后必须要修改其 id;
3)调用接收的 bool 参数最好传入,如果不传入该参数,不同浏览器对其默认值的处理可能不同。
4)如果是通过 addEventListener 或如 onclick 进行绑定事件,则副本节点不会绑定该事件;
5)如果是内联方式绑定比如<div onclick="showParent()"></div>,这样的话,副本节点同样会触发事件。

createDocumentFragment
createDocumentFragment 方法用来创建一个 DocumentFragment。在前面我们说到 DocumentFragment 表示一种轻量级的文档,它的作用主要是存储临时的节点用来准备添加到文档中。
createDocumentFragment 方法主要是用于添加大量节点到文档中时会使用到。假设要循环一组数据,然后创建多个节点添加到文档中,初始版本如下:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>hello, world!</title>
  6. </head>
  7. <body>
  8. <ul id="list"></ul>
  9. <button type="button" id="btnAdd">点我添加</button>
  10. <script>
  11. document.getElementById('btnAdd').onclick = function() {
  12. let list = document.getElementById('list');
  13. for (let i = 0, item; i < 1000; i++) {
  14. item = document.createElement('li');
  15. item.textContent = i;
  16. list.appendChild(item);
  17. }
  18. };
  19. </script>
  20. </body>
  21. </html>

可以看出,每循环一次,就要调用一次 list.appendChild(item),即每次循环都要修改 DOM 树。这个过程会造成浏览器的回流,所谓回流简单说就是指元素大小和位置会被重新计算,如果添加的元素太多,会造成性能问题。这个时候就需要使用 createDocumentFragment 了,无论多少次循环,都只是在内存中添加节点而已,只需在循环结束后一次性添加到 DOM 树即可。改进版本如下:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>hello, world!</title>
  6. </head>
  7. <body>
  8. <ul id="list"></ul>
  9. <button type="button" id="btnAdd">点我添加</button>
  10. <script>
  11. document.getElementById('btnAdd').onclick = function() {
  12. let list = document.getElementById('list');
  13. let frag = document.createDocumentFragment();
  14. for (let item, i = 0; i < 1000; i++) {
  15. item = document.createElement('li');
  16. item.textContent = i;
  17. frag.appendChild(item);
  18. }
  19. list.appendChild(frag);
  20. };
  21. </script>
  22. </body>
  23. </html>

创建型 API 总结
创建型 API 主要包括 createElementcreateTextNodecloneNodecreateDocumentFragment 四个方法,需要注意下面几点:
1)新创建的节点只是一个孤立的节点,要通过 appendChild 等方法添加到文档中
2)cloneNode 要注意被复制的节点是否包含子节点以及事件绑定等问题
3)使用 createDocumentFragment 来解决添加大量节点时的性能问题

页面修改型 API
前面我们提到创建型 API,它们只是创建节点(只存在于内存中),并没有真正修改页面内容,必须要调用 appendChild 等方法来将其添加到文档树中。修改页面内容的 API 主要包括:appendChildinsertBeforeremoveChildreplaceChild

appendChild
appendChild 我们在前面已经用到多次,就是将指定的节点添加到调用节点的子元素的末尾。调用方法如下:parent.appendChild(child);
child 节点将会作为 parent 节点的最后一个子节点。appendChild 这个方法很简单,但是还有一点需要注意:如果被添加的节点是一个页面中已存在(同一个引用)的节点,则执行后这个节点将会添加到指定位置,其原本所在的位置将移除该节点,也就是说不会同时存在两个相同节点在页面上,相当于把这个节点移动到另一个地方。我们来看例子:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>hello, world!</title>
  6. </head>
  7. <body>
  8. <ul id="list">
  9. <li id="1th-child">第一个子节点</li>
  10. <li id="2th-child">第二个子节点</li>
  11. <li id="3th-child">第三个子节点</li>
  12. </ul>
  13. <button type="button" id="btnAdd">点击添加</button>
  14. <button type="button" id="btnMov">点击移动</button>
  15. <script>
  16. document.getElementById('btnAdd').onclick = function() {
  17. let child3 = document.getElementById('3th-child');
  18. let child4 = child3.cloneNode(true);
  19. child4.id = '4th-child';
  20. child4.textContent = '第四个子节点';
  21. document.getElementById('list').appendChild(child4);
  22. };
  23. document.getElementById('btnMov').onclick = function() {
  24. let child1 = document.getElementById('1th-child');
  25. document.getElementById('list').appendChild(child1);
  26. };
  27. </script>
  28. </body>
  29. </html>

insertBefore
insertBefore 用来在指定节点前面插入一个新节点,用法如下:
parentNode.insertBefore(newNode, refNode);
parentNode 表示新节点所属的父节点,newNode 表示要添加的新节点,refNode 表示参照节点,新节点会添加到这个节点之前。

个人感觉这个 API 设计的不太好,parentNode 完全是多余的,我只需要知道 refNode 即可,即 refNode.insertBefore(newNode)

和 appendChild 一样,如果插入的节点是页面上的节点(同一引用),则会移动该节点到指定位置,并且保留其绑定的事件。

关于第二个参数参照节点还有几个注意的地方:
1)refNode 是必传的,如果不传该参数会报错
2)如果 refNode 是 undefined 或 null,则 insertBefore 会将节点添加到子元素的末尾

removeChild
删除调用节点的指定子节点,并返回被删子节点的引用(依然存在于内存之中),用法:parentNode.removeChild(node)。为了确保不会出错,我们可以先通过子节点自身获取它的父节点,然后再删除它自己,即 node.parentNode.removeChild(node)

replaceChild
替换调用节点的指定子节点,并返回被替换掉的子节点引用。用法如下:
parent.replaceChild(newChild, oldChild);
newChild 是替换的节点,可以是新的节点,也可以是页面上的节点,如果是页面上的节点,它将被移动到新的位置;oldChild 是被替换的节点。

页面修改型 API 总结
页面修改型 API 主要是这四个接口,要注意是:
不管是新增还是替换节点,如果新增或替换的节点是原本存在页面上的,则只会移动原有节点;并且节点本身绑定的事件不会消失,会一直保留着。

节点查询型 API

节点关系型 API
父关系型 API
parentNode:返回当前节点的父节点,根节点的父节点为 null。
parentElement:返回当前节点的父元素,父节点必须是 Element,否则返回 null。

兄弟关系型 API
previousSibling:返回当前节点的前一兄弟节点。
nextSibling:返回当前节点的后一兄弟节点。
previousElementSibling:返回当前节点的前一兄弟节点,其类型须为 Element,否则返回 null。
nextElementSibling:返回当前节点的后一兄弟节点,其类型须为 Element,否则返回 null。

子关系型 API
childNodes:返回当前节点的直接子节点(实时 NodeList)。
children:返回当前节点的直接子节点(实时 HTMLCollection),类型均为 Element。
firstNode:返回当前节点的第一个直接子节点。
lastNode:返回当前节点的最后一个直接子节点。
hasChildNodes():判断当前节点是否包含子节点。

元素属性型 API
hasAttribute(attrName):判断当前元素是否存在给定属性,返回布尔值。
getAttribute(attrName):获取当前元素的给定属性值(字符串),不存在则返回 null 或空串,建议在 getAttribute 前使用 hasAttribute 来检测是否存在给定属性。
setAttribute(attrName, attrValue):设置当前元素的给定属性值(修改或新增)。
removeAttribute(attrName):删除当前元素的给定属性。

HTMLCollection 与 NodeList 的区别,都是类数组对象。

window/document 对象

  1. 'use strict';
  2. /* 文档结构,从上到下 */
  3. console.log(window); // 浏览器窗口
  4. console.log(document); // 整个 HTML 文档
  5. console.log(document.doctype); // DOCTYPE 声明
  6. console.log(document.documentElement); // <html>...</html>
  7. console.log(document.head); // <head>...</head>
  8. console.log(document.body); // <body>...</body>
  9. /* window.location */
  10. $('<button type="button">window.location.reload()</button>').click(function() {
  11. window.location.reload(); // 重新加载当前页面
  12. }).appendTo(document.body);
  13. $('<br>').appendTo(document.body);
  14. $('<button type="button">window.location.assign()</button>').click(function() {
  15. window.location.assign('https://www.baidu.com/'); // 跳转至给定 url,相当于点击超链接
  16. }).appendTo(document.body);
  17. $('<br>').appendTo(document.body);
  18. $('<button type="button">window.location.replace()</button>').click(function() {
  19. window.location.replace('https://www.baidu.com/'); // 跳转至给定 url,并删除当前页面的浏览历史
  20. }).appendTo(document.body);
  21. $('<br>').appendTo(document.body);
  22. console.log(window.location.href); // http://www.zfl.com/index.html?lang=zh_CN#debug
  23. console.log(window.location.protocol); // 协议 http:
  24. console.log(window.location.hostname); // 主机 www.zfl.com
  25. console.log(window.location.port); // 端口 (空,默认 80)
  26. console.log(window.location.host); // 主机+端口 www.zfl.com
  27. console.log(window.location.pathname); // 路径 /index.html
  28. console.log(window.location.search); // 参数 ?lang=zh_CN
  29. console.log(window.location.hash); // 描点 #debug
  30. /* window.history */
  31. console.log(history.length); // 历史记录的长度
  32. $('<button type="button">后退一页</button>').click(function() {
  33. history.back();
  34. }).appendTo(document.body);
  35. $('<br>').appendTo(document.body);
  36. $('<button type="button">前进一页</button>').click(function() {
  37. history.forward();
  38. }).appendTo(document.body);
  39. $('<br>').appendTo(document.body);
  40. // history.go(offset|url);
  41. $('<button type="button">后退 2 页</button>').click(function() {
  42. history.go(-2);
  43. }).appendTo(document.body);
  44. $('<br>').appendTo(document.body);
  45. $('<button type="button">前进 2 页</button>').click(function() {
  46. history.go(2);
  47. }).appendTo(document.body);
  48. $('<br>').appendTo(document.body);
  49. /* window 输入输出 */
  50. // alert() 方法用于显示带有一条指定消息和一个确认按钮的警告框
  51. $('<button type="button">alert()</button>').click(() => alert('hello, world!')).appendTo(document.body);
  52. $('<br>').appendTo(document.body);
  53. // confirm() 方法用于显示一个带有指定消息和确认及取消按钮的对话框
  54. // 如果访问者点击"确定",此方法返回 true,否则返回 false
  55. $('<button type="button">confirm()</button>').click(
  56. () => console.log(confirm('hello, world!'))
  57. ).appendTo(document.body);
  58. $('<br>').appendTo(document.body);
  59. // prompt() 方法用于显示可提示用户进行输入的对话框,并返回输入的字符串
  60. $('<button type="button">prompt()</button>').click(function() {
  61. // prompt(msg, defaultText), 两个参数均为可选的
  62. console.log(prompt('请输入任意字符串', '请输入任意字符串'));
  63. }).appendTo(document.body);
  64. $('<br>').appendTo(document.body);
  65. /* window.close() 关闭当前窗口 Chrome 无效 */
  66. $('<button type="button">close()</button>').click(() => window.close()).appendTo(document.body);
  67. $('<br>').appendTo(document.body);

DOM 其他 API
node.nodeName:节点的名称
node.nodeType:节点的类型(整数)
node.nodeValue:节点的文本值(可读写),适用于 Text、Comment、CDATA 节点
node.textContent:节点的文本内容(当前节点及其后代节点,忽略 HTML 标签,可读写,自动对 HTML 标签转义)

node.contains(testNode):测试当前节点是否包含给点节点
node.isEqualNode(other):测试当前节点是否与给定节点相等,即类型相同、属性相同、子节点相同等

Element 元素相关 API
element.attrName:获取给定属性的值
element.attrName = attrValue:修改给定属性的值

element.style:获取当前元素的 CSS 样式
element.style = CSS_Style:修改当前元素的 CSS 样式
element.style.prop:获取当前元素的给定 CSS 样式
element.sytle.prop = propValue:修改当前元素的给定 CSS 样式

element.innerHTML:获取/修改当前元素的内部 HTML
element.innerText:获取/修改当前元素的内部 Text
element.outerHTML:获取/修改当前元素的外部 HTML
element.outerText:获取/修改当前元素的外部 Text

读取 innerHTML:返回当前元素的起始标签和结束标签之间的 HTML 内容
写入 innerHTML:覆盖当前元素的起始标签和结束标签之间的 HTML 内容

读取 outerHTML:返回当前元素(包含起始标签和结束标签)的 HTML 内容
写入 outerHTML:覆盖当前元素(包括起始标签和结束标签)的 HTML 内容

读取 innerText:返回当前元素的起始标签和结束标签之间的文本(忽略HTML标签)
写入 innerText:覆盖当前元素的起始标签和结束标签之间的内容(覆盖HTML标签)

读取 outerText:返回当前元素(包含起始标签和结束标签)的文本
写入 outerText:覆盖当前元素(包含其实标签和结束标签)的内容

一般来说:element.innerText === element.outerText,因为都会忽略 HTML 标签。

DOM 事件机制

事件冒泡:从内往外传播(默认)
事件捕获:从外往内传播

DOM 事件列表:
事件类型一览表 - MDN
HTML DOM 事件类型 - 菜鸟教程

AJAX

举个栗子:不使用 AJAX 的情况下,填写完表单然后点击提交时,默认会刷新当前页面,等待服务器响应,这对于用户来说显然是不太友好的。用户可能仅需要表单提交成功的提示,这种情况下,我们完全可以使用 AJAX 在后台异步的提交表单,提交成功后显示成功提示即可,在此期间,用户可以无阻碍的浏览网页的其他内容。

先来一个简单的 AJAX 例子吧:
index.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>hello, world!</title>
  6. </head>
  7. <body>
  8. <div id="ajax-show">AJAX 是什么?</div>
  9. <button type="button" id="ajax-button">点击获取</button>
  10. <script src="index.js"></script>
  11. </body>
  12. </html>

index.js

  1. 'use strict';
  2. document.getElementById('ajax-button').addEventListener('click', function() {
  3. let xmlhttp = new XMLHttpRequest();
  4. xmlhttp.onreadystatechange = function() {
  5. if (xmlhttp.readyState === XMLHttpRequest.DONE) {
  6. if (xmlhttp.status === 200)
  7. document.getElementById('ajax-show').innerText = xmlhttp.responseText;
  8. else
  9. document.getElementById('ajax-show').innerText =
  10. 'Error (' + xmlhttp.status + ' ' + xmlhttp.statusText + ')';
  11. }
  12. };
  13. xmlhttp.open('GET', '/ajax.txt');
  14. xmlhttp.send();
  15. });

用户点击 button 按钮时,div 标签的内容将被替换为 /ajax.txt 文档的内容。

其实 AJAX 的关键还是 XMLHttpRequest 对象,几个主要步骤:

是不是很简单?如果是 GET 请求一般就这 4 个步骤。那如果是 POST 请求呢?例子:

  1. 'use strict';
  2. document.getElementById('ajax-button').addEventListener('click', function() {
  3. let xmlhttp = new XMLHttpRequest();
  4. xmlhttp.onreadystatechange = function() {
  5. if (xmlhttp.readyState === XMLHttpRequest.DONE) {
  6. if (xmlhttp.status === 200)
  7. document.getElementById('ajax-show').innerText = xmlhttp.responseText;
  8. else
  9. document.getElementById('ajax-show').innerText =
  10. 'Error (' + xmlhttp.status + ' ' + xmlhttp.statusText + ')';
  11. }
  12. };
  13. xmlhttp.open('POST', '/ajax.php');
  14. xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  15. xmlhttp.send('userName=' + encodeURIComponent('Otokaze'));
  16. });

XMLHttpRequest 构造函数
let xmlhttp = new XMLHttpRequest();

XMLHttpRequest 实例属性

XMLHttpRequest 实例方法

URI 编码/解码方法

encodeURI/decodeURI:对含有非 ASCII 字符的正常 URI 进行百分号编码
encodeURIComponent/decodeURIComponent:对非 ASCII 字符和 URI 保留字符进行百分号编码

字符串:https://www.zfl9.com/index.php?key1=中文&key2=English
encodeURIhttps://www.zfl9.com/index.php?key1=%E4%B8%AD%E6%96%87&key2=English
encodeURIComponenthttps%3A%2F%2Fwww.zfl9.com%2Findex.php%3Fkey1%3D%E4%B8%AD%E6%96%87%26key2%3DEnglish

encodeURI 常用于对含有非 ASCII 字符的正常 URI 编码
encodeURIComponent 常用于对含有非 ASCII 字符或 URI 保留字符的 URI 查询参数编码

AJAX 提交表单
使用 AJAX 提交表单(XMLHttpRequest)主要有两种方法:手动获取表单数据、使用 FormData 获取表单数据。

手动解析
index.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>hello, world!</title>
  6. </head>
  7. <body>
  8. <form id="my-form" method="post" action="form.php">
  9. username: <input id="username" type="text" name="username"><br>
  10. password: <input id="password" type="password" name="password"><br>
  11. <input id="form-submit" type="button" value="点击登录">
  12. </form>
  13. <script src="index.js"></script>
  14. </body>
  15. </html>

index.js

  1. 'use strict';
  2. document.getElementById('form-submit').addEventListener('click', function() {
  3. let form = document.getElementById('my-form');
  4. let username = document.getElementById('username');
  5. let password = document.getElementById('password');
  6. let xhr = new XMLHttpRequest();
  7. xhr.onreadystatechange = function() {
  8. if (xhr.readyState === XMLHttpRequest.DONE) {
  9. if (xhr.status === 200)
  10. alert('提交成功');
  11. else
  12. alert('提交失败 (' + xhr.status + ' ' + xhr.statusText + ')');
  13. }
  14. };
  15. xhr.open('POST', form.action);
  16. xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  17. xhr.send(encodeURIComponent(username.value) + '=' + encodeURIComponent(password.value));
  18. });

FormData 对象

  1. 'use strict';
  2. document.getElementById('form-submit').addEventListener('click', function() {
  3. let formElem = document.getElementById('my-form');
  4. let formData = new FormData(formElem);
  5. let xhr = new XMLHttpRequest();
  6. xhr.onreadystatechange = function() {
  7. if (xhr.readyState === XMLHttpRequest.DONE) {
  8. if (xhr.status === 200)
  9. alert('提交成功');
  10. else
  11. alert('提交失败 (' + xhr.status + ' ' + xhr.statusText + ')');
  12. }
  13. };
  14. xhr.open('POST', formElem.action);
  15. xhr.send(formData);
  16. });

不需要显式的设置 HTTP 请求头,直接调用 xhr.send() 方法即可。

FormData 构造函数
new FormData([formElem]):formElem 是可选的,如果没有则创建一个空的 FormData,然后调用 append() 方法设置 name-value 名值对;如果不为空,则从传入的 formElem 中解析 name-value 名值对,然后被 xmlhttprequest.send() 方法提交到服务器上。

FormData 实例方法

可以直接通过 XMLHttpRequest 实例的 send() 方法发送 FormData 实例,并且不需要显式的设置 HTTP 请求头部。

AJAX 跨域请求
要了解跨域问题,必须先了解浏览器的同源策略。同源是指:两个 URL 的 协议主机端口 都相同。不同源即跨域

而所谓的同源策略是指:JS 脚本中不可以向其它域发起网络请求。典型的就是 XMLHttpRequest 了,一般 AJAX 就是利用 XMLHttpRequest API 发起 HTTP 请求了。同源限制的目的主要是为了安全,但是另一方面也带来了所谓的 跨域问题

目前解决 AJAX 跨域的方案有两种:JSONP(非标)、CORS(标准)。

我们先来介绍 JSONP(JSON with Padding),注意不要与 JSON 混淆了,JSON 是一个数据交换格式JSONP 是 JSON 的一种 "使用模式",简单的说,JSONP 是 JSON 的一种使用方式,主要用来解决跨域问题。JSONP 的主要原理就是:利用 <script> 标签,script 标签可以引用外部 JS 文件,且不受同源策略的限制。JSONP 跨域需要浏览器和服务器的双方协作:服务器上要提供一个可以动态查询的 API 页面(返回的数据为 JSON),约定一个 URL 查询参数(一般为 callback),用于传递 callback 函数名,然后在该页面上做一个小改动,如果接收到了 callback 查询参数,则返回 funcName + '(' + originJSON + ');',即拼接字符串,使得该 script 标签执行它,从而实现跨域的效果。

单纯的文字叙述可能不太清楚,没关系,我们一步一步来解释:

先来一个段简单的代码,暂不涉及 JSONP 跨域:
index.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>hello, world!</title>
  6. <script src="local.js"></script>
  7. <script src="http://home.zfl9.com:88/remote.js"></script>
  8. </head>
  9. <body>
  10. </body>
  11. </html>

local.js

  1. 'use strict';
  2. function func() {
  3. alert('local.js::func()');
  4. }

remote.js

  1. 'use strict';
  2. func();

很明显,打开上述网页将弹出提示窗口:local.js::func(),让它接收一个参数:
local.js

  1. 'use strict';
  2. function func(data) {
  3. alert(data);
  4. }

remote.js

  1. 'use strict';
  2. func('hello, world!');

刷新页面,弹出提示:hello, world!,给我们的感觉就是我可以通过一个函数,来在不同的域中传递数据,是不是有了一点跨域的感觉?!对的,很近了,我们可以在 remote.js 中传递 JSON 数据给 func() 方法,然后在 local.js 中解析接收到的 JSON 数据。
local.js

  1. 'use strict';
  2. function func(data) {
  3. data = JSON.parse(data);
  4. alert(data.key1);
  5. alert(data.key2);
  6. alert(data.key3);
  7. }

remote.js

  1. 'use strict';
  2. func(`{
  3. "key1": "百度",
  4. "key2": "谷歌",
  5. "key3": "微软"
  6. }`);

既然是动态查询页面,肯定不能是静态的啦,因此需要在目标服务器上设置 PHP 动态页面,返回 JSON 数据,同时我们还需要约定一个被调用的函数名,通常的做法是:通过 URL 的 callback 查询参数来传递被调用的函数名。整个 PHP 页面的思路是:如果没有 callback 查询参数,则正常返回 JSON 响应数据;如果存在 callback 查询参数,则返回一个 JavaScript 文件,其内容是 callback 函数名 + ( + JSON 字符串 + );,其实不难,就是拼接字符串而已。因此 JSONP 的一般形式是:
index.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>hello, world!</title>
  6. <script>
  7. function func(data) {
  8. alert(data.key1);
  9. alert(data.key2);
  10. alert(data.key3);
  11. // TODO
  12. }
  13. let script = document.createElement('script');
  14. script.src = 'http://home.zfl9.com:88/remote.php?callback=func';
  15. document.getElementsByTagName('head')[0].appendChild(script);
  16. </script>
  17. </head>
  18. <body>
  19. </body>
  20. </html>

CORS 跨域方案

跨域资源共享(Cross-Origin Resource Sharing,简称 CORS)是一份浏览器技术的规范,提供了 Web 服务从不同网域传来沙盒脚本的方法,以避开浏览器的同源策略,是 JSONP 模式的现代版。与 JSONP 不同,CORS 除了 GET 请求方法以外也支持其他的 HTTP 请求方法。用 CORS 可以让网页设计师用一般的 XMLHttpRequest,这种方式的错误处理比 JSONP 要来的好。另一方面,JSONP 可以在不支持 CORS 的老旧浏览器上运作。现代的浏览器都支持 CORS(IE8 及以上)。

CORS 是 W3C 的标准跨域资源共享方法,是 JSONP 的替代品,JSONP 并不是官方推荐的方式,但是在 CORS 出现之前是主要的跨域解决方案,并且 JSONP 还有一个优点,兼容性好,支持古董级的 IE 浏览器。而 CORS 只有在 IE8 之后(含)才被支持。仔细观察上面的 JSONP 例子,你会发现,其实它和 AJAX 跨域没多大关系,只不过是利用了 script 标签的 src 属性而已,完全和 XMLHttpRequest 不搭边。但是 CORS 就不一样了,我们依旧使用 XMLHttpRequest 对象,并且一般不需要做什么改动,只需要服务器支持即可(前提是使用现代浏览器,支持 CORS 规范)。

CORS 需要浏览器和服务器同时支持。目前,所有现代浏览器都支持该功能,IE 浏览器不能低于 IE10(正式支持)。

整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。

简单请求、非简单请求
浏览器将 CORS 请求分成两类:简单请求(simple request)非简单请求(not-so-simple request)

只要同时满足以下两大条件,就属于简单请求。
(1)请求方法是以下三种方法之一:

(2)HTTP的头信息不超出以下几种字段:

简单请求
对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是在头信息之中,增加一个 Origin 字段。

下面是一个例子,浏览器发现这次跨源 AJAX 请求是简单请求,就自动在头信息之中,添加一个 Origin 字段。

  1. GET /cors HTTP/1.1
  2. Origin: http://api.bob.com
  3. Host: api.alice.com
  4. Accept-Language: en-US
  5. Connection: keep-alive
  6. User-Agent: Mozilla/5.0...

上面的头信息中,Origin 字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果 Origin 指定的源不在许可范围内,服务器会返回一个正常的 HTTP 响应。浏览器发现,这个响应的头信息没有包含 Access-Control-Allow-Origin 字段(详见下文),就知道出错了,从而抛出一个错误,被 XMLHttpRequest 的 onerror 回调函数捕获。注意,这种错误无法通过状态码识别,因为 HTTP 回应的状态码有可能是 200。(比如支持 CORS 的浏览器发送跨域请求到未启用 CORS 的服务器上,服务器会返回一个正常的 HTTP 响应,而不会添加额外的 CORS 响应头部)

如果 Origin 指定的域名在许可范围内,服务器返回的响应头部,会多出几个头信息字段。

  1. Access-Control-Allow-Origin: http://api.bob.com
  2. Access-Control-Allow-Credentials: true
  3. Access-Control-Expose-Headers: FooBar
  4. Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与 CORS 请求相关的字段,都以 Access-Control-* 开头。相关的头部如下:

请求头部

响应头部

其中,请求头 Origin、响应头 Access-Control-Allow-Origin 就足以构成最简的 CORS 请求-响应了。

预检请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUT 或 DELETE,或者 Content-Type 字段的类型是 application/json 等。

非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为"预检"请求(preflight)

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 方法和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。

下面是一段浏览器的 JavaScript 脚本。

  1. var url = 'http://api.alice.com/cors';
  2. var xhr = new XMLHttpRequest();
  3. xhr.open('PUT', url, true);
  4. xhr.setRequestHeader('X-Custom-Header', 'value');
  5. xhr.send();

上面代码中,HTTP 请求的方法是 PUT,并且发送一个自定义头信息 X-Custom-Header。

浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的 HTTP 头信息。

  1. OPTIONS /cors HTTP/1.1
  2. Origin: http://api.bob.com
  3. Access-Control-Request-Method: PUT
  4. Access-Control-Request-Headers: X-Custom-Header
  5. Host: api.alice.com
  6. Accept-Language: en-US
  7. Connection: keep-alive
  8. User-Agent: Mozilla/5.0...

"预检"请求用的请求方法是 OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是 Origin,表示请求来自哪个源。除了 Origin 字段,"预检"请求的头信息包括两个特殊字段(见上述)。

服务器收到"预检"请求以后,检查了 Origin、Access-Control-Request-Method 和 Access-Control-Request-Headers 字段以后,确认允许跨源请求,就可以做出回应。

  1. HTTP/1.1 200 OK
  2. Date: Mon, 01 Dec 2008 01:15:39 GMT
  3. Server: Apache/2.0.61 (Unix)
  4. Access-Control-Allow-Origin: http://api.bob.com
  5. Access-Control-Allow-Methods: GET, POST, PUT
  6. Access-Control-Allow-Headers: X-Custom-Header
  7. Content-Type: text/html; charset=utf-8
  8. Content-Encoding: gzip
  9. Content-Length: 0
  10. Keep-Alive: timeout=2, max=100
  11. Connection: Keep-Alive
  12. Content-Type: text/plain

上面的 HTTP 回应中,关键的是 Access-Control-Allow-Origin 字段,表示 http://api.bob.com 可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

如果浏览器否定了"预检"请求,会返回一个正常的 HTTP 回应,但是没有任何 CORS 相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被 XMLHttpRequest 对象的 onerror 回调函数捕获。控制台会打印出如下的报错信息。

  1. XMLHttpRequest cannot load http://api.alice.com.
  2. Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

一旦服务器通过了"预检"请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。

下面是"预检"请求之后,浏览器的正常 CORS 请求。

  1. PUT /cors HTTP/1.1
  2. Origin: http://api.bob.com
  3. Host: api.alice.com
  4. X-Custom-Header: value
  5. Accept-Language: en-US
  6. Connection: keep-alive
  7. User-Agent: Mozilla/5.0...

下面是服务器正常的回应。

  1. Access-Control-Allow-Origin: http://api.bob.com
  2. Content-Type: text/html; charset=utf-8

CORS 与 JSONP 的比较
CORS 与 JSONP 的使用目的相同,但是比 JSONP 更强大。
JSONP 只支持 GET 请求,CORS 支持所有类型的 HTTP 请求。
JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。

CORS 跨域方案总结
绝大多数现代浏览器都支持 CORS 规范(IE8 初步支持,IE10 正式支持),因此,CORS 跨域的关键在于服务器,服务器上启用 CORS 也很简单,比如 nginx,添加几个 HTTP 响应头部即可应付简单的 CORS 请求。而 JSONP 则需要服务器上的动态页面,并且只支持 GET 方式,限制很多,甚至和 AJAX 跨域没有联系,因此,尽量考虑使用 CORS 跨域。

jQuery

jQuery 是什么

jQuery 是一套跨浏览器的 JavaScript 库,简化 HTML 与 JavaScript 之间的操作。由约翰·雷西格(John Resig)在 2006 年 1 月的 BarCamp NYC 上发布第一个版本。目前是由 Dave Methvin 领导的开发团队进行开发。全球前 10,000 个访问最高的网站中,有 65% 使用了 jQuery,是目前最受欢迎的 JavaScript 库。

jQuery 是一个 JavaScript 函数库。
jQuery 是一个轻量级的"写的少,做的多"的 JavaScript 库。

jQuery库包含以下功能:

除此之外,jQuery 还提供了大量的插件,我们也可以开发自己的 jQuery 插件。

如何安装 jQuery

安装 jQuery 很简单,只需要在 HTML 文档中引入它即可。jquery.js 可以自己下载到网站中,也可以直接引用 CDN 的在线脚本(推荐此方式)。

为什么推荐使用 CDN 的 jquery.js 文件呢?因为很有可能其它网站也引用了 CDN 的 jquery.js 文件(很多大网站都使用了 jQuery,并且都提供它的 CDN 服务),这样浏览器发现已经存在已下载的 jquery.js,就不会再次下载它了,而是直接从本地磁盘中读取,这样速度会快一些。当 CDN 用的人很多时,这个效果越明显。

国内的常用 CDN:http://www.bootcdn.cn/jquery/
国外的常用 CDN:http://code.jquery.com/(官网)

当前我使用的版本是 jquery-3.3.1,使用的是 bootcdn。在 head 标签中添加:
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>

jQuery 在线文档

中文:https://www.jquery123.com/
英文:https://api.jquery.com/(官方)

jQuery 对象

所有 jQuery 功能都包含在了函数对象 jQuery 中,jQuery 函数对象也存在一个别名 $,它们指向同一个函数,因为后者只需要写一个字符,因此我们通常都是使用 $ 而不是 jQuery

  1. 'use strict';
  2. console.log(typeof jQuery); // function
  3. console.log(typeof $); // function
  4. console.log(jQuery === $); // true

jQuery 函数是构造函数,但是创建 jQuery 对象却并不使用 new $(param...) 语法,而是直接使用 $(param...),因为 jQuery 构造函数内部已经封装了 new jQuery(param...) 语句了。这也符合 jQuery 一直强调的设计理念:写的少,做的多,大家都很忙的,能省一个字符是一个字符。

常用的一个构造函数是 $(selector),其中 selector 是 CSS 选择器(字符串)。因为使用 CSS 选择 DOM 元素,因此编写的代码也很简洁,再也不用像原生 DOM API 那样 document.querySelectorAll(selector) 了。

调用 jQuery 构造函数返回的都是 jQuery 对象,因此 $(params) instanceof jQuery 返回 true。jQuery 对象是类数组对象,拥有 length 属性,同时可以通过下标(从 0 开始)来获取原生 DOM 对象。因此,可以将 jQuery 对象理解为 DOM 对象(可以有多个)的包装对象。jQueryObj.length 为所包装的 DOM 对象的数量,jQueryObj[0] 为所包装的第 0 个 DOM 对象,或者使用 jQueryObj.get(0)(作用是等价的,推荐使用前者,减少一次函数调用开销)。

DOM对象、jQuery对象之间的互转

jQuery 构造函数

这些构造函数均返回 jQuery 对象!接下来我们主要介绍其中几个主要的构造函数。

$(selector[, context])
参数 selector 是字符串,CSS 选择器
参数 context 是上下文对象,可选参数

selector 没什么可讲的,主要看一下 context 上下文参数。那么 context 参数的作用和意义是什么呢?context 参数的类型可以是 Element(被包装为 jQueryObj)、jQueryObj。所谓的上下文对象就是指我该在什么范围内查找 DOM 元素,默认情况下,jQuery 会在 document 对象中查找(相当于全局查找),如果需要在指定 DOM/jQuery 对象中查找,则需要指定 context 参数。

$(callback) vs $(document).ready(callback)
这两者是完全等价的,前者是后者的简写形式。我们主要解释一下它的作用。ready 是准备好了,准备完毕的意思,放在这里就是说当 DOM 准备完毕时要执行的回调函数。

那么什么是 DOM 准备完毕 呢。DOM 准备完毕也可以理解为浏览器已经生成了完整的 DOM 对象,通俗一点就是说,网页的骨架已经加载完毕了,此时可以进行大部分 DOM 操作,但是,网页的内容不一定全部加载完毕了(比如大图片还未加载完成),如果需要操作图片,那么 ready 事件就不足以应付了,需要使用 window.onload = callback(只允许使用一次,后面的覆盖前面的),或者使用 $(window).on('load', callback)(可以使用多次,依次被调用,不会被覆盖)。

$(document).ready()事件是 jQuery 独有的;
$(window).on('load')是标准 DOM 事件。

jQuery 独有事件应使用 jQueryObj.event() 方法;
标准 DOM 事件应使用 jQueryObj.on(event) 方法。

$(html[, ownerDocument])
参数 html 是 HTML 字符串,jQuery 会自动解析并创建对应的 DOM 对象
参数 ownerDocument 是新创建 DOM 元素的所属 Document 文档(意义不明确)

注意,html 参数必须符合 HTML 语法,jQuery 内部会使用正则匹配出具体的 HTML 元素,然后调用原生 DOM 方法创建并设置相应的属性、样式、属性值。其中 html 字符串中可以存在多个 HTML 标签,它们都会被创建。jQuery 解析时,会创建与一级标签对应多个的元素数量,注意是一级,不是嵌套的。

$('<h1>txt</h1><h2>txt</h2><h3><span>txt</span></h3>') 会创建 3 个 DOM 元素:h1h2h3。而 span 元素则包含在 h3 的内部。返回的 jQuery 对象的 length 为 3。

注意,使用该语法创建 DOM 元素和使用原生 API 创建 DOM 元素一样,创建之后,这些新元素都是游离状态,并未添加到文档中,只是存在于内存之中。

如何将这些元素添加到当前文档对象中?有 8 种方式(正向 4 种,逆向 4 种)

例子:

  1. 'use strict';
  2. $('<div id="show">div 元素内容</div>').css({
  3. 'color': 'red',
  4. 'border': '1px solid gray',
  5. 'padding': '15px 15px 15px 15px',
  6. 'margin': '15px 15px 15px 15px'
  7. }).appendTo(document.body);
  8. $('#show').before('<strong>before</strong>');
  9. $('#show').prepend('<strong>prepend</strong><br>');
  10. $('#show').append('<br><strong>append</strong>');
  11. $('#show').after('<strong>after</strong>');
  12. // or
  13. // $('<strong>before</strong>').insertBefore('#show');
  14. // $('<strong>prepend</strong><br>').prependTo('#show');
  15. // $('<br><strong>append</strong>').appendTo('#show');
  16. // $('<strong>after</strong>').insertAfter('#show');

效果:
jQuery 添加元素的 8 种方式

$(html, attributes)
创建新 DOM 元素,新建的元素是游离状态,需要使用上述方法添加到 DOM 树中。

参数 html:单个 HTML 元素的字符串,如 <div></div><input>
参数 attributes:包含元素属性、元素样式、DOM事件、元素内容的对象

在 attributes 对象中,普通属性是 name-value 名值对,DOM事件是 event: function() {...} 回调函数,元素内容分为纯文本和 HTML 文本,前者使用 text 属性定义,后者使用 html 属性定义。例子:

  1. 'use strict';
  2. $(document).ready(ready);
  3. function ready() {
  4. $('<span></span>', {
  5. 'text': '<a href="https://www.baidu.com">百度一下,你就知道</a>',
  6. 'style': 'color: red; font-weight: bold',
  7. 'click': function() {
  8. alert('你点击了红色文本');
  9. }
  10. }).appendTo(document.body);
  11. $('<br>').appendTo(document.body);
  12. $('<span></span>', {
  13. 'html': '<a href="https://www.baidu.com">百度一下,你就知道</a>',
  14. 'style': 'color: green; font-weight: bold',
  15. 'click': function() {
  16. alert('你点击了绿色文本');
  17. }
  18. }).appendTo(document.body);
  19. }

jQuery 添加 DOM 元素 - 方法二

jQuery 核心 API

jQuery.holdReady(hold)
参数 hold 是布尔值,指示是否暂停/恢复(true/false) jQuery ready 事件。

该方法允许调用者延迟 jQuery 的 ready 事件,该方法通常被用来在 ready 事件发生之前动态的加载其它的 javascript 脚本,如 jQuery 插件,即使 DOM 可能已经准备就绪。该方法必须尽早调用,如在 head 标签内加载了 jQuery 库后,立即调用该方法,否则,如果 ready 事件已经开始被执行,则该方法无效。

在需要延迟/暂停 ready 事件时,调用 $.holdReady(true),在准备开始执行 ready 事件时,调用 $.holdReady(false)$.holdReady(true) 可以被多次调用,如果需要 ready 事件被执行,则需要同样次数的 $.holdReady(false) 被调用。

jQuery.noConflict([removeAll])
参数 removeAll 是布尔值,指示是否交出 jQuery 变量的控制权,默认只交出 $ 变量的控制权。为什么会存在这个方法呢?因为 jQuery 的别名 $ 可能也被其它 JavaScript 库使用了,或者希望多个版本的 jQuery 共存,而需要交出 jQuery 变量。

比如 Prototype 库也使用 $ 别名,Prototype 库和 jQuery 库一起使用,Prototype 库先加载,jQuery 库后加载,特别注意先后顺序的不同,因为后加载的会覆盖先加载的变量值,而 jQuery 会在给 $jQuery 变量赋值前,先保存之前的值(如果存在的话),这就是能够交出被覆盖变量的原理了。

特别声明的是,交出变量的控制权是指:将 $jQuery 变量恢复先前被覆盖的值,因此,如果存在不只两个相冲突的库,需要多次调用该方法,来依次让出控制权。

该方法返回当前 jQuery 变量的引用。

jQuery 元素操作

元素的内容
jQueryObj.text():获取或修改所选元素的内容(纯文本)
jQueryObj.html():获取或修改所选元素的内容(HTML码)
jQueryObj.val():获取或修改所选表单元素(表单字段)的值
它们三个均接收一个回调函数,回调函数的返回值作为要设置的新值,该函数接收两个参数,callback(index, oldValue),index 是当前 DOM 元素在 jQuery 对象中的索引值,oldValue 是当前的旧值。函数中的 this 指向当前 DOM 元素。

元素的属性
自定义元素属性建议使用 attr(),属性值类型为 String
jQueryObj.attr(attrName):获取所选元素的给定属性值
jQueryObj.attr(attrName, attrValue):修改所选元素的给定属性值
jQueryObj.attr({name1: value1, ..., nameN: valueN}):修改所选元素的所有属性值
jQueryObj.attr(attrName, function(index, oldValue)):使用给定函数的返回值来修改给定属性的值,回调函数中的 this 指向当前 DOM 元素。
jQueryObj.removeAttr(attrName):删除给定的属性,从 1.7 起,多个属性由空格隔开

原生的元素属性建议使用 prop(),属性值类型为 String、Number、Boolean
jQueryObj.prop(propName):获取第一个匹配元素的属性值
jQueryObj.prop(propName, propValue):修改所有匹配元素的属性值
jQueryObj.prop({name1: value1, ..., nameN: valueN}):修改所有匹配元素的属性值
jQueryObj.prop(propName, function(index, oldValue)):修改所有匹配元素的属性值
jQueryObj.removeProp(propName):删除所有匹配元素的一个属性(貌似删除不了)

元素的样式
jQueryObj.addClass(className):为所选元素添加一个或多个样式类名
jQueryObj.removeClass(className):从所选元素移除一个或多个样式类名
jQueryObj.toggleClass(className):切换样式名,存在则移除,不存在则添加

jQueryObj.css(propName):获取给定样式属性的值
jQueryObj.css(propNames):获取给定(数组)样式属性的值,返回相应的键值对

jQueryObj.css(propName, propValue):设置给定样式属性的值
jQueryObj.css(propName, function(index, oldValue)):回调函数的具体细节同上
jQueryObj.css(properties):设置多个 CSS 属性值(参数为 属性-属性值 的对象)

DOM 事件监听器
绑定事件处理函数
jQueryObj.on(events[, selector][, eventData], handler(eventObj))
jQueryObj.on(eventsWithHandler[, selector][, eventData])

解绑事件处理函数
jQueryObj.off(events[, selector][, handler(eventObj)]):selector 参数同 on,handler 参数如果省略则解绑所有函数(因为可以为同一个事件绑定多个函数)
jQueryObj.off(eventsWithHandler[, selector]):同 on() 的第二种形式,解绑

自解绑的事件处理函数(执行一次后自动解绑)
jQueryObj.one(events[, selector][, eventData], handler(eventObj))
jQueryObj.one(eventsWithHandler[, selector][, eventData])

删除元素/内容
jQueryObj.empty():删除所选元素的子节点(只删子孙)
jQueryObj.remove([selector]):删除所有的所选元素(自己也删),参数 selector 是过滤条件,只有符合 selector 的元素才会被删除。

元素的大小
jQuery 大小 - 图解

jQueryObj.width():获取元素内容的宽度(px)
jQueryObj.width(value):设置元素内容的宽度,数值/字符串,前者单位为 px
jQueryObj.width(function(index, oldValue)):设置元素内容的宽度,细节同上

jQueryObj.height():获取元素内容的高度(px)
jQueryObj.height(value):设置元素内容的高度,数值/字符串,前者单位为 px
jQueryObj.height(function(index, oldValue)):设置元素内容的高度,细节同上

jQueryObj.innerWidth():获取元素的内部宽度(px)
jQueryObj.innerHeight():获取元素的内部高度(px)

jQueryObj.outerWidth([includeMargin = false]):获取元素的外部宽度(px)
jQueryObj.outerHeight([includeMargin = false]):获取元素的外部高度(px)

jQuery 元素遍历

祖先节点
jQueryObj.parent([selector]):返回由所选元素的直接父元素组成的 jQuery 对象,selector 参数是可选的,只有被 selector 匹配的父元素才会被返回。
jQueryObj.parents([selector]):返回由所选元素的所有父元素组成的 jQuery 对象,selector 参数是可选的,只有被 selector 匹配的父元素才会被返回。
jQueryObj.parentsUntil([until][, filter]):查找所选元素的所有父元素,直到遇到 until 参数匹配的元素为止(不包括所匹配的元素),最后将这些元素用 filter 过滤,返回最终 jQuery 对象。其中 until 可以是 selectorelementjQueryObj,而 filter 就是 selector 选择器。

后代节点
jQueryObj.children([selector]):返回由所选元素的直接子元素组成的 jQuery 对象,selector 参数是可选的,只有被 selector 匹配的子元素才会被返回。和大多数 jQuery 方法一样,该方法不返回文本节点,如果需要建议使用 contents() 方法。
jQueryObj.find(condition):搜寻所选元素的所有符合 condition 过滤条件的后代元素,然后返回由他们组成的 jQuery 对象。其中 condition 可以是 selectorelementjQueryObj

兄弟节点
jQueryObj.siblings([selector]):返回所有同胞元素,可选参数为过滤条件

jQueryObj.prev([selector]):同 next(),方向相反
jQueryObj.prevAll([selector])::同 nextAll(),方向相反
jQueryObj.prevUntil([until][, filter]):同 nextUntil(),方向相反

jQueryObj.next([selector]):返回下一个相邻的同胞元素,可选参数为过滤条件
jQueryObj.nextAll([selector]):返回后面的所有同胞元素,可选参数为过滤条件
jQueryObj.nextUntil([until][, filter]):查找后面的所有同胞元素,直到遇到 until 参数匹配的元素为止(不包括所匹配的元素),最后将这些元素用 filter 过滤,返回最终 jQuery 对象。其中 until 可以是 selectorelementjQueryObj,而 filter 就是 selector 选择器。

jQuery 遍历方法
jQueryObj.first():构造一个新 jQueryObj,其包含当前对象的第一个元素
jQueryObj.last():构造一个新 jQueryObj,其包含当前对象的最后一个元素
jQueryObj.eq(index):构造一个新 jQueryObj,若为负,则等价于 length + index

jQueryObj.filter(condition):构造一个新的 jQueryObj,其中包含通过测试的元素
jQueryObj.not(condition):构造一个新的 jQueryObj,其中包含未通过测试的元素

filter()not() 接收的 condition 参数可以是:

添加元素,返回组成的新对象
jQueryObj.add(selector[, context])
jQueryObj.add(htmlString)
jQueryObj.add(element)
jQueryObj.add(elementArray)
jQueryObj.add(jQueryObj)

jQueryObj.each(function(index, element)):遍历 jQueryObj 中的 DOM 元素,index 为当前元素的索引值,element 为当前元素(多余的),this 也指向当前元素,如需中断循环,可以在函数中返回 false。

jQueryObj.slice(start[, end]):提取子 jQueryObj 对象,区间 [start, end)

jQuery AJAX

底层方法ajax*() ajax 开头的方法
一般方法load()get()post()
包装方法getJSON()getScript()
工具方法param()serialize()serializeArray()

一般方法
jQueryObj.load(url[, data][, callback(respData, respStatus, jqXHR)])

$.get(url[, data][, success(respData, respStatus, jqXHR)][, respDataType])

$.post(url[, data][, success(respData, respStatus, jqXHR)][, respDataType])
使用 HTTP POST 方法获取服务器资源,静态方法,参数同 $.get()

包装方法

工具方法
$.param(obj[, traditional]):将对象序列化为 URL 查询字符串(百分号编码,UTF-8),默认会使用深层递归的方式序列化对象,假设某个 key 是一个对象,它包含两个属性 name,age,则深层递归的结果为 key[name]=nameValue&key[age]=ageValue,如果还有一层递归,则结果为 key[A][B]=value,以此类推。traditional 默认为 false,也即上述的深层递归方式,如果该参数为 true,则不进行递归,只会序列化第一层,即 key=[object Object]

jQueryObj.serialize():将表单元素/表单序列化为 URL 查询字符串(URL-encoded,UTF-8 编码)。

jQueryObj.serializeArray():将表单元素/表单序列化为对象数组,数组的每个元素都是一个拥有 namevalue 属性的对象,分别对应表单元素的 name、value 属性。

jQuery 动画

动画实现原理:我们只需要以固定的时间间隔(例如,0.1 秒),每次把 DOM 元素的 CSS 样式修改一点(例如,高宽各增加 10%),看起来就像动画了。

隐藏/显示
jqObj.hide():直接隐藏,不显示动画效果
jqObj.hide(duration[, complete]):duration 是动画持续时间,单位毫秒,字符串 'fast''slow' 分别代表 200ms、600ms;complete 为动画完成时执行的函数。

jqObj.show():直接显示,不显示动画效果
jqObj.show(duration[, complete]):duration 是动画持续时间,单位毫秒,字符串 'fast''slow' 分别代表 200ms、600ms;complete 为动画完成时执行的函数。

jqObj.toggle():显示或隐藏,切换状态,无动画效果
jqObj.toggle(duration[, complete]):显示或隐藏,切换状态,有动画效果

淡入淡出
jqObj.fadeIn([duration = 400][, complete]):以淡入方式显示元素,duration 的取值及意义同上,complete 为动画完成时执行的函数。
jqObj.fadeOut([duration = 400][, complete]):以淡出方式隐藏元素,duration 的取值及意义同上,complete 为动画完成时执行的函数。
jqObj.fadeToggle([duration = 400][, complete]):切换淡入淡出状态,参数同上。
jqObj.fadeTo(duration, opacity[, complete]):调整元素的透明度,参数 opacity 为不透明度,取值 [0, 1] 间的浮点数,其它参数同上。

滑动
jqObj.slideUp([duration = 400][, complete]):以滑动方式隐藏元素
jqObj.slideDown([duration = 400][, complete]):以滑动方式显示元素
jqObj.slideToggle([duration = 400][, complete]):切换元素的滑动方向

jQuery 插件

jQuery 插件库jQuery插件库-收集最全最新最好的jQuery插件

如何编写 jQuery 插件

一般我们都是使用第二种方式开发插件,第一种方式太简单,第二种方式太复杂。
如果你仔细观察,会发现,$.fn 就是 $.prototype,也就是 jQuery 对象的原型。
因为 jQuery 对象方法可以链式调用,因此自己编写的插件也要在最后返回 this 哦!

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注