ECMAScript6语法简介

ECMAScript6语法简介

块作用域构造let和const

块级声明用于声明在指定块的作用域之外无法访问的变量,块级作用域存在于函数内部或块中( {}之间的区域 )。

let声明

在函数作用域或全局作用域中通过关键字var声明的变量无论在哪里都会被当成在当前作用域顶部声明的变量,这就是JavaScript的变量提升机制。

//函数内部
function changeState(flag){
    if(flag){
        var color = "red";
    }
    else{
        console.log(color);  //此处可访问变量color,因为它被提升,其值为undefined
        return null;
    }
}
changeState(false);

//块中
{
    var a = 1;
}
console.log("a= " + a);     //此处可访问a,a=1

//for 循环中
for(var i = 0;i<10;i++){
}
conlose.log("i = " + i);    //此处可访问i,输出i=10

这种变量提升机制在开发时会造成困扰,所以引入let,用let声明的变量不会被提升,可以将变量的作用域限制在当前代码块中。

//函数内部
function changeState(flag){
    if(flag){
        let color = "red";
    }
    else{
        console.log(color);  //此处不能访问变量color,报错:color is not defined
        return null;
    }
}
changeState(false);

//块中
{
    var a = 1;
}
console.log("a= " + a);     //此处不能访问变量a,报错:a is not defined

//for 循环中
for(var i = 0;i<10;i++){
}
conlose.log("i = " + i);    //此处不能访问变量i,报错:i is not defined

使用let声明变量还可以防止变量的重复声明。例如,在某个作用域下已经存在某个标识符,此时再使用let声明它就会抛出错误。

var index = 0;
var index = 10;  //ok
let index = 100; //报错

在同一作用域下,不能使用let重复声明已经存在的标识符,但如果在不同的作用域下,是可以的。

var index = 10;
{
    let index = 10;   //ok
}

const声明

const关键字用于声明常量。const关键字声明的常量必须在声明的时候初始化。与let类似,在同一作用域下用const声明已经存在的标识符会报错。

如果使用const声明对象,对象本身的绑定不能修改,但对象的属性和值是可以修改的:

const person = {
    name:"zhangsan"
};
person.name = "lisi";  //ok
person.age = 20;      //ok

//报错
person = {
    name:"ww"
};

全局块作用域绑定

在全局作用域中使用var声明的变量或对象将作为浏览器环境中的window对象的属性。这意味着使用var很可能会无意中覆盖一个已经存在的全局属性,例如:

<script>
    var greeting = "welcome";
    console.log(window.greeting);  //welcome

    console.log(window.Screen);    //function Screen(){[native code]}
    var Screen = 'liquid crystal';
    console.log(window.Screen);    //liquid crystal
</script>

greeting被定义为一个全局变量,并立即成为window对象的属性。定义的全局变量Screen则覆盖了window对象中原有的Screen属性。

如果在全局作用域下使用let或const,则会在全局作用域下创建一个新的绑定,但该绑定不会成为window对象的属性:

<script>
    let greeting = 'welcome';
    console.log(window.greeting)  //undefined

    const Screen = 'asd';
    console.log(Screen === window.Screen);  //false
</script>

模板字面量

多行字符串

