面试二 es6新特性

let 命令和const命令

let命令

新增了let和const命令,用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。

  • 变量提升:let声明的变量不存在变量提升(只可以先声明,在使用)

  • 在相同的作用域不能重复声明同一个变量

  • 暂时性死区(temporal dead zone,简称 TDZ):只要块级作用域存在let,它所声明的变量就会被绑定在这个区域,不在受外部影响

    var tmp = 123;
    if (true) {
      tmp = 'abc'; // ReferenceError
      let tmp;
    }
    

    如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

    if (true) {
      // TDZ开始
      tmp = 'abc'; // ReferenceError
      console.log(tmp); // ReferenceError
    
      let tmp; // TDZ结束
      console.log(tmp); // undefined
    
      tmp = 123;
      console.log(tmp); // 123
    }
    

    ES6 规定暂时性死区和letconst语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为

暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

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/catchcatch分句会创建一个块作用域,其中声明的变量只可以在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,所以也就不能用作构造函数。

不适合使用箭头函数的场景

  1. 定义对象的方法时,且该方法内部包含this时,这个时候不适合使用箭头函数
  2. 需要动态this的时候,也不适合使用箭头函数

对象扩展

属性的可枚举性

对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。

let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
//  {
//    value: 123,
//    writable: true,
//    enumerable: true,
//    configurable: true
//  }

描述对象的enumerable属性,称为“可枚举性”,如果该属性为false,就表示某些操作会忽略当前属性。

目前,有四个操作会忽略enumerablefalse的属性。

  • for...in循环:只遍历对象自身的和继承的可枚举的属性。
  • Object.keys():返回对象自身的所有可枚举的属性的键名。
  • JSON.stringify():只串行化对象自身的可枚举的属性。
  • Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。

前三个是 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...infor...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 有两个区别。

  1. 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]}
    
  2. 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构造函数的参数。这就是说,SetMap都可以用来生成新的 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就是一个键

    undefinednull也是两个不同的键

    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 元素'],
];

上面代码中,e1e2是两个对象,我们通过arr数组对这两个对象添加一些文字说明。这就形成了arre1e2的引用。

一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放e1e2占用的内存。

// 不需要 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对象的特点:

  1. 对象的状态不受外界的干扰

    只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

  2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。

promise对象的缺点:

  1. 无法取消Promise,一旦新建它就会立即执行,无法中途取消。

  2. 如果不设置回调函数(使用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 会吃掉错误”。

  3. 当处于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方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。其中,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接口的场合

  1. for…of循环遍历

  2. 解构赋值

    对数组和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'];
    
  3. 扩展运算符...

    // 例一
    var str = 'hello';
    [...str] //  ['h','e','l','l','o']
    // 例二
    let arr = ['b', 'c'];
    ['a', ...arr, 'd']
    // ['a', 'b', 'c', 'd']
    

    只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。

    let arr = [...iterable];

  4. 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 }
    
  5. 其他场合

    由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。

    • 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的区别

  • 相同点:

    都能返回紧跟在语句后面的那个表达式的值

  • 不同点:

    1. 每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能
    2. 一个函数里面,只能执行一次(或者说一个)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表达式本身没有返回值,或者说总是返回undefinednext方法可以带一个参数,该参数就会被当作上一个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

  • 类的所有实例共享一个原型对象。

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

  • 与函数一样,类也可以使用表达式的形式定义。

不同点:

  • 定义“类”的方法的时候,前面不需要加上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;
}

私有方法和私有属性

https://es6.ruanyifeng.com/#docs/class#%E7%A7%81%E6%9C%89%E6%96%B9%E6%B3%95%E5%92%8C%E7%A7%81%E6%9C%89%E5%B1%9E%E6%80%A7

私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问

这是常见需求,有利于代码的封装,但 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

    1. 下载require.js
    2. 引入
    3. 加载模块:require(['foo'], function(foo) {... })使用require开始加载执行模块的代码
    4. 定义模块:如果一个模块不依赖其他,那么直接使用define(function)即可
  • CMD(即Common Module Definition通用模块定义)

    SeaJS就是CMD的优秀实现方案

    1. 下载SeaJS

    2. 引入

      // 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 引擎静态分析,先于模块内的其他语句执行

importexport命令只能在模块的顶层,不能在代码块之中(比如,在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 模块使用importexport

要想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方法返回falseevery方法返回true,回调函数都不会执行。

reduce()、reduceRight()、indexOf()、lastIndexOf()

数组去重

  1. 使用Set的构造函数

    const set = new Set([1, 2, 3, 4, 4]);
    [...set]
    // [1, 2, 3, 4]
    
  2. 使用Array.from()将Set结构转为数组

    function dedupe(array) {
      return Array.from(new Set(array));
    }
    dedupe([1, 1, 2, 3]) // [1, 2, 3]
    

ES6新特性总结

  1. 新增了let、const关键字,用于声明只在块级作用域中起作用的变量

  2. 新增symbol基本数据类型

  3. 新增Set和Map数据结构

  4. for…of遍历,可遍历具有iterator 接口的数据结构。

  5. 变量的解构赋值

    一种新的变量赋值方式。常用于交换变量值、提取函数返回值、设置默认值

  6. 模板字符串

  7. 函数参数的默认值、箭头函数

  8. super关键字

  9. class定义类(class的继承)

  10. promise对象

  11. ES6模块(模块的循环加载)

其他小点

es5的继承和es6的继承的区别?

es5 的继承是通过原型或者是构造函数机制来实现

es6 用过 class 关键字定义类,里面有构造方法,类之间通过 extends 关键字实现,子类必须在 constructor 方法中调用 super 方法。

项目亮点

proxy代理

模块的循环加载

import()的按需加载

WeakSet存储dom节点,从而避免了内存泄露

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页