前序 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对象),未来这些功能可以通过模块提供。
- 支持严格模式
模块功能主要由两个命令构成:export
和import
。
-
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中通过
export
和import
指令完成模块的导出和引入 -
export
命令可以导出变量,对象,函数和类 -
import
命令用于引入导出的变量,对象,函数和类 -
export
和import
属于静态操作,在编译时完成,不能使用运行时语法(比如变量,条件控制等) -
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>