模板字面量的基础语法就是用反引号`替换字符串的单双引号。比如:

let m = `k`;

在ES5中,如果一个字符串字面量要分为多行书写,那么可以采用两种方式,一是在一行结尾的时候添加反斜杠\表示承接下一行代码,二是使用加号+来拼接字符串:

let A = 'hello \
world';

let B = 'hello'
	+ 'world';
console.log(A);
console.log(B);

结果均未跨行显示,如果要跨行要手动加入换行符\n。

在ES6中使用模板字面量语法只需要在代码中直接换行即可。

let A = `hello
world`;

字符串占位符

在一个模板字面量中,可以将JavaScript变量或任何合法的JavaScript表达式嵌入占位符并将其作为字符串的一部分输出到结果。

let name = 'zzd';
let mes = `hello, ${name}`;
console.log(mes);

let amout = 5;
let price = 87;
let total = `The total price is ${price*amout}`;
console.log(total)

默认参数

在ES5中没有提供直接在函数的参数列表中指定参数默认值的语法,ES6中可以直接在参数列表中为形参指定默认值:

function A(url='/home',timeout=2000)

而且可以为任意参数指定默认值,在指定默认值的参数后还可以继续声明无默认值的参数:

function A(url='/home',timeout=2000,callback){
    
}
A();   //使用url、timeout默认值

//在这种情况下,只有在没有为url、timeout传值。或者主动为它们传入undefined时才会使用它们的默认值
A(undefined,undefined,function(){}); 

A('/login',null,function(){});  // 不使用timeout的默认值,最终值为null。

rest参数

JavaScript无论在函数定义中声明了多少形参,都可以传入任意数量的实参。在函数内部可以通过arguments对象接收传入的参数:

function calculate(op){
    if(op === '+'){
        let result = 0;
        for(let i = 1;i<arguments.length;i++){
            result += arguments[i];
        }
        return result;
    }
    else if(op === '*'){
         let result = 1;
         for(let i = 1;i<arguments.length,i++){
             result*=arguments[i];
         }
        return result;
    }
}

calculate()函数根据传入的操作符的不同而执行不同的计算,计算的数据可以是任意多个,因此在函数声明时无法明确地定义要传入的所有参数。但是我们可以通过arguments对象解决任意数量参数传入的问题。不过这种方式也有一些不足之处:首先,调用者需要知道该函数可以接收任意数量的参数,单从函数声明的参数列表是看不出来的;其次,因为第一个参数是命名参数且已被使用,因此遍历arguments对象时,索引要从1开始。

ES6引入了rest参数,在函数的命名参数前添加3个点,就表明这是一个rest参数,用于获取函数的多余参数。rest参数是一个数组,包含自它之后传入的所有参数。

用rest参数重写上面的calculate函数:

function calculate(op,...data){
    if(op === '+'){
        let result = 0;
        for(let i = 1;i<data.length;i++){
            result += data[i];
        }
        return result;
    }
    else if(op === '*'){
         let result = 1;
         for(let i = 1;i<data.length,i++){
             result*=data[i];
         }
        return result;
    }
}

rest参数包含的是op之后传入的所有参数(arguments对象包含所有传入的参数)。每个函数最多只能声明一个rest参数且只能放在最后。

展开运算符

展开运算符在语法上与rest参数相似,也是三个点,它可以将一个数组转换为各个独立的参数,也可用于取出对象的所有可遍历属性,而rest参数是指定多个独立的参数,并通过整合后的数组来访问。

function sum(a,b,c){
    return a+b+c;
}
let arr = [1,2,3];
sum(...arr);

展开运算符可以用来复制数组:

let arr1 = [1,2,3];
let arr2 = arr1;   //arr2和arr1是同一个数组对象
let arr3 = [...arr1];  //arr3和arr1是两个不同的数组对象

arr1[0] = 4;
console.log(arr2[0]);   //4
console.log(arr3[0]);   //1

从上面可以看出,当需要复制一个新的数组对象时可以使用展开运算符。

展开运算符也可以用来合并数组:

let arr1 = ['a'];
let arr2 = ['b','c'];
let arr3 = ['d','e'];
console.log([...arr1,...arr2,...arr3]);   //['a','b','c','d','e']

展开运算符还可以取出对象的所有可遍历属性,复制到当前对象中:

let book = {
    title:'vue',
    price:20,
}
let bookdetail = {...book,desc:'a fine book'}
console.log(bookdetail);   //{title:'vue',price:20,desc:'a fine book'}

对象字面量语法扩展

属性初始值的简写

在ES5及早期版本,对象字面量只是简单的键值对集合。

function createCar(color,doors){
    retuen {
        color:color,
        doors:doors
    }
}

在ES6中通过使用属性初始值的简写语法,可以消除这种属性名称与本地变量之间的重复书写。当一个对象的属性与本地变量同名时,可以不用写冒号和值,简单地只写属性名即可:

<script>
    function createCar(color,doors){
    return {
        color,
        doors
    }
}
let name = 'zzd';
let age = 18;
let person = {name,age};
</script>

对象方法的简写语法

ES6也改进了为对象字面量定义方法的语法。在ES5及早期版本,如果要为对象添加方法必须通过指定名称并完整定义函数来实现:

var car = {
    color:'red',
    doors:4,
    showColor:function(){
        console.log(this.color);
    }
}

而ES6中可以省略冒号和function关键字:

var car = {
    color:'red',
    doors:4,
    showColor(){
        console.log(this.color);
    }
}
car.showColor();   //red
console.log(car.showColor.name);  //showColor

通过对象方法的简写语法创建的方法有一个name属性,其值为圆括号前面的名称。

可计算的属性名

在JavaScript中,访问对象的属性可以通过点号.或方括号[],如果属性名包含了特殊字符或中文,或者

需要通过计算得到属性名,则只能使用[]:

let suffix = 'name';
let person = {};
person['first name'] = 'san';   //属性名中有空格
person['last ' + suffix] = 'zhang';  //属性名由表达式计算得到
person.age = 20;          //常规的属性可以通过.得到
console.log(person);     //{'first name':'san','last name':'zhang',age:20}

如果采用对象字面量的语法定义对象,在ES5及早期版本不允许出现带有表达式的属性名,而在ES6中,则可以:

let suffix = 'name';
let person = {
    ['first ' + suffix]:'san',
    ['last ' + suffix]:'zhang',
    age:20
}

解构赋值

在JavaScript中,我们经常需要从某个对象或数组中提取特定的数据赋给变量,这种操作重复且无趣:

//在真实应用场景中,book对象通常是从服务端得到的数据。
let book = {
    title:'Vue',
    isbn:'2321342',
    price:23,
    category:{
        id:1,
        name:'web前端'
    }
}
//提取对象中的数据赋给变量
let title = book.title;
let isbn = book.isbn;
let price = book.price;
let category = book.category.name;
//提取数组中的数据赋给变量
let arr = [1,2,3];
let a = arr[0],b = arr[1],c=arr[2]

ES6为对象和数组提供了解构功能,允许按照一定模式从对象和数组中提取值,并对变量进行赋值。

对象解构

对象解构的语法形式是在一个赋值操作符的左边放置一个对象字面量:

let book = {
    title:'Vue',
    isbn:'2321342',
    price:23,
}
let {title,isbn,price} = book; //声明了3个变量接收
console.log(title);  //Vue

如果使用var、let或const声明变量,则必须提供初始化程序,即等号右侧必须提供值,下面的代码都会报错:

let {title,isbn,price};
var {title,isbn,price};
const {title,isbn,price};

如果变量之前已经声明,之后想要结构语法给变量赋值,那么需要用圆括号包裹整个解构赋值语句:

let book = {
    title:'Vue',
    isbn:'2321342',
    price:23,
}
let title,isbn,price;
{title,isbn,price} = book;  //报错
({title,isbn,price} = book);   //正确

JavaScript引擎将一对开放的花括号视为一个代码块,而语法规定代码块语句不允许出现在赋值语句的左侧,添加圆括号可以将语句视为一个表达式,从而实现解构赋值。

整个解构赋值表达式的值与表达式右侧的值相等,这样就可以实现一些有趣的操作,例如给变量赋值的同时向函数传参:

let book = {
    title:'Vue',
    isbn:'2321342',
    price:23,
}
let title,isbn;

function A(book){
    console.log(book);
}
//给title、isbn赋值后,因解构表达式的值是等号右侧的值
//所以此处传递给A的参数是book对象
A({title,isbn} = book);   //输出Object
console.log(title);   //Vue

使用解构赋值表达式时如果指定的局部变量名称在对象中不存在,那么这个局部变量会被赋值为undefined,在这种情况下可以考虑为该变量定义一个默认值,在变量名称后面添加一个等号和默认值即可:

let book = {
    title:'Vue',
    isbn:'2321342',
    price:23,
}
let {title,isbn,salesVolume = 0} = book;
console.log(salesVolume);   //0

当book对象中没有salesVolume属性或者该属性为undefined时会使用默认值。

如果希望在使用解构赋值时,使用与对象属性名不同的局部变量名,那么可以采用属性名:局部变量名的形式:

let {title:bookTitle,isbn:bookIsbn} = book;}

在JavaScript中,对象经常会有嵌套,那么如何通过解构语法提取值呢:

let book = {
    title:'Vue',
    isbn:'2321342',
    price:23,
    category:{
        id:1,
        name:'web前端'
    }
}
let {title,isbn,category:{name:category}} = book;
console.log(category);    //web前端

在找到book对象的category属性后继续深入下一层查找name属性并将其赋给category局部变量。

在解构语法中,冒号前面的标识符是对象中的检索位置,其右侧是为要被赋值的变量名;如果冒号右侧是花括号,则表示要赋予的最终值嵌套在对象内部更深层次中。

展开运算符和对象解构结合使用:

let person = {
    name:'zzd',
    age:24,
}
let {...newObject} = person;
let {anotherObject} = person;
let {name,age,gender} = {...person,gender:'男'};

console.log(newObject);      //{name:'zzd',age:24}
console.log(anotherObject);  //undefined
console.log(name);    //zzd
console.log(gender);  //男

第六行是提取person对象的anotherObject属性并赋值给anotherObject变量,person里没有该属性。

数组解构

数组解构使用方括号,且没有对象属性名的问题:

let arr = [1,2,3];
let [a,b,c] = arr;

变量值是根据数组中元素的顺序进行选取的。如果要获取指定位置的数组元素值,可以只为该位置的元素提供变量名:

let arr = [1,2,3];
let [,,c] = arr;
console.log(c);  //3

与对象解构不同,如果为已经声明过的变量进行数组解构赋值,不需要使用圆括号:

let arr = [1,2,3];
let a,b,c;
[a,b,c] = arr   //ok

也可以在数组解构赋值表达式中为数组中的任意位置添加默认值,当指定位置的元素不存在或其值为undefined时使用默认值:

let arr = [1,2,3];
let [a,b,c,d=0] = arr;
console.log(d); //0

嵌套数组解构与嵌套对象解构的语法类似,在原有的数组解构模式中插入另一个数组解构模式:

let categories = ['c/c++',['vue','react'],'java'];
let [language1,[,language2]] = categories;

console.log(language1);    //c/c++
console.log(language2);    //react

与展开运算符一起使用:

let arr = [1,2,3];
let [a,...others] = arr;
let [...newArr] = arr;   

箭头函数

箭头函数语法

单一参数、函数体只有一条语句的箭头函数形式:

let welcome = msg => msg;
//相当于
// function welcome(msg){
//     return msg
// }

如果函数有多个参数,则要加圆括号:

let welcome = (user,msg) => `${user},${msg}`;
// 相当于
// function welcome(user,msg){
//     return user + "," + msg;
// }

如果函数没有参数,要使用一对空的圆括号。

如果函数有多条语句,则需要使用花括号包裹函数体:

let add = (a,b) => {
    let c = a+b;
    return c;
}

如果要创建一个空函数,则需要使用一对没有内容的圆括号和一对没有内容的花括号。

如果箭头函数的返回值是一个对象字面量,则需要将该对象字面量包裹在圆括号中:

let createCar = (color,doors) => ({color:color,doors:doors});
//相当于
//function createCar(color,doors){
	//return {
	//color:color,
	//doors:doors}
//}

将对象字面量包裹在圆括号是为了将其与函数体区分开。

箭头函数可以和对象解构结合使用:

let personInfo = ({name,age}) => `${name}'s age is ${age} years old`;//这里用到对象解构
let person = {name:'zhangsan',age:18};
console.log(personInfo(person));    //zhangsan's age is 18 years old

