ES 6语言基础

ES 6(于 2015 年 6 月正式发布)是 JavaScript 语言的下一代标准,相对于 ES 5(于 2011 年 6 月正式发布)新增了一些语法规则和数据结构方法,例如比较典型的 SetMap 数据结构和箭头函数等,可以理解成传统 JavaScript 的升级版,后续还会有 ES 7、ES 8 版本等。Vue 3 发布以来,极力推荐采用 ES 6 的语法来开发代码,另外本书的实战项目将全部采用 ES 6 代码。

由于移动端操作系统和浏览器兼容性问题的限制,虽然大部分机型原生就支持 ES 6 语法的 JavaScript,但是仍有一部分市场占有率较低的机型无法支持 ES 6 语法,例如 Android 系统 4.4 及以下版本和 iOS 系统 8.4 及以下版本。因此,为了项目的健壮性和更强的适配性,会采用 Node.jsBabel 工具来将 ES 6 代码转换成兼容性更强的 ES 5 代码。

由于 ES 6 的语法内容很多,相对复杂,因此本章只会对实战项目中用到的 ES 6 语法结合 ES 5 的写法来对比讲解和演示。

变量声明

let、var 和 const

在 ES 6 语法中,新增了 letconst 来声明变量,在 ES 6 之前,ES 5 中只有全局作用域和函数作用域,代码如下:

if(true) {
    var a = 'Tom'
}
console.log('a',a) // Tom

作用域是一个独立的地盘,让变量不外泄出去,但是上面的代码中的变量 a 就作为全局作用域外泄了出去,所以此时 JavaScript 没有区块作用域(或称为块级作用域)的概念。

在 ES 6 中加入区块作用域之后,代码如下:

if(true) {
    let a = 'Tom'
}
console.log('a',a) // Uncaught ReferenceError: a is not defined

letvar 都可以用来声明变量,但是在 ES 6 中,有下面一些区别:

  • 使用 var 声明的变量没有区块的概念,可以跨块访问。

  • 使用 let 声明的变量只能在区块作用域中访问,不能跨块访问。

在相同的作用域下,使用 varlet 具有相同的效果,建议在 ES 6 语法中使用 let 来声明变量,这样可以更加明确该变量所处的作用域。

const 表示声明常量,一般用于一旦声明就不再修改的值,并且 const 声明的常量必须经过初始化,代码如下:

const a = 1
a = 2 // Uncaught TypeError: Assignment to constant variable
const b // Uncaught SyntaxError: Missing initializer in const declaration

总结一下,如果在 ES 5 中习惯了使用 var 来声明变量,在切换到 ES 6 时,就需要思考一下变量的用途和类型,选择合适的 letconst 来使代码更加规范和语义化。

箭头函数

ES 6 新增了使用“箭头”(=>)声明函数,代码如下:

let f = v => v
// 等同于
var f = function (v) {
    return v
}

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分,当函数的内容只有返回语句时,可以省去大括号和 return 指令,代码如下:

let f = () => 5
// 等同于
var f = function () { return 5 }
let sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
    return num1 + num2
}

如果箭头数的内容部分多于一条语句,就要用大括号将它们括起来,并且使用 return 语句返回,代码如下:

let sum = (num1, num2) => {
    let num = 0
    return num1 + num2 + num;
}

箭头函数会默认绑定外层的上下文对象 this 的值,因此在箭头函数中,this 的值和外层的 this 是一样的,不需要使用 bind 或者 call 的方法来改变函数中的上下文对象,例如下面的代码:

mounted () {
    this.foo = 1
    setTimeout(function(){
        console.log(this.foo)  // 打印出1
    }.bind(this),200)
}
//相当于
mounted () {
    this.foo = 1
    setTimeout(() => {
        console.log(this.foo)  // 同样打印出1
    },200)
}

上面的代码中,在 Vue.jsmounted 方法中,this 指向当前的 Vue 组件的上下文对象,如果想要在 setTimeout 的方法中使用 this 来获取当前 Vue 组件的上下文对象,那么非箭头函数需要使用 bind,箭头函数则不需要。

箭头函数是实战项目中使用最多的 ES 6 语法,所以掌握好其规则和用法是非常重要的。

对象属性和方法的简写

ES 6 允许在大括号中直接写入变量和函数,作为对象的属性和方法,这样的书写更加简洁,代码如下:

const foo = 'bar'
const baz = {foo}
// 等同于
const baz = {foo: foo}
console.log(baz) // {foo: "bar"}

对象中如果含有方法,也可以将 function 关键字省去,代码如下:

