Vue 3 的概述以及 ES 6 基本语法的介绍

第 1 章 Vue.js 概述

本章首先介绍前端架构模式,然后在此基础上介绍 Vue 及其安装方式,接着介绍 Vue 3的新特性,最后介绍 ECMAScript6 (简称ES 6)的语法。

1.1 认识 MVC和 MVVM 模式

在学习 Vue.js之前,我们先来了解一下 MVVM (Model-View-ViewModel, 模型-视图-视图模型)模式,它是一种基于前端开发的架构模式。MVVM 最早出现于 2005年微软推出的基于 Windows 的用户界面框架 WPF,它其实是一种编程设计思想,既然是思想,就不限于在什么平台或者用什么语言开发。基于 MVVM的诸多优点,其在当今移动和前端开发中应用得越来越广泛。

1.1.1 传统的 MVC 模式

如果读者了解 MVC (Model-View-Controller) 模式,那么 MVVM 模式应该更好理解。传统的 MVC 模式包括以下三部分:

  • 视图(View):用户界面。
  • 控制器(Controller):业务逻辑。
  • 模型(Model):数据存储。

Model 代表数据存储,主要用于实现数据的持久化;View 代表用户界面(UI),主要用于实现页面的显示;Controller 代表业务逻辑,串联起 View 和 Model,主要用来实现业务的逻辑代码。在MVC模式中,用户的交互行为在 View中触发,由 View 通知 Controller 去进行对应的逻辑处理,处理完成之后酒知 Modl改变状态,Model 完成状态改变后,北到对应的 Viev.去更新用户界面的你宗为容,至此完成对用户交互行为的反微。由此可见,整个流程电 Yiew 发起,最终在 View 做出改变,这是一个单向的过程。当年流行的 backbone.js就是MVC的典型代表。

1.1.2 流行的 MVVM 模式

MVVM 是把 MVC中的 Controller 去除了,相当于变薄了,取而代之的是 ViewModel。所谓ViewModel,是一个同步的 View和 Model 的对象,在前端 MVVM中, ViewModel 最典型的作用是操作 DOM,特点是双向数据绑定(Data-Binding)。

在双向数据绑定中,开发者无须关注如何找到 DOM 节点和如何修改 DOM 节点,因为每一个在View 中需要操作的 DOM 都会有一个在 Model 中对应的对象,通过改变这个对象,DOM 就会自动改变:反之,当 DOM 改变时,对应的 Model中的对象也会改变。ViewModel 将 View 和 Model 关联起来,因此开发者只需关注业务逻辑,不需要手动操作DOM, 这就是 ViewModel 带来的优势。

MVVM 让开发者更加专注于页面视图,从视图出发来编写业务逻辑,这也符合正常的开发流程,而 Vue.js 就是一个典型的从视图 (View) 出发的前端 MVVM 框架。从 Vue 的英文发音类似 View 就可以参透其中的奥秘。

1.2 Vue.js 简介

1.2.1 Vue.js 的由来

Vue.js的作者是尤雨溪(Evan You), 曾就职于 Google Creative Lab, 当时 Angular.js 由 Google 公司推出不久,但 Angular.js1 被人诟病过于庞大、功能复杂、上手难度高,于是,尤雨溪从 Angular.js 中提取了自己喜欢的部分,摒弃了影响性能的部分,构建出了一款相当轻量的框架 Vue.js。所以,现在大家看到的Vue.js的一些语法和 Angular.js 1 版本的语法有不少相似之处。在作者尤雨溪完成第一个版本时,曾将这款框架命名为 Seed.js、 View.js,但是发现这两个名字在当时的 NPM 库中已经被使用,而 View在法语中和 Vue 同音,所以便将Vue.js 赋予了这款框架。

Vue.js最早发布于2014年2月,尤雨溪在 Hacker News2、Echo JS3 与 Reddit4 的 /r/javascript 版块发布了最早的版本,在一天之内,Vue.js 就登上了这3个网站的首页。之后 Vue.js 成为GitHub上最受欢迎的开源项目之一。

同时,在JavaScript框架→函数库中,Vue.js 所获得的星标数已超过 React,并高于 Backbone.js、Angular 2、jQuery 等项目。

Vue.js 是一套构建用户界面的渐进式框架。与其他重量级框架不同的是,Vue.js 采用自底向上增量开发的设计。Vue.js 所关注的核心是 MVVM 模式中的视图层,同时,它也能方便地获取数据更新,并通过组件内部特定的方法实现视图与模型的交互。