箭头函数语this

JavaScript中的this并不是指向对象本身,其指向是可以改变的,根据当前执行上下文的变化而变化。

<script>
var greeting = 'welcome';
function sayHello(user){
    alert(this.greeting + ',' + user);
}

var obj = {
    greeting:'hello',
    sayHello:sayHello
}
sayHello('zzd');   //welcome,zzd
obj.sayHello('lisi');  //hello,lisi
var sayHi = obj.sayHello;
sayHi('wangwu');   //welcome wangwu
</script>

上述代码分析:

  1. 调用sayHello('zzd')时相当于执行window.sayHello('zzd'),因此函数内部this指向window对象,在代码第一行定义的全局变量greeting将自动变成window对象的属性。
  2. 调用obj.sayHello('lisi')时,this指向obj对象。
  3. 调用sayHi('wangwu'),虽然该函数是obj.sayHello赋值得到的,但是在执行sayHi()函数时,当前的执行上下文对象是window对象,相当于调用了window.sayHi('wangwu'),所以this指向window。
<script>
    var obj = {
        greeting:'Hello',
        sayHello:function(){
            setTimeout(function(){
                alert(this.greeting);
            },2000);
        }
    }
obj.sayHello();   //undefined
</script>

上面的例子中当调用了obj.sayHello()时只是执行了setTimeout函数,2秒之后才开始执行setTimeout函数参数中定义的匿名函数,而该匿名函数的执行上下文对象是window,因此this指向window。

为了解决this指向的问题,可以使用函数对象的bind方法,将this明确的绑定到某个对象上:

<script>
    var greeting = 'welcome';
function sayHello(user){
    alert(this.greeting + user);
}
var obj = {
    greeting:'hello',
    sayHello:sayHello
}
var sayHi = obj.sayHello.bind(obj);
sayHi('wangwu');    //hello,wangwu