{
    name: 'item',
    data () {
        return {
            name:'bar'
        }
    }
    mounted () {
    },
    methods: {
        clearSearch () {
        }
    }
}
// 相当于
{
    name: 'item',
    data :function() {
        return {
            name:'bar'
        }
    }
    mounted :function() {
    },
    methods: {
        clearSearch :function() {
        }
    }
}

在上面的代码中,展示了采用 ES 6 语法来创建 Vue 组件所需的方法和属性,包括 name 属性、mounted 方法、data 方法等,是后面实战项目中经常使用的写法。

对象解构

在 ES 6 中,可以使用解构从数组和对象中提取值并赋给独特的变量,代码如下:

// 数组
const input = [1, 2];
const [first, second] = input;
console.log(first,second) // 1 , 2
// 对象
const o = {
    a: "foo",
    b: 30,
    c: "Johnson"
};
const {a, b, c} = o;
console.log(a,b,c) // foo , 30 , Johnson

在上面的代码中,花括号 “{ }” 表示被解构的对象,abc 表示要将对象中的属性存储到其中的变量中。

模块化

ES 6模块化概述

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

好在广大的 JavaScript 程序员自己制定了一些模块加载方案,主要有 CommonJSAMD 两种。前者用于 Node.js 服务器,后者用于浏览器。

import和export

随着 ES 6 的到来,终于原生支持了模块化功能,即 importexport,而且实现得相当简单,完全可以取代 CommonJSAMD 规范成为浏览器和服务器通用的模块化解决方案。

在 ES 6 的模块化系统中,一个模块就是一个独立的文件,模块中的对外接口采用 export 关键字导出,可以将 export 放在任何变量、函数或类声明的前面,从而将它们暴露给外部代码使用,代码如下:

要导出数据,在变量前面加上 export 关键字:

export var name = "小明";
export let age = 20;
// 上面的写法等价于下面的写法
var name = "小明";
let age = 20;
export {
    name:name,
    age:age
}
// export对象简写的方式
export {name,age}

要导出函数,需要在函数前面加上 export 关键字:

export function sum(num1,num2){
    return num1 + num2;
}
// 等价于
let sum = function (num1,num2){
    return num1 + num2;
}
export sum

所以,如果没有通过 export 关键字导出,在外部就无法访问该模块的变量或者函数。

有时会在代码中看到使用 export default,它和 export 具有同样的作用,都是用来导出对外提供接口的,但是它们之间还有一些区别:

  • export default 用于规定模块的默认对外接口,并且一个文件只能有一个 export default,而 export 可以有多个。

  • 通过 export 方式导出,在导入时要加 { }export default 则不需要。

在一个模块中可以采用 import 来导入另一个模块 export 的内容。

导入含有多个 export 的内容,可以采用对象简写的方式,也是现在使用比较多的方式,代码如下:

//other.js
var name = "小明"
let age = 20
// export对象简写的方式
export {name,age}
//import.js
import {name,age} from "other.js"
console.log(name) // 小明
console.log(age) // 20

导入只有一个 export default 的内容,代码如下:

//other.js
export default function sum(num1,num2) {
    return num1 + num2;
}
//import.js
import sum from "other.js"
console.log(sum(1,1)) // 2

有时也会在代码中看到 module.exports 的用法,这种用法是从 Node.jsCommonJS 演化而来的,它其实就相当于:

module.exports = xxx
// 相当于
export xxx

ES 6 的模块化方案使得原生 JavaScript 的 “拆分” 能力提升了一个大的台阶,几乎成为当下最流行的写法,并且应用在大部分的企业项目中。

Promise和async/await

Promise

Promise 是一种适用于异步操作的机制,比传统的回调函数解决方案更合理和更强大。从语法上说,Promise 是一个对象,从它可以获取异步操作的结果:成功或失败。在 Promise 中,有三种状态:pending(进行中)、resolved(已成功)和 rejected(已失败)。只有异步操作的结果可以决定当前是哪一种状态,无法被 Promise 之外的方式改变。这也是 Promise 这个名字的由来,它的英语意思就是 “承诺”,表示其他手段无法改变。创建一个 Promise 对象,代码如下:

var promise = new Promise(function(resolve, reject) {
    ...
    if (/* 异步操作成功 */){
        resolve(value);
    } else {
        reject(error);
    }
});

在上面的代码中,创建了一个 Promise 对象,Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolvereject。这是两个内置函数,resolve 函数的作用是将 Promise 对象的状态变为 “成功”,在异步操作成功时调用,并将异步操作的结果作为参数传递出去;reject 函数的作用是将 Promise 对象的状态变为 “失败”,在异步操作失败时调用,并将异步操作报出的错误作为参数传递出去。当代码中出现错误(Error)时,就会调用 catch 回调方法,并将错误信息作为参数传递出去。

