JavaScript基本语法

JS 的语法,参考了廖雪峰的博客以及阮一峰的ES6

基础语法

数据类型和变量

Number

JS不区分整数浮点数,统一用Number表示:

1
2
3
1.2345e3; // 科学计数法表示1.2345x1000,等同于1234.5
NaN; // NaN表示Not a Number,当无法计算结果时用NaN表示
Infinity; // Infinity表示无限大,当数值超过了JavaScript的Number所能表示的最大值时,就表示为Infinity

Number可以直接运算:

1
2
3
4
5
(1 + 2) * 5 / 2; // 7.5
2 / 0; // Infinity
0 / 0; // NaN
10 % 3; // 1 (取余)
10.5 % 3; // 1.5

字符串

以单引号'或双引号"括起来的任意文本.如'abc'

布尔值

truefalse表示。

比较运算符

JavaScript在设计时,有两种比较运算符:
第一种是==比较,它会自动转换数据类型再比较.
第二种是===比较,它不会自动转换数据类型,如果数据类型不一致,返回false,如果一致,再比较。

如果是比较类对象,就是比较两者的地址。如果地址不同,===== 都返回 false。

NaN这个特殊的Number与所有其他值都不相等,包括它自己:

1
2
NaN == NaN; // false
NaN === NaN; // false

唯一能判断NaN的方法是通过isNaN()函数:

1
isNaN(NaN); // true

浮点数在运算过程中会产生误差,因为计算机无法精确表示无限循环小数。要比较两个浮点数是否相等,只能计算它们之差的绝对值,看是否小于某个阈值:

1
Math.abs(1 / 3 - (1 - 2 / 3)) < 0.0000001; // true

数组

JavaScript的数组可以包括任意数据类型。例如:

1
[1, 2, 3.14, 'Hello', null, true];

另一种创建数组的方法是通过Array()函数实现:

1
new Array(1, 2, 3); // 创建了数组[1, 2, 3]

然而,出于代码的可读性考虑,强烈建议直接使用[]

数组的元素可以通过索引来访问。索引的起始值为0:

1
2
3
4
var arr = [1, 2, 3.14, 'Hello', null, true];
arr[0]; // 返回索引为0的元素,即1
arr[5]; // 返回索引为5的元素,即true
arr[6]; // 索引超出了范围,返回undefined

对象

JavaScript的对象是一组由键-值组成的无序集合,例如:

1
2
3
4
5
6
7
8
var person = {
name: 'Bob',
age: 20,
tags: ['js', 'web', 'mobile'],
city: 'Beijing',
hasCar: true,
zipcode: null
};

JavaScript对象的键都是字符串类型,值可以是任意数据类型。

要获取一个对象的属性,可以用一下两种方式:

1
2
3
4
5
6
7
person.name; // 'Bob'
person.zipcode; // null

function getName() {
return 'name'
}
person[getName()]

变量与常量

ES6 之前,变量用 var 修饰,但是会引起问题,包括作用域问题与变量提升问题。使用 let 代替。

1
let b = 1; // 申明了变量$b,同时给$b赋值,此时$b的值为1

ES6 之后,常量用 const 修饰,表示不会再改变的值:

1
const b = '234'

Swift 中变量为 var 常量为 let 略有不同。

字符串

模板字符串

要把多个字符串连接起来,可以用+号连接:

1
2
3
4
var name = '小明';
var age = 20;
var message = '你好, ' + name + ', 你今年' + age + '岁了!';
alert(message);

如果有很多变量需要连接,用+号就比较麻烦。ES6新增了一种模板字符串,表示方法和上面的多行字符串一样,但是它会自动替换字符串中的变量:

1
2
3
4
var name = '小明';
var age = 20;
var message = `你好, ${name}, 你今年${age}岁了!`;
alert(message);

注意这里不只是 ${} 外边的不再是单引号了,而是 ` `。

在 Swift 中有类似的模板字符串:\(要打印的对象),类似于这里的${}。不过 Swift 写起来比这个好简单好记多了。

操作字符串

字符串常见的操作如下:

1
2
3
4
var s = 'Hello, world!';
s.length; // 13
s[7]; // 'w'
s[12]; // '!'

字符串相当于是一个不能改变的数组。

indexOf

indexOf()会搜索指定字符串出现的位置:

1
2
3
var s = 'hello, world';
s.indexOf('world'); // 返回7
s.indexOf('World'); // 没有找到指定的子串,返回-1

substring

substring()返回指定索引区间的子串:

1
2
3
var s = 'hello, world'
s.substring(0, 5); // 从索引0开始到5(不包括5),返回'hello'
s.substring(7); // 从索引7开始到结束,返回'world'

数组

展开运算符

展开运算符使用 ... 将一个数组转为用逗号分隔的参数序列:

1
2
3
console.log(1, ...[2, 3, 4], 5)
// 相当于 =>
// console.log(1, 2, 3, 4, 5)

length

要取得Array的长度,直接访问length属性:

1
2
var arr = [1, 2, 3.14, 'Hello', null, true];
arr.length; // 6

indexOf

String类似,Array也可以通过indexOf()来搜索一个指定的元素的位置:

1
2
3
4
5
var arr = [10, 20, '30', 'xyz'];
arr.indexOf(10); // 元素10的索引为0
arr.indexOf(20); // 元素20的索引为1
arr.indexOf(30); // 元素30没有找到,返回-1
arr.indexOf('30'); // 元素'30'的索引为2

万能方法 splice

为数组添加和删除元素有很多方法,包括 push,pop,unshift,shift,slice,concat。这些都可以使用 splice 代替:

1
2
3
4
5
6
7
8
9
10
var arr = ['Microsoft', 'Apple', 'Yahoo', 'AOL', 'Excite', 'Oracle'];
// 从索引2开始删除3个元素,然后再添加两个元素:
arr.splice(2, 3, 'Google', 'Facebook'); // 返回删除的元素 ['Yahoo', 'AOL', 'Excite']
arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']
// 只删除,不添加:
arr.splice(2, 2); // ['Google', 'Facebook']
arr; // ['Microsoft', 'Apple', 'Oracle']
// 只添加,不删除:
arr.splice(2, 0, 'Google', 'Facebook'); // 返回[],因为没有删除任何元素
arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']

第一个元素是起始索引(从0开始),第二个参数是删除元素个数。后面的参数是要添加的元素。

join

join 方法是一个非常实用的方法,用于当前Array的每个元素都用指定的字符串连接起来,然后返回连接后的字符串:

1
2
var arr = ['A', 'B', 'C', 1, 2, 3];
arr.join('-'); // 'A-B-C-1-2-3'

数组的高阶函数

map

map()方法定义在JavaScript的Array中,我们调用Arraymap()方法,传入我们自己的函数,就得到了一个新的Array作为结果:

1
2
3
4
5
6
function pow(x) {
return x * x;
}

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]

map()将传入的函数一一作用在数组的每一个元素上。

reduce

Arrayreduce()把一个函数作用在这个Array[x1, x2, x3...]上,这个函数必须接收两个参数,reduce()把结果继续和序列的下一个元素做累积计算,其效果就是:

1
[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)

比如对Array求和:

1
2
3
4
var arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
return x + y;
}); // 25
filter

用于把Array的某些元素过滤掉,然后返回剩下的元素
Arrayfilter()接收一个函数,把传入的函数依次作用于每个元素,然后根据返回值是true还是false决定保留还是丢弃该元素。

例如,在一个Array中,删掉偶数,只保留奇数,可以这么写:

1
2
3
4
5
var arr = [1, 2, 4, 5, 6, 9, 10, 15];
var r = arr.filter(function (x) {
return x % 2 !== 0;
});
r; // [1, 5, 9, 15]
sort

可以接收一个比较函数来实现自定义的排序
要按数字大小排序,我们可以这么写:

1
2
3
4
5
6
7
8
9
10
var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
if (x < y) {
return -1;
}
if (x > y) {
return 1;
}
return 0;
}); // [1, 2, 10, 20]

注意sort()方法会直接对Array进行修改,它返回的结果仍是当前Array