var obj = {
    greeting:'Hello',
    sayHello:function(){
        setTimeout((function(){
            alert(this.greeting);
        }).bind(this),2000);
    }
    //或者
    /*sayHello:function(){
                var that = this;
                setTimeout(function(){
                    alert(that.greeting);
                },2000);
            }*/
}
</script>

使用bind()方法实际上是创建了一个新的函数,称为绑定函数,该函数的this被绑定到参数传入的对象。为了避免创建一个额外的函数,下面是用箭头函数来解决this的问题。

箭头函数中没有this绑定,必须通过查找作用域决定其值。如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this;否则,this的值会被设置为全局对象。使用箭头函数修改上述代码:

sayHello:function(){
setTimeout(() => alert(this.greeting),2000);
}

alert()函数参数中的this与sayHello()方法中的this一致,而这个this指向的是obj对象,因此调用obj.sayHello()的结果是Hello。

箭头函数中的this值取决于该函数外部非箭头函数的this值,且不能通过call()、apply()或bind()方法改变this值。

箭头函数在使用时有几个需要注意的地方:

  • 没有this、super、arguments、new.target绑定。箭头函数中的this、super、arguments、new.target这些值由外围最近一层非箭头函数决定。
  • 不能通过new关键字调用。箭头函数不能被用作构造函数,也就是说不可以使用new关键字调用箭头函数。
  • 没有原型。由于不可以通过new关键字调用箭头函数,因而没有构建原型的需求,所以箭头函数不存在prototype这个属性。
  • 不可以改变this的绑定。函数内部的this不可被改变,在函数的生命周期内始终保持一致。
  • 不支持arguments对象。箭头函数没有arguments绑定,所以只能通过命名参数和rest参数访问函数的参数。

Symbol

创建Symbol

ES6引入了一种新的原始数据类型——Symbol,表示独一无二的值。一个具有Symbol数据类型的值可以称为符号类型值。一个符号类型值是通过调用函数Symbol()函数创建的,这个函数动态地生成了一个匿名的、唯一的值:

let sn1 = Symbol();

console.log(typeof(sn1));  //symbol
console.log(sn1);  //Symbol()

let sn2 = Symbol();
console.log(sn1 === sn2);   //false

需要注意的是,Symbol是原始值,所以不能用new Symbol()创建Symbol值,这会导致程序抛出错误。每一个Symbol实例都是唯一且不可改变的。

Symbol()函数可以接受一个可选的字符串参数,用于为新创建的Symbol实例提供描述,这个描述不可用于属性访问,主要用于调试目的,以及方便阅读代码。

let sn1 = Symbol('sn1');
let sn2 = Symbol('sn2');
console.log(sn1);   //Symbol(sn1)
console.log(sn2);   //Symbol(sn2)

let sn3 = Symbol('sn1');
console.log(sn1 === sn3);  //false

sn1和sn2是两个Symbol值。如果不加参数,他们在Console窗口中的输出都是Symbol(),不利于区分。有了参数以后就等于为它们各自加上了描述,输出的时候就能够分清到底是哪一个值。

每一个Symbol实例都是唯一的,Symbol函数的参数只是表示对当前Symbol值的描述,因此相同参数的Symbol函数的返回值是不相等的。

如果调用Symbol函数传入的参数是一个对象,就会调用该对象的toString()方法,将其转换为字符串,然后再生成一个Symbol值:

let obj = {
    toString(){
        return 'sn';
    }
}
console.log(Symbol(obj));  //Symbol(sn)

Symbol与类型转换

自动转型是JavaScript中的重要特性,利用这个特性能够在特定的场景下将某个数据强制转换为其他类型,然而Symbol类型比较特殊,其他类型没有与Symbol逻辑等价的值,因此不能将Symbol值与其他类型的值进行运算。

下面的代码将一个Symbol值与一个字符串进行相加操作,运行时将提示“不能将一个Symbol值转换为字符串”。

let sn = Symbol('sn');
'My sn is' + sn;

虽然Symbol不能与字符串进行运算,但是可以显示得调用String()函数将Symbol值转换为字符串:

let sn = Symbol('sn');
let str = String(sn);
console.log(str);     //Symbol(sn)  这是一个字符串
console.log(sn.toString());  //Symbol(sn)  这也是一个字符串

当使用console.log()方法输出Symbol的值时,它实际上也是调用sn的toString()方法。

有一个例外是,Symbol可以参与逻辑运算,这是因为JavaScript将非空值都看成true:

let sn = Symbol('sn');
console.log(Boolean(sn));   //true

作为属性名使用

Symbol类型唯一合理的用法是用变量存储Symbol值,然后使用存储的值创建对象属性。由于每一个Symbol值是不相等的,所以Symbol作为对象的属性名,可以保证属性不重名。

let sn = Symbol('sn');

//第一种方式
let obj = {},
obj[sn] = '1111-11';
console.log(obj);   //{[Symbol(sn)]:'1111-11'}

//第二种方式
let obj = {
    [sn]:'1111-11'
};
console.log(obj);   //{[Symbol(sn)]:'1111-11'}

//第三种方式
let obj = {};
Object.defineProperty(obj,sn,{value:'1111-11'});
comsole.log(obj[sn]);  //1111-11

需要注意的是,Symbol作为对象属性名时不能使用点运算符,要用方括号。因为点运算符后面是字符串,所以不会读取变量sn所表示的Symbol值,而是直接将sn作为字符串属性名。

Symbol值作为属性名时,该属性是公有属性而不是私有属性,可以在类的外部访问,但是不会出现在for...in和for...of循环中,也不会被object.keys()、object.getOwnPropertyNames()函数返回。如果要读取一个对象的Symbol属性,可以通过object.getOwnPropertySymbols()和Reflect.ownKeys()方法得到。

let sn = Symbol('sn');
let obj = {};
obj[sn] = '1111-11';
console.log(obj);    //{[Symbol(sn)]:'1111-11'}

