前序 ES6的新语法

学习vue的语法之前,首先要掌握一些ES6的新语法,以便更容易理解vue中的一些编程风格。

1. 变量与常量的声明

1.1 let关键字
1.1.1 let基本语法规则

ES6推荐使用let声明局部变量,相比之前的var(无论声明在何处,都会被视为声明在函数的最顶部)
let和var声明的区别可以理解为类似全局变量与局部变量的区别。

var x = '全局变量';
{
  let x = '局部变量';
  console.log(x); // 局部变量
}
console.log(x); // 全局变量

使用let声明的变量绑定其所在的代码块,在代码块之外的地方无法访问和使用

{
  var x = 1;
  let y = 2;
}
console.log(x); // 正常显示1
console.log(y); //控制台报错:y is not defined
1.1.2 变量提升

如果使用var关键字声明变量,如下代码执行不会报错,但会得到undefined结果。

console.log(x);  //显示undefined
var x = 5;

此种情况被称为变量提升,可以理解为代码在执行时实际上是

var x;
console.log(x); 
x = 5;

这样做虽然方便但不符合一般编程语言的习惯,变量提升允许一个变量未声明即可使用。
另一个变量提升的示例

var tmp = new Date();
function f() {
  console.log(tmp);  //显示为undefined
  if (false) {
    var tmp = "hello world";  
  }
}
f(); 