1.2.2 Vue.js、前端工程化和 Webpack

前端工程化这个词相信读者并不陌生,在早期的 Web 应用中,前端开发顶多是写写 HTML代码,实现页面的布局,最后交给后端工程师,甚至有些业务都是由后端工程师一肩挑,但随着业务和复杂性、技术的发展,前端已经不是简单地写页面和样式了,而是包括一系列可以流程化和规范化的能力,称作前端工程化,这主要包括以下几个部分:

  • 静态资源和动态资源的处理。
  • 代码的预编译。
  • 前端的单元测试和自动化测试。
  • 开发调试工具。
  • 前端项目的部署。

随着前端工程化的不断流行,仅仅靠手工来完成这些操作显得效率很低,前端迫切需要一款支持上面几个部分功能的工具,随后便出现了诸如 Webpack 或 Browserify5 模块的打包工具。越来越多的前端框架需要结合模块打包工具一起使用,Vue.js 也不例外,目前和 Vue.js 结合使用最多的模块打包工具非Webpack莫属。

Webpack 的主要功能是将前端工程所需要的静态资源文件(例如CSS、JavaScript、图片等)打包成一个或者若干个JavaScript文件或CSS文件。同时提供了模块化方案来解决Vue组件之间的导入问题。本书后续的第8章会用到它,但是由于篇幅有限,本书并不会对 Webpack进行详细的讲解,读者如果想了解更多有关 Webpack 的内容,可以到官网上去查阅,网址为 https://webpack.js.org/。

1.3 Vue.js 的安装和导入

对于刚开始使用 Vue.js 的读者,可以采用最简单、最原始的方式来安装或者导入 Vue.js。当然,也可以通过 npm 工具来安装或导入 Vue.js。

1.3.1 通过 <script> 标签导入

与大多数的前端框架库一样,在 HTML 页面中,可以通过 <script> 标签的方式导入 Vue.js,这里我们引入 Vue3,如示例代码所示。

<script src="https://unpkg.com/vue@next"></script>

当然,可以将这个链接指向的 JavaScript 文件下载到本地计算机中,再从本地计算机导入。需要说明的是,Vue.js 3 有多个版本,例如 Vue.js 3.0.0、Vue.js 3.1.0等,同时也在不断更新中,通过上面配置的链接可以获取到最新的 Vue 3版本。当然,如果想固定使用某个版本,也可以将链接修改为 https://umpkg.com/vue@3.2.28/dist/vue.global.prod.js,本书中与Vue.js 相关的内容都基于 3.2.28 版本。

1.3.2 通过 npm 导入

在使用 Vue.js开发大型项目时,推荐使用 npm工具来安装 Vue.js。 npm 可以很好地和诸如Webpack 或 Rollup 等模块打包工具配合使用,如示例代码所示。

npm install vue@next

1.3.3 通过 Vue Cli 和 Vite 导入

对于真实的生产项目而言,笔者更推荐采用 Vue Cli 或者 Vite 的方式来创建Vue项目,这更符合前端工程化的思想。

Vue Cli 是一个官方的脚手架工具,基于 Webpack,提供页面应用的快速搭建,并为现代前端工作流提供了功能齐备的构建设置。只需要几分钟的时间就可以运行起来,并带有热重载、保存时 lint 校验以及生产环境可用的构建版本。

Vite是一个Web开发构建工具,基于Rollup,伴随着 Vue 3 而来,由于其原生的 ES6 模块导入方式,可以实现闪电般的冷服务器启动。我们将会在后面的章节深入讲解这两个工具的使用。

1.4 Vue 3 新特性概览

截至目前,Vue.js 的新版本是 3.2。据 Vue.js 的作者表示,新的 Vue3 编写的应用程序性能和运行效果非常好,相较于 Vue2.x 版本,Vue 3 主要有以下几个方面大的改动以及提升:

  • 更快
  • 更小
  • 更易于维护

本节主要对这些新的改动来做一下简单的概述,可能涉及 Vue 3 的新语法,各位读者如果看不懂,我们还会在后面的章节深入讲解的。

