文章目录
let 命令和const命令
let命令
新增了let和const命令,用法类似于var
,但是所声明的变量,只在let
命令所在的代码块内有效。
-
变量提升:let声明的变量不存在变量提升(只可以先声明,在使用)
-
在相同的作用域不能重复声明同一个变量
-
暂时性死区(temporal dead zone,简称 TDZ):只要块级作用域存在let,它所声明的变量就会被绑定在这个区域,不在受外部影响
var tmp = 123; if (true) { tmp = 'abc'; // ReferenceError let tmp; }
如果区块中存在
let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。if (true) { // TDZ开始 tmp = 'abc'; // ReferenceError console.log(tmp); // ReferenceError let tmp; // TDZ结束 console.log(tmp); // undefined tmp = 123; console.log(tmp); // 123 }
ES6 规定暂时性死区和
let
、const
语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为
暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
const命令
- 不可以提升
- 存在暂时性死区
- 一旦声明就必须初始化
本质
并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
-
简单数据类型(String、Boolean、Number):值就保存在变量指向的那个内存地址,因此等同于常量
-
引用数据类型(对象和数组):变量指向的内存地址,保存的只是一个指向实际数据的指针
const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了
块级作用域
由来
ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
第一种场景,内层变量可能会覆盖外层变量
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world'; // 变量提升:tmp = undefined,覆盖了全局的tmp
}
}
f(); // undefined
第二种场景,用来计数的循环变量泄露为全局变量。
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
如何创建块级作用域
let
实际上为 JavaScript 新增了块级作用域。
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
-
const
-
try/catch
ES3规范中规定
try/catch
的catch
分句会创建一个块作用域,其中声明的变量只可以在catch
分句内部有效
变量的解构赋值
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
只要等号两边的模式相同,左边的变量就会被赋予对应的值
用处:交换变量值,提取函数返回值,设置默认值
字符串的扩展
模板字符串
可以更方便的处理字符串和变量的拼接
在过去我们想要将字符串和变量拼接起来,只能通过运算符“+”来实现,若内容过多还要用“\”来表示换行,
在ES6中,可以将反引号(``)将内容括起来,在反引号中,可以使用${}来写入需要引用到的变量
函数扩展
函数参数的默认值
ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
rest参数
ES6 引入 rest 参数(形式为...变量名
),用于获取函数的多余参数,这样就不需要使用arguments
对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
箭头函数
- 函数体内的
this
,绑定定义时所在的作用域,而不是指向运行时所在的作用域 - 不可以当作构造函数,也就是说,不可以使用
new
命令,否则会抛出一个错误。 - 不可以使用
arguments
对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。 - 不可以使用
yield
命令,因此箭头函数不能用作 Generator 函数。
箭头函数根本没有自己的
this
,导致内部的this
就是外层代码块的this
。正是因为它没有this
,所以也就不能用作构造函数。
不适合使用箭头函数的场景
- 定义对象的方法时,且该方法内部包含this时,这个时候不适合使用箭头函数
- 需要动态this的时候,也不适合使用箭头函数
对象扩展
属性的可枚举性
对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor
方法可以获取该属性的描述对象。
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123,
// writable: true,
// enumerable: true,
// configurable: true
// }
描述对象的enumerable
属性,称为“可枚举性”,如果该属性为false
,就表示某些操作会忽略当前属性。
目前,有四个操作会忽略enumerable
为false
的属性。
for...in
循环:只遍历对象自身的和继承的可枚举的属性。Object.keys()
:返回对象自身的所有可枚举的属性的键名。JSON.stringify()
:只串行化对象自身的可枚举的属性。Object.assign()
: 忽略enumerable
为false
的属性,只拷贝对象自身的可枚举的属性。
前三个是 ES5 就有的,最后一个
Object.assign()
是 ES6 新增的
for...in
会返回继承的属性,其他三个方法都会忽略继承的属性,只处理对象自身的属性。
属性遍历
ES6 一共有 5 种方法可以遍历对象的属性。
(1)for…in 自身+继承+可枚举(不含 Symbol 属性)
for...in
循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
**(2)Object.keys(obj)**自身+可枚举(不含 Symbol 属性)
Object.keys
返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
**(3)Object.getOwnPropertyNames(obj)**自身+可枚举+不可枚举(不含 Symbol 属性)
Object.getOwnPropertyNames
返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
**(4)Object.getOwnPropertySymbols(obj)**自身且所有Symbol属性
Object.getOwnPropertySymbols
返回一个数组,包含对象自身的所有 Symbol 属性的键名。
**(5)Reflect.ownKeys(obj)**自身+可枚举+不可枚举+Symbol(所有的自身属性)
Reflect.ownKeys
返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。
- 首先遍历所有数值键,按照数值升序排列。
- 其次遍历所有字符串键,按照加入时间升序排列。
- 最后遍历所有 Symbol 键,按照加入时间升序排列。
super关键字
this
关键字总是指向函数所在的当前对象
ES6 又新增了另一个类似的关键字super
,指向当前对象的原型对象。
注意,
super
关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。
const proto = {
foo: 'hello'
};
const obj = {
foo: 'world',
find() {
return super.foo;
}
};
Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
Symbol数据类型
七大数据类型:String、Boolean、Number、Undefined、Null、Object、Symbol
基本数据类型:String、Boolean、Number、Undefined、Null、Symbol
引用数据类型:Object
Symbol:表示独一无二的值。
目的:防止属性名冲突
对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
Symbol在作为对象的属性是必须是通过
[]
或者Object.defineProperty
的形式指定属性名为一个Symbol值let mySymbol = Symbol(); // 第一种写法 let a = {}; a[mySymbol] = 'Hello!'; // 第二种写法 let a = { [mySymbol]: 'Hello!' }; // 第三种写法 let a = {}; Object.defineProperty(a, mySymbol, { value: 'Hello!' }); // 以上写法都得到同样结果 a[mySymbol] // "Hello!"
注意,Symbol 值作为对象属性名时,不能用点运算符。
const mySymbol = Symbol(); const a = {}; a.mySymbol = 'Hello!'; //这里的mySymbol仅仅是一个字符串,而不是一个Symbol类型所代表的数据 a[mySymbol] // undefined a['mySymbol'] // "Hello!"
Symbol 值通过Symbol
函数生成
Symbol
函数前不能使用new
命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。
let s = Symbol();
typeof s
// "symbol"
Symbol
函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
let s1 = Symbol('foo');
let s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
- Symbol不可以和其他值进行运算
- Symbol值可以显示转为字符串
- Symbol值可以转为布尔类型,但是不可以转为数值
属性名的遍历
Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...in
、for...of
循环中,也不会被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。
for...in
:自身+继承+可枚举for ...of
:自身+可枚举Object.keys()
自身+可枚举Object.getOwnPropertyNames()
:自身+可枚举+不可枚举JSON.stringify()
:自身+可枚举(串序列化)
Set和Map数据结构
Set
类似于数组,但是成员的值都是唯一的,没有重复的值。
Set
本身是一个构造函数,用来生成 Set 数据结构。
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4
-
向 Set 加入值的时候,不会发生类型转换,所以
5
和"5"
是两个不同的值。 -
向 Set 实例添加了两次
NaN
,但是只会加入一个。这表明,在 Set 内部,两个NaN
是相等的。let set = new Set(); let a = NaN; let b = NaN; set.add(a); set.add(b); set // Set {NaN}
-
两个对象总是不相等的。
let set = new Set(); set.add({}); set.size // 1 set.add({}); set.size // 2 //两个空对象不相等,所以它们被视为两个值。
初始化
Set可以接受一个数组或(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
// 例三
const set = new Set(document.querySelectorAll('div'));
set.size // 56
// 类似于
const set = new Set();
document
.querySelectorAll('div')
.forEach(div => set.add(div));
set.size // 56
操作方法
Set.prototype.add(value)
:添加某个值,返回 Set 结构本身。(增)Set.prototype.delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。(删)Set.prototype.has(value)
:返回一个布尔值,表示该值是否为Set
的成员。(查)Set.prototype.clear()
:清除所有成员,没有返回值。(删)
遍历方法
Set.prototype.keys()
:返回键名的遍历器Set.prototype.values()
:返回键值的遍历器Set.prototype.entries()
:返回键值对的遍历器Set.prototype.forEach()
:使用回调函数遍历每个成员
keys
方法、values
方法、entries
方法返回的都是遍历器对象。遍历器对象可以使用for ...of
来进行遍历由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以
keys
方法和values
方法的行为完全一致。let set = new Set(['red', 'green', 'blue']); for (let item of set.keys()) { console.log(item); } // red // green // blue for (let item of set.values()) { console.log(item); } // red // green // blue for (let item of set.entries()) { console.log(item); } // ["red", "red"] // ["green", "green"] // ["blue", "blue"]
entries
方法返回的遍历器,同时包括键名和键值,所以每次输出一个数组,它的两个成员完全相等。
【要知道的:】
Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的values
方法。
Set.prototype[Symbol.iterator] === Set.prototype.values
// true
这意味着,可以省略values
方法,直接用for...of
循环遍历 Set。
let set = new Set(['red', 'green', 'blue']);
/**
for (let item of set.values()) {
console.log(item);
} **/
for (let x of set) {
console.log(x);
}
// red
// green
// blue
WeakSet
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
-
WeakSet 的成员只能是对象,而不能是其他类型的值。
const ws = new WeakSet(); ws.add(1) // TypeError: Invalid value used in weak set ws.add(Symbol()) // TypeError: invalid value used in weak set const a = [[1, 2], [3, 4]]; const ws = new WeakSet(a); // WeakSet {[1, 2], [3, 4]}
-
WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用
如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
由于上面这个特点,WeakSet 的成员是不适合引用的,因为它会随时消失
由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。
构造函数初始化
WeakSet是一个构造函数,可以是会用new命令来创建WeakSet数据结构
const ws = new WeakSet();
WeakSet 可以接受一个数组或类似数组的对象作为参数。
实际上,任何具有 Iterable 接口的对象,都可以作为 WeakSet 的参数
该数组的所有成员,都会自动成为 WeakSet 实例对象的成员(是数组的成员,而不是该数组)
const a = [[1, 2], [3, 4]]; const ws = new WeakSet(a); // WeakSet {[1, 2], [3, 4]}
是
a
数组的成员成为 WeakSet 的成员,而不是a
数组本身。这意味着,数组的成员只能是对象。const b = [3, 4]; const ws = new WeakSet(b); // Uncaught TypeError: Invalid value used in weak set(…) // 数组b的成员不是对象,加入 WeakSet 就会报错。
WeakSet 结构有以下三个方法
- WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
- WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
- WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。
WeakSet的用处:储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏。
Map
类似于对象,也是键值对的集合
传统对象:只能用字符串当作键
map:“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键
如果需要“键值对”的数据结构,Map 比 Object 更合适。
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
-
Map.prototype.set(key, value):
set
方法设置键名key
对应的键值为value
,然后返回整个 Map 结构。如果key
已经有值,则键值会被更新,否则就新生成该键。const m = new Map(); m.set('edition', 6) // 键是字符串 m.set(262, 'standard') // 键是数值 m.set(undefined, 'nah') // 键是 undefined
set
方法返回的是当前的Map
对象,因此可以采用链式写法。let map = new Map() .set(1, 'a') .set(2, 'b') .set(3, 'c');
构造函数初始化
Map作为构造函数可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组
数组的成员时数组,且该数组是键值对的形式
const map = new Map([ //新建 Map 实例时,并指定了两个键name和title。
['name', '张三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"
不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构。都可以当作Map
构造函数的参数。这就是说,Set
和Map
都可以用来生成新的 Map。
const set = new Set([
['foo', 1],
['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1
const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3
-
如果对同一个键多次赋值,后面的值将覆盖前面的值。
-
如果读取一个未知的键,则返回
undefined
。 -
Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键
比如
0
和-0
就是一个键undefined
和null
也是两个不同的键NaN
不严格相等于自身,但 Map 将其视为同一个键。
操作方法:
-
Map.prototype.get(key)
get
方法读取key
对应的键值,如果找不到key
,返回undefined
。 -
Map.prototype.has(key)
has
方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。 -
Map.prototype.delete(key)
delete
方法删除某个键,返回true
。如果删除失败,返回false
。 -
Map.prototype.clear()
clear
方法清除所有成员,没有返回值。
遍历方法:
Map.prototype.keys()
:返回键名的遍历器。Map.prototype.values()
:返回键值的遍历器。Map.prototype.entries()
:返回所有成员的遍历器。Map.prototype.forEach()
:遍历 Map 的所有成员。
Map 结构的默认遍历器接口(Symbol.iterator
属性),就是entries
方法。
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
WeakMap()
https://es6.ruanyifeng.com/#docs/set-map#WeakSet
WeakMap()设计的目的:
有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。请看下面的例子。
const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
[e1, 'foo 元素'],
[e2, 'bar 元素'],
];
上面代码中,e1
和e2
是两个对象,我们通过arr
数组对这两个对象添加一些文字说明。这就形成了arr
对e1
和e2
的引用。
一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放e1
和e2
占用的内存。
// 不需要 e1 和 e2 的时候
// 必须手动删除引用
arr [0] = null;
arr [1] = null;
上面这样的写法显然很不方便。一旦忘了写,就会造成内存泄露。
WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。一个典型应用场景是,在网页的 DOM 元素上添加数据,就可以使用WeakMap
结构。当该 DOM 元素被清除,其所对应的WeakMap
记录就会自动被移除。
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"
WeakMap()的特点:
WeakMap
结构与Map
结构类似,也是用于生成键值对的集合。
-
WeakMap
只接受对象作为键名(null
除外),不接受其他类型的值作为键名。const map = new WeakMap(); map.set(1, 2) // TypeError: 1 is not an object! map.set(Symbol(), 2) // TypeError: Invalid value used as weak map key map.set(null, 2) // TypeError: Invalid value used as weak map key
-
WeakMap
的键名所指向的对象,不计入垃圾回收机制。 -
没有遍历操作(即没有
keys()
、values()
和entries()
方法),也没有size
属性。 -
无法清空,即不支持
clear
方法。
WeakMap
只有四个方法可用:get()
、set()
、has()
、delete()
。
WeakMap()的用途
WeakMap 应用的典型场合就是 DOM 节点作为键名。下面是一个例子。
let myWeakmap = new WeakMap();
myWeakmap.set(
document.getElementById('logo'),
{timesClicked: 0});
document.getElementById('logo').addEventListener('click', function() {
let logoData = myWeakmap.get(document.getElementById('logo'));
logoData.timesClicked++;
}, false);
上面代码中,document.getElementById('logo')
是一个 DOM 节点,每当发生click
事件,就更新一下状态。我们将这个状态作为键值放在 WeakMap 里,对应的键名就是这个节点对象。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。
Promise对象
promise对象的特点:
-
对象的状态不受外界的干扰
只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
-
一旦状态改变,就不会再变,任何时候都可以得到这个结果。
promise对象的缺点:
-
无法取消
Promise
,一旦新建它就会立即执行,无法中途取消。 -
如果不设置回调函数(使用
catch()
方法指定错误处理的回调函数),Promise
内部抛出的错误,不会反应到外部const someAsyncThing = function() { return new Promise(function(resolve, reject) { // 下面一行会报错,因为x没有声明 resolve(x + 2); }); }; someAsyncThing().then(function() { console.log('everything is great'); }); setTimeout(() => { console.log(123) }, 2000); // Uncaught (in promise) ReferenceError: x is not defined // 123
someAsyncThing()
函数产生的 Promise 对象,内部有语法错误。浏览器运行到这一行,会打印出错误提示ReferenceError: x is not defined
,但是不会退出进程、终止脚本执行,2 秒之后还是会输出123
。Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。
-
当处于
pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
promise用法推荐
一般来说,不要在then()
方法里面定义 Reject 状态的回调函数(即then
的第二个参数),总是使用catch
方法。
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
第二种写法要好于第一种写法,理由是第二种写法可以捕获前面
then
方法执行中的错误,也更接近同步的写法(try/catch
)一般总是建议,Promise 对象后面要跟
catch()
方法,这样可以处理 Promise 内部发生的错误。
Promise.prototype.finally()
不管 Promise 对象最后状态如何,都会执行的操作
finally
方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled
还是rejected
。这表明,finally
方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
应用
加载图片
将图片的加载写成一个Promise
,一旦加载完成,Promise
的状态就发生变化。
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = resolve; // 图片加载成功,调用resolve()更改promise状态
image.onerror = reject; // 图片加载失败,调用reject()更改promise状态
image.src = path;
});
};
Iterator和for…of
js中表示“集合”的数据结构:数组、对象、Set、Map
上面四种数据集合,后面的两个是ES6新增的
遍历器(Iterator)是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator的作用
- 为各种数据结构,提供一个统一的、简便的访问接口
- 使得数据结构的成员能够按某种次序排列
- ES6 创造了一种新的遍历命令
for...of
循环,Iterator 接口主要供for...of
消费。
Iterator的遍历过程
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的next
方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next
方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next
方法,直到它指向数据结构的结束位置。
每一次调用next
方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value
和done
两个属性的对象。其中,value
属性是当前成员的值,done
属性是一个布尔值,表示遍历是否结束。
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
Iterator 接口的目的
为所有数据结构,提供了一种统一的访问机制,即for...of
循环
当使用
for...of
循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。
ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator
属性
一个数据结构只要具有
Symbol.iterator
属性,就可以认为是“可遍历的”(iterable)
-
Symbol.iterator
属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。 -
属性名
Symbol.iterator
,它是一个表达式,返回Symbol
对象的iterator
属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内
const obj = {
// Symbol对象的iterator属性,一个预定义好的、类型为Symbol的特殊值,而不是一个字符串,因此放在方括号里面
[Symbol.iterator] : function () { // 函数
return { // 返回一个遍历器
next: function () { // 对象的根本特征就是具有next方法。
return { // 返回一个代表当前成员的信息对象,具有value和done两个属性。
value: 1,
done: true
};
}
};
}
};
原生具备 Iterator 接口的数据结构如下:
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
// 数组的Symbol.iterator属性。 let arr = ['a', 'b', 'c']; let iter = arr[Symbol.iterator](); // 调用这个属性,就得到遍历器对象(这个属性是一个函数) iter.next() // { value: 'a', done: false } iter.next() // { value: 'b', done: false } iter.next() // { value: 'c', done: false } iter.next() // { value: undefined, done: true }
另外一些数据结构没有(比如对象)
需要自己在
Symbol.iterator
属性上面部署,这样才会被for...of
循环遍历。(原型链上的对象具有该方法也可)对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定
调用Iterator接口的场合
-
for…of循环遍历
-
解构赋值
对数组和Set进行结构赋值时,会默认调用
Symbol.iterator
方法let set = new Set().add('a').add('b').add('c'); let [x,y] = set; // x='a'; y='b' let [first, ...rest] = set; // first='a'; rest=['b','c'];
-
扩展运算符
...
// 例一 var str = 'hello'; [...str] // ['h','e','l','l','o'] // 例二 let arr = ['b', 'c']; ['a', ...arr, 'd'] // ['a', 'b', 'c', 'd']
只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。
let arr = [...iterable];
-
yield*
yield*
后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。let generator = function* () { yield 1; yield* [2,3,4]; yield 5; }; var iterator = generator(); iterator.next() // { value: 1, done: false } iterator.next() // { value: 2, done: false } iterator.next() // { value: 3, done: false } iterator.next() // { value: 4, done: false } iterator.next() // { value: 5, done: false } iterator.next() // { value: undefined, done: true }
-
其他场合
由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。
- for…of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(比如
new Map([['a',1],['b',2]])
) - Promise.all()
- Promise.race()
for…of
目的:作为遍历所有数据结构的统一的方法。
一个数据结构只要部署了Symbol.iterator
属性,就被视为具有 iterator 接口,就可以用for...of
循环遍历它的成员。
for...of
循环内部调用的是数据结构的Symbol.iterator
方法。
for…of适用的范围:
- 数组
- Set和Map数据结构
- 类似于数组对象(arguments对象、DOM NodeList对象、Generator对象、字符串)
JavaScript 原有的for...in
循环,只能获得对象的键名,不能直接获取键值。ES6 提供for...of
循环,允许遍历获得键值。
let arr = [3, 5, 7];
arr.foo = 'hello';
for (let i in arr) {
console.log(i); // "0", "1", "2", "foo"
}
for (let i of arr) {
console.log(i); // "3", "5", "7"
}
//for...of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性
//上面代码中,for...of循环不会返回数组arr的foo属性。
Generator函数
https://es6.ruanyifeng.com/#docs/generator
语法上:Generator函数是一个状态机,封装了多个内部状态
执行Generator函数会返回一个遍历器对象。返回的遍历器对象可以依次遍历Generator函数内的每一个状态
即Generator既是状态机又是遍历器对象生成函数
形式上:Generator是一个普通的函数,但是有两个特征:
function
关键字与函数名之间有一个星号- 函数体内部使用
yield
表达式,定义不同的内部状态
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next() // Generator 函数开始执行,直到遇到第一个yield表达式为止。
// { value: 'hello', done: false }
hw.next() // 从上次yield表达式停下的地方,一直执行到下一个yield表达式
// { value: 'world', done: false }
hw.next() // 上次yield表达式停下的地方,一直执行到return语句
// { value: 'ending', done: true }
hw.next() // 此时 Generator 函数已经运行完毕
// { value: undefined, done: true }
如果没有return
语句,就执行到函数结束。next
方法返回的对象的value
属性,就是紧跟在return
语句后面的表达式的值(如果没有return
语句,则value
属性的值为undefined
),done
属性的值true
,表示遍历已经结束。
yield表达式
Generator 函数返回的遍历器对象,只有调用next
方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield
表达式就是暂停标志。
yield
表达式只能用在 Generator 函数里面,用在其他地方都会报错。
yield
表达式如果用在另一个表达式之中,必须放在圆括号里面。function* demo() { console.log('Hello' + yield); // SyntaxError console.log('Hello' + yield 123); // SyntaxError console.log('Hello' + (yield)); // OK console.log('Hello' + (yield 123)); // OK }
yield
表达式用作函数参数或放在赋值表达式的右边,可以不加括号。function* demo() { foo(yield 'a', yield 'b'); // OK let input = yield; // OK }
遍历器对象的next
方法的运行逻辑如下:
(1)遇到
yield
表达式,就暂停执行后面的操作,并将紧跟在yield
后面的那个表达式的值,作为返回的对象的value
属性值。(2)下一次调用
next
方法时,再继续往下执行,直到遇到下一个yield
表达式。(3)如果没有再遇到新的
yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return
语句后面的表达式的值,作为返回的对象的value
属性值。(4)如果该函数没有
return
语句,则返回的对象的value
属性值为undefined
。
yield
表达式后面的表达式,只有当调用next
方法、内部指针指向该语句时才会执行
yield和return的区别:
-
相同点:
都能返回紧跟在语句后面的那个表达式的值
-
不同点:
- 每次遇到
yield
,函数暂停执行,下一次再从该位置继续向后执行,而return
语句不具备位置记忆的功能 - 一个函数里面,只能执行一次(或者说一个)
return
语句,但是可以执行多次(或者说多个)yield
表达式
- 每次遇到
正常函数和Generator函数的区别:
正常函数只能返回一个值,因为只能执行一次return
;Generator 函数可以返回一系列的值,因为可以有任意多个yield
。
Generator 函数可以不用
yield
表达式,这时就变成了一个单纯的暂缓执行函数。function* f() { console.log('执行了!') } var generator = f(); setTimeout(function () { generator.next() }, 2000);
函数
f
如果是普通函数,在为变量generator
赋值时就会执行。但是,函数f
是一个 Generator 函数,就变成只有调用next
方法时,函数f
才会执行。
Generator和Iterator接口
Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator
属性,从而使得该对象具有 Iterator 接口。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
//Generator 函数赋值给Symbol.iterator属性,从而使得myIterable对象具有了 Iterator 接口,可以被...运算符遍历了。
next方法的参数
yield
表达式本身没有返回值,或者说总是返回undefined
。next
方法可以带一个参数,该参数就会被当作上一个yield
表达式的返回值。
async函数
class
class Point {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
ES6引入了 Class(类)这个概念,作为对象的模板。通过class
关键字,可以定义类。
ES6 的
class
可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
class Point {
constructor(x, y) { // 构造方法 《==》对应ES5的构造函数
this.x = x; // this代表实例对象
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
/**
1. 定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了
2. 方法之间不需要逗号分隔
**/
constructor方法
constructor
方法是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。一个类必须有constructor
方法,如果没有显式定义,一个空的constructor
方法会被默认添加。
class Point {
}
// 等同于
class Point {
constructor() {}
}
constructor
方法默认返回实例对象(即this
),完全可以指定返回另外一个对象。
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo
// false constructor函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。
类的实例
生成类的实例的写法,与 ES5 完全一样,也是使用new
命令。
如果忘记加上
new
,像函数那样调用Class
,将会报错。
Class定义类和ES5的异同
相同点:
生成类的实例的写法,与 ES5 完全一样,也是使用
new
命令。实例的属性除非显式定义在其本身(即定义在
this
对象上),否则都是定义在原型上(即定义在class
上)。
prototype
对象的constructor
属性,直接指向“类”的本身
Point.prototype.constructor === Point // true
类的所有实例共享一个原型对象。
在“类”的内部可以使用
get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。与函数一样,类也可以使用表达式的形式定义。
不同点:
定义“类”的方法的时候,前面不需要加上
function
这个关键字,直接把函数定义放进去了就可以了方法之间不需要逗号分隔
类的内部所有定义的方法,都是不可枚举的;ES5中定义在原型原型上的方法是可枚举的
class Point { constructor(x, y) { // ... } toString() { // ... } } Object.keys(Point.prototype) // [] Object.getOwnPropertyNames(Point.prototype) // ["constructor","toString"] // ES5 var Point = function (x, y) { // ... }; Point.prototype.toString = function() { // ... }; Object.keys(Point.prototype) // ["toString"] Object.getOwnPropertyNames(Point.prototype) // ["constructor","toString"]
类必须使用
new
调用,否则会报错。ES5中的普通构造函数不使用new也可以执行类和模块的内部,默认就是严格模式
类不存在变量提升(hoist),ES5的函数存在变量提升。
两者对比:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
// ES5
function Point(x, y){
this.x = x;
this.y - y
}
Point.prototype.toString = function(){
return '(' + this.x + ', ' + this.y + ')';
}
“类”里面的
constructor
方法,就是构造方法,而this
关键字则代表实例对象。也就是说,ES5 的构造函数Point
,对应 ES6 的Point
类的构造方法。
静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承
如果在一个方法前,加上static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
如果静态方法包含
this
关键字,这个this
指的是类,而不是实例。父类的静态方法,可以被子类继承。不可以被该父类的实例调用
静态方法可以与非静态方法重名。
静态方法也是可以从
super
对象上调用的。class Foo { static classMethod() { return 'hello'; } } class Bar extends Foo { static classMethod() { return super.classMethod() + ', too'; } } Bar.classMethod() // "hello, too"
同理静态属性:静态属性指的是 Class 本身的属性,即Class.propName
,而不是定义在实例对象(this
)上的属性。
// 老写法
class Foo {
// ...
}
Foo.prop = 1;
// 新写法
class Foo {
static prop = 1;
}
私有方法和私有属性
私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问
这是常见需求,有利于代码的封装,但 ES6 不提供,只能通过变通方法模拟实现。
私有属性:class
加了私有属性。方法是在属性名之前,使用#
表示。
const counter = new IncreasingCounter();
counter.#count // 报错
counter.#count = 42 // 报错
new.target属性
该属性一般用在构造函数之中,返回new
命令作用于的那个构造函数
如果构造函数不是通过
new
命令或Reflect.construct()
调用的,new.target
会返回undefined
,因此这个属性可以用来确定构造函数是怎么调用的。
function Person(name) {
if (new.target !== undefined) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}
// 另一种写法
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}
var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三'); // 报错:必须使用 new 命令生成实例
子类继承父类时,new.target
会返回子类。
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
// ...
}
}
class Square extends Rectangle {
constructor(length, width) {
super(length, width);
}
}
var obj = new Square(3); // 输出 false
Class实现继承
ES5:通过修改原型链实现继承
实质:先创造子类的实例对象
this
,然后再将父类的方法添加到this
上面(Parent.apply(this)
)
ES6:通过extends
关键字实现继承(更清晰、方便)
实质:先将父类实例对象的属性和方法,加到
this
上面(所以必须先调用super
方法),然后再用子类的构造函数修改this
。
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
class Point { /* ... */ }
class ColorPoint extends Point {
constructor() {
}
}
let cp = new ColorPoint(); // ReferenceError
子类必须在constructor
方法中调用super
方法,否则新建实例时会报错
子类自己的
this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法
如果子类没有定义constructor
方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor
方法。
class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
在子类的构造函数中,只有调用super
之后,才可以使用this
关键字,否则会报错
因为子类实例的构建,基于父类实例,只有
super
方法才能调用父类实例。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y);
this.color = color; // 正确
}
}
super关键字
https://es6.ruanyifeng.com/#docs/class-extends#super-%E5%85%B3%E9%94%AE%E5%AD%97
super
这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
函数使用:super
作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super
函数。
作为函数时,
super()
只能用在子类的构造函数之中,用在其他地方就会报错。
对象使用:
-
在普通方法中,指向父类的原型对象
由于
super
指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super
调用的。子类普通方法中通过
super
调用父类的方法时,(父类)方法内部的this
指向当前的子类实例。由于
this
指向子类实例,所以如果通过super
对某个属性赋值,这时super
就是this
,赋值的属性会变成子类实例的属性。 -
在静态方法中,指向父类。
在子类的静态方法中通过
super
调用父类的方法时(super指向父类),(父类)方法内部的this
指向当前的子类,而不是子类的实例。
由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super
关键字
var obj = {
toString() {
return "MyObject: " + super.toString();
}
};
obj.toString(); // MyObject: [object Object]
module的语法
https://es6.ruanyifeng.com/#docs/module
ES6之前的模块加载方案:
浏览器方面的模块化:AMD和CMD
服务器方面的模块化:CommonJS(Node是CommonJS一个具有代表性的实现)
ES6提出的模块加载方案:(服务器和浏览器通用)
ES6的模块:编译时就能确定模块的依赖关系,以及输入和输出的变量。
CommonJS 和 AMD 模块:只能在运行时确定这些东西
ES6的模块思想:尽量静态化,在编译时就可以确定模块的依赖关系,以及输入和输出的变量。
ES6的加载实质:从模块中引入指定的方法,而不是整个文件作为一个模块进行加载
这种加载也称编译时加载或静态加载,即ES6在编译时就可以完成模块的加载
CommonJS加载实质:将一个文件整体加载为一个模块,并生成一个对象,然后通过这个对象来获取里面的方法
这种加载也称为运行时加载,因为只有运行时才可以得到这个对象
CommonJS
Node中模块的引入和导出就是CommonJS的一个具体代表
- 导出模块:exports或module.exports
- 导入模块:require
require导入模块的细节:https://blog.csdn.net/qq_43952245/article/details/106602068
缺点:是同步加载模块
只有等到对应的模块加载完毕,当前模块中的内容才能被运行;
AMD和CMD
CommonJS加载模块是同步的,但是在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快;
如果将它应用于浏览器呢?
浏览器加载js文件需要先从服务器将文件下载下来,之后在加载运行;那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作;
所以在浏览器中,我们通常不使用CommonJS规范:在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD:
-
ADM(即Asynchronous Module Definition 异步模块定义)
AMD实现的比较常用的库是require.js和curl.js;这里我们以require.js为例讲解:
https://mp.weixin.qq.com/s/6Cc5RMw3pAHEUM58KLB-MQ
- 下载require.js
- 引入
- 加载模块:
require(['foo'], function(foo) {... })
使用require开始加载执行模块的代码 - 定义模块:如果一个模块不依赖其他,那么直接使用define(function)即可
-
CMD(即Common Module Definition通用模块定义)
SeaJS就是CMD的优秀实现方案
-
下载SeaJS
-
引入
// index.js文件中:引入foo模块 define(function(require, exports, module) { const foo = require('./modules/foo'); }) // bar.js 暴露指定内容 define(function(require, exports, module) { const name = 'lilei'; const age = 20; const sayHello = function(name) { console.log("你好 " + name); } module.exports = { name, age, sayHello } }) // foo.js 引入bar模块,并使用里面的内容 define(function(require, exports, module) { const bar = require('./bar'); console.log(bar.name); console.log(bar.age); bar.sayHello("韩梅梅"); })
-
ES6和CommonJS的不同
ES6:
- 静态加载,在编译时就完成了模块的加载
- 从模块中引入指定的方法或属性,是一种静态定义,在代码静态解析阶段就会生成
- export输出的接口与模块内变量的对应关系是动态的,通过该接口可以获取模块内部实时的值
- import命令是异步加载(有一个独立的模块依赖的解析阶段)
- 顶层的this指向undefined
CommonJS:
- 动态加载,只有运行时,才可以获得加载的模块对象
- 加载的是一个对象(即module.exports属性),然后通过对象来使用里面的方法(该对象只有在脚本运行时才会生成)
- CommonJS 模块输出的是值的缓存,不存在动态更新
- require()是同步加载模块
- 顶层的this指向当前模块
CommonJS加载原理
CommonJS的一个模块就是一个脚本文件。使用require命令第一次加载该脚本时,就会执行整个脚本,然后在内存中生成一个对象
{
id: '...', // 模块名
exports: { ... }, // 模块输出的各个接口。以后需要用到这个模块的时候,就会到exports属性上面取值
loaded: true, // 表示该模块的脚本是否执行完毕
...
}
CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
ES6加载模块:ES6 模块是动态引用
使用
import
从一个模块加载变量(即import foo from 'foo'
),那些变量不会被缓存,而是成为一个指向被加载模块的引用
export命令
用于规定模块的对外接口
注意:export命令就是来规定模块的对外接口,因此export暴露的接口名必须和模块内的变量建立一一对应的关系
// profile.js 方法一
export var firstName = 'Michael';
export function multiply(x, y) {
return x * y;
};
// profile.js 方法二
export { firstName};
错误写法:
// 报错
export 1; // 直接输出1,1只是一个值,不是接口
// 报错
var m = 1;
export m; // 通过变量 m 直接输出1, 1只是一个值,不是接口
正确写法:
/**
规定了对外的接口m,其他脚本可以通过这个接口,取到值1
实质是:在接口名与模块内部变量之间,建立了一一对应的关系。
**/
// 写法一
export var m = 1; // 模块内部变量 m <=> 接口名m
// 写法二
var m = 1;
export {m}; // 模块内部变量 m <=> 接口名 m 暴露出一个对象作为接口
// 写法三
var n = 1;
export {n as m}; // 模块内部变量 n <=> 接口名 m
通常情况下,export
输出的变量就是本来的名字,但是可以使用as
关键字重命名。(接口名默认是输出变量的名字)
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion //重命名后,v2可以用不同的名字输出两次。
};
import命令
引入其他模块提供的功能
使用
export
命令定义了模块的对外接口以后,其他 JS 文件就可以通过import
命令加载这个模块。
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
逐一加载:
// main.js
import { area, circumference } from './circle';
console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));
整体加载:
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
export 和 export default
export
:
- 一个模块内可以有多个
- import引入时需加
{}
export default
:
- 一个模块内最多只可以有一个
- import引入时不加
{}
因为export default
命令其实只是输出一个叫做default
的变量,所以它后面不能跟变量声明语句。
// 正确
export var a = 1;
// 正确
var a = 1;
export default a; //将后面的值,赋给default变量
// 错误
export default var a = 1;
// 正确
export default 42; // 将2赋值给default
// 报错
export 42;
import()
import
命令会被 JavaScript 引擎静态分析,先于模块内的其他语句执行
import
和export
命令只能在模块的顶层,不能在代码块之中(比如,在if
代码块之中,或在函数之中)否则会报句法错误,而不是执行时错误
优点:有利于编译器提高效率。
缺点:导致无法在运行时加载模块,在语法上,条件加载就不可能实现
如果
import
命令要取代 Node 的require
方法,这就形成了一个障碍。因为require
是运行时加载模块,import
命令无法取代require
的动态加载功能。
解决办法:
引入import()
函数,支持动态加载模块。
import(specifier)
:import
函数的参数specifier
,指定所要加载的模块的位置。import
命令能够接受什么参数,import()
函数就能接受什么参数,两者区别主要是后者为动态加载。
-
import()
返回一个 Promise 对象 -
import()
函数可以用在任何地方不仅仅是模块,非模块的脚本也可以使用
-
它是运行时执行
什么时候运行到这一句,就会加载指定的模块
-
import()
函数与所加载的模块没有静态连接关系import()
类似于 Node 的require
方法,区别主要是前者是异步加载,后者是同步加载。
import()使用场合
-
按需加载:
import()
可以在需要的时候,再加载某个模块。import()
方法放在click
事件的监听函数之中,只有用户点击了按钮,才会加载这个模块。button.addEventListener('click', event => { import('./dialogBox.js') .then(dialogBox => { dialogBox.open(); }) .catch(error => { /* Error handling */ }) });
-
条件加载:
import()
可以放在if
代码块,根据不同的情况,加载不同的模块。 -
动态的模块路径:
import()
允许模块路径动态生成。
module的加载实现
浏览器加载
传统方法:HTML 网页中,浏览器通过<script>
标签加载 JavaScript 脚本。
缺点:
浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到
<script>
标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞
解决办法:
浏览器允许脚本异步加载,下面就是两种异步加载的语法。
<script src="path/to/myModule.js" defer></script> <script src="path/to/myModule.js" async></script>
defer
是“渲染完再执行”,async
是“下载完就执行”。另外,如果有多个defer
脚本,会按照它们在页面出现的顺序加载,而多个async
脚本是不能保证加载顺序的。defer中指的“页面的正常渲染结束”指得是:DOM 结构完全生成,以及其他脚本执行完成
浏览器加载ES6模块
加载规则:
也使用
<script>
标签,但是要加入type="module"
属性。
<script type="module" src="./foo.js"></script>
浏览器对于带有
type="module"
的<script>
,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>
标签的defer
属性。如果网页有多个
<script type="module">
,它们会按照在页面出现的顺序依次执行。一旦使用了async属性,
node加载ES6模块
JavaScript 现在有两种模块。一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS。
服务器端的模块加载机制:CommonJS 和 ES6的module
CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容
语法上:
CommonJS 模块使用
require()
和module.exports
ES6 模块使用
import
和export
。
要想ES6的module在node中使用,就必须为ES6中的module和CommonJS规定好各自的加载方案
-
Node.js 要求 ES6 模块采用
.mjs
后缀文件名只要脚本文件里面使用
import
或者export
命令,那么就必须采用.mjs
后缀名如果不希望将后缀名改成
.mjs
,可以在项目的package.json
文件中,指定type
字段为module
。一旦设置了以后,该目录里面的 JS 脚本,就被解释用 ES6 模块。{ "type": "module" }
-
如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成
.cjs
。如果没有
type
字段,或者type
字段为commonjs
,则.js
脚本会被解释成 CommonJS 模块。
总结:
>`.mjs`文件总是以 ES6 模块加载,`.cjs`文件总是以 CommonJS 模块加载,`.js`文件的加载取决于`package.json`里面`type`字段的设置。
循环加载
对象遍历
-
for...in
:遍历普通对象的key值(自身+原型+可枚举)可以用来遍历数组,但是不建议因为 key 输出为字符串形式,而不是数组需要的数字下标,这意味着在某些情况下,会发生字符串运算,导致数据错误
for in 循环的时候,不仅遍历自身的属性,还会找到 prototype 上去,所以最好在循环体内加一个判断,就用 obj[i].hasOwnProperty(i),这样就避免遍历出太多不需要的属性。
例1: Object.prototype.clone = function() {} var obj = { name: 'jack', age: 33 } for (var n in obj) { console.log(n) } //多出了在原型上定义的方法 输出:name, age, clone
例2: Object.prototype.clone = function () {}; var obj = { name: "jack", age: 33, }; for (var n in obj) { if (obj.hasOwnProperty(key)) { console.log(n); } } 输出:name, age #推荐总是使用 hasOwnProperty 方法,这将会避免原型对象扩展带来的干扰:
- Object.keys(obj)
返回一个数组,包括对象的 自身+可枚举属性
语法 var obj = { p1: 123, p2: 456 }; Object.keys(obj) // ["p1", "p2"] var obj = { p1: 123, p2: 456 }; Object.getOwnPropertyNames(obj) // ["p1", "p2"] var obj = {'0':'a','1':'b','2':'c'}; Object.keys(obj).forEach(function(key){ console.log(key,obj[key]); }); // 0 a // 1 b // 2 c
-
Object.getOwnPropertyNames(obj)
返回一个数组,包含对象的 自身+可枚举+不可枚举
var obj = {'0':'a','1':'b','2':'c'}; Object.getOwnPropertyNames(obj).forEach(function(key){ console.log(key,obj[key]); }); // 0 a // 1 b // 2 c
-
Object.getOwnPropertySymbols(obj)
返回一个数组,包含对象 自身且所有Symbol属性的键名
-
Reflect.ownKeys(obj)自身+可枚举+不可枚举+Symbol(所有的自身属性)
返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
var obj = {'0':'a','1':'b','2':'c'}; Reflect.ownKeys(obj).forEach(function(key){ console.log(key,obj[key]); });
数组遍历
数组项的全部遍历
-
for循环 ==》数组索引
是最原始,也是性能最高的一种遍历方法。
-
forEach循环(只可遍历数组)==》数组索引
是构造函数Array的原型上的函数,即
Array.prototype.forEach
,因此所有数组都可以用这个方法。循环数组中每一个元素并采取操作, 没有返回值
let arr = [1,2,3]; arr.forEach(function(i,index){ console.log(i,index) }) // 1 0 // 2 1 // 3 2
forEach 循环在所有元素调用完毕之前是不能停止的,它没有 break 语句
-
map()方法(只可遍历数组)==>数组元素
是构造函数Array的原型上的函数,即
Array.prototype.map
,因此所有数组都可以用这个方法。返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。
let arr = [1,2,3]; let tt = arr.map(function(i){ console.log(i) return i*2; }) // [2,4,6]
-
for … of (es6新增) ==》数组元素
可以正确响应break、continue和return语句
let arr = ['name','age']; for(let i of arr){ console.log(i) } // name // age
可以用来遍历数组、字符串、Maps(映射)、Sets(集合)等可迭代的数据结构
-
数组项的其他遍历
https://www.cnblogs.com/cjh1996/p/12699512.html#scroller-12
1. 过滤:过滤数组成员,满足条件的成员组成一个新数组返回。
-
filter()方法 ==》数组元素
let arr = [1,2,3]; let tt = arr.filter(function(i){ return i>1; }) // [2,3]
是 Array 对象内置方法,返回新数组,新数组的元素通过过滤的元素,不改变原来的数组。
2. 类似断言:返回一个布尔值,表示判断数组成员是否符合某种条件。
它们接受一个函数作为参数,所有数组成员依次执行该函数。该函数接受三个参数:当前成员、当前位置和整个数组,然后返回一个布尔值
-
some()
只要一个成员的返回值是
true
,则整个some
方法的返回值就是true
且不再往下遍历,否则返回false
。代码中,如果数组`arr`有一个成员大于等于3,`some`方法就返回`true` var arr = [1, 2, 3, 4, 5]; arr.some( (elem, index, arr)=> { return elem >= 3; }); // true
-
every()
所有成员的返回值都是
true
,整个every
方法才返回true
,否则返回false
。代码中,数组`arr`并非所有成员大于等于`3`,所以返回`false`。 var arr = [1, 2, 3, 4, 5]; arr.every(function (elem, index, arr) { return elem >= 3; }); // false
注意:对于空数组,
some
方法返回false
,every
方法返回true
,回调函数都不会执行。
-
reduce()、reduceRight()、indexOf()、lastIndexOf()
数组去重
-
使用Set的构造函数
const set = new Set([1, 2, 3, 4, 4]); [...set] // [1, 2, 3, 4]
-
使用Array.from()将Set结构转为数组
function dedupe(array) { return Array.from(new Set(array)); } dedupe([1, 1, 2, 3]) // [1, 2, 3]
ES6新特性总结
-
新增了let、const关键字,用于声明只在块级作用域中起作用的变量
-
新增symbol基本数据类型
-
新增Set和Map数据结构
-
for…of遍历,可遍历具有iterator 接口的数据结构。
-
变量的解构赋值
一种新的变量赋值方式。常用于交换变量值、提取函数返回值、设置默认值
-
模板字符串
-
函数参数的默认值、箭头函数
-
super关键字
-
class定义类(class的继承)
-
promise对象
-
ES6模块(模块的循环加载)
其他小点
es5的继承和es6的继承的区别?
es5 的继承是通过原型或者是构造函数机制来实现
es6 用过 class 关键字定义类,里面有构造方法,类之间通过 extends 关键字实现,子类必须在 constructor 方法中调用 super 方法。
项目亮点
proxy代理
模块的循环加载
import()的按需加载
WeakSet存储dom节点,从而避免了内存泄露