在调用f()函数时,函数内部在下方声明了tmp,因此产生了变量提升的情况,先打印的tmp变为undefined
使用let声明变量时,不支持变量提升
上述代码如果更改为let声明,控制台会报错,提示变量x没有定义(x is not defined

1.1.3 暂时性死区

只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
如果同时使用var和let进行变量声明,在代码块内部let变量的优先级要高于var变量。
这与一般编程语言局部变量优先级高于全局变量的理解是一致的。

var x = 1;
if(true){
  x = 3;  //此处x指的是使用let声明的局部变量。
  let x;
}

上面代码中,存在全局变量x,但是块级作用域内let又声明了一个局部变量x,导致后者绑定这个块级作用域,所以在let声明变量前,对x赋值会报错(x is not defined)。

1.2 const关键字

const表示声明常量,与let命令一致,也是一个块级作用域命令。const的功能与Java编程语言中的final语法类似。
const 声明的变量都会被认为是常量,意思就是它的值被设置完成后就不能再修改了

const x = 1
x = 0 //报错

如果const的是一个对象,对象所包含的值是可以被修改的,不可改变的是对象的内存地址。

const x = { name: 'cc' }
x.name = '赵四';// 不报错
x = { name: '赵四' };// 报错

x.name = '赵四';不报错的的原因是只更改了对象的内容,对象的内存地址没有改变
x = { name: '赵四' };报错的原因是该句代码相当于重新在内存新建了一个对象赋值给x。
一般情况下,遵守编码规范应将常量名设置为全部大写


总结

  • let 命令声明的变量不具备变量提升特性
  • let 和 const 是块级作用域命令,只在其声明所在的代码块中有效
  • const 命令声明常量时必须赋值

2. 模板字符串

在ES6之前,通过拼接字符串的方式来构建文本输出模板
比如:

let msg = {
  name: '赵四',
  age: 32,
  job: '亚洲舞王'
};
var div = document.getElementById("divx");
div.innerHTML = "姓名: "+msg.name+"<br>年龄: "+msg.name+"<br>职业: "+msg.name+"<br>";

ES6使用反引号:``,配合EL语法${变量名}完成字符串拼接
上述示例使用ES6语法模板字符串完成如下

let msg = {
  name: '赵四',
  age: 32,
  job: '亚洲舞王'
};
var div = document.getElementById("divx");
div.innerHTML =`姓名: ${msg.name}<br>年龄: ${msg.name}<br>职业:${msg.name}<br>`;

总结

  • 模板字符串使用反引号(`)表示字符串,不是引号
  • 模板字符串使用EL语法${...}引用变量值替代原有的拼接方式

3. 函数声明与传参

3.1 参数默认值

在ES6之前,如果函数的参数在未参入参数时提供默认值,需要如下方式实现

function printText(text) {
    text = text || 'default';  //如果text为传入数据,则默认设置为default
    console.log(text);
}

ES6支持在函数声明时直接指定参数默认值,上述示例修改如下

function printText(text = 'default') {
    console.log(text);
}
3.2 Spread / Rest 操作符

Spread / Rest 操作符指的是...,具体是 Spread 还是 Rest 需要看代码的上下文。可以在函数调用/数组构造时, 将数组表达式或者string在语法层面展开;还可以在构造对象时, 将对象表达式按key-value的方式展开。

3.2.1 Spread

...被用于函数实参时,它是一个 Spread 操作符。

let a = [1,2,3];
function f(x,y,z) {
    console.log(x,y,z);
}
f(...a);
3.2.2 Rest

...被用于函数形参时,它是一个 Rest 操作符

function f(...x) {
    console.log(x);
}
f(1,2,3,4,5);
3.3 箭头函数

ES6中允许使用箭头=>定义函数
常规方式定义函数

var f1 = function (){
  return 5;
}

var f2 = function(a, b){
  return a+b;
}

var f3 = function(a, b){
  console.log(a);
  console.log(b);
  return a+b;
}

箭头函数

var f1 = () => 5;
var f2 = (a,b) => a+b;
var f3 = (a,b) => {
  console.log(a);
  console.log(b);
  return a+b;
}
  • 箭头函数如果只有一行代码,=>后直接书写即可
  • 箭头函数如果包含多行代码,=>后编写{},在代码块中完成函数代码

由于{}会被解析为代码块,如果箭头函数返回的结果是一个对象,需要使用()将对象的{}括起来

var f = (a,b) => ({name : a, age : b}); 

除此之外,箭头函数在使用中需要注意

  • 函数体内的this对象,是定义时所在的对象,而不是使用时所在的对象。此项在vue开发中十分关键,vue中箭头函数的this对象是window,而常规函数的this对象是VUE对象
  • 不可以当作构造函数,不可以使用new命令,否则抛出错误。
  • 不可以使用arguments对象,如果需要使用类似功能,可以用Rest参数代替。

由此可见,箭头函数更适合一些逻辑简单,只有少量代码的函数编写。如果逻辑复杂代码量较大,相对常规函数,箭头函数并没有优势。


总结

  • 函数声明时直接对形参进行赋值,即相当于对该形参设置默认值
  • ...被用于函数实参时,它是一个Spread 操作符
  • ...被用于函数实参时,它是一个 Rest 操作符
  • =>函数可以简化函数声明,但它有一些需要注意的事项,比如this对象的指向

4. 二进制与八进制

ES6 支持二进制和八进制的字面量。

4.1 二进制数据赋值

通过在数字前面添加0b或者0B表示二进制值

let bValue = 0b10; 
console.log(bValue); // 2
4.2 八进制数据赋值

通过在数字前面添加0o或者0O表示八进制值

let oValue = 0o10;
console.log(oValue); // 8

总结

  • 使用0b或者0B表示二进制数据
  • 使用0o或者0O表示二进制数据

5. 对象与数组的解构

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。

5.1 数组的解构

数组的元素是按次序排列的,变量的取值由它的位置决定,因此数组的解构是位置相同才能控制。

5.1.1 数组的解构

比如常规声明变量可以用如下方式

let a = 1;
let b = 2;
let c = 3;

也可以利用解构方式声明

let[a,b,c] = [1,2,3]
5.1.2 函数默认参数中的数组解构

使用解构处理函数数组类型参数

function getRectDesc([width = 5, height = 5]) {
  return `矩形的大小是:${width} x ${height}`;
}

此时调用函数可以用如下方式

getRectDesc([]); // 矩形的大小是:5 x 5
getRectDesc([2]); // 矩形的大小是:2 x 5
getRectDesc([2, 3]); // 矩形的大小是:2 x 3
getRectDesc([undefined, 3]); // 矩形的大小是:5 x 3

getRectDesc函数预期传入的是数组。它通过解构将数组中的第一项设为 width,第二项设为 height。如果数组为空,或者只有一项,那么就会使用默认参数,并将缺失的参数设为默认值 5。
但此时有个问题,如果方法调用时没有提供数组类型的实参,执行时控制台会出现异常。

getRectDesc();  // 控制台抛出异常

因为 getRectDesc() 预期传入的是数组,然后对其进行解构。因为函数被调用时没有传入数组,所以出现问题。
此时解决办法是继续使用默认的函数参数值

function getRectDesc([width = 5, height = 5] = []) {
  return `矩形的大小是:${width} x ${height}`;
}
getRectDesc(); // 矩形的大小是:5 x 5

默认的数组没有提供任何值,此时解构数组使用默认的width和height,所有结果是5x5

5.1.3 箭头函数中的数组解构

上一小节中的示例也可以使用箭头函数来实现。

var getRectDesc = ([width = 5, height = 5] = []) => `矩形的大小是:${width} x ${height}`;
5.2 对象的解构

对象的解构与数组的解构不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性名相同,才能取到正确的值。

5.2.1 对象的解构

对象的解构取决于变量与属性名相同,与顺序无关。

let {job, name} = { name:"赵四", bar:"亚洲舞王"};
console.log(name); //赵四
console.log(job);  //亚洲舞王

如果使用的变量名在属性中不存在,则变量没有赋值,为undefined

let {age, name} = { name:"赵四", bar:"亚洲舞王"};
console.log(name); //赵四
console.log(age);  //undefined
5.2.2 函数默认参数中的对象解构

使用解构处理函数对象类型参数

function createArtist({sname = '赵四', job = '亚洲舞王'}){
    return `${sname}的职业是${job}`;
}

此时调用函数可以用如下方式

createArtist({}); //赵四的职业是亚洲舞王
createArtist({sname:'刘能'});  //刘能的职业是亚洲舞王
createArtist({job: '喜剧演员'}); //赵四的职业是喜剧演员
createArtist({sname:'刘能',job:'喜剧演员'});  //刘能的职业是喜剧演员

createArtist函数预期传入的是对象。它通过解构将对象中的属性sname和job。如果对象为空,或者只有其中一个属性赋值,那么就会使用默认参数,并将缺失的参数设为对应的默认值。
但此时有个问题,如果方法调用时没有提供对象类型的实参,执行时控制台会出现异常。

createArtist();  // 控制台抛出异常

因为 createArtist() 预期传入的是对象,然后对其进行解构。因为函数被调用时没有传入对象,所以出现问题。
此时解决办法是继续使用默认的函数参数值

function createArtist({sname = '赵四', job = '亚洲舞王'} = {}){
    return `${sname}的职业是${job}`;
}
createArtist(); //赵四的职业是亚洲舞王

默认的对象没有提供任何值,此时解构对象使用默认的sname和job,所有结果是“赵四的职业是亚洲舞王”

5.2.3 箭头函数中的对象解构

上一小节中的示例也可以使用箭头函数来实现。

var createArtist = ({sname = '赵四', job = '亚洲舞王'} = {}) => `${sname}的职业是${job}`;

总结

  • 数组进行解构操作时,依赖的关键是数据的位置
  • 对象进行解构操作时,依赖的关键是属性的名字

6. 循环与遍历

6.1 for...in

for...in可以用于遍历数组的下标

let a = ['a', '123', {x: 1, y: 2}];
for(let key in a){
    console.log(key);  //0,1,2
}

for...in可以用于遍历对象的属性名

let a = {name:'赵四',job:'亚洲舞王', age: 23};
for(let key in a){
    console.log(key);//name,job,age
}
6.2 for...of

for...of用于遍历数组的数值

let a = ['a', '123', {x: 1, y: 2}];
for(let value of a){
    console.log(value);  //a, 123, {x:1, y:2}
}

for...in不能用于遍历对象的属性名,控制台会报错a is not iterable

let a = {name:'赵四',job:'亚洲舞王', age: 23};
for(let value of a){
    console.log(value);//报错! a is not iterable
}

总结

  • for...in 可以用于数组对象的遍历,得到类似于key的数据:数组的下标和对象的属性名
  • for...of 可以用于数组的遍历,得到数组中的数值,功能类似于Java语法中的for-each句式
  • for...of 不能用于对象数据的遍历

7. 类的声明与继承

7.1 类的声明
7.1.1 ES5标准中的类声明

在ES5标准中没有明确的关键字支持类,需要利用原型链来完成类的实现。
如下示例是一个关于鸟类的ES5实现

function Bird(name){
    this.name = name;
}
Bird.prototype.fly = function (){
    console.log(this.name+"正在飞");
}
var b = new Bird("燕子");
b.fly();  //燕子正在飞

其中name表示类中的一个属性,fly为类中的一个常规方法。需要将fly方法挂载在prototype上完成类的实现。

7.1.2 ES6标准中的类声明

ES6 中支持 class 语法,不过,ES6的class不是新的对象继承模型,它只是原型链语法糖表现形式。
上一小节中的示例使用ES6实现

class Bird{
    constructor(name) {
        this.name = name;
    }
    fly(){
        console.log(`${this.name}正在飞`);
    }
}
var b = new Bird("燕子");
b.fly();  //燕子正在飞

此处使用了class关键字声明类,constructor关键字声明构造方法,直接定义了fly方法
需要强调的是:虽然写法更接近面向对象式编程,但这些仅仅是原型链方式的语法糖,本质上在底层实现上没有变化。

7.2 构造方法

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。
Java语言的语法类似,一个类必须有constructor方法,如果没有显式定义,系统会提供默认的空构造方法。

constructor() {}

构造方法默认返回实例对象(即this),完全可以指定返回另外一个对象。

class Bird{
  constructor() {
    return Object.create(null);
  }
}
new Bird() instanceof Bird// false

类的构造函数,必须通过创建对象的new关键字调用。这是它跟普通方法的一个主要区别,后者不用new也可以执行。

class Bird{
  constructor() {
    return Object.create(null);
  }
}
Bird();  //控制台抛出异常
7.3 对象

与ES5一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。

class Bird{
  constructor(name) {
    this.name = name;
  }
  fly() {
    console.log(`${this.name}正在飞`);
  }
}
var b = new Bird("燕子");
b.fly() // 燕子正在飞
console.log(b.hasOwnProperty('name')); // true
console.log(b.hasOwnProperty('fly')); // false
console.log(b.__proto__.hasOwnProperty('fly')); // true

上面代码中,name是实例对象point自身的属性(因为定义在this变量上),
所以hasOwnProperty方法返回true,而toString是原型对象的属性(因为定义在Point类上),所以hasOwnProperty方法返回false。
由此验证,类的所有实例共享一个原型对象,普通方法实际上是定义在原型上,被所有实例共享。

var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__ //true

进一步推导出,如果修改对象p1的原型,同时会影响到p2,因为两者共享同一个原型对象

p1.__proto__.printName = function () { return 'Oops' }; //为p1对象的原型新增一个printName函数
p1.printName() // "Oops"  
p2.printName() // "Oops"

var p3 = new Point(4,2);
p3.printName() // "Oops"

可以看到当为p1对象的原型增加一个printName方法时,其余对象均可以使用这个方法。
这意味着,使用实例的__proto__属性改写原型,必须相当谨慎,不推荐使用,因为这会改变class的原始定义,影响到所有实例。

如果将方法绑定在构造方法中,可使方法绑定在this变量上而不是原型对象上

class Bird{
  constructor(name) {
    this.name = name;
    this.fly = function (){
      console.log(`${this.name}正在飞`);
    }
  }
}
var b = new Bird("燕子");
b.fly() // 燕子正在飞
console.log(b.hasOwnProperty('name')); // true
console.log(b.hasOwnProperty('fly')); // true
console.log(b.__proto__.hasOwnProperty('fly')); // false
7.4 继承

Java语法中关于继承的操作类似,ES6中允许使用extends关键字完成继承

class Bird{
  constructor(name) {
    this.name = name;
  }
  fly(){
    console.log(`${this.name}正在飞`);
  }
  eat(){
    console.log(`${this.name}正在吃`);
  }
}
class Duck extends Bird{
    constructor(name, color) {
        super(name);
        this.color = color;
    }
    swim(){
        console.log(`${this.color}的${this.name}正在游泳`);
    }
    fly(){
        super.fly();
        console.log(`但${this.name}飞的不远`);
    }
    
}
var b = new Bird("燕子");
var d = new Duck("大黄鸭","黄色");
b.eat(); //燕子正在吃
b.fly(); //燕子正在飞
d.eat(); //大黄鸭正在吃
d.fly();  //大黄鸭正在飞  但大黄鸭飞的不远
d.swim();  //黄色的大黄鸭正在游泳

继承的特点是:

  • extends 允许一个子类继承父类,需要注意的是,子类的构造方法中第一句需要执行 super() 函数。
  • 子类可以继承父类的属性和方法并直接调用使用,子类可以扩展自己的属性和方法
  • 子类可以通过super关键字在子类方法中调用父类的方法,比如子类fly方法中的super.fly()
  • 子类可以重写父类的方法,比如Duck类中的fly方法

可以看到很多语法规则与Java语言的继承语法几乎一致

7.5 静态

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”

7.5.1 静态方法

ES6中使用static关键字描述静态方法

class Bird{
  constructor(name) {
    this.name = name;
  }
  fly(){
    console.log(`${this.name}正在飞`);
  }
  static show(){
    console.log('鸟类拥有吃的技能');
  }
}
var b = new Bird("燕子");
b.fly();  //燕子正在飞
Bird.show(); //鸟类拥有吃的技能
b.show();  //控制台报错   b.show is not a function

需要注意的是

  • 静态方法中不能使用this关键字访问属性,其this指向的是鸟类的原型,也就是Bird。
  • 子类可以继承父类的静态方法,也可以进行重写,但无论如何,都不能使用对象去调用。
7.5.2 静态属性

ES6中无法使用static关键字设置静态属性,只能通过类名.静态属性名方式声明

class Bird{
  ...
}
Bird.number = 100;
console.log(Bird.number);  //100

总结

  • ES6关于类的操作实际上是一种语法糖,本质上底层实现依然依赖原型
  • constructor表示构造方法,其语法规则与普通方法相同,但只能通过创建对象时,使用关键字new调用
  • 普通方法的定义不用显式写明挂载类的原型上,但本质没有变化
  • 关于对象的一系列操作,与ES5相比没有变化
  • 继承的关键字是extends,规则与Java语法中的继承几乎一致。
  • 子类的构造方法第一句必须调用父类构造方法super()
  • 子类可以继承父类的属性方法并可以在子类的内部和子类的对象中使用
  • 子类可以重写父类的方法,优先执行子类重写后的方法

8. 模块化

8.1 使用模块化的必要

历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的require、Python 的import,甚至就连 CSS 都有@import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。

这是最原始的 JavaScript 文件加载方式,如果把每一个文件看做是一个模块,那么他们的接口通常是暴露在全局作用域下,也就是定义在 window 对象中,不同模块的接口调用都是一个作用域中,一些复杂的框架,会使用命名空间的概念来组织这些模块的接口

<script src="module1.js"></script>
<script src="module2.js"></script>
<script src="libraryA.js"></script>

这种原始的加载方式暴露了一些显而易见的弊端

  • 全局作用域下容易造成变量冲突
  • 文件只能按照<script> 的书写顺序进行加载
  • 开发人员必须主观解决模块和代码库的依赖关系
  • 在大型项目中各种资源难以管理,长期积累的问题导致代码库混乱不堪
  • window对象仅存在浏览器客户端,如此加载无法支持跨平台

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 AMD 和 CMD 两种。前者用于浏览器,后者用于服务器。

8.2 模块系统的演进
8.2.1 AMD

对于依赖的模块,AMD 是提前执行。
AMD 的 API 默认是一个当多个用,require 分全局 require 和局部 require,都叫 require。

define("module", ["dep1", "dep2"], function(d1, d2) {// 依赖必须一开始就写好
  return someExportedValue;
});
require(["module", "../file"], function(module, file) { /* ... */ });

优点:

  • 适合在浏览器环境中异步加载模块
  • 可以并行加载多个模块

缺点:

  • 提高了开发成本,代码的阅读和书写比较困难,模块定义方式的语义不顺畅
  • 不符合通用的模块化思维方式,是一种妥协的实现
8.2.2 CMD

对于依赖的模块,CMD 是延迟执行。CMD 推崇 as lazy as possible。
CMD 的 API 严格区分,推崇职责单一,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。

define(function(require, exports, module) {
  var $ = require('jquery');
  var Spinning = require('./spinning');// 依赖可以就近书写
  exports.doSomething = ...
  module.exports = ...
})

优点:

  • 依赖就近,延迟执行
  • 可以很容易在 Node.js 中运行

缺点:

  • 依赖 SPM 打包,模块的加载逻辑偏重
8.3 ES6的模块指令

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CMD 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
其具有以下特点

  • 静态加载模块,效率比CMD模块的加载方式高
  • ES6 模块是编译时加载,使得静态分析成为可能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
  • 不再需要通用模块(UMD)定义,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点
  • 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。
  • 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。
  • 支持严格模式

模块功能主要由两个命令构成:exportimport

  • export命令用于规定模块的对外接口。
  • import命令用于输入其他模块提供的功能

在chrome中可以通过输入chrome://flags/指令打开实验室,将Experimental JavaScript变为enabled
如果在html中引入模块化的js文件,需要设置

<script type="module" src="....">
8.3.1 export命令

export命令用于规定模块的对外接口
下面代码是dancer.js文件,保存了用户信息。ES6 将其视为一个模块,里面用export命令对外部输出了三个变量。

// dancer.js
export var name = '赵四';
export var job = '亚洲舞王';
export var age = 42;

另外一种写法,dancer.js文件,用export命令对外部输出了三个变量组成的对象。

// dancer.js
var name = '赵四';
var job = '亚洲舞王';
var age = 42;
export{name,job,age};

export命令可以输出函数,允许同时输出多个函数
下面示例输出了compute.js的multiply和addition函数

//compute.js
export function multiply(x, y) {
  return x * y;
};
export function addition(x, y) {
    return x + y;
}

对输出函数进行重命名,使用as关键字进行重命名

//compute.js
function multiply(x, y) {
  return x * y;
};
function addition(x, y) {
    return x + y;
}
export {
    multiply as cheng,
    addition as jia
}

export命令可以输出类
下方示例输出bird.js中的Bird类

//bird.js
export class Bird{
  constructor(name) {
    this.name = name;
  }
  fly() {
    console.log(`${this.name}正在飞`);
  }
}

export default 命令
很多时候,一个js文件中可能输出很多内容,为了方便使用者import,可以使用export default指定默认的输出内容
下方示例test.js中默认export的匿名函数

//test.js
export default function  () {
  console.log('foo');
}

也可以指定非匿名函数
下方示例test.js中默认export的foo函数

//test.js
export default function foo() {
  console.log('foo');
}

//test.js
function foo() {
  console.log('foo');
}
export default foo;
8.3.2 import命令

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块
import对象或变量

// 加载dancer.js
import { name, job, age } from './dancer.js‘
console.log(`${name},${job},${age}`);

利用as创建别名

// 加载dancer.js
import { name, job, age as nl } from './dancer.js‘
console.log(`${name},${job},${nl}`);

import命令加载函数

import {multiply} from './compute.js'
console.log(multiply(3,4));

import命令加载类

//main.js
import {Bird} from './bird.js'
var b = new Bird('燕子');
b.eat();

import命令具有提升效果,会提升到整个模块的头部,首先执行
下面的代码不会报错,因为import的执行早于foo的调用。这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。

foo();
import { foo } from './test.js';

import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
下面三种写法都会报错,因为它们用到了表达式、变量和if结构。在静态分析阶段,这些语法都是没法得到值的。

// 报错
import { 'f' + 'oo' } from ‘./test.js';
// 报错
let module = ‘./test.js';
import { foo } from module;

// 报错
if (x === 1) {
  import { foo } from './test1.js';
} else {
  import { foo } from './test2.js';
}

import语句是 Singleton 模式。次重复执行同一句import语句,那么只会执行一次,而不会执行多次

import { name} from './dancer.js';
import { age } from './dancer.js';

// 等同于
import { name, age } from './dancer.js';

除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。

// circle.js
export function area(radius) {
  return Math.PI * radius * radius;
}
export function round(radius) {
  return 2 * Math.PI * radius;
}

逐一加载

// main.js逐一指定要加载的方法
import { area, round} from './circle.js';
console.log('圆面积:' + area(4));
console.log('圆周长:' + round(14));

整体加载

//main.js整体加载
import * as circle from './circle.js';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.round(14));

总结

  • 模块化是支持Javascript能够完成大型复杂结构项目的基石
  • ES6中通过exportimport指令完成模块的导出和引入
  • export命令可以导出变量,对象,函数和类
  • import命令用于引入导出的变量,对象,函数和类
  • exportimport属于静态操作,在编译时完成,不能使用运行时语法(比如变量,条件控制等)
  • import命令具有提升效果,会提升到整个模块的头部,首先执行

9. Promise对象

Promise 对象用于一个异步操作的最终完成(或失败)及其结果值的表示。简单点说,它就是用于处理异步操作的,异步处理成功了就执行成功的操作,异步处理失败了就捕获错误或者停止后续操作。

9.1 使用Promise对象的优势

接触过Node的人都知道,Node是以异步(Async)回调著称的,其异步性提高了程序的执行效率,但同时也减少了程序的可读性。如果我们有几个异步操作,并且后一个操作需要前一个操作返回的数据才能执行,这样按照Node的一般执行规律,要实现有序的异步操作,通常是一层加一层的回调函数嵌套下去,这种情况被称为回调地狱

回调地狱

Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更强大。所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。Promise提供统一的API,各种异步操作都可以用同样的方法进行处理。
有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

9.2 Promise三种操作的状态

对于Promise对象来说,它也有三种状态:

  • pending:初始状态,也称为未定状态,就是初始化Promise时,调用executor执行器函数后的状态。
  • fulfilled:完成状态,意味着异步操作成功。
  • rejected:失败状态,意味着异步操作失败。

它只有两种状态可以转化,即

  • 操作成功:pending -> fulfilled
  • 操作失败:pending -> rejected
    并且这个状态转化是单向的,不可逆转,已经确定的状态(fulfilled/rejected)无法转回初始状态(pending)。
9.3 Promise的使用
const promise = new Promise((resolve, reject) => {
    // do something here ...
    if (success) {
        resolve(value); // fulfilled   可以理解为是成功的回调函数
    } else {
        reject(error); // rejected   可以理解为是失败的回调函数
    }
});

由上述代码我们可知:

  • 该构造函数接收两个函数作为参数,分别是resolve和reject。
  • 当异步操作执行成功后,会将异步操作结果作为参数传入resolve函数并执行,此时 Promise对象状态从pending变为fulfilled;
  • 失败则会将异步操作的错误作为参数传入reject函数并执行,此时 Promise对象状态从pending变为rejected;

接下来,
我们通过then方法,分别指定resolved状态和rejected状态的回调函数

promise.then(function(value) {
      // success
}, function(error) {
      // failure
});

then方法可以接收两个回调函数作为参数。

  • 第一个回调函数就是fulfilled状态时调用;
  • 第二个回调函数就是rejected时调用,可选
9.4 Promise的API
9.4.1 Promise.all(iterable)

Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。

let p1 = new Promise((resolve, reject) => {
  resolve('成功了')
})

let p2 = new Promise((resolve, reject) => {
  resolve('success')
})

let p3 = Promse.reject('失败')

Promise.all([p1, p2]).then((result) => {
  console.log(result)               //['成功了', 'success']
}).catch((error) => {
  console.log(error)
})

Promise.all([p1,p3,p2]).then((result) => {
  console.log(result)
}).catch((error) => {
  console.log(error)      // 失败了,打出 '失败'
})

Promse.all在处理多个异步处理时非常有用,比如说一个页面上需要等两个或多个ajax的数据回来以后才正常显示,在此之前只显示loading图标。

let wake = (time) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`${time / 1000}秒后醒来`);
      console.log(`${time} is over`);
    }, time)
  })
}

let p1 = wake(3000)
let p2 = wake(2000)

Promise.all([p1, p2]).then((result) => {
  console.log(result)       // [ '3秒后醒来', '2秒后醒来' ]
}).catch((error) => {
  console.log(error)
})

需要特别注意的是

  • 请求的过程是异步的,也就是先执行了p2的打印语句(2000 is over),再执行了p1的打印语句(3000 is over)
  • Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,即p1的结果在前,即便p1的结果获取的比p2要晚。
9.4.2 Promise.race(iterable)

顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。

let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  },1000)
})

let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('failed')
  }, 500)
})

Promise.race([p1, p2]).then((result) => {
  console.log(result)
}).catch((error) => {
  console.log(error)  // 打开的是 'failed'
})
9.4.3 Promise.resolve(value)

有时需要将现有对象转为Promise对象,Promise.resolve方法就起到这个作用。
将jQuery生成的deferred对象,转为一个新的Promise对象。

const jsPromise = Promise.resolve($.ajax('/whatever.json'));

Promise.resolve等价于下面的写法。

Promise.resolve('foo');
// 等价于
new Promise(resolve => resolve('foo'));

reslolve参数有以下四种情况

  • 参数是一个Promise实例,promise.resolve将不做任何修改、原封不动地返回这个实例。
  • 参数是一个thenable对象,thenable对象指的是具有then方法的对象
    比如
let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

Promise.resolve方法会将这个对象转为Promise对象,然后就立即执行thenable对象的then方法。

  • 参数时不具备then方法的对象或者根部不是对象,如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为resolved。
  • 不带有任何参数,直接返回一个resolved状态的Promise对象。
    所以,如果希望得到一个Promise对象,比较方便的方法就是直接调用Promise.resolve方法。
const p = Promise.resolve();
p.then(function () {
  // ...
});
9.4.4 Promise.reject()

返回一个新的Promise实例,该实例的状态为rejected。

const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
  console.log(s)
});
9.4.5 Promise.Promise.prototype.then()

Promise 的实例具有 then 方法,主要作用是为 Promise 实例发生状态改变时添加回调函数。
它接收两个回调函数作为参数,第一个参数是 fulfilled状态时的回调函数;第二个参数是rejected状态时的回调函数,可不传入。并且该方法返回一个新的Promise对象。

p.then(onResolve, onReject);

p.then(function(value) {
   // fulfillment
  }, function(reason) {
  // rejection
});
9.4.6 Promise.Promise.prototype.catch()

返回一个Promise,并且处理拒绝的情况。它的行为与调用Promise.prototype.then(undefined, onRejected)相同。推荐使用catch方法,不要在then方法中定义rejected状态的回调函数;这是因为使用catch还可以捕获在then方法执行中存在的错误。

p.catch(onReject)

 p.catch(function(reason) {
     // 拒绝
 });
9.4.7 Promise.Promise.prototype.finally()

返回一个Promsie。是指,在上一轮 promise 运行结束后,无论fulfilled还是 rejected,都会执行指定的回调函数。该方法适合无论结果如何都要进行的操作,例如清除数据。finally不接收任何参数。

 p.finally(onFinally);

  p.finally(function() {
   
  })
9.5 Promise对象实例演示
9.5.1 加载图片
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
        <script>
            let preloadImg = function (img, path) {
              return new Promise(function (resolve, reject) {
                img.onload  = resolve;
                img.onerror = reject;
                img.src = path;
              });
            };
            
            let img1 = new Image();
            let p1 = preloadImg(img1, 'img/1.jpg')
            p1.then(function (){
                console.log('图片加载成功');
                document.getElementById("div1").appendChild(img1);  
            }).catch(function (){
                document.getElementById("div1").innerHTML = "图片加载失败";   
            });
            
            let img2 = new Image();
            let p2 = preloadImg(img2, 'img/1.txt');
            p2.then(function (){
                console.log('图片加载成功');
                document.getElementById("div2").appendChild(img2);  
            }).catch(function (){
                document.getElementById("div2").innerHTML = "图片加载失败";   
            });
        </script>
    </head>
    <body>
        <div id="div1"></div>
        <div id="div2"></div>
    </body>
</html>
9.5.2 链式加载
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
        <script>
            function runAsync1(){
                var p = new Promise(function(resolve, reject){
                    //做一些异步操作
                    setTimeout(function(){
                        console.log('异步任务执行完成1');
                        resolve('数据1');
                    }, 1000);
                });
                return p;            
            }
            function runAsync2(){
                var p = new Promise(function(resolve, reject){
                    //做一些异步操作
                    setTimeout(function(){
                        console.log('异步任务执行完成2');
                        resolve('数据2');
                    }, 5000);
                });
                return p;            
            }
            function runAsync3(){
                var p = new Promise(function(resolve, reject){
                    //做一些异步操作
                    setTimeout(function(){
                        console.log('异步任务执行完成3');
                        resolve('数据3');
                    }, 3000);
                });
                return p;            
            }
             
            runAsync1()
            .then(function(data){
                console.log(data);
                return runAsync2();
            })
            .then(function(data){
                console.log(data);
                return runAsync3();
            })
            .then(function(data){
                console.log(data);
            }); 
        </script>
    </head>
    <body>
    </body>
</html>

推荐阅读更多精彩内容

  • [TOC] 参考阮一峰的ECMAScript 6 入门参考深入浅出ES6 let和const let和const都...
    郭子web阅读 891评论 0 1
  • 第一章:块级作用域绑定 块级声明 1.var声明及变量提升机制:在函数作用域或者全局作用域中通过关键字var声明的...
    BeADre_wang阅读 371评论 0 0
  • 以下内容是我在学习和研究ES6时,对ES6的特性、重点和注意事项的提取、精练和总结,可以做为ES6特性的字典;在本...
    科研者阅读 1,812评论 2 9
  • ES6参考手册简化版 ES6简介 ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的...
    凌雨微尘阅读 335评论 0 2
  • 1、新的声明方式 以前我们在声明时只有一种方法,就是使用var来进行声明,ES6对声明的进行了扩展,现在可以有三种...
    令武阅读 322评论 0 7