for(let prop in obj){
    console.log(prop);      //无输出
}
console.log(Object.keys(obj));     //[]
console.log(Object.getOwnPropertyNames(obj));  //[]
console.log(Object.getOwnPropertySymbols(obj)); //[Symbol(sn)]
console.log(Reflect.ownKeys(obj)); //[Symbol(sn)]

共享的Symbol

有时候我们希望在不同的代码中使用同一个Symbol值,为此,可以使用Symbol.for()方法创建一个可共享的Symbol,该方法接受一个字符串参数,即要创建的Symbol的标识符,同时这个参数也被用作Symbol的描述。

ES6提供了一个可以随时访问的全局Symbol注册表,当调用了Symbol.for()方法时,它首先在全局Symbol注册表搜索以参数作为名称的Symbol值,找到了就返回已有的Symbol,没找到就创建一个新的Symbol,以参数作为key,注册到全局Symbol注册表中,然后返回新创建的Symbol。

let sn1 = Symbol.for('sn');
let sn2 = Symbol.for('sn');
let sn3 = Symbol('sn');
console.log(sn1);    //Symbol(sn)
console.log(sn1===sn2); //true
console.log(sn1===sn3);//false

调用Symbol.for()和Symbol()方法都会生成新的Symbol值,区别在于前者会被注册到全局Symbol注册表,之后以相同的key调用Symbol.for()方法则会返回同一个Symbol值;而后者每次调用都会创建一个新的Symbol值。

另一个共享Symbol相关的方法是Symbol.keyFor(),该方法在全局Symbol注册表中搜索已注册的Symbol的key。

let sn1 = Symbol.for('sn');
console.log(Symbol.keyFor(sn1)); //sn

let sn2 = Symbol('sn');
console.log(Symbol.keyFor(sn2)); //undefined

在全局注册表并不存在sn2这个Symbol,所以返回undefined。

类的定义

在ES5及早期版本中没有类的概念,可以通过构造函数和原型混合使用的方式模拟定义一个类:

<script>
    function Car(sColor,iDoors){
    this.Color = sColor;
    this.doors = iDoors;
}
Car.prototype.showColor = function(){
    console.log(this.color);
};

var oCar = new Car('red',4);
oCar.showColor();
</script>

ES6引入了class关键字:

class Car{
    constructor(sColor,iDoors){
        this.Color = sColor;
        this.doors = iDoors;
    }
    showColor(){
        console.log(this.color);
    }
}
let oCar = new Car('red',4);
oCar.showColor();

在类声明语法中,使用特殊的constructor方法名定义构造函数,且由于这种类使用简写语法定义方法,因此不需要加function关键字。

自有属性是对象实例中的属性,不会出现在原型上,如本例中的color、doors。自有属性只能在类的构造函数constructor或方法中创建,一般建议在构造函数中创建所有的自有属性。本例中的showColor方法实际是Car.prototype的一个方法。

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

let Car = class {
    constructor(sColor,iDoors){
        this.color = sColor;
        this.doors = iDoors;
    }
    showColor(){
        console.log(this.color)
    }
}
let oCar = new Car('red',4);
oCar.showColor();

使用表达式可以实现立即调用类构造函数从而创建一个类的单例对象。使用new调用类表达式,跟着一对圆括号调用这个表达式:

let car = new class {
    constructor(sColor,iDoors){
        this.color = sColor;
        this.doors = iDoors;
    }
    showColor(){
        console.log(this.color)
    }
}('red',4);    // 创建了一个匿名类表达式然后立即执行

oCar.showColor();

访问器属性

访问器属性是通过关键字get和set创建的,语法为关键字后跟一个空格和相应的标识符,实际上是为某个属性定义取值和设值函数,在使用时以属性访问的方式使用。与自有属性不同的是,访问器属性是在原型上创建的:

<script>
    class Car{
        constructor(sName,iDoors){
            this._name = sName;
            this.doors = iDoors;
        }
        //只读属性
        get desc(){
            return `${this.name} is worth having`;
        }
        get name(){
            return this._name;
        }
        set name(value){
            this._name = value;
        } 
    }

let car = new Car('benz',4);
console.log(car.name);
console.log(car.desc);
car.prototype.desc='aaa';   //TypeError:cannot set property'desc' of undefined
</script>

在构造函数中定义了一个_name属性,下划线是一种常用的约定记号,用于表示只能通过对象方法访问的属性。当访问属性name时,实际上是调用它的取值方法;当给name赋值时实际上是调用它的设值方法。因为是方法实现,所以定义访问器属性时可以写一些访问控制或额外的代码逻辑。

如果需要只读的属性,用get方法即可,如desc属性,如果需要只写的属性,用set方法。

静态方法

ES6引入了关键字static用于定义静态方法。除构造函数外,类中所有的方法和访问器属性都可以用static关键字定义:

class Car {
    constructor(sname,iDoors){
        this.name = sName;
        this.doors = iDoors;
    }
    showName(){
        console.log(this.name);
    }
    static createDefault(){
        return new Car('audi',4);
    }
}
let car = Car.createDefault();
car.showName();     //audi
car.createDefault();   //TypeError:car.createDefault is not a function

使用static关键字定义的静态方法只能通过类名访问,不能通过实例访问。

类的继承

ES6提供了extends关键字:

<script>
    class Person{
        constructor(name){
            this.name = name;
        }
        work(){
            console.log('working...');
        }
    }
class Student extends Person{
    constructor(name,no){
        super(name);  //调用父类的constructor(name)
        this.no = no;
    }
}
let stu = new Student('zhangsan',1);
stu.work();   //working...
</script>

Student被称为派生类。用super函数调用父类的构造函数并传入相应的参数。如果在派生类中定义了构造函数则必须调用super函数,而且要在访问this之前调用。如果在派生类中没有定义构造函数,那么当创建派生类的实例时会自动调用super函数并传入所有参数。例如下面代码定义了Teacher类,从Person类继承且没有定义构造函数:

