第五篇注重ES6
ES6
ES6简介
ES6与JavaScript的关系
JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给标准化组织 ECMA,希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,这个版本就是 1.0 版。
因此,ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 JScript 和 ActionScript)。日常场合,这两个词是可以互换的。
ES6与ES5
EMCA的标准委员会决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。接下来的时间,就在这个版本的基础上做改动,直到下一年的 6 月份,草案就自然变成了新一年的版本。这样一来,就不需要以前的版本号了,只要用年份标记就可以了。
ES6 的第一个版本,就这样在 2015 年 6 月发布了,正式名称就是《ECMAScript 2015 标准》(简称 ES2015)。2016 年 6 月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)如期发布,这个版本可以看作是 ES6.1 版,因为两者的差异非常小(只新增了数组实例的includes方法和指数运算符),基本上是同一个标准。2017 年 6 月发布 ES2017 标准。
ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。
考虑到未来所有的代码,其实都是运行在模块之中,ES6 实际上把整个语言升级到了严格模式。
let、const与块级作用域
const声明一个只读的常量,一旦声明,常量的值就不能修改
let和const只在声明的块级作用域内有效
const和let声明的变量不可重复声明
const变量一旦声明,就必须立即初始化,不能留到以后赋值,只声明不赋值就会报错
1 | const foo |
const声明Object或者Array时,只是已经声明的对象属性不能变化,但是对象和数组仍然可变,可以添加新的属性或者值,如果想要对象不变,使用object.freeze冻结对象
const声明的对象,虽然属性可变,但是内存地址不可变
暂时性死区
ES5中只有全局作用域和函数作用域,ES6新增了块级作用域,用{}表示。
1 | function f1() { |
块级作用域的出现使得获得广泛应用的匿名立即执行函数表达式(匿名IIFE)不再必要了
ES5中规定,函数只能在顶层作用域和函数作用域中声明,不能在块级作用域中声明
ES6中中明确规定允许在块级作用域声明函数,函数声明语句类似于let,只能在块级作用域中引用
在const/let变量声明之前调用变量会报错,在var变量声明之前调用会返回undefined
定义不可变对象
1.使用object.freeze冻结对象
2.使用 es6 (Object.defineProperty()) 修改对象属性的属性描述符
1 | let obj = { |
3.使用proxy代理 get,set 方法,实现不可变
1 | //使用属性代理 |
proxy
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
方法:
get方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。
set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。
has方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。has方法可以接受两个参数,分别是目标对象、需查询的属性名。
deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。
defineProperty()方法拦截了Object.defineProperty()操作。
getOwnPropertyDescriptor()方法拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined。
getPrototypeOf()方法主要用来拦截获取对象原型。具体来说,拦截下面这些操作。
Object.prototype.__proto__Object.prototype.isPrototypeOf()Object.getPrototypeOf()Reflect.getPrototypeOf()instanceof
isExtensible()方法拦截Object.isExtensible()操作。
ownKeys()方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作。
Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.keys()for...in循环
preventExtensions()方法拦截Object.preventExtensions()。该方法必须返回一个布尔值,否则会被自动转为布尔值。
setPrototypeOf()方法主要用来拦截Object.setPrototypeOf()方法。
Proxy.revocable()方法返回一个可取消的 Proxy 实例。
Generator函数
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
Generator 函数是一个普通函数,形式上有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object)
下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
1 | function* helloWorldGenerator() { |
yield表达式与return语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式。正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是它的名称的来历
next函数传参
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
next函数传参这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
1 | function* foo(x){ |
由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
如果想要第一次调用next方法时,就能够输入值,可以在 Generator 函数外面再包一层。
1 | function wrapper(generatorFunction){ |
为了防止手动遍历generator函数,js提供co函数库操作generator函数
generator最大的特点是交出函数的执行权,即暂停执行,异步操作需要暂停的地方使用yield注明,此处引入协程的概念。
进程有变量隔离,自动切换运行上下文
线程没有变量隔离,自动切换运行上下文
协程不进行变量隔离,不自动切换运行上下文
async函数
async可以理解为generator+promise的语法糖,async可以看作是多个异步操作包装成的一个promise对象,而await命令是内部.then的语法糖
async对generator的改进体现在以下四点:
1.内置执行器。generator需要co模块或者调用next方法才能执行,而async函数自带执行器可以向普通函数一样。
2.更好的语义。比起generator的yield和*,async和await更直接
3.更广的适用性。yield命令返回的是promise对象或者thunk函数,而await后面可以是promise对象或者任意原始类型(数值、字符串、布尔值等),方便操作。
4.返回值是promise。generator的返回值是iterator对象,而async返回的是promise对象,可以用.then方法指定下一步的操作。
asnyc函数如果不跟await直接return会返回一个promise对象,
1 | async function :<Promise Number> { |
错误处理
await后面跟promise对象时,可能会reject,此时将await写在try里
1 | function getUsers() { |
async与promise的区别
async相比promise的优势:处理 then 的调用链,能够更清晰准确的写出代码
async相比promise的劣势:
有多个接口的情况下,async/await是继发,也就是一个一个接口请求,promise.all是同步触发
如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低,代码没有依赖性的话,完全可以使用 Promise.all 的方式。
async同步触发写法
1 | // 写法一 |
反过来,如果是有依赖性的接口,那么async的语法更直观更符合语义
新增class类
JavaScript 语言中,生成实例对象的传统方法是通过构造函数。
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。
基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
类的实例
使用new命令生成类的实例,类的所有实例共享一个原型对象。
1 | class Point { |
在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
1 | class MyClass { |
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。静态方法可以与非静态方法重名。
每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
Super关键字
super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。
super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
Mixin模式
Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。
用最简单的实现实现Mix如下
1 | const a = { |
新增数据类型和数据结构
ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型。
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set本身是一个构造函数,用来生成 Set 数据结构。
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
WeakSet 的成员只能是对象,而不能是其他类型的值。
WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
Map
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
Map类型的属性和方法
属性
size属性返回 Map 结构的成员总数。
set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。
get方法读取key对应的键值,如果找不到key,返回undefined。
has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
Map.prototype.delete(key)方法删除某个键,返回true。如果删除失败,返回false。
Map.prototype.clear()方法清除所有成员,没有返回值。
方法
Map.prototype.keys():返回键名的遍历器。Map.prototype.values():返回键值的遍历器。Map.prototype.entries():返回所有成员的遍历器。Map.prototype.forEach():遍历 Map 的所有成员。
WeakMap
WeakMap结构与Map结构类似,也是用于生成键值对的集合。
WeakMap与Map的区别有两点。
1.WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
2.WeakMap的键名所指向的对象,不计入垃圾回收机制。
WeakMap只有四个方法可用:get()、set()、has()、delete()。
weakmap的用途:
1.将DOM 节点作为键名。获取dom节点后,每当发生click事件,就更新一下状态。我们将这个状态作为键值放在 WeakMap 里,对应的键名就是这个节点对象。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。
2.WeakMap 的另一个用处是部署私有属性。
Map数据结构实现
Map底层使用hash+链表结构实现保证插入顺序
1 | function MyMap() { |
新增模块化
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的require、Python 的import,甚至就连 CSS 有@import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。
export命令除了输出变量,还可以输出函数或类(class)。
export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。
如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。
import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。
import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径。如果不带有路径,只是一个模块名,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
import命令具有提升效果,会提升到整个模块的头部,首先执行。
import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。
除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。
从前面的例子可以看出,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。
上面代码是一个模块文件export-default.js,它的默认输出是一个函数。
export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。
其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。
Js模块化方案对比
模块化这个话题在ES6之前不存在,因此也被诟病为早期Javascript开发全局污染和依赖管理混乱的源头
require是运行时加载模块,import命令无法取代require的动态加载功能。
commonjs加载时,是整体加载如模块的所有方法,再生成对象,例如
1 | // CommonJS模块 |
上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
commonjs
弥补JavaScript在服务器端缺少模块化机制,NodeJS、webpack都是基于该规范开发
特点:
所有代码都运行在独立的模块作用域,不会污染全局作用域
模块可以多次加载,但是只会在第一次加载时运行,然后运行结果就会被缓存,以后再加载就读取缓存结果,要想让模块再次运行就必须清除缓存
模块加载的顺序按照在代码中出现的顺序
优点:服务器端模块重用,NPM中模块包多,有将近20万个。
缺点:
无法在编译阶段确认产物,且可以在代码中随意使用require,比如全局、函数、if/else条件语句中等等
加载模块是同步的,只有加载完成后才能执行后面的操作,也就是当要用到该模块了,现加载现用,不仅加载速度慢,而且还会导致性能、可用性、调试和跨域访问等问题。Node.js主要用于服务器编程,加载的模块文件一般都存在本地硬盘,加载起来比较快,不用考虑异步加载的方式,因此,CommonJS规范比较适用。然而,这并不适合在浏览器环境,同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
此外,ES6 模块输出的是值的引用,输出接口动态绑定,而 CommonJS 输出的是值的拷贝
AMD与requirejs
commonJS规范很好,但是不适用于浏览器环境,于是有了AMD和CMD两种方案。AMD全称Asynchronous Module Definition,即异步模块定义。它采用异步方式加载模块,
1 | define("module", ["dep1", "dep2"], function(d1, d2) { |
AMD草案的作者以RequireJS实现了AMD规范,所以一般说AMD是RequireJS
CMD
CMD全称Common Module Definition,是Sea.js所推广的一个模块化方案的输出。SeaJS与RequireJS并称,作者为阿里的玉伯
与AMD的主要区别:
1.对于依赖的模块,AMD是提前执行,CMD是延迟执行。不过RequereJS从2.0开始也改成可以延迟执行,CMD推崇as lazy as possible。延迟执行的意思是只有到require时依赖模块才执行
2.CMD推崇依赖就近,AMD推崇依赖前置
Common Module Definition 规范和 AMD 很相似,尽量保持简单,并与 CommonJS 和 Node.js 的 Modules 规范保持了很大的兼容性。
1 |
|
UMD,全称Universal Module Definition,即通用模块规范,既然CommonJS和AMD风格一样流行,就需要一个统一浏览器端和非浏览器端的模块化方案的规范
UMD的实现很简单:
先判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块
再判断是否支持Nodejs模块格式(exports是否存在),存在则使用Nodejs模块格式
前两个都不存在,则将模块公开到全局(window或者global)
ES6 Modules
以上这些都是社区提供的方案,历史上Javascript一直没有模块化系统,直到ES6在语言标准的层面实现了它。
CommonJS和AMD模块都只能在运行时确定模块的依赖关系,以及输入输出的变量,而ES6的设计思想是尽可能静态化,在编译时就能确定这些东西。
总结
AMD依赖前置,提前执行,语法是define,require
CMD依赖就近,延迟执行,语法是define,seajs。use 。延迟执行的意思是只有到require时依赖模块才执行
Commonjs首次执行会被缓存,再次加载只返回缓存结果,require返回的值时输出值的拷贝,对于引用类型是浅拷贝
循环加载
“循环加载”(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。
1 | // a.js |
通常,“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。
但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现a依赖b,b依赖c,c又依赖a这样的情况。这意味着,模块加载机制必须考虑“循环加载”的情况。
在commonjs中,循转引用会只输出循环引用之前执行的部分,不能代表整个代码的执行逻辑,
1 | // a.js |
总之,CommonJS 输入的是被输出值的拷贝,不是引用。由于 CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。
ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用,如果使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
1 | // a.mjs |
ES6 循环加载的处理方式:首先,执行a.mjs以后,引擎发现它加载了b.mjs,因此会优先执行b.mjs,然后再执行a.mjs。接着,执行b.mjs的时候,已知它从a.mjs输入了foo接口,这时不会去执行a.mjs,而是认为这个接口已经存在了,继续往下执行。执行到第三行console.log(foo)的时候,才发现这个接口根本没定义,因此报错。
解决这个问题的方法,就是让b.mjs运行的时候,foo已经有定义了。这可以通过将foo写成函数来解决。
1 | // a.mjs |
因为函数具有提升作用,在执行import {bar} from './b'时,函数foo就已经有定义了,所以b.mjs加载的时候不会报错。这也意味着,如果把函数foo改写成函数表达式,也会报错。
遍历器Iterator
JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator 的遍历过程是这样的。
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。
for…of
ES6 借鉴 C++、Java、C# 和 Python 语言,引入了for...of循环,作为遍历所有数据结构的统一的方法。
一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。
for...of循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。
JavaScript 原有的for...in循环,只能获得对象的键名,不能直接获取键值。ES6 提供for...of循环,允许遍历获得键值。
对于普通的对象,for...of结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。但是,这样情况下,for...in循环依然可以用来遍历键名。
1 | for (let e in es6) { |
for...in循环有几个缺点。
- 数组的键名是数字,但是
for...in循环是以字符串作为键名“0”、“1”、“2”等等。 for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。- 某些情况下,
for...in循环会以任意顺序遍历键名。
总之,for...in循环主要是为遍历对象而设计的,不适用于遍历数组。
for...of循环相比上面几种做法,有一些显著的优点。
- 有着同
for...in一样的简洁语法,但是没有for...in那些缺点。 - 不同于
forEach方法,它可以与break、continue和return配合使用。 - 提供了遍历所有数据结构的统一操作接口。
装饰器
装饰器不能用于函数,因为会存在函数提升
Reflect
Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。
将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
Reflect对象一共有 13 个静态方法。
Reflect.get方法查找并返回target对象的name属性,如果没有该属性,则返回undefined。
Reflect.set方法设置target对象的name属性等于value。
Reflect.has方法对应name in obj里面的in运算符。
Reflect.deleteProperty方法等同于delete obj[name],用于删除对象的属性。
Reflect.construct方法等同于new target(...args),这提供了一种不使用new,来调用构造函数的方法。
Reflect.getPrototypeOf方法用于读取对象的__proto__属性,对应Object.getPrototypeOf(obj)。Reflect.setPrototypeOf方法用于设置目标对象的原型(prototype),对应Object.setPrototypeOf(obj, newProto)方法。它返回一个布尔值,表示是否设置成功。
Reflect.apply方法等同于Function.prototype.apply.call(func, thisArg, args),用于绑定this对象后执行给定函数。
对象扩展
Object对象的扩展
Object.assign()方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)
Object.assign()方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
super关键字
this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。
math对象的扩展
Number对象的扩展
数组对象的扩展
Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
Array.of方法用于将一组值,转换为数组。
数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
fill方法使用给定值,填充一个数组。
Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。
Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。数组的成员有时还是数组。flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1。
如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。
如果原数组有空位,flat()方法会去掉空位。
flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。
copyWithin方法 在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
字符串对象的扩展
String.raw()方法。该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法。
String.includes():返回布尔值,表示是否找到了参数字符串。
String.startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
String.endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
String.repeat()方法返回一个新字符串,表示将原字符串重复n次。
如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。
trimStart()和trimEnd()这两个方法。它们的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。
函数对象扩展
ES6允许使用箭头定义函数
箭头函数的存在是为了方便在很多地方执行小函数的情况。比如foreach、settimeout等,这种情况下我们并不想离开当前上下文,这时就使用箭头函数。
1 | // 箭头函数,包含一个name参数 |
没有参数时使用空括号,有多个参数时用逗号隔开
箭头函数没有this、arguments、super、new.target,全部指向外层函数的对应变量,所以也就不能用call()、apply()、bind()这些方法去改变this的指向。
不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,
箭头函数与普通函数的区别
1.语法更加简洁清晰
2.箭头函数不会创建自己的this。箭头函数没有自己的this,它会捕获自己在定义时(注意,是定义时,不是调用时)所处的外层执行环境的this,并继承这个this值。所以,箭头函数中this的指向在它被定义的时候就已经确定了,之后永远不会改变。.call()/.apply()/.bind()也无法改变箭头函数中this的指向
3.箭头函数没有原型prototype,没有自己的arguments,在箭头函数中访问arguments实际上获得的是外层局部(函数)执行环境中的值。
实例
1 | function outer(val1, val2) { |
4.箭头函数不能作为构造函数使用,不能用作Generator函数,不能使用yeild关键字、new关键字
箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
箭头函数vsbind
箭头函数没有创建任何绑定,箭头函数只是没有this,this的查找与常规变量的搜索方式完全相同:在外部词法环境中查找
。bind创建了一个函数参数的绑定版本
尾调用与尾递归(非常重要)
尾调用时函数式编程的一个重要概念,本身非常简单,就是某个函数在最后一步调用另一个函数
1 |
函数调用时会在内存中形成一个调用记录,又称调用帧,保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方还会形成一个B的调用帧,等到B运行结束之后,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推,所有的调用帧就形成一个调用栈
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量信息等不会被再用到,只有直接用内层函数的调用帧,取代外层函数的调用帧就可以
这个就叫做尾调用优化,只保留内层函数的调用帧。如果所有的函数都是尾调用,那么完全可以做到每次调用时调用帧只有一项,这将大大节省内存,这就是尾调用的意义
函数调用自身的过程,称为递归。递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生栈溢出错误。但是对于尾递归来说,由于只存在一个调用帧,所以永远也不会发生栈溢出错误。
比如常见的斐波那契数列的非尾递归写法
1 | function Fibonacci(n) { |
尾递归优化之后的代码
1 | function Fibonacci2(n,ac=1,ac2=1) { |
尾调用的意义非常重大,因此ES6规定所有ECMA的实现都必须采用尾调用优化
递归本质上是一种循环操作,但是纯粹的函数式编程没有循环操作命令,所有的循环都通过递归实现,这就是尾递归对这些语言的重要意义
尾递归调用要注意的问题
尾递归调用不能使用函数中的其他变量,因此写的时候要注意写法
通常是在另一个函数中调用递归函数,这样去实现避免中间变量
1 | //阶乘函数,用普通递归函数实现 |
也可以用函数科里化实现
解构赋值与扩展运算符
ES6允许按照一定模式从对象和数组中提取值,对变量进行赋值,称为解构
数组解构
1 | // 解构不成功时为undefined |
对象解构
1 | //对象与数组的不同是,数组的元素是按次序排列的,变量的取值由位置决定,而对象的属性没有次序,必须同名才能取到正确的值 |
字符串解构
1 | const [a,b,c,d,e] = 'hello', |
数值和布尔值的解构赋值
1 | let {toString: s} = 123; |
函数参数的解构赋值
1 | function add([x,y]){ |
解构赋值的应用
1.变量交换
1 | let x=1;let y=2; |
2.从函数返回多个值
1 | function example(){ |
3.函数参数定义
1 | // |
4.提取JSON数据
1 | let jsonData = { |
5.输入模块的指定方法。解构赋值能使输入语句变得十分清晰
1 | const { SourceMapConsumer, SourceNode } = require("source-map") |
其他:函数参数默认值、遍历Map结构
解构赋值和扩展运算符都是浅拷贝
扩展运算符使用object
扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
该运算符主要用于函数调用时使用,用于将数组的每个元素转化为逐个参数。
扩展运算符与正常的函数参数可以结合使用,非常灵活。
模版字符串
传统的 JavaScript 语言,输出模板使用jquery通常是这样写的
1 | $('#result').append( |
ES6引入了模板字符串简化了写法
1 | $('#result').append(` |
ES6与ES5转码
Babel 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在老版本的浏览器执行。这意味着,你可以用 ES6 的方式编写程序,又不用担心现有环境是否支持。
安装Babel
1 | npm install --save-dev @babel/core |
配置文件babelrc
Babel 的配置文件是.babelrc,存放在项目的根目录下。使用 Babel 的第一步,就是配置这个文件。
该文件用来设置转码规则和插件,基本格式如下。
1 | { |
presets字段设定转码规则,官方提供以下的规则集,你可以根据需要安装。
1 | 最新转码规则 |
然后,将这些规则加入.babelrc。
1 | { |
Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API,比如Iterator、Generator、Set、Map、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。
举例来说,ES6 在Array对象上新增了Array.from方法。Babel 就不会转码这个方法。如果想让这个方法运行,可以使用core-js和regenerator-runtime(后者提供generator函数的转码),为当前环境提供一个垫片。
安装
1 | npm install --save-dev core-js regenerator-runtime |
然后在脚本头部加入如下代码
1 | import 'core-js'; |
@babel/node模块的babel-node命令,提供一个支持 ES6 的 REPL 环境。它支持 Node 的 REPL 环境的所有功能,而且可以直接运行 ES6 代码。
安装
1 | npm install --save-dev @babel/node |
执行babel-node就进入 REPL 环境。
@babel/register模块改写require命令,为它加上一个钩子。此后,每当使用require加载.js、.jsx、.es和.es6后缀名的文件,就会先用 Babel 进行转码。
1 | npm install --save-dev @babel/register |
使用时,必须首先加载@babel/register。
Babel 提供一个REPL 在线编译器,可以在线将 ES6 代码转为 ES5 代码。转换后的代码,可以直接作为 ES5 代码插入网页运行。

