十、Map数据结构
ECMAScript 6 中的 map 类型包含一组有序的键值对,其中键和值可以是任何类型。
键的比较结果由 Object.is() 来决定,所以你可以同时使用 5 和 "5" 做为键来存储,因为它们是不同的类型。
这和使用对象属性做为值的方法大相径庭,因为 **对象的属性会被强制转换为字符串类型**。
10.1 创建Map对象和Map的基本的存取操作
- Map创建也是使用Map构造函数
- 向Map存储键值对使用set(key, value);方法
- 可以使用get(key),来获取指定key对应的value
10.2 Map与Set类似的3个方法
- has(key) - 判断给定的 key 是否在 map 中存在
- delete(key) - 移除 map 中的 key 及对应的值
- clear() - 移除 map 中所有的键值对
10.3 初始化Map
创建Map的时候也可以像Set一样传入数组。但是传入的数组中必须存储的也是数组,而且每个数组中有两个元素,分别是键和值
也就是传入的实际是一个二维数组!
10.4 Map的forEach方法
十一、迭代器(iterator)和for...of循环
11.1 循环问题
var colors = ["red", "green", "blue"];
for (var i = 0, len = colors.length; i < len; i++) {
console.log(colors[i]);
}
上面的代码写起来简单,但是实际使用的过程中,我们需要自己去控制变量,如果有嵌套的情况下,还要控制多个变量,很容易出错。
迭代器就是为了解决这个问题的。
11.2 什么是迭代器
- 迭代器是一个对象
- 迭代器提供一个方法next() 这个方式总是能够返回迭代到的对象。
- next返回的对象中,至少有两个属性:done 是一个boolean值(表示数据是否迭代完)。 value:具体的数据(迭代到的具体数据)
迭代器只是带有特殊接口(方法)的对象。所有迭代器对象都带有 next() 方法并返回一个包含两个属性的结果对象。这些属性分别是 value 和 done,前者代表下一个位置的值,后者在没有更多值可供迭代的时候为 true 。迭代器带有一个内部指针,来指向集合中某个值的位置。当 next() 方法调用后,指针下一位置的值会被返回。
若你在末尾的值被返回之后继续调用 next(),那么返回的 done 属性值为 true,value 的值则由迭代器设定。该值并不属于数据集,而是专门为数据关联的附加信息,如若该信息并未指定则返回 undefined 。迭代器返回的值和函数返回值有些类似,因为两者都是返回给调用者信息的最终手段。
我们可以用ES5之前的知识手动创建一个迭代器:
function createIterator(items) {
var i = 0;
return {
next: function() {
var done = (i >= items.length);
var value = !done ? items[i++] : undefined;
return {
done: done,
value: value
};
}
};
}
//创建一个可以在指定数组上面迭代的迭代器对象。
var iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// for all further calls
console.log(iterator.next()); // "{ value: undefined, done: true }"
从以上的示例来看,根据 ECMAScript 6 规范模拟实现的迭代器还是有些复杂。
幸运的是,ECMAScript 6 还提供了生成器,使得迭代器对象的创建容易了许多。
11.3 生成器函数
生成器函数就是返回迭代器的函数!
生成器函数由 function 关键字和之后的星号(*)标识,同时还能使用新的 yield 关键字。
看下面代码:
迭代器函数也是函数,所以他可以像正常的函数一样调用,但是迭代器生成器函数会自动返回一个迭代器对象。
每调用一次迭代器的next方法,如果碰到 yield 都会返回一个迭代到的一个对象,然后停止执行,直到下次调用next方法,会从上次停止的地方继续执行。
//这个迭代器函数返回的迭代器可以迭代传入的数组中的所有元素。
function *createIterator(items) {
for (let i = 0; i < items.length; i++) {
//每调用一次next,碰到yild程序就会停止,并返回迭代到的对象 {value : items[i], done : true}
yield items[i];
}
}
let iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// 进一步调用
console.log(iterator.next()); // "{ value: undefined, done: true }"
注意:
- yield 关键字只能 直接用在生成器内部 。在其它地方甚至是生成器内部的函数中使用都会抛出语法错误。
11.4 生成器函数表达式
你可以使用函数表达式来创建生成器,只需在 function 关键字和圆括号之间添加星号(*)。例如:
let createIterator = function *(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
};
let iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// 进一步调用
console.log(iterator.next()); // "{ value: undefined, done: true }"
注意:无法使用箭头函数来创建生成器。
11.5 可迭代类型和for-of迭代循环
迭代器的主要工作就是迭代数据,但是不是所有的数据都是可以迭代的。
与迭代器紧密相关的是,可迭代类型是指那些包含 Symbol.iterator 属性(方法)的对象。
该 symbol 类型定义了返回迭代器的函数。在 ECMAScript 6 中,所有的集合对象(数组,set 和 map)与字符串都是可迭代类型,因此它们都有默认的迭代器。可迭代类型是为了 ECMAScript6 新添加的 **for-of** 循环而设计的。
换句话说,默认情况下只有 **数组、set、Map和字符串**才可以使用迭代器去迭代。 (也就可以使用for...of了)
for…of循环只迭代出来的元素,根本不管索引!不管索引!不管索引!重要的问题重复三遍!
使用 for…of 迭代数组:
使用 for…of 迭代Set:
使用 for…of 迭代Map:
使用for … of迭代字符串
注意:for...of 只能迭代可以迭代的对象,对于非可迭代对象使用for...of会抛出异常
说明:以数组为例。
for-of 循环首先会调用 values 数组的 Symbol.iterator 方法来获取迭代器(Symbol.iterator 方法由幕后的 JavaScript 引擎调用)。之后再调用 iterator.next() 并将结果对象中的 value 属性值,即 1,2,3,依次赋给 num 变量。当检测到结果对象中的 done 为 true,循环会退出,所以 num 不会被赋值为 undefined 。
如果你只想简单的迭代数组或集合中的元素,那么 for-of 循环比 for 要更好。for-of 一般不容易出错,因为要追踪的条件更少。所以还是把 for 循环留给复杂控制条件的需求吧。
11.6 访问可迭代类型的默认迭代器
Symbol.iterator是可迭代类型的一个方法,调用这个方法就可以获取到他的默认迭代器。
因为Symbol可以返回一个对象的默认迭代器,所以我们可以使用它来判断一个对象是否可迭代
11.7 自定义可迭代类型
开发者自定义的对象默认是不可迭代类型,但是你可以为它们创建 Symbol.iterator 属性并指定一个生成器来使这个对象可迭代。例如:
let collection = {
items: [],
*[Symbol.iterator]() {
for (let item of this.items) {
yield item;
}
}
};
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);
for (let x of collection) {
console.log(x);
}
十二、类
和大多数面向对象的语言(object-oriented programming language)不同,JavaScript 在诞生之初并不支持使用类和传统的类继承并作为主要的定义方式来创建相似或关联的对象。
这很令开发者困惑,而且在早于 ECMAScript 1 到 ECMAScript 5 这段时期,很多库都创建了一些实用工具(utility)来让 JavaScript 从表层上支持类。
尽管一些 JavaScript 开发者强烈主张该语言不需要类,但由于大量的库都对类做了实现,ECMAScript 6 也顺势将其引入。
12.1 ES5之前的模拟的类
在 ECMAScript 5 或更早的版本中,JavaScript 没有类。和类这个概念及行为最接近的是创建一个构造函数并在构造函数的原型上添加方法,这种实现也被称为自定义的类型创建,例如:
function PersonType(name) {
this.name = name;
}
PersonType.prototype.sayName = function() {
console.log(this.name);
};
let person = new PersonType("Nicholas");
person.sayName(); // 输出 "Nicholas"
console.log(person instanceof PersonType); // true
console.log(person instanceof Object); // true
说明:
前面的PersonType我们以前一直叫做构造函数,其实他就是一个类型,因为他确实表示了一种类型。
12.2 ES6中基本的类声明
在ES6直接借鉴其他语言,引入了类的概念。所以再实现上面那种模拟 的类就容易了很多。
//class关键字必须是小写。 后面就是跟的类名
class PersonClass {
// 等效于 PersonType 构造函数。
constructor(name) { //这个表示类的构造函数。constuctor也是关键字必须小写。
this.name = name; //创建属性。 也叫当前类型的自有属性。
}
// 等效于 PersonType.prototype.sayName. 这里的sayName使用了我们前面的简写的方式。
sayName() {
console.log(this.name);
}
}
let person = new PersonClass("Nicholas");
person.sayName(); // 输出 "Nicholas"
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"
说明:
- 自有属性:属性只出现在实例而不是原型上,而且只能由构造函数和方法来创建。在本例中,name 就是自有属性。我建议 尽可能的将所有自有属性创建在构造函数中,这样当查找属性时可以做到一目了然。
- 类声明只是上例中自定义类型的语法糖。PersonClass 声明实际上创建了一个行为和 constructor 方法相同的构造函数,这也是 typeof PersonClass 返回 "function" 的原因。sayName() 在本例中作为 PersonClass.prototype 的方法,和上个示例中 sayName() 和 PersonType.prototype 关系一致。这些相似度允许你混合使用自定义类型和类而不需要纠结使用方式。
虽然类和以前的使用构造函数+原型的方式很像,但是还是有一些不太相同的地方,而且要牢记
- 类声明和函数定义不同,类的声明是不会被提升的。类声明的行为和 let 比较相似,所以当执行流作用到类声明之前类会存在于暂存性死区(temporal dead zone)内。
- 类声明中的代码自动运行在严格模式下,同时没有任何办法可以手动切换到非严格模式。
- 所有的方法都是不可枚举的(non-enumerable),这和自定义类型相比是个显著的差异,因为后者需要使用 Object.defineProperty() 才能定义不可枚举的方法。
- 所有的方法都不能使用 new 来调用,因为它们没有内部方法 [[Construct]]。
- 不使用 new 来调用类构造函数会抛出错误。也就是 必须使用new 类() 的方式使用
- 试图在类的方法内部重写类名的行为会抛出错误。(因为在类的内部,类名是作为一个常量存在的)
12.2 匿名类表达式
函数有函数表达式,类也有类表达式。
类表达式的功能和前面的类的声明是一样的。
let PersonClass = class {
// 等效于 PersonType 构造函数
constructor(name) {
this.name = name;
}
// 等效于 PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
};
let person = new PersonClass("Nicholas");
person.sayName(); // 输出 "Nicholas"
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"
12.3 具名类表达式
let PersonClass = class PersonClass2{
// 等效于 PersonType 构造函数
constructor(name) {
this.name = name;
}
// 等效于 PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
};
注意:具名类表达式中PersonClass2这个类名只能在类的内部访问到,在外面是访问不到的.
12.4 作为一等公民的类型
在JavaScript中,函数是作为一等公民存在的。(也叫一等函数)。
类也是一等公民。
- 类可以作为参数传递
function createObject(classDef) {
return new classDef();
}
let obj = createObject(class {
sayHi() {
console.log("Hi!");
}
});
obj.sayHi(); // "Hi!"
- 立即调用类构造函数,创建单例
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}("Nicholas");
person.sayName(); // "Nicholas"
12.5 动态计算类成员的命名
类的成员,也可以像我们前面的对象的属性一样可以动态计算.( 使用[ ] 来计算)
let methodName = "sayName";
class PersonClass {
constructor(name) {
this.name = name;
}
[methodName]() {
console.log(this.name);
}
}
let me = new PersonClass("Nicholas");
me.sayName(); // "Nicholas"
12.6 静态成员
在ES5中,我们可以直接给构造函数添加属性或方法来模拟静态成员。
function PersonType(name) {
this.name = name;
}
// 静态方法。 直接添加到构造方法上。 (其实是把构造函数当做一个普通的对象来用。)
PersonType.create = function(name) {
return new PersonType(name);
};
// 实例方法
PersonType.prototype.sayName = function() {
console.log(this.name);
};
var person = PersonType.create("Nicholas");
在上面的create方法在其他语言中一般都是作为静态方法来使用的。
下面高能,请注意:
ECMAScript 6 的类通过在方法之前使用正式的 static 关键字简化了静态方法的创建。例如,下例中的类和上例相比是等效的:
class PersonClass {
// 等效于 PersonType 构造函数
constructor(name) {
this.name = name;
}
// 等效于 PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
// 等效于 PersonType.create。
static create(name) {
return new PersonClass(name);
}
}
let person = PersonClass.create("Nicholas");
注意:静态成员通过实例对象不能访问,只能通过类名访问!!!
通过和ES5模拟静态方法的例子你应该知道为啥了吧
12.7 ES6中的继承
在ES6之前要完成继承,需要写很多的代码。看下面的继承的例子:
12.7.1 继承的基本写法
如果在ES6通过类的方式完成继承就简单了很多。
需要用到一个新的关键字:extends
这种继承方法,和我们前面提到的构造函数+原型的继承方式本质是一样的。但是写起来更简单,可读性也更好。
关于super的使用,有几点需要注意:
- 你只能在派生类中使用 super(),否则(没有使用 extends 的类或函数中使用)一个错误会被抛出。
- 你必须在构造函数的起始位置调用 super(),因为它会初始化 this。任何在 super() 之前访问 this 的行为都会造成错误。也即是说super()必须放在构造函数的首行。
- 在类构造函数中,唯一能避免调用 super() 的办法是返回一个对象。
12.7.2 在子类中屏蔽父类的方法
如果在子类中声明与父类中的同名的方法,则会覆盖父类的方法。(这种情况在其他语言中称之为 方法的覆写、重写 )
如果在子类中又确实需要调用父类中被覆盖的方法,可以通过super.方法()来完成。
注意:
- 如果是调用构造方法,则super不要加点,而且必须是在子类构造方法的第一行调用父类的构造方法
- 普通方法调用需要使用super.父类的方法() 来调用。
12.7.3 静态方法也可以继承
十三、Babel
到目前2017年为止,也不是所有的浏览器都支持ES6的特性。
所以我们需要把ES6转换成ES5的代码,就要用到所谓的转码器。Babel就是目前使用最广泛的把ES6代码转换成ES5及以前代码的转码器。
有了babel我们就可以放心的使用ES6的最新的语法,而不用担心浏览器不支持了!!!
为了方便使用,我直接使用
webstorm
的filewatcher
功能
13.1 安装Babel
首先要保证电脑上已经安装了npm
使用如下命令就可以安装babel安装(我们这里使用的是全局安装):
npm install -g babel-cli
13.2 给你当前项目配置filewatcher
[图片上传失败...(image-f74234-1636424176194)]
13.3 安装babel-present-env
npm install babel-preset-env --save-dev
13.4 自动转换为es5代码
js文件中的es6代码会自动转换为es5代码
[图片上传失败...(image-cd9db4-1636424176195)]
十四、Moudle
JavaScript 采用 “共享一切” 的代码加载方式是该语言中最令人迷惑且容易出错的方面之一。
其它语言使用包(package)的概念来定义代码的作用范围,然而在 ECMAScript 6 之前,每个 JavaScript 文件中定义的内容都由全局作用域共享。
当 web 应用变得复杂并需要书写更多的 JavaScript 代码时,上述加载方式会出现命名冲突或安全方面的问题。
ECMAScript 6 的目标之一就是解决作用域的问题并将 JavaScript 应用中的代码整理得更有条理,于是模块应运而生。
很不幸的是:目前,所有的浏览器都还不能支持ES6的模块。只能通过第三方的工具转成ES5的代码
14.1 什么是模块
模块是指采取不同于现有加载方式的 JavaScript 文件(与 script 这种传统的加载模式相对)。这种方式很有必要,因为它和 script 使用不同的语义:
- 模块中的代码自动运行在严格模式下,并无任何办法修改为非严格模式。
- 模块中的顶级(top level)变量不会被添加到全局作用域中。它们只存在于各自的模块中的顶级作用域。
- 模块顶级作用域中的 this 为 undefined 。
- 模块不允许存在 HTML 式的注释(JavaScript 历史悠久的遗留特性)。
- 模块必须输出可被模块外部代码使用的相关内容。
- 一个模块可以引入另外的模块。
14.2 导出模块
可以使用 export 关键字来对外暴露模块中的部分代码。
一般情况下,可以在任何变量,函数或类声明之前添加这个关键字来输出它们,
看下面的代码:
声明一个文件:a.js 代码如下
// 输出变量
export var color = "red";
export let name = "Nicholas";
export const magicNumber = 7;
// 输出函数
export function sum(num1, num2) {
return num1 + num1;
}
// 输出类
export class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
}
// 该函数没有使用export关键字 所以该函数是模块私有的。也就是说只能在当前文件访问,出了这个文件就访问不到
function subtract(num1, num2) {
return num1 - num2;
}
// 定义一个函数...
function multiply(num1, num2) {
return num1 * num2;
}
// 可以把这个函数的引用导出。 和导出函数是一样的。
export { multiply };
注意:在上面的代码中,除了exprot关键字,其他和我们以前的代码没有任何不同。
14.3 引入模块
一旦有了导出内容的模块,则可以在另一个模块中使用import关键字来获取他们。
引入模块的语法:
import { identifier1, identifier2 } from "./a.js";
import 之后的花括号表示从模块中引入的绑定。from 关键字表示从哪个模块引入这些绑定。模块由一个包含模块路径的字符串表示(称为模块指示符,module sepcifier)。浏览器中的