<script>
    class Person{
        constructor(name){
            this.name = name;
        }
        work(){
            console.log('working...');
        }
    }
	class Teacher extends Person{
        
    }
	let teacher = new Teacher('lisi');
	teacher.work(); //working...
</script>

在派生类中可以重写基类中的方法,这将覆盖基类中的同名方法:

class Person{
        constructor(name){
            this.name = name;
        }
        work(){
            console.log('working...');
        }
    }
class Student extends Person{
    constructor(name,no){
        super(name);  //调用父类的constructor(name)
        this.no = no;
    }
    //覆盖Person.prototype.work()方法
    work(){
        console.log('studying...')
    }
}
let stu = new Student('zhangsan',1);
stu.work();   //studying...

如果在Student的work()方法中需要调用基类的work()方法,可以使用super关键字调用:

class Person{
	...
    }
class Student extends Person{
    ...
    work(){
        super.work();
        console.log('studying...');
    }
}
let stu = new Student('zhangsan',1);
stu.work();  //working... 
			//studying...

模块

一个模块通常是一个独立的JS文件,该文件内部定义的变量和函数除非被导出,否则不能被外部所访问。

使用export关键字放置在需要暴露给其他模块使用的变量、函数或类声明前面,以将它们从模块中导出:

                           Modules.js
//导出数据
export var color = 'red';
export let name = 'module';
export const sizeOfPage = 10;

//导出数据
export function sum(a,b){
    return a+b;
}

//将在模块末尾进行导出
function subtract(a,b){
    return a-b;
}
//将在模块末尾进行导出
function multiply(a,b){
    return a*b;
}
//将在模块末尾进行导出
function divide(a,b){
    if(b != 0)
        return a/b;
}
//导出类
export class Car{
    constructor(sColor,iDoors){
        this.color = sColor;
        this.doors = iDoors;
    }
    showColor(){
        console.log(this.color);
    }
}
//模块私有的变量
var count = 0;
//模块私有的函数
function changeCount(){
    count++
}

//导出multiply函数
export {multiply};
//subtract是本地名称,sub是导出时使用的名称
export {subtract as sub}
//导出模块默认值
export default divide;

说明:

  1. 导出时可以分别对变量、函数、类进行导出,也可以将导出语句集中书写在模块的尾部(43-47)行,当导出内容较多时,使用后者会更加清晰。

  2. 没有添加export而定义的变量、函数和类(35-40)行在模块外部是不允许被访问的。

  3. 导出的函数和类声明都需要一个名字,如果要用一个不同的名字导出可以使用as指定。(45)行

  4. 一个模块可以导出且只能导出一个默认值,默认值是通过使用default关键字指定的单个变量、函数或类。非默认值的导出需要使用一对花括号包裹名称,默认值的导出则不需要。

  5. 默认值的导出还可以采用下面两种方式:

    //第二种使用default关键字导出一个函数作为模块的默认值
    //因为导出的函数被模块所代表,所以他不需要一个名称
    export default function(a,b){
        if(b != 0)
            return a/b
    }
    //-----------------------------------------------//
    function divide(a,b){
        if(b!=0)
            return a/b;
    }
    //第三种方式
    export {divide as default}
    

如果想在一条导出语句中指定多个导出(包括默认导出),那么就需要用到第三种语法形式。下面将Modules.js中模块尾部的导出(43-47)行合并为一条导出语句:

export {multiply,subtract as sub,divide as default};

下面来看一下导入。导入是使用import关键字引入其他模块导出的功能。import语句由两部分组成:要导入的标识符和标识符应当从哪个模块导入:

                 index.js
// 导入模块默认值
import divide from "./Modules.js";
// 导入多个绑定
import {color, name, sizeOfPage} from "./Modules.js";
// 导入单个绑定
import {multiply} from "./Modules.js";
// 因Modules模块中导出subtract函数时使用了名称sub,这里导入也要用该名称
import {sub} from "./Modules.js";
// 导入时重命名导入的函数
import {sum as add} from "./Modules.js";
// 导入类
import {Car} from "./Modules.js";
// 导入整个模块
import * as example from "./Modules.js";

console.log(color);           //red
console.log(name);            //module
console.log(sizeOfPage);      //10
// 只能用add而不能用sum了
console.log(add(6, 2));       //8
console.log(sub(6, 2));       //4
console.log(multiply(6, 2));  //12
console.log(divide(6, 2));    //3
let car = new Car("black", 4);
car.showColor();              //black
console.log(example.name);    //module
// 注意这里是sum,而不是add
console.log(example.sum(6, 2)); // 8
// count是Modules模块中私有的变量,在外部不能访问
console.log(example.count);   //undefined
// changeCount()函数是Modules模块中私有的函数,在外部不能访问
console.log(example.changeCount());   //TypeError: example.changeCount is not a function

说明:

  1. 导入模块时,模块文件的位置可以使用相对路径也可以使用绝对路径。使用相对路径时,对于同一目录下的文件不能使用Modules.js引入,而要使用"./Modules.js",即由"."代表当前目录。
  2. 导入非默认值要用花括号,默认值则不需要。
  3. 可以导入整个模块作为一个单一对象,然后所有的导出将作为该对象的属性使用。
  4. 多个import语句引用同一个模块,该模块也只执行一次。被导入的模块代码执行后,实例化后的模块被保存在内存中,只要另一个import语句引用它就可以重复使用它。

Promise

JavaScript引擎是基于单线程事件循环的概念构建的,采用任务队列的方式,将要执行的代码块放到队列中,当JavaScript引擎中的一段代码执行结束,事件循环会指定队列中的下一个任务来执行。事件循环是JavaScript引擎中的一段程序,负责监控代码执行并管理任务队列。

JavaScript执行异步调用的传统方式是事件和回调函数,随着应用的复杂化,事件和回调函数无法完全满足开发者要实现的需求,所以ES6给出了Promise这一强大的异步编程解决方案。