Promise 对象实例生成后,可以用 then 方法分别指定 resolved(成功)状态和 rejected(失败)状态的回调函数以及 catch 方法,比如:

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

then() 方法返回的是一个新的 Promise 实例(不是原来那个 Promise 实例)。因此,可以采用链式写法,即 then() 方法后面再调用另一个 then() 方法,比如:

getJSON("/1.json").then(function(post) {
    return getJSON(post.nextUrl);
}).then(function (data) {
    console.log("resolved: ", data);
}, function (err){
    console.log("rejected: ", err);
});

下面是一个用 Promise 对象实现的 Ajax 操作 get 方法的例子。

var getJSON = function(url) {
    // 返回一个Promise对象
    var promise = new Promise(function(resolve, reject){
        var client = new XMLHttpRequest(); //创建XMLHttpRequest对象
        client.open("GET", url);
        client.onreadystatechange = onreadystatechange;
        client.responseType = "json";              //设置返回格式为json
        client.setRequestHeader("Accept", "application/json");//设置发送格式为json
        client.send();//发送
        function onreadystatechange() {
            if (this.readyState !== 4) {
                return;
            }
            if (this.status === 200) {
                resolve(this.response);
            } else {
                reject(new Error(this.statusText));
            }
        };
    });
    return promise;
};
getJSON("/data.json").then(function(data) {
    console.log(data);
}, function(error) {
    console.error(error);
});

了解 Promise 的基本知识可以便于后续学习使用服务端渲染。当然,Promise 的应用场合还是比较多的,如果想要深入了解,可以访问网址: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise ,进行系统的学习。

async/await

async/await 语法在 2016 年就已经提出来了,属于 ES 7 中的一个测试标准(目前来看是直接跳过 ES 7,列为 ES 8 的标准了),它主要为了解决下面两个问题:

  • 过多的嵌套回调问题。

  • Promise 为主的链式回调问题。

前面讲解过 Promise,虽然 Promise 解决了恐怖的嵌套回调问题,但是解决得并不彻底,过多地使用 Promise 会引发以 then 为主的复杂链式调用问题,同样会让代码阅读起来不那么顺畅,而 async/await 就是它们的救星。

async/await 是两个关键字,主要用于解决异步问题,其中 async 关键字代表后面的函数中有异步操作,await 关键字表示等待一个异步方法执行完成。这两个关键字需要结合使用。

当函数中有异步操作时,可以在声明时在其前面加一个关键字 async,代码如下:

async function myFunc() {
    //异步操作
}

使用 async 声明的函数在被调用时会将返回值转换成一个 Promise 对象,因此 async 函数通过 return 返回的值会进入 Promiseresolved 状态,成为 then 方法中回调函数的参数,代码如下:

// myFunc()返回一个Promise对象
async function myFunc() {
    return 'hello';
}
// 使用then方法就可以接收到返回值
myFunc().then(value => {
    console.log(value); // hello
})

如果不想使用 Promise 的方式接收 myFunc() 的返回值,可以使用 await 关键字更加简洁地获取返回值,代码如下:

async function myFunc() {
    return 'hello';
}
let foo = await myFunc(); // hello

await 表示等待一个 Promise 返回,但是 await 后面的 Promise 对象不会总是返回 resolved 状态,如果发生异常,则进入 rejected 状态,那么整个 async 异步函数就会中断执行,为了记录错误的位置和编写异常逻辑的代码,需要使用 try/catch,代码如下:

try {
    let foo = await myFunc(); // hello
} catch (e) {
    // 错误逻辑
    console.log(e)
}

下面举一个例子,在后面的实战项目开发中,经常会用到数据接口请求数据,接口请求一般是异步操作,例如在 Vuemounted 方法中请求数据,代码如下:

async mounted () {
    // 代码编写自上而下,一行一行,以便于阅读
    let resp = await ajax.get('weibo/list')
    let top = resp[0]
    console.log(top)
}

在上面的代码中,ajax.get() 方法会返回一个 Promise,采用 await 进行了接收,并且 await 必须包含在一个用 async 声明的函数中。

可以看出,在使用了 async/await 之后,整个代码的逻辑更加清晰,没有了复杂的回调和烦琐的换行。

至此,对于实战项目中用到的相关 ES 6 语法基本讲解完毕,如果读者想进一步了解 ES 6 的更多语法知识,可以自行在其官网上学习。