对象

基本使用

现在 ES6 中有了 class,我觉得现在对象更像是结构体。

定义方式如下:

1
2
3
var xiaohong = {
name: '小红',
};

获取分为两种形式,点语法和[]语法:

1
2
xiaohong.name; // '小红'
xiaohong['name']; // '小红'

由于JavaScript的对象是动态类型,你可以自由地给一个对象添加或删除属性:

1
2
3
4
5
6
7
8
9
10
11
var xiaoming = {
name: '小明'
};
xiaoming.age; // undefined
xiaoming.age = 18; // 新增一个age属性
xiaoming.age; // 18
delete xiaoming.age; // 删除age属性
xiaoming.age; // undefined
delete xiaoming['name']; // 删除name属性
xiaoming.name; // undefined
delete xiaoming.school; // 删除一个不存在的school属性也不会报错

如果我们要检测xiaoming是否拥有某一属性,可以用in操作符,自己的和继承的都会返回 true:

1
2
'name' in xiaoming; // true
'toString' in xiaoming; // true

要判断一个属性是否是xiaoming自身拥有的,而不是继承得到的,可以用hasOwnProperty()方法:

1
2
3
4
5
var xiaoming = {
name: '小明'
};
xiaoming.hasOwnProperty('name'); // true
xiaoming.hasOwnProperty('toString'); // false

对象的属性名

属性名为方法返回值

JS 中属性必须要是字符串型的,不过你也可以使用方法的返回值动态的设置属性名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Birth = {
birth: 'birth'
}

const Person = {

name: '张三',

//等同于birth: birth
[Birth.birth]: '94-4-15',

// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }

};
属性的简写

ES6 中允许创建对象的时候,简写属性,直接以变量名称作为属性名。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
let birth = '2000/01/01';

const Person = {

name: '张三',

//等同于birth: birth
birth,

// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }

};

prototype

在 ES6 之前,没有类的概念,只有一个个函数,通过 prototype实现。

prototype 是函数的属性,这个属性是指向一个对象的引用,这个对象称为原型对象,原型对象包含函数实例共享的方法和属性,也就是说将函数用作构造函数调用(使用new操作符调用)的时候,新创建的对象会从原型对象上继承属性和方法。

所以,如果我们想要 new 出来的对象能够使用的属性或者方法,就将它们赋给 prototype

另一方面,有一些属性和方法应该是属于类的,ES6 中用 static 标识,ES6 之前则直接将其赋值给函数,它会被加载到 constructor 中。constructor 本身也是 prototype 的一个默认属性。具体例子如下:

new Person() 后的实例对象,都可以获得 prototype 上的属性,但是其中一个对象改变其值的时候,另一个对象相应属性值不变。

prototype 中的属性能随着原型链被继承下去。

Object.assign()

这个方法是非常有用的一个方法。用于为对象添加属性,方法,克隆对象,合并对象。第一个是目标对象,我们可以使用空对象,后面对象的属性会覆盖前面对象的属性:

1
2
3
4
5
6
7
const target = {};

const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {b:2, c:3}

这个和闭包一样,也是指针传递,生成的 target 对象或者源 source 对象,两者任一改动都会触发另外对象的改动。

除了方法是基本类型是值传递,对象类型是引用传递。其他都是指针传递。

(网上纠结的什么传递地址也是值传递,就当是扯淡吧,理解意思就行)

Object.entries()

返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键值对数组:

1
2
3
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]

配合 for...of... 可以拿到所有键值。

可以用它来将对象转化为 Map

JSON.stringify()

一个对象你在打印log的时候,可能会给你返回 [object object]。此时,你需要将对象展开,可以使用 JSON.stringify() 方法。

Map和Set

JS中默认对象表达方式{}可以视为其他语言中的MapDictionary的数据结构,即一组键值对。
但是JavaScript的对象有个小问题,就是键必须是字符串。但实际上Number或者其他数据类型作为键也是非常合理的。
为了解决这个问题,最新的ES6规范引入了新的数据类型Map

Map

Map 可以接受一个数组,数组元素是键值对的数组:

1
2
3
4
var m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]);
m.delete('Adam'); // 删除key 'Adam'
m.get('Michael'); // 95
m.set('Zachary', 100);

这个结构是不是很熟悉?Object.entries() 返回的就是这样的数组中包含了键值对数组的形式。所以说,可以用 Object.entries() 创建 Map。因为直接把返回值作为参数传入 Map 即可。

对象可以变为 Map,但是 Map 不一定能变为对象。所以创建 Map 的时候的入参不能以对象的形式。

方法
  • size 属性返回 Map 结构的成员总数,类似数组的 length
  • set(key, value):设置键名key对应的键值为value,然后返回整个 Map 结构
  • get(key): 读取key对应的键值,如果找不到key,返回undefined
  • has(key):返回一个布尔值,表示某个键是否在当前 Map 对象之中。
  • delete(key):delete方法删除某个键,返回true。如果删除失败,返回false
  • clear():清除所有成员,没有返回值。
  • entries():返回所有成员的遍历器。配合 for...of...

WeakMap

WeakMap 的键是对象类型。如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。

注意弱引用的是键,而不是值

Set

重复元素在Set中自动被过滤:

1
2
var s = new Set([1, 2, 3, 3, '3']);
s; // Set {1, 2, 3, "3"}

注意数字3和字符串'3'是不同的元素。

方法
  • add(value):添加某个值。
  • delete(value):删除某个值,返回一个布尔值
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除所有成员。

WeakSet

WeakSet 中只能存对象。并且都是弱引用。

WeakSet 不能遍历。因为所有对象都是弱引用,随时可能消失。

iterable

ES6标准引入了新的iterable类型,ArrayMapSet都属于iterable类型。iterable 类型其实就是

具有iterable类型的集合可以通过新的for ... of循环来遍历。用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
var a = ['A', 'B', 'C'];
var s = new Set(['A', 'B', 'C']);
var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
for (var x of a) { // 遍历Array
alert(x);
}
for (var x of s) { // 遍历Set
alert(x);
}
for (var x of m) { // 遍历Map
alert(x[0] + '=' + x[1]);
}

for ... of循环和for ... in循环有何区别?
for ... in实际上是对象的属性名称(注意,是键,不是值)。不要用在Array,Set,Map上,会出现奇怪的问题。
for ... of循环则完全修复了这些问题,它只循环得到集合内该出现的元素(注意,是值,不是键),例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = ['A', 'B', 'C'];
a.name = 'Hello';
a['4'] = 'D';
console.log(a);
for (var num of a){
console.log(num)
}
输出:
[ 'A', 'B', 'C', , 'D', name: 'Hello' ]
A
B
C
undefined
D

name由于不是正常Array该有的,所以在for ... of循环时,其对应的值不会被输出。

变量的解构赋值

有两种解构赋值,分别为数组的解构赋值和对象的解构赋值

数组的解构赋值

多出的部分为 undefined,可以使用展开运算符。

1
2
let [a, b, c] = [1, 2];	// a = 1, b = 2, c = undefined
let [x, ...y] = [1, 2, 3, 4]; // x = 1, y = [2, 3, 4]

解构赋值允许指定默认值,当对应的值为 undefined 时,等于默认值:

1
let [x, y = 'b'] = ['a']; // x='a', y='b'

对象的解构赋值

1
let { foo, bar } = { foo: "aaa", bar: "bbb" };

想要改变变量的名字也是可以的,上面其实等效于:

1
let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };

所以如果我们要改变变量名,可以这样:

1
2
let { foo: param1, bar: param2 } = { foo: "aaa", bar: "bbb" };
// param1 = aaa, param2 = bbb

对象也是可以指定默认值的:

1
2
3
var {x, y: z = 5} = {x: 1};
x // 1
z // 5

函数

函数定义和调用

定义函数

方式一:

1
2
3
4
5
6
7
function abs(x) {
if (x >= 0) {
return x;
} else {
return -x;
}
}

由于JavaScript的函数也是一个对象,上述定义的abs()函数实际上是一个函数对象,而函数名abs可以视为指向该函数的变量。