基本用法

一个Promise可以通过Promise构造函数创建,这个构造函数只接收一个参数:包含初始化Promise代码的执行器(executor)函数,在该函数内包含需要异步执行的代码。执行器函数接受两个参数,分别是resolve()函数和reject()函数,这两个函数由JavaScript引擎提供,不需要自己编写。异步操作结束成功时调用resolve函数,失败时调用reject函数。

const promise = new Promise(function(resolve,reject){
    //开启异步操作
    setTimeout(function(){
        try{
            let c = 6/2;
            //执行成功时调用
            resolve(c);
        }catch(ex){
            //执行失败时调用
            reject(ex);
        }
    },1000);
});

在执行器函数内包含了异步调用,在1秒后执行两个数的除法,如果成功就用相除的结果作为参数调用resolve函数,失败就调用reject函数。

每个Promise都会经历一个短暂的生命周期:先是处于进行中(pending)的状态,此时操作尚未完成,所以它也是未处理的(unsettled),一旦异步操作执行结束,Promise则变为已处理的(settled)状态。操作结束后,根据异步操作执行成功与否可以进入以下两个状态之一。

  • fulfilled:Promise异步操作成功完成。
  • rejected:由于程序错误或其他原因,Promise异步操作未能成功。

一旦Promise状态改变就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变只有两种可能:从pending变为fulfilled或者从pending变为rejected。

在Promise状态改变后,我们怎么根据不同的状态做相应的处理呢?Promise对象有一个then()方法,它接受两个参数:第一个是当Promise的状态变为fulfilled时要调用的函数,与异步操作相关的附加数据通过调用resolve函数传递给这个完成函数;第二个是当Promise的状态变为rejected时要调用的函数,所有与失败相关的附加数据通过调用reject函数传递给这个拒绝函数。

在上面代码之后添加then方法:

promise.then(function(value){
    //完成
    console.log(value);   //3
},function(err){
    //拒绝
    console.error(err.message);
})

then方法的两个参数都是可选的。例如只在执行失败后进行处理,可以给then方法的第一个参数传递null。

Promise对象还有一个catch()方法,用于在失败后进行处理,等价于上述只给then传递拒绝处理函数的代码。

通常是将then方法和catch方法一起使用来对异步操作的结果进行处理,这样能更清楚地指明操作结果是成功还是失败:

promise.then(function(value){
    //完成
    console.log(value);  //3
}).catch(function(err){
    //拒绝
    console.error(err.message);
});

上述代码使用箭头函数会更简洁:

promise.then(value => console.log(value)).catch(err => console.error(err.message));

Promise支持方法链的调用形式,如上面所示。每次调用then或catch方法实际上会创建并返回另一个Promise,因此可以将Promise串联调用。在串联调用时,只有在前一个Promise完成或被拒绝时,第二个才会被调用:

const promise = new Promise((resolve,reject) => {
    //调用setTimeout模拟异步操作
    setTimeout( ()=> {
        let intArray = new Array(20);
        for(let i = 0;i<20;i++){
            intArray[i] = parseInt(Math.random() * 20,10);
        }
        //成功后调用resolve
        resolve(intArray);
    },1000);
    //该代码会立即执行
    console.log('开始生成一个随机数的数组')
});

promise.then(value => {
    value.sort((a,b) => a-b);
    return value;
}).then(value => console.log(value));

说明:

  1. Promise的执行器函数内的代码会立即执行,无论setTimeout指定的回调函数执行成功与否。
  2. 在20个随机数生成完毕后,调用resolve(intArray)函数,因而then方法的完成处理函数被调用,对数组进行排序,之后返回value;接着下一个then方法的完成处理函数开始调用,输出排序后的数组。
  3. Promise链式调用时有一个重要特性就是可以为后续的Promise传递数据,只需要在完成处理函数中指定一个返回值,如上述的value。

在完成处理程序或拒绝处理程序中也有可能产生错误,使用Promise链式调用可以很好地捕获这些错误。

const promise = new Promise((resolve,reject)=>{
    resolve('hello world');
});
promise.then((value)=>{
    console.log(value);
    throw new Error('错误');
}).catch(err => console.error(err.message));

需要注意的是与JavaScript中的try/catch块不同,如果没有使用catch方法指定错误处理的回调函数,那么Promise对象抛出的错误不会传递到外层代码,即不会有任何反应。

创建已处理的Promise

如果要将一个现有的对象转换为Promise对象可以调用Promise.resolve()方法,该方法接收一个参数并返回一个完成状态的Promise,之后在返回的Promise对象上调用then方法来获取值,例如:

const promise = Promise.resolve('hello vue');
promise.then(value => console.log(value));   //hello vue

Promise.resolve()方法的参数有以下3种情况:

  1. 如果参数是一个Promise实例,那么将直接返回这个Promise,不做任何改动。

  2. 如果是一个thenable对象(具有then方法的对象),那么会创建一个新的Promise对象并立即执行thenable对象的then方法,返回的Promise对象的最终状态由then方法的执行决定:

    const thenable = {
        then(resolve,reject){
            resolve('hello');
        }
    }
    const promise = Promise.resolve(thenable);  //会执行then方法
    promise.then(value => console.log(value));  //hello
    
  3. 如果参数为空或者是基本数据类型或者是不带then方法的对象,那么返回Promise对象状态为fulfilled,并且将参数值传递给对应的then方法。

通常来说,如果不知道一个值是否为Promise对象,使用Promise.resolve(value)方法返回一个Promise对象,这样就能将该value以Promise对象形式使用。

Promise.reject(reason)方法也会返回一个新的Promise对象,并将给定的失败信息传递给对应的处理方法,返回的Promise对象的状态为rejected。例如:

const promise = Promise.reject('fail');
promise.catch(err=>console.log(err));  //fail

响应多个Promise