1.4.1 更快、更小、更易于维护

  • 更快

    更快主要体现在Vue3在性能方面的提升,以及在源码层面的改动,主要包括以下方面:

    • 重构虚拟DOM。
    • 事件缓存。
    • 基于Proxy的响应式对象。
    1. 重构虚拟 DOM

      Vue 3 重写了虚拟 DOM 的实现方法,使得初始渲染/更新可以提速达100%,对于 Vue 2.x 版本的虚拟 DOM 来说,Vue 会遍历 <template> 模板中的所有内容,并根据这些标签生成对应的虚拟 DOM (虚拟 DOM 一般指采用 key/value 对象来保存标签元素的属性和内容),当有内容改变时,遍历虚拟 DOM 来找到变化前后不同的内容,我们称这个过程叫作 diff(different) ,并找到针对这些变化的内容所对应的DOM节点,并改变其内部属性。例如下面这段代码:

      当触发响应式时,遍历所有的 <div> 标签和 <p> 标签,找到 {{count}} 变量对应的 <p> 标签的 DOM 节点,并改变其内容。对于那些纯静态 <p> 标签的节点进行 diff 其实是比较浪费资源的,当节点的数量很少时,表现并不明显,但是一旦节点的数量过大,在性能上就会慢很多。对此,Vue 3 在此基础上进行了优化,主要有:

      • 标记静态内容,并区分动态内容(静态提升)。
      • 更新时只 diff 动态的部分。

      针对上面的代码,Vue 3 中首先会区分出 {{count}} 这部分动态的节点,在进行 diff 时,只针对这些节点进行,从而减少资源浪费,提升性能。

    2. 事件缓存

      我们知道在 Vue 2.x 中,在绑定 DOM 事件时,例如@click,这些事件被认为是动态变量,所以每次更新视图的时候都会追踪它的变化,然后每次触发都要重新生成全新的函数。在 Vue 3中,提供了事件缓存对象 cacheHandlers,当 cacheHandlers 开启的时候,@click绑定的事件会被标记成静态节点,被放入 cacheHandlers 中,这样在视图更新时也不会追踪,当事件再次触发时,就无须重新生成函数,直接调用缓存的事件回调方法即可,在事件处理方面提升了 Vue 的性能。未开启 cacheHandlers 编译后的代码如下:

      <div @click="hi">Hello World</div>
      //编译后
      export function render(_ctx, _cache, $props, $setup, Sdata, $options){
      	return (_openBlock(),_createElementBlock("div", {onClick:_ctx.hi},"Hello World1",/*PROPS*/,["onClick"]))
      }
      

      开启cacheHandlers编译后的代码如下:

      <div @click="hi">Hello World</div>
      // 编译后
      export function render(_ctx, _cache, Sprops, $setup, Sdata, $options) {
      	return (_openBlock(),_createElementBlock("div",{onClick:_cache[0] || (_cache[0]=(...args) => (_ctx.hi && _ctx.hi(...args)))},"Hello World1")) 
      }
      

      可以看到主要区别在于onClick那一行,直接从缓存中读取了回调函数。

    3. 基于 Proxy 的响应式对象

      在 Vue2.x 中,使用 Object.defineProperty)来实现响应式对象,对于一些复杂的对象,需要循环递归地给每个属性增加 getter/setter 监听器,这使得组件的初始化非常耗时,而 Vue 3中,引入了一种新的创建响应式对象的方法 reactive,其内部就是利用 ES6 的 Proxy API 来实现的,这样可以不用的为每个属性来一一进行添加,以减少开销,提升性能,我们会在后续章节具体讲解 Vue 3 的响应式 和 Proxy API。

  • 更小

    更小主要体现在包所占容量的大小,我们知道,前端资源一般都属于静态资源,例如JavaSript 文件、HTML 文件等,这些资源都托管在服务器上,用户在使用浏览器访问时,会将这些资源下载下来,所以精简文件包大小是提升页面性能的重要因素。Vue 3 在这方面可以让开发者打包构建出来的资源更小,从而提升性能。

    Tree Shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code),就像一棵大树,将那些无用的叶子都剪掉。它依赖于 ES6 模块语法的静态结构特性,例如 import 和 export ,这个术语和概念在打包工具 Rollup 和 Webpack 中普及开来。例如下面这段ES6代码:

    import {get} from'./api.js'
    let doSome () =>{
    	get ()
    }
    doSome ()
    // api.js
    let post =()=>{
    	console.log('post')
    }
    export post
    let get = () =>{
    	console.log('get')
    }
    export get
    

    上面的代码中,api.js代码中的 post 方法相关内容是没有被引入和使用的,有了 Tree Shaking 之后,这部分内容是不会被打包的,这就在一定程度上减少了资源的大小。使用Tree Shaking 的原理是引入了 ES6 的模块静态分析,这就可以在编译时正确判断到底加载了什么代码,但是要注意 import 和 export 是 ES6 原生的,而不是通过 Babel 或者 Webpack 转化的。
    在 Vue 3中,对代码结构进行了优化,让其更加符合 Tree Shaking 的结构,这样使用相关的 API 时,就不会把所有的都打包进来,只会打包用户用到的API,例如:

    <!-- vue 2.x-->
    import Vue from 'vue'
    new Vue()
    Vue.nextTick(()=>())
    const obj = Vue.observable(())
    
    <1-- vue 3.x -->
    import ( nextTick, observable,createApp ) from 'vue'
    nextTick(()=>())
    const obj = observable(1))
    createApp(())
    

    同理,例如 <keep-alive>、<transition> 和 <teleport> 等内置组件,如果没有使用,也不会被打包到资源中。

  • 更易于维护

    1. 从Flow迁移到TypeScript

      TypeScript 是微软开发的一个开源的编程语言,通过在 JavaScript 的基础上添加静态类型定义构建而成,其通过 TypeScript 编译器或 Babel 转译为 JavaScript 代码,可运行在任何浏览器和操作系统上。TypeScript 引入了很多新的特性,例如类型监测、接口等,这些特性在框架源码的维护上有很大的提升。

      在Vue 3的源码结构层面,从 Flow 改成了 TypeScript 来编写,Flow 是一个静态类型检测器,有了它就可以在 JavaScript 运行前找出常见的变量类型的 bug,类似于Java语言中给变量强制指定类型,它的功能主要包括:

      • 自动类型转换。
      • null引用。
      • 处理 undefined is not a function。

      例如:

      // @flow
      function foo(x: number): number {
      	return x + 10
      }
      foo('hi') //参数x须为number类型,否则会报错
      
      错误信息:
      '[flow] string (This type is incompatible with number See also: function call)'
      

      上面这段代码采用了Flow后,如果类型不对就会报错。一般来说,对于 JavaScript 源码框架,引入类型检测是非常重要的,不仅可以减少 bug 的产生,还可以规范一些接口的定义,这些特性和 TypeScript 非常吻合,所以在 Vue 3 中直接采用了 TypeScript 来进行重写,从源码层面来提升项目的可维护性。

    2. 源代码目录结构遵循 Monorepo

      Monorepo 是一种管理代码的方式,它的核心观点是所有的项目在一个代码仓库中,但是代码分割到一个个小的模块中,而不是都放在 src 这个目录下。这样的分割,使得每个开发者大部分时间只是工作在少数的几个文件夹内,并且也只会编译自己负责的模块,不会导致一个 IDE 打不开太大的项目之类的事情,这样很多事情就简单了很多。

      目前很多大型的框架(例如 Babel、React、Angular、Ember、Meteor、Jest等) 都采用了 Monorepo 这种方式来进行源码的管理,当然在自己的业务项目中,也可以使用 Monorepo 来管理代码。