方式二:

1
2
3
4
5
6
7
var abs = function (x) {
if (x >= 0) {
return x;
} else {
return -x;
}
};

在这种方式下,function (x) { ... }是一个匿名函数,它没有函数名。但是,这个匿名函数赋值给了变量abs,所以,通过变量abs就可以调用该函数。

上述两种定义完全等价,注意第二种方式按照完整语法需要在函数体末尾加一个;,表示赋值语句结束。

Swift 中使用 func 定义函数

调用函数

由于JavaScript允许传入任意个参数而不影响调用,因此传入的参数比定义的参数多也没有问题,虽然函数内部并不需要这些参数:

1
2
abs(10, 'blablabla'); // 返回10
abs(-9, 'haha', 'hehe', null); // 返回9

传入的参数比定义的少也没有问题:

1
abs(); // 返回NaN

此时abs(x)函数的参数x将收到undefined,计算结果为NaN

要避免收到undefined,可以对参数进行检查:

1
2
3
4
5
6
7
8
9
10
function abs(x) {
if ((typeof x) !== 'number') {
throw 'Not a number';
}
if (x >= 0) {
return x;
} else {
return -x;
}
}

rest参数

由于JavaScript函数允许接收任意个参数,ES6标准引入了rest参数,可以将剩余的参数放在rest中,rest 是个数组(不是一定要叫 rest,任何名称都可以):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function foo(a, b, ...rest) {
console.log('a = ' + a);
console.log('b = ' + b);
console.log(rest);
}

foo(1, 2, 3, 4, 5);
// 结果:
// a = 1
// b = 2
// Array [ 3, 4, 5 ]

foo(1);
// 结果:
// a = 1
// b = undefined
// Array []

其实相当于把 rest 展开,然后和入参对应

如果传入的参数连正常定义的参数都没填满,rest参数会接收一个空数组(注意不是undefined)。

参数默认值

ES6 给 js 的函数提供了默认值:

1
2
3
4
5
6
7
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
}

const p = new Point();
p // { x: 0, y: 0 }

带默认值的参数需要写在末尾。

特性

name 属性

函数的 name属性,返回函数的函数名:

1
2
function foo() {}
foo.name // "foo"

箭头函数

ES6标准新增了一种新的函数:Arrow Function(箭头函数)。

1
x => x*x

相当于一个输入为x输出为x*x的匿名函数

1
2
3
function (x) {
return x * x;
}

如果参数不是一个,就需要用括号()括起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 两个参数:
(x, y) => x * x + y * y

// 无参数:
() => 3.14

// 可变参数:
(x, y, ...rest) => {
var i, sum = x + y;
for (i=0; i<rest.length; i++) {
sum += rest[i];
}
return sum;
}

语法糖,类似于python中的lambda函数

当然,箭头函数还是有点用处的,由于是es6的新特性,箭头函数内部的this是词法作用域,由上下文确定。
试做比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//由于JavaScript函数对this绑定的错误处理,下面的例子无法得到预期结果
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var fn = function () {
return new Date().getFullYear() - this.birth; // this指向window或undefined
};
return fn();
}
};
//箭头函数完全修复了this的指向,this总是和函数外部的 this 指代相同的东西:
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var fn = () => new Date().getFullYear() - this.birth; // this 和 this.birth 指的是一个东西
return fn();
}
};
obj.getAge(); // 25

如果使用箭头函数,以前 var that = this; 以及 .apply().call() 这种写法就不需要了。

箭头函数并不创建它自身执行的上下文,使得 this 取决于它在定义时的外部函数

箭头函数用在嵌套函数里,不能作为某一个属性的值,比如用成这样:

1
2
3
4
5
6
7
> var obj = {
> birth: 1990,
> getAge: () => {
> var b = this.birth; // this 是 undefined
> }
> }
>

这里并没有外部函数,所以箭头函数里的 this 永远是 undefined

装饰器

利用apply(),我们可以动态改变函数的行为。

JavaScript的所有对象都是动态的,即使内置的函数,我们也可以重新指向新的函数

