let & const
let
let
声明的变量只在let
命令所在的代码块内有效。- for 循环
let
声明的 i,当前 i 只在本轮循环有效 let
**不存在变量声明***,变量一定要在声明后使用,否则报错let
不可以在相同作用域内重复声明同一个变量
const
const
用来声明常量,一旦声明,其值就不可改变。这意味着,const
一旦声明常量,就必须立即初始化,不能以后赋值。只声明不赋值不报错- 对于对象而已,常量存储的是一个地址,指向一个对象。地址不可变,但对象本身是可变的。
- 想将对象冻结,应该用
const p = Object.freeze(obj)
解构赋值
从对象和对象中提取值,来对变量进行赋值,称之为解构(Destructuring)
数组的解构赋值
- 只要模组数据结构具有 Iterator 接口(可遍历),就可以采用数组形式的解构赋值
- 如果解构不成功,变量的值就等于
undefined
- 也可以 不完全解构
1 | let [, , c] = [1, 2, 4]; |
指定默认值
- ES6 使用
===
判断一个位置是否有值。当一个数组成员=== undefined
,默认值才生效,是null
则不会生效。 - 如果默认值是一个表达式,则该表达式是惰性求值的,在用到时才会求值。
- 默认值可以引用解构赋值的其他已经声明的变量。
1 | let [x = 1] = []; // 默认值 |
对象的解构赋值
变量必须同属性同名,才能取到正确的值;没有同名属性,为 undefined
如果变量名与属性名不同,则必须是以下形式。实际上 对象解构赋值的内部机制,是先找到同名属性,然后再赋值给对应的变量,即(被赋值的是后者,而非前者)
1
2
3
4
5
6
7let { a: b } = { a: "aaa", b: "bbbb" };
b; // 'aaa'
a; // error: a is not defined
let { a, b } = { a: "aaaa", b: "bbbb" }; // {a:a, b:b} = {a:'aaaa', b:'bbbbb'}
a; // 'aaaa'
b; // 'bbbb'
指定默认值
- 默认值生效的条件,对象的属性严格等于 undefined
- 如果解构失败,变量的值等于 undefined
1 | let { x = 3, y = 4 } = { x: undefined }; |
字符串的解构赋值
字符串的解构赋值,此时字符串被转换成一个类数组对象,有length
属性
1 | let [a, b, c] = "abc"; |
布尔值、数值的解构赋值
解构赋值的规则: 只要等号右边不是对象,就将其转换为对象。布尔值,数值被转换为对应的包装对象
1 | let { toString: s } = 123; |
函数参数的解构赋值
参数也可以解构赋值,通过解构得到各变量的值
1
2
3
4
5// 函数参数 不是 一个数值,而是通过解构得到的变量 x, y
function add([x, y]) {
return x + y;
}
add([1, 2]);
函数参数的 默认值
函数参数是对象, 2 种写法,不同结果
为 x 和 y 指定默认值
1
2
3
4
5
6
7function fn({ x = 0, y = 0 } = {}) {
return [x, y];
}
fn({ x: 3, y: 8 }); // [3, 8]
fn({ x: 3 }); // [3, 0]
fn({}); // [0,0]
fn(); // [0, 0]
为 参数 指定 默认值,而非为变量 x、y。
1
2
3
4
5
6
7function fn({ x, y } = { x: 0, y: 0 }) {
return [x, y];
}
console.log(fn({ x: 3, y: 8 })); // [3, 8]
console.log(fn({ x: 3 })); // [3, undefined]
console.log(fn({})); // [undefined,undefined]
console.log(fn()); // [0, 0]
数组的扩展
Array.from()
: 将 类数组对象(NodeList,arguments 对象)和可遍历对象(Set, Map, String)转换为数组。例外 扩展运算符(...)
也可以将某些数据结构转换为数组1
2
3
4
5
6
7
8
9
10// NodeList
let links = document.querySelectorAll("a");
let arrayLinks = Array.from(as);
function fn() {
var args = [...arguments];
}
// ES 5 方法
let arr = [].slice.call(arguments);Array.from()
还可接受第二各个参数,作用类似数组的 map 方法,用来对每个数组元素进行处理。1
2
3
4
5
6
7
8Array.from([1, 2, 3], (x) => x + 1); // [2, 3, 4]
// 取出DOM节点内容
let spans = document.querySelectorAll("span");
// es5
Array.prototype.map.call(spans, (s) => s.textContent);
// es6
Array.form(spans, (s) => s.textContent);
Array.of()
,总是返回参数值组成的数组,没有参数,返回空数组find(callback)
,findIndex(callback)
, 参数是一个回调函数。比indexOf
方法的优势:都可以发现NaN
1
2[NaN].indexOf(NaN); // -1
[1, 2, NaN].findIndex((s) => Object.is(s, NaN)); // 2fill(value[, start[, end]])
,使用给定值填充数组,数组中已有的元素会被抹去。entires()
,keys()
,values()
,都会返回一个遍历器对象,可用于for...of
循环遍历includes(valueToFind[, fromIndex])
,返回一个布尔值,表示数组是否包含给定的值
函数的扩展
函数参数可设置默认值
非尾部的参数设置默认值,实际上这个参数是无法省略的。通常情况下,定义了默认值的参数应该是函数的为参数
函数的
length
属性是预期传入的参数个数,不包含指定了默认值的参数rest
参数,fn(a, b, ...rest)
,用于获取函数的多余参数,其变量是一个数组。rest 参数之后不能再有其他参数,否则报错,函数的length
属性不包括 rest 参数扩展运算符
(...)
,将一个数组转换为逗号分隔的参数序列。只要具有 Iterator 接口的对象,都可以使用1
2
3Math.max(...[1, 2, 3]); // es6
Math.max(1, 2, 3); // es5
[..."abcd"]; // ['a','b','c','d']
// Generator 函数返回 一个遍历器对象
let go = function*() {
yield 1;
yield 2;
yield 3;
}
[…go()] // [1,2,3]
1 |
|
尾调用优化
函数调用会在内存中形成一个”调用记录”,即调用帧(call frame),保存着调用位置和内部变量等信息。例如,在函数 A 中调用函数 B,则在 A 的调用帧上方,形成 B 的调用帧,等到函数 B 运行结束,将结果返回到 A,B 的调用帧才移除。如果函数 B 内还调用了函数 C,那就还有一个 C 的调用帧,在 B 上。依此类推,所以的调用帧就形成了一个调用栈(call stack)
尾调用由于是函数的最后一步操作,所以不在需要保留外层函数的调用帧,因为调用位置、内部变量等信息不会再用到了,所以可以直接将内层函数的调用帧取代外层函数
1 | let g = (m, n) => m + n; |
如果函数 g 不是尾调用,函数 f 就要保存内部的变量 m 和 n 的值,g 的调用位置等信息。但是由于调用 g 之后,函数 f 就结束了,所以执行到最后一步,完全可以删除 f()的调用帧,只保留 g(3)的调用帧。如下动图
尾递归
函数调用自身称为递归。递归因为要同时保存成百上千个调用帧,非常消耗内存,且很容易发生栈溢出(stack overflow)。但是,如果尾调用自身(尾递归)的话,只需保存一个调用帧,则永远不会发生栈溢出错误
1 | function factorial(n) { |
上述阶乘函数,最多需要保存 n 个调用帧,O(n)。如果改为 尾递归的话,则需保存 一个调用帧,,O(1)
1 | function factorial2(n, total) { |
柯里化
实现尾递归需要改写递归函数,就是要把所有用的的内部变量改写成函数的参数。但是这样做后,函数的作用就不太直观了,很难理解,计算 5 的阶乘,为什么要传参数 5 和 1。
柯里化是函数编程中一个概念,将多个参数的函数转换成单个参数的形式
1 | function currying(fn, n) { |
对象的扩展
ES6 允许在对象中只写属性名,不写属性名,此时属性值等于属性名所代表的变量
1
2
3
4
5let person = {
name: "jiang",
age, // 等同于 age:age
hello() {}, // 等同于 hello: function() {}
};在用字面量定义对象时,可使用表达式作为对象的属性名、方法名
1
2
3
4let person = {
["n" + "ame"]: "jiang",
["h" + "ello"]() {}, // 等同于 hello: function() {}
};Object.is(value1, vaule2)
,比较两个值是否严格相等,除两点不同之外。+0
不等于-0
NaN
等于自身
Object.assign(target, ...sources)
,将所有可枚举的属性的值 从一个或多个源对象 复制到目标对象。返回目标对象。如果有同名属性,则后面的属性会覆盖前面的属性
只复制自身可枚举的属性,不可枚举的属性、继承的属性不会被复制
可用于处理数组,但会将其视为对象
1
2Object.assign([1, 2, 3, 4, 5], [7, 8]);
// [7,8,3,4,5]
属性的描述对象
Object.getOwnPropertyDescriptor(obj, prop)
获取对象上一个自有属性的描述对象
1 | let obj = { |
属性的遍历
ES6 可遍历对象的属性的方法
for in
循环遍历对象的 自身的 和 继承的可枚举 属性Object.keys(obj)
, 返回一个数组,包含 对象自身的可枚举属性Object.getOwnPropertyNames(obj)
,返回一个数组,包含 对象自身所有属性(包括不可枚举的属性)Object.getOwnPrropertySymbols(obj)
,返回一个数组,包含 对象自身的所有 Symbol 属性
__proto__
属性 & Object.setPrototypeOf()
& Object.getPrototypeOf()
& Object.create()
__proto__
属性是浏览器内,用来读取和设置当前对象的原型(prototype)
对象。因为只要浏览器内部署该属性,其他环境不一定支持,可使用Object.create() (生成 原型对象)
,Object.getPrototypeOf() (读取 原型对象)
,Object.setPrototypeOf() (设置 原型对象)
3 个方法替代。在实际上,__proto__
调用的是object.prototype.__proto__
Object.create(prototype[, propertiesObject])
,使用指定的原型对象 prototype 及其属性 propertiesObject 去创建一个新的对象。具体 3 个步骤:创建一个新对象,设置新对象的原型对象,为新对象扩展新属性。1
let obj = Object.create(Object.prototype); // 等同于 let obj = {} 创建要给空对象,空对象的原型是 Object.prototype (即 obj.__proto__ === Object.prototype)
Object.getPrototypeOf(obj)
,返回指定对象的原型对象 (内部[[Prototype]]属性的值
)1
Object.prototype === Object.getPrototypeOf({}); // true
Object.setPrototypeOf(obj, prototype)
,设置指定对象的原型 为 另一个对象或 null.1
2
3
4let obj = { a: 1 };
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
Object.setPrototypeOf(obj, null);
console.log(Object.getPrototypeOf(obj) === null); // true
字符串的扩展
includes(searchString[, fromIndex])
,一个字符串是否包含另一个字符串,返回 Boolean。startWith()
,endWith()
,判断一个字符串是否在另一个字符头部或尾部repeat(count)
,返回一个重复count
的 新字符串padStart(len[, padString])
,padStart(len[, padString])
,字符串的补全功能。padString
默认为 空格" "
1
2"abcd", padEnd(5); // 'abcd '
"abc".padStart(5, "01234"); // '01abc'
Iterator & for … of 循环
遍历器(Iterator)是以中接口,为不同的数据结构提供统一从访问机制。任何数据结构,只要部署Iterator
接口,就可以完成遍历操作(for..of
)依次处理该数据结构的所有成员。
要想成为可迭代的对象,该对象(或其原型链中的任意对象)必须有实现Iterator
接口的方法,通常使用常量Symbol.iterator
访问该属性
内置可迭代对象
String
,Array
,Map
,Set
以及 类数组的对象
都是内置可迭代对象,因为它们的原型对象都有一个Symbol.iterator
方法。 返回一个符合有 next()
的对象 具体内容。
Set & Map
Set
Set 类似于数组,但其成员的值是唯一的,无论是原始值或是对象引用。
Set 本身就是一个构造函数,可接受一个可迭代对象作为参数,可迭代对象中所有元素将不重复地被添加到新的 Set 实例中。
向 Set 添加值时,不会发生类型转换。Set 内部判断两个值是否相等,使用类似于全等运算符(
===
),NaN 之间视为相同的值。
Set 实例 - 操作方法
- add(value): 末尾添加某个值,返回 Set
- delete(value): 移除 Set 中与参数相等的成员,返回一个布尔值。
- has(value): 返回一个布尔值,表示参数是否为 Set 的成员
- clear(): 移除 Set 中所有成员
Set 实例 - 遍历方法
- keys(): 返回一个 键名 的迭代器
- values(): 返回一个 键值 的迭代器
- entries(): 返回一个 键值对 的迭代器
- forEach(): 使用回调函数遍历每个成员,按照插入顺序
Set - 使用场景
去重
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26var arr = [2, 4, 3, 3, 4, 2, 3, 254, 234, 4];
// ES6 使用Set 去重
function unique3(arr) {
return [...new Set(arr)];
}
// ES5 过滤掉 下标不同 元素
function unique(arr) {
return arr.filter((item, index, self) => {
return self.indexOf(item) == index;
});
}
// ES5 使用 reduce方法,遍历数组,只要不在新数组中的元素 才 添加
function unique2(arr) {
return arr.reduce((pre, cur) => {
if (!pre.includes(cur)) {
pre.push(cur);
}
return pre;
}, []);
}
console.log(unique(arr));
console.log(unique2(arr));
console.log(unique3(arr));
并集
1
2
3
4function union(set1, set2) {
return new Set([...set1, ...set2]);
}
console.log(union(new Set([1, 2, 3]), new Set([2, 3, 4, 5]))); // [1,2,3,4,5]
交集
1
2
3
4function intersect(set1, set2) {
return new Set([...set1].filter((x) => set2.has(x)));
}
console.log(intersect(new Set([1, 2, 3]), new Set([2, 3, 4, 5]))); // [2,3]
差集
1
2
3
4function difference(set1, set2) {
return new Set([...set1].filter((x) => !set2.has(x)));
}
console.log(difference(new Set([1, 2, 3]), new Set([2, 3, 4, 5]))); // [1]
Map
在 JavaScript 的对象本质上是键值对的集合(Hash 结构),但是只能 String、Symbol 类型作为键,所以使用起来有很大限制。
Map 作为构造函数,可接受要给数组或其他iterable对象。例如 new Map([['name','lzj'],['age', 25]])
Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。键的比较是基于 sameValueZero算法, NaN
与NaN
相等
ES6 新增的 Map 结构,也类似于对象,是键值对的集合,但是键的类型没有限制,可以是任何类型的值,包括对象。
Map 和 Object 的比较
Map | Object | |
---|---|---|
键的类型 | 任意值,包括函数、对象或任何基本类型 | String 或 Symbol |
键的顺序 | Map 中的 key 是有序的。因此,迭代时,以插入的顺序返回 | 无序的 |
Size | Map 键值对个数可以通过size属性获取 | 需手动计算 |
迭代 | Map 是 iterable(可迭代的),所以可直接被迭代 | 需手动获取它的键,然后迭代 |
性能 | 在 频繁 增、删键值对的场景下 表现更好 | 未作优化 |
Map 实例 - 操作方法
- set(key, value): 设置 Map 对象中键的值,返回该 Map 对象。如果 key 已经有值,则键值被更新,否则生成新的键。
- get(key): 读取 key 对应的键值,如果不存在,返回 undefined
- has(key): 返回一个布尔值,表示 Map 对象是否包含该键
Map 实例 - 遍历方法
- entries(): 返回所有成员的迭代器,
[key, vlaue]数组
- keys(): 返回键名的迭代器
- values(): 返回值的迭代器
- forEach(): 使用回调函数遍历每个成员
Map 与 其他数据结构的转换
Map & Object (所有键都是字符串的对象)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function strMapToObject(strMap) {
let obj = Object.create(null); // 原型为null的 空对象
strMap.forEach((v, k) => {
obj[k] = v;
});
return obj;
}
function objectToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
console.log(strMapToObject(myMap));
console.log(objectToStrMap({ name: "jiang", age: 25 }));
Map & Array
1
2
3
4
5
6
7
8// 数组作为参数传入Map构造函数
let myMap = new Map([
["name", "lzj"],
["age", 25],
]);
// 使用扩展运算符
let myArray = [...myMap];
Map(键都是字符串) & JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26function strMapToObject(strMap) {
let obj = Object.create(null); // 原型为null的 空对象
strMap.forEach((v, k) => {
obj[k] = v;
});
return obj;
}
function objectToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
// Map 转 JSON, Map 键都是字符串
function strMapToJSON(strMap) {
return JSON.stringify(strMapToObject(strMap));
}
// JSON 转 Map
function jsonToStrMap(jsonStr) {
return objectToStrMap(JSON.parse(jsonStr));
}
console.log(jsonToStrMap('{"name":"lzj","age":25}'));
Promise
Promise,是一个对象。用来传递异步操作的消息。它代表了 某个未来才知道结果的事件,并为这个事件提供了统一的 API,可进一步处理该事件。
用法
通过 Promise 构造函数生成 Promise 实例,实例生成后,可以用then
方法指定 Resolved状态
和Rejected状态
的回调函数。
1 | new Promise((resolve, reject) => { |
如果调用 resolve 函数和 reject 函数时带有参数,那么这些参数就会被传递给回调函数。
reject 函数的参数通常 是 Error 对象的实例,表示抛出错误。
resolve 函数的参数除了正常想传递的值外,还可以是另一个 Promise 实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("fail"));
}, 3000);
});
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(p1);
}, 1000);
});
p2.then(
(resolve) => console.log(resolve),
(error) => console.log(error)
);
// 3S 后
// Error: failp1 是一个 Promise,3s 后状态改为 Rejected。p2 的状态由 p1 决定,1s 后,p2 调用
resolve(p1)
,此时 p1 的状态还没发生改变,因此 p2 状态也不会变,又过了 2s,p1 变为 Rejectd,p2 也跟着变为 Rejected
Promise.all(iterable)
Promise.all 方法将多个 Promise 实例包装成一个 Promise 实例。例如let p = Promise.all[p1,p2,p3]
。 p 的状态由 p1, p2, p3 决定,2 中情况:
- 只有 p1, p2, p3 的状态都变成 Resolved, p 的状态才变成 Resolved,此时 p1,p2,p3 的返回值组成一个数组,传递给 p 的回调函数。
- 只要 p1,p2,p3 中任意一个的状态变成 Rejected, p 的状态就变成 Rejected,此时第一个被 Rejected 的实例的返回值会传递给 p 的回调函数。
Promise.race(iterable)
Promise.race 方法同样是将多个 Promise 实例包装成一个新的 Promise 实例。例如 let p = Promise.race([p1,p2,p3])
,p1,p2,p3 之间是竞争关系,z 只要有一个实例的状态率先发生改变,p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数。
Class
ES6 中的 Class 可以看作是一个语法糖,它只 ES5 的构造函数的一层包装,新的 class 写法只是让对象原型的写法更加清晰,更像面向对象的语法罢了。
“类”的数据类型就是函数,”类”本身就指向构造函数
“类”的所有方法都定义在“类”的 prototype 属性上,同 ES5 构造函数的 prototype 属性一致。
在“类”的实例上调用方法,其实就是调用原型上的方法
“类”内定义的所有方法都是不可枚举的,此点同 ES5 不一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class B {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `${this.x}, ${this.y}`;
}
}
let b = new B();
typeof B; // "function"
B.prototype.constructor === B; // true
b.constructor === B.prototype.constructor; // true
Object.keys(B.prototype); // []
Object.getOwnPropertyNames(B.prototype)[("constructor", "toString")];一个类必须有 constructor 方法,如果没有显示声明,则默认添加一个空的 constructor 方法。constructor 方法默认返回实例对象(即 this),也可以指定返回另外一个对象
类不存在变量提升
类内部默认就是严格模式
Class 的 继承
Class 之间通过extends
关键字实现继承。
- 子类必须在 constructor 方法中调用
super
方法,否则新建实例时报错。只是因为:子类没有自己家的 this 的对象,而是继承了父类的 this 对象,然后再对其修改 - 只要调用 super 方法才能返回父类的实例(this),之后子类才可以使用 this,所有必须要调用 super 方法,且最后放于子类构造函数首行,之后就可以使用 this
- 子类中
super
关键字代表 父类实例
类的 prototype 属性 和 __proto__
属性
ES5 多数浏览器中,每个对象都有__proto__
属性,指向对应的构造函数的 prototype 属性。Class 作为构造函数的语法糖,同时具有__proto__
和prototype
属性,因此同时存在两条继承链
- 子类的
__proto__
属性表示构造函数的继承,总指向父类 - 子类
prototype
属性的__proto__
属性表示方法的继承,总指向父类的prototype
属性. 这是因为类的继承模式如下:
1 | class A {} |