1.4.2 新特性初体验

  • 组合式API

    在 Vue 2.x 中,组件的主要逻辑是通过一些配置项来编写,包括一些内置的生命周期方法或者组件方法,例如下面的代码:

    export default{
    	name: 'test',
    	components:{},
    	Props:{},
    	data(){
    		return {}
    	},
    	created(){},
    	mounted(){},
    	watch:{},
    	methods:{}
    }
    

    上面的代码中,这些基于配置的组件写法称为 Options API (配置式API),Vue 3 的一大核心新特性是引入了 Composition API(组合式API),这使得组件的大部分内容都可以通过 setup方法进行配置。将上述代码改造成组合式 API,代码如下:

    import{onMounted, reactive,watch} from 'vue'
    export default{
    	props:{
    		name:String,
    	},
    	name: 'test',
    	components:{},
    	setup (props,ctx){
    		console.log(props.name)
    		console.log('created')
    		const data = reactive({
    			a:1
    		})
    		watch{
    			() => data.a,
    			(val, oldVal) => {
    				console.log(val)
    			}
    		}
    		onMounted(()=>{
    		})
    		const myMethod = (obj) =>{
    		}
    		retrun{
    			data,
    			myMethod
    		}
    	}
    }
    

    上面的代码采用了ES6的语法,并且使用了 Vue3 的 Composition API 中的 setup 方法,可能读者有些看不懂,没关系,我们会在后续章节中具体讲解。

  • 内置组件 Teleport、Suspense 和 Fragments 片段

    <teleport> 和 <suspense> 都是 Vue 3里面新增的内置组件,这里把内置组件称作可以直接写在<ctemplate>里面,而不需要格外引入的组件,例如 <keep-alive> 就是一个内置组件。而 Fragments 是一种新的特性,让开发者可以不用在 <template> 中强制包裹一个根元素,关于<teleport> 和 <suspense> 的内容会在第3章深入讲解。

  • 服务端渲染

    在服务端渲染方面,Vue 3 优化了返回 HTML 字符串的逻辑。在 Vue2.x 中,所有的节点(包括一些静态节点)在服务端返回时都会转换为虚拟 DOM,再转换成 HTML 字符串返回给浏览器;Vue 3 则将静态节点剥离成字符串,这部分内容不会转换成虚拟 DOM,而是直接拼接返回,在效率上进行了提升。

    Vue 2.x 的服务端渲染代码如下:

    <div>
    	<div>abc</div>
    	<div>abc</div>
    	<div>abc</div>
    	<div>{{msgl}}</div>
    </div>
    //编译后
    function anonymous(){
    	var _vm = this;
    	var _h = _vm.$createElement;
    	var _c = _vm._self._c || _h;
    	return _c('div', [_vm._ssrNode("<div>abc</div> <div>abc</div> <div>abc</div> <div>" + _vm._ssrEscape(_vm._s(_vm.msg))+"</div>")])
    }
    

    Vue 3 的服务端渲染代码如下:

    <div>
    	<div>abc</div>
    	<div>abc</div>
    	<div>abc</div>
    	<div>{{msg}}</div>
    </div>
    // 编译后
    export function ssrRender(_ctx, _push, _parent, attrs, $props, $setup, $data, $options){
    	const _cssVars= { style: { color: _ctx.color }}
    	_push('<div${ _ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}>xdiv>abc</div><div>abc</div><div>abc</div><div>${_ssrInterpolate(_ctx.msg)}</div></div>`)
    }
    
  1. Vite

    伴随着 Vue 3,Vue 团队也推出了自己的开发构建工具 Vite,可以在一定程度上取代 Vue Cli 和 webpack-dev-server 的功能。基于此,Vite 主要有以下特性:

    • 快速的冷启动
    • 即时的模块热更新
    • 真正的按需编译

    Vite 在开发环境下基于浏览器原生 ES6 Modules 开发,在生产环境下基于 Rollup 打包,我们会在后续章节深入讲解 Vite 的相关使用。

第 2 章 ES 6 语言基础

ES 6 是一次重大的版本升级,与此同时,由于 ES 6 秉承着最大化兼容已有代码的设计理念,过去编写的 JS 代码还能正常运行。事实上,许多浏览器已经支持部分 ES 6 特性,并继续努力实现其余特性。这意味着,在一些已经实现部分特性的浏览器中,开发者符合标准的 JavaScript 代码已经可以正常运行,可以更加方便地实现很多复杂的操作,提高开发人员的工作效率。

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

以下是 ES 6 排名前10位的最佳特性列表(排名不分先后):

  • Default Parameters(默认参数)
  • Template Literals(模板文本)
  • Multi-line Strings(多行字符串)
  • Destructuring Assignment(解构赋值)
  • Enhanced Object Literals(增强的对象文本)
  • Arrow Functions(箭头函数)
  • Promises
  • Block-Scoped Constructs Let and Const (块作用域构造 Let 和 Const )
  • Classes(类)
  • Modules(模块)

1. 块作用域构造 Let 和 Const

在 ES 6 语法中,新增了 let 和 const 来声明变量,在 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 s not defined

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

  • 使用 var 声明的变量没有区块的概念,可以跨块访问
  • 使用 let 声明的变量只能在区块作用域中访问,不能跨块访问

在相同的作用域下,使用 var 和 let 具有相同的效果,建议在 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 时,就需要思考一下变量的用途和类型,选择合适的 let 和 const 来使代码更加规范和语义化。

2. 模板字面量

ES 6 引入了模板字面量(Template Literals),主要通过多行字符串(Multi-line Strings)和字符串占位符对字符串的操作进行增强。

多行字符串

ES 5 中,如果一个字符串面量要分为多行,那么可以采用以下两种方法来实现。

(1)、在一行的结尾添加反斜杠表示承接上一行的代码。这种方式是利用JavaScript的语法Bug来实现的:

//多行字符串
var roadPoem = "江南好,\
风景旧曾谙。";

(2)、使用加号来拼接字符串。

//多行字符串
var roadPoem = "江南好,风景旧曾谙。"
+ "日出江花红胜火,春来江水绿如蓝。"
+ "能不忆江南?";

ES 6 的多行字符串是一个非常实用的功能。模板字面量的基础语法就是使用反引号替换字符串的单、双引号。例如:

let roadpoem = `江南好,风景旧曾谙。`;

如果需要在字符串中使用反引号,可以使用反斜杠将它转义。

let roadPoem = `江南好,\`风景旧曾谙。`;

ES 6 中,使用模板字面量语法可以非常方便地创建多行字符串。

//多行字符串
let roadPoem = `江南好,风景旧曾谙。
日出江花红胜火,春来江水绿如蓝。`;
console.log(roadPoem);

输出结果为:

江南好,风景旧曾谙。
日出江花红胜火,春来江水绿如蓝。

字符串占位符

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

ES 6 中,占位符是使用语法 ${NAME} 的,并将包含的NAME变量或者表达式放在反号中:

let name = "xiaoming";
let names = `zhang ${name}`;
let price = 18.8;
let num = 8;
let total = `商品的总价是:${price * num}`;

由于模板字面量本身也是 JavaScript 表达式,因此也可以在一个模板字面量中嵌入另一个模字面量。

let price = 10;
let num = 8;
let total = `经过计算,${
`商品的总价是:${price ★ num}`
}`;
console.log(total);

输出结果为:

经过计算,商品的总价是:80

3. 默认参数和 rest 参数

ES 5 中,JavaScript 定义默认参数的方式如下:

//以前的 JavaScript 定义默认参数的方式
function funl(height, color, url){
	var height = height || 150;
	var color = color || "red";
	var url = url || "http://www.baidu.com";
	...
}

ES 6 中,可以直接把默认值放在函数声明中:

//新的 JavaScript 定义方式
function fun2(height = 50, color = "red", url = "http://www.baidu.com"){
	...
}

ES 6 中,声明函数时,可以为任意参数指定默认值,在已指定默认值的参数后还可以继续声明无默认值的参数。

function fun3(height = 50, color = "red", ur1){}

在这种情况下,只有在没有为 height 和 color 传值,或者主动为它们传入 undefined 时才会使用它们的默认值。

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

function data(){
	console.log(arguments);
}
data("li","cc";

ES 6 中引入了 rest 参数,在函数的命名参数前添加了3个点,用于获取函数的实参。rest 参数是一个数组,包含自它之后传入的所有参数,通过这个数组名就可以逐一访问里面的参数。

function data(...args){
	console.log(args);
}
data("苹果","香蕉","橘子");

rest参数必须放到参数最后位置

function fn(a, b,...args){
	console.log(a);
	console.log(b);
	console.log(args);
}
fn(100, 200, 300, 400, 500, 600);

4. 解构赋值

ES 5 中,如果需要从某个对象或者数组中提取需要的数据赋给变量,可以采用如下方式:

let goods = {
	name : "苹果",
	city : "烟台",
	price : "烟台"
} 
//提取对象中的数据赋给变量
let name = goods.name;
let city = goods.city;
let price = goods.price;
//提取数组中的数据赋给变量
let arr= [100, 200, 300, 400];
let a1 = arr[0], a2 = arr[1], a3 = arr[2], a4 =arr[3];

ES 6 中,通过使用解构赋值的功能,可以从对象和数组中提取数值,并对变量进行赋值数

对象解构

对象解构的方法是在一个赋值操作符的左边放置一个对象字面量。

let goods = {
	name : "苹果",
	city : "烟台",
	price : "烟台"
}
//使用解构赋值的功能
let {name,city,price} = goods;

如果变量已经声明了,之后想要用解构语法给变量赋值,则需要把整个解构赋值语句放到圆括号中。

let goods = {
	name : "苹果",
	city : "烟台",
	price : "烟台"
}
//先声明变量,然后解构赋值
let name,city,price;
({name,city,pzice}= goods) ;

数组解构

因为没有对象属性名的问题,所以数组解构相对比较简单,使用方括号即可。

let arr = [100,200, 300, 400];
let [a1,a2,a3,a4] = arr;

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

let arr = [100,200,300,400];
//获取第 4 个位置的元素
let [,,,a4] = arr;
console.log(a4)//输出 400

和对象解构不同,如果为已经声明过的变量进行数组解构赋值,不需要把整个解构赋值语句到一个圆括号中。

let arr =[100,200,300,400];
let a1, a2, a3, a4;
[a1, a2, a3, a4] = arr;

5. 展开运算符

展开运算符(Spread Operator)也是3个点,允许一个表达式在某处展开。展开运算符在多个参数(用于函数调用)、多个元素(用于数组字面量)或者多个变量(用于解构赋值)的地方可以使用。

在函数调用中使用展开运算符

ES 5 中可以使用 apply 方法将一个数组展开成多个参数:

function test(a, b,c) {}
var args = [100, 200, 300};
test.apply(null, args);

上面的代码中,把args数组当作实参传递给了a、b和c。

ES 6 中可以更加简洁地来传递数组参数:

function test(a,b,c) {}
var args = [100, 200, 300];
test(...args) ;

这里使用展开运算符把 args 直接传递给 test 函数。

在数组字面量中使用展开运算符

ES 6 中,可以直接加一个数组并合并到另一个数组中:

var arr1 = ['a', 'b', 'c'];
var arr2 = [...arr1, 'd', 'e']; //['a','b','c','d','e']

展开运算符也可以用在 push 函数中,可以不需要再使用 apply 函数来合并两个数组:

var arr1 = ['a', 'b', 'c'];
var arr2 = ['d', 'e'];
arrl.push(...arr2); //['a','b','c','d','e']

用于解构赋值

解构赋值也是 ES 6 中新添加的一个特性,这个展开运算符可以用于部分情景:

let [argl,arg2,...,arg3]=[1, 2, 3, 4];
argl //1
arg2 //2
arg3 //['3', '4']

展开运算符在解构赋值中的作用跟之前的作用看上去是相反的,它将多个数组项组合成了一个新数组。
不过要注意,解构赋值中的展开运算符只能用在最后。

let [arg1,...,arg2,arg3] = [1, 2, 3, 4]; //报错

类数组对象变成数组

展开运算符可以将一个类数组对象变成一个真正的数组对象:

var list = document.getElementsByTagName('div');
var arr = [...list];

list 是类数组对象,这里通过使用展开运算符使其变成了数组。

6. 增强的对象文本

ES 6 添加了一系列功能来增强对象文本,从而使得处理对象更加轻松。

通过变量进行对象初始化

ES 5 中,对象的属性通常是由具有相同名称的变量创建的。例如:

var
	a = 100,b = 200, c = 300;
	obj = {
		a : a,
		b : b,
		c : c
	};
// obj.a = 100, obj.b = 200, obj.c = 300

ES 6 中,简化如下:

const
	a = 100, b = 200, c = 300;
	obj = {
		a
		b
		c
	};

简化定义对象方法

ES 5 中,定义对象的方法需要 function 语句。例如:

var lib = {
	sum : function(a, b) { return a + b;},
	mult : function(a, b) { return a * b;}
};
console.log( lib.sum(100, 200)); // 300
console.log( lib.mult(100, 200)); // 20000

ES 6 中,定义对象的方法简化如下:

const lib = {
	sum(a, b) { return a + b; },
	mult(a, b) { return a * b; }
};
console.log( lib.sum(100,200));// 300
console.log( lib.mult(100, 200));//20000

这里不能使用 ES 6 的箭头函数(=>),因为该方法需要一个名称。如果直接命名每个方法,则可以使用箭头函数(=>)。例如:

const lib = {
	sum : (a, b) => a + b,
	mult : (a, b)=> a * b
};
console.log( lib.sum(100,200));// 300
console.log( lib.mult(100, 200)); //20000

动态属性键

ES 5 中,虽然可以在创建对象之后添加变量,但是不能使用变量作为键名称。例如

var
	key1 = 'one',
	obj = {
		two : 200,
		three : 300
	};
obj[key1] = 100;
//表示obj.one = 100,obj.two = 200, obj.three = 300

通过在方括号(Ⅱ)内放置表达式,可以在ES6中动态分配对象键。例如:

const
	key1 = 'one',
	obj = {
		[key1]: 100,
		two : 200,
		three : 300
	}
//表示obj.one = 100, obj.two = 200, obj.three = 300

解构对象属性中的变量

ES 5 中,可以将对象的属性值提取到另一个变量中。例如:

var myobject = { 
	one : '洗衣机',
	two : '冰箱',
	three : '空调'
);
var
	one = myobject.one,//洗衣机
	two = myobject.two,//冰箱
	three = myobject.three; //空调

ES 6 中,通过解构可以创建与等效对象属性同名的变量。例如:

var myobject = { 
	one : '洗衣机',
	two : '冰箱',
	three : '空调'
);
const { one, two, three } =  myobject;
//表示one = '洗衣机',two = '冰箱',three = '空调'

7. 箭头函数

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(numl, num2) {
	return numl + num2
}

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

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

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

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

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

  1. 对象属性和方法的简写

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

    const foo = 'bar'
    const baz = {foo}
    // 等同于
    const baz = {foo: 'bar'}
    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 方法等,是后面实战项目中经常使用的写法。

8. Modules (模块)

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

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

import 和 export

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

在 ES 6 的模块化系统中,一个模块就是一个独立的文件,模块中的对外接口采用 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(numl,num2){
	return numl + num2;
}
//等价于
let sum = function (numl,num2){
	return numl + 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(numl,num2){
	return numl + num2;
}
//import.js
import sum from "other.is"
console.log(sum(1,1)) // 2

有时也会在代码中看到 module.exports 的用法,这种用法是从 Node.js 的 CommonJS 演化而来的

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

9. 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 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject 。这是两个内置函数,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 返回的值会进入 Promise 的 resolved 状态,成为 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)
}

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

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

在上面的代码中,ajax.get 方法会返回一个 Promise,采用 await 进行了接收,并且 await 必须包含在一个用 async 声明的函数中。
可以看出,在使用了 async/await 之后,整个代码的逻辑更加清晰,没有了复杂的回调和烦的换行。

10. Classes(类)

在之前的 JavaScript 版本中,JavaScript 不支持类和类继承的特性,只能使用其他模拟类的定义和类的继承**。ES 6** 引入了类的概念,通过关键字 class 使类的定义更接近面向对象语言。

ES 5 中,没有类的概念,可以通过构造函数和原型混合使用的方式来模拟定义类。

function Goods(gName,gPrice) {
	this.name = gName;
	this.price = gPrice;
}
Goods.prototype.showName = function(){
	console.log(this.name);
};
Var sGoods = new Goods("洗衣机”, 6800);
sGoods.showname();

ES 6 中,使用类可以改写上面的代码:

class Goods{
	constructor(gName,gPrice) (
		this.name = gName;
		this.price = gPrice;
	}
	showName () {
		console.log(this.name);
	}
}
let sGoods = new Goods("洗衣机”,6800);
sGoods.showname();

ES 6 中,可以通过 extends 关键字来继承类:

class Goods{
	constructor(gName) (
		this.name = gName;
	}
	showName () {
		console.log(this.name);
	}
}
//通过extends关键字来继承类Goods
class Goods1 extends Goods{
	constructor(gName,gPrice) {
		super(gName); //调用父类的 constructor(gName)
		this.price = gPrice;
	}
}
let gl = new Goods1("洗衣机",6800);
gl.showname();

  1. Angular.js 1 也叫作 AngularJS,是由 Google 公司在 2012 年发布的一个 JavaScript的 MVC框架,目前还有 Angular 2、Angular 4 两个版本。 ↩︎

  2. Hacker News 是一家关于计算机黑客和创业公司的社会化新闻网站,由保罗·格雷厄姆的创业孵化器 Y Combinator 创建,网站内容主要由来自用户提交的外链构成,是国外比较流行的技术信息交流网站之一 ↩︎

  3. Echo JS是一个由国外社区驱动的信息交流网站,网站内容主要由来自用户提交的外链构成,完全专注于JavaScript开发、HTML5和前端信息 ↩︎

  4. Reddit 是一个国外娱乐、社交及新闻网站,包含众多模块,注册用户可以将文字或链接提交到该网站上发布,使它基本上成为了一个电子布告栏系统 ↩︎

  5. Browserify是一个开源的前端模块打包工具,功能上和Webpack类似,但是名气不如Webpack ↩︎

  • 19
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海绵不是宝宝817

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值