现在假定我们想统计一下代码一共调用了多少次parseInt(),可以把所有的调用都找出来,然后手动加上count += 1,不过这样做太傻了。最佳方案是用我们自己的函数替换掉默认的parseInt()

1
2
3
4
5
6
7
8
9
10
11
12
13
var count = 0;
var oldParseInt = parseInt; // 保存原函数

window.parseInt = function () {
count += 1;
return oldParseInt.apply(null, arguments); // 调用原函数
};

// 测试:
parseInt('10');
parseInt('20');
parseInt('30');
count; // 3

apply() 接收两个参数,第一个参数表示调用的对象,第二个参数是入参数组。此处的 arguments 表示的是外部 function 的入参,是 js 提供的一个参数。

闭包

这是因为 JS 中的闭包对外部变量都是指针传递,而不是引用传递。

iOS 中的闭包都是引用传递,外部变量相当于是以参数的形式传入的,也就是说,闭包内改变外部变量的值,外部变量不会变化。

js 中的闭包,相当于外部变量的地址都获得了。闭包内改变外部变量的值,外部变量的值相应改变。

Promise

ES6 提供了原生的 Promise 对象。可以将异步操作以同步的形式表达出来。

基本用法

1
2
3
4
5
6
7
8
9
const promise = new Promise(function(resolve, reject) {
asyncFunc((success) => {
if (success){
resolve(value);
} else {
reject(error);
}
})
});

如上所示,Promise 接受一个函数,入参为成功和失败回调,函数内执行异步操作,在必要时执行成功和失败回调。Promise 不需要返回值

Promise 实例生成后,可以使用 then 方法指定 resolvereject 的回调函数:

1
2
3
4
5
promise.then(function(value) {
// success
}, function(error) {
// failure
});

Promise 的大致原理是通过 then 在 Promise 里注册回调,然后创建 Promise 时传入的函数会调用注册的回调函数。

注意,创建 Promise 的时候,函数就被执行了。但是执行到 resolve 或者 reject 方法时并不立刻执行,而是等到最后

1
2
3
4
5
6
7
8
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1

这是因为,Promise 将 then 注册的回调函数都包裹上了 setTimeout(resolved,0),将回调函数放置到 JS 任务队列末尾。

链式调用then()

每一个 promise 就是一个异步调用。多个异步调用串行可以使用链式 .then() 的方式。

这个时候 then 方法向调用的 Promise 注入成功回调的时候需要返回一个新的 Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
new Promise((resolve, reject) => {
...异步操作回调 {
let data = xxx
resolve(data)
}
}.then((res) => {
return Promise((resolve, reject) => {
...异步操作回调 {
let data = xxx
resolve(data)
}
})
}).then((res) => {
...最终对 res 处理。
})

并不是说 .then 返回一个 promise,使用者就必须要自己创建一个 promise。如果 .then 返回的是某个值 someObject,那么会被默认包装为一个 promise,并且立刻调用 resolve(someObject),即执行后一个 .then 的而方法。

###失败调用 catch()

catch() 方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数:

1
2
3
4
5
6
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});

注意,Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。也就是说,本例中,无论是 getJSON 还是 .then() 生成的 Promise 产生的错误都会被 catch() 捕获。

一般来说不要在 then() 方法中定义失败回调。总是使用 catch() 方法。

catch() 方法返回的还是 Promise 对象,会接着运行后面的 then() 方法。

永远执行 finally()

1
2
3
4
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中,不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。

合并多个Promise all()

Promise.all方法接受一个 Promise 数组,用于将多个 Promise 实例,包装成一个新的 Promise 实例:

1
2
3
4
5
6
const p = Promise.all([p1, p2, p3]);
p.then(function (posts) {
// ...
}).catch(function(reason){
// ...
});

p的状态由p1p2p3决定,分成两种情况。

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3返回值组成一个数组,传递给p的回调函数

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数

合并多个Promise race()

Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例:

1
const p = Promise.race([p1, p2, p3]);

上面代码中,只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

async 函数

async 函数式 Generator 函数的语法糖.

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。

当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句:

1
2
3
4
5
6
7
8
9
async function getStockPriceByName(name) {
const symbol = await getStockSymbol(name);
const stockPrice = await getStockPrice(symbol);
return stockPrice;
}

getStockPriceByName('goog').then(function (result) {
console.log(result);
});

上面代码是一个获取股票报价的函数,函数前面的async关键字,表明该函数内部有异步操作。调用该函数时,会立即返回一个Promise对象不是说 stockPrice 是一个 Promise 而是说 async 修饰的这个方法最终返回一个 Promise,stockPrice 只是作为 resolve 的 input 而已

如果确实希望多个请求并发执行,可以使用Promise.all方法:

1
2
3
4
5
6
7
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));

let results = await Promise.all(promises);
console.log(results);
}
  1. async 相当于告诉外界,这个方法会返回一个 promise
  2. await 必须在 async 中才能使用
  3. await 后面的方法会延迟到 await 方法执行完后执行,相当于 then 操作
  4. await 的错误捕获需要使用 try…catch 的方式

Proxy

Proxy 就如字面所说的,是 ES6 提供的代理模式的封装。ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例:

1
var proxy = new Proxy(target, handler);

第一个参数是被代理对象,第二个参数是要拦截的行为对象。当调用代理对象的被拦截行为时,触发代理方法。常用的拦截行为有以下几种:

get(),set()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});

obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2

其中,代理对象是 obj,被代理的对象是 {} 空对象,拦截的行为是 get,set 方法。也就是说,任何对于 obj 的读取写入操作都将执行代理方法。

get,set 方法的参数也很好理解。target 表示被代理对象,key 表示要进行读写操作的属性,value 就是写操作的值。receiver 就是代理对象 obj。(先不用关 Reflect)。

如果handler没有设置任何拦截,那就等同于直接通向原对象:

1
2
3
4
5
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"

apply()

apply 方法拦截函数的调用、callapply 操作。可以接受三个参数:目标对象(函数)、目标对象上下文对象(this)、目标对象的参数数组

1
2
3
4
5
6
7
8
9
10
11
12
var twice = {
apply (target, context, args) {
return Reflect.apply(...arguments) * 2;
}
};
function sum (left, right) {
return left + right;
};
var proxy = new Proxy(sum, twice);
proxy(1, 2) // 6
proxy.call(null, 5, 6) // 22
proxy.apply(null, [7, 8]) // 30

注意这里的 ...arguments 表示的是 target,context以及args的全部。

construct()

construct 方法用于拦截 new 命令。接受两个参数:目标对象(类对象),构建函数的参数对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {
constructor() {
this.a = 1
}
}

let Aproxy = new Proxy(A, {
construct: function(target, args){
return new target(...args)
}
});

let a = new Aproxy()
console.log(a.a) //1

注意,拦截的是 new 命令,不是拦截的 constructor 命令,所以,拦截方法要返回一个 target 创建的新对象。

class

基本用法

创建和使用

新的关键字class从ES6开始正式被引入到JavaScript中。class的目的就是让定义类更简单。

如果用新的class关键字来编写Student,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
class Student {
constructor(name) {
this.name = name;
}

hello() {
alert('Hello, ' + this.name + '!');
}
}

var xiaoming = new Student('小明');
xiaoming.hello();

name 属性

上面说到,函数有 name 属性,可以返回函数的名字。ES6 的 class 其实就是 ES5 函数的包装。所以 Class 也包含 name 属性:

1
2
class Point {}
Point.name // "Point"

get set 方法

在“类”的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'

static 方法

如果在一个方法前,加上static关键字,表示类方法:

1
2
3
4
5
6
7
8
9
10
11
class Foo {
static classMethod() {
return 'hello';
}
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

ES6 明确规定,Class 内部只有静态方法,没有静态属性。

new.target 属性

ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。需要注意的是,子类继承父类时,new.target会返回子类。利用这个特点,可以写出不能独立使用、必须继承后才能使用的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error('本类不能实例化');
}
}
}

class Rectangle extends Shape {
constructor(length, width) {
super();
// ...
}
}

var x = new Shape(); // 报错
var y = new Rectangle(3, 4); // 正确