如果需要等待多个异步任务完成后再执行下一步操作,那么可以调用Promise.all()方法,该方法接受一个参数并返回一个新的Promise对象,参数是一个包含多个Promise的可迭代对象,如数组。返回的Promise对象在参数给出的所有Promise对象都成功的时候才会触发成功,一旦有任何一个Promise对象失败,则立即触发该Promise对象的失败。这个新的Promise对象在触发成功状态后会把所有Promise返回值的数组作为成功回调的返回值,顺序与可迭代对象中的Promise顺序保持一致;如果这个新的Promise对触发了失败状态,它会把可迭代对象中第一个触发失败的Promise对象的错误信息作为它的失败错误信息。

const promise1 = Promise.resolve(5);
const promise2 = 10
const promise3 = new Promise((resolve,reject) => {
    setTimeout(resolve,100,'hello');
});
const promise4 = new Promise((resolve,reject) => {
    throw new Error('错误');
});

Promise.all([promise1,promise2,promise3]).then(values => {
    console.log(values);
});
Promise.all([promise1,promise2,promise4]).then(values => {
    console.log(values);
}).catch(err => console.log(err.message));

如果Promise.all()方法的参数包含非Promise值,这些值将被忽略,但仍然会放到返回的数组中。

ES6还提供了一个Promise.race()方法,同样也是传入多个Promise对象,但与Promise.all()方法的区别是该方法是只要有任意一个Promise成功或失败,则返回的新的Promise对象就会用这个Promise对象的成功返回值或失败信息作为参数调用对应的回调函数。

const promise1 = Promise.resolve(5);
const promise2 = new Promise((resolve,reject) => {
    resolve(10);
});
const promise3 = new Promise((resolve,reject) => {
    setTimeout(resolve,100,'hello');
});
 Promise.race([promise1,promise2,promise3]).then(value => {
     console.log(value);
 })

上述运行结果为5。因为第一个成功了。

async函数

async函数是使用async关键字声明的函数,async函数是AsyncFunction构造函数的实例。在async函数内部可以使用await关键字,表示紧跟在后面的表达式需要等待结果。async和await关键字可以用一种更简洁的方式写出基于Promise的异步行为,而无须刻意地链式调用Promise。

基本用法

async函数会返回一个Promise对象,如果一个async函数的返回值不是Promise,那么它会被隐式地包装到一个Promise中。

async function helloAsync(){
    return 'hello';
}
/*
    等价于
    function helloAsync(){
        return Promise.resolve('hello');
    }
    */
console.log(helloAsync())   //Promise {'hello'}

helloAsync().then(v=>{
    console.log(v);       //hello
})

async函数内部return语句返回的值会成为then()方法回调函数的参数。

async函数中可以有await表达式,async函数执行时如果遇到await就会先暂停执行,等到触发的异步操作完成后再恢复async函数的执行并返回解析值。

function asyncOp(){
    return new Promise(resolve => {
        setTimeout(() => {
            console.log('延时任务');
            resolve();
        },1000);
    });
}

async function helloAsync(){
    await asyncOp();
    console.log('hello');
}
helloAsync();
//延时任务
//hello

async函数的函数体可以看做是由0个或多个await表达式分割开来的,从第一行代码开始直到第一个await表达式都是同步运行的。如果函数体内有一个await表达式,async函数就一定会异步执行。

在await关键字后面,可以是Promise对象和原始类型的值,如果是原始类型的值,会自动转成立即resolved的Promise对象。

await和并行任务执行

在async函数中可以有多个await任务,如果任务之间不要求顺序执行那么可以在await后面接Promise.all()方法并行执行多个任务。

const resolveAfter2Seconds = function(){
    console.log('starting slow promise');
    return new Promise(resolve => {
        setTimeout(function(){
            resolve('slow');
            console.log('slow promise is done');
        },2000);
    });
};

const resolveAfter1Second = function(){
    console.log('starting fast promise');
    return new Promise(resolve => {
        setTimeout(function(){
            resolve('fast');
            console.log('fast promise is done');
        },1000);
    });
};

const parallel = async function(){
    console.log('使用await Promise.all并行执行任务');
    //并行启动两个任务,等待两个任务都完成
    await Promise.all([
        (async()=>console.log(await resolveAfter2Seconds()))(),
        (async()=>console.log(await resolveAfter1Seconds()))()
    ]);
}

parallel();
//执行结果
使用await Promise.all并行执行任务
starting slow promise
starting fast promise
fast promise is done
fast
slow promise is done
slow

使用async函数重写Promise链

返回Promise的API将产生一个Promise链,它将函数拆解成许多部分:

function getProcessedData(url){
    return downloadData(url).catch(e => {     //返回一个promise对象
        return downloadFallbackData(url)        //返回一个promise对象
    }).then(v => {
        return processDataInWorker(v);        //返回一个promise对象
    });
}

可以重写为单个async函数:

async function getProcessedData(url){
    let v;
    try {
        v = await downloadData(url);
    } catch(e){
        v = await downloadFallbackData(url);
    }
    return processDataInWorker(v);
}

错误处理

如果async内部抛出错误则会导致返回的Promise对象变为reject状态。抛出的错误对象会被catch()方法回调函数接收到:

async function helloAsync(){
    await new Promise(function(resolve,reject){
        throw new Error('错误');
    });
}

helloAsync().then(v => console.log(v)).catch(e => console.log(e.message));  //错误

在上面的代码中,async函数helloAsync执行后,await后面的Promise对象会抛出一个错误对象,导致catch方法的回调函数被调用,它的参数就是抛出的错误对象。

防止出错的方法就是将await放到try/catch语句中。

async function helloAsync(){
    try {
        await new Promise(function(resolve,reject){
        throw new Error('错误');
    });
    } catch(e){
        //错误处理
    }
    return await('hello');
    
}
posted @ 2021-08-09 21:39  KKKyrie  阅读(35)  评论(0编辑  收藏  举报