class继承

class定义对象的另一个巨大的好处是继承更方便了,直接通过extends来实现:

1
2
3
4
5
6
7
8
9
10
class PrimaryStudent extends Student {
constructor(name, grade) {
super(name); // 记得用super调用父类的构造方法!
this.grade = grade;
}

myGrade() {
alert('I am at grade ' + this.grade);
}
}

通过super(name)来调用父类的构造函数。PrimaryStudent已经自动获得了父类Studenthello方法,我们又在子类中定义了新的myGrade方法。

export 与 import

基本用法

1
2
3
4
5
6
export let name = 'zachary'
export function getName() {
return 'zachary'
}

等价于=> export {name, getName}

注意,不能直接写export name,要写也要写成 export {name}。为什么呢?其实就相当于给 export 的对象起一个名字,export name 就相当于 export 'zachary' ,这样别人就无法知道如何引入了,所以就需要起一个名字:export let name = 'zachary' .引入方式如下:

1
import {name, getName} from './myName'

import 和 require 都不需要写 .js 后缀!!!

重命名 as

导出导入的名字有时候可能需要改变,所以提供了 as 这个关键字:

1
2
export {name as yourName, getName as getYourName}
import {yourName as myName, getYourName as getMyName} from './myName'

导出的时候用 yourName 作为内部 name 的别名。导入的时候用 myName 作为 youName 的别名。

default

有时候,我们不需要知道导出的叫什么,可以使用 default :

1
2
3
4
5
6
export default name
import myName from './myName'

相当于 =>
export {name as default}
import {default as myName} from './myName'

其实就是 as 的语法糖。注意,import 的时候就不需要大括号了。

ES6 和 CommonJs 在引用方式的差异

CommonJS的一个模块,就是一个脚本文件。require命令第一次执行的时候,加载该脚本,然后执行整个脚本

ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,直接执行相应的文件

require 的好处是可以在代码中使用,而 import 只能在头部。这样 require 就可以在执行到当前代码的时候,根据代码的上下文动态的引入想要的模块。

import 是强制静态化的

混用产生的问题

export 和 module.exports 都是导出模块,但是最好不要在一个文件中一起使用。混用会产生一定的问题。

首先,ES6 的 export 在 webpack 打包后都会变成 CommonJS 的语法。因此 export 最终会变为 CommonJS 的 exports。当 exports 和 module.exports 同时存在的时候,最终导出的将会是 module.exports 的对象。exports 导出的对象会被覆盖。

这个结论非常重要。牢记这个结论在混用的时候就能明白为什么很多时候 import 的时候是 undefined 的了。

归类

… 的使用场景

一般用在数组前,把数组转为用逗号隔开的一个个值。可以正用逆用都是等价的:

1
2
...[a, b, c] => a, b, c
a, b, c => ...[a, b, c]

一般用于:

  1. 将数组拆开作为入参
  2. 将入参合并为数组
  3. 数组解构赋值
1
2
3
4
5
6
// 1. 做入参
someFunc(1, ...[2, 3, 4]) => someFunc(1, 2, 3, 4)
// 2. 合并入参
function someFunc(1, ...args) {} => args = [2, 3, 4]
// 3. 数组解构 类似于合并入参
let [1, ...args] = [1, 2, 3, 4] => args = [2, 3, 4]

另外,在 JSX 中,... 可以把对象中的键值对拿出来,以等号连接。

对象和 Map

对象和 Map 初步的不同就在于键是否是字符串。

创建与增减键值对

对象:

1
2
3
let obj = {}
obj.haha = '123'
delete obj.haha

Map:

1
2
3
4
let map = new Map()
map.set('haha','123')
map.get('haha')
map.delete('haha')

遍历

对象:

1
Object.entries(obj) => [['haha', '123'], ['lala', '234']]

Map:

1
map.entries() => [['haha', '123'], ['lala', '234']]

没有看的

  • symbol 部分
  • reflect 部分(感觉可以直接调用,不需要使用 reflect)
  • generator 部分(习惯于用promise的回调写法,不习惯这种写法)
  • async 部分(同 generator)
  • port