es2015学习笔记>经典入门教程
es2015也称为ES6,是JavaScript语言的下一代标准,下面将分享如何一步步解开它的面纱,哟没有一种幸福感O(∩_∩)O哈哈~
目录
简介
为什么要了解es6
ES-Checker
Babel
开发工具
5.1. Sublime Text
5.2. WebStorm
- es6新语法
6.1. Let/const和块级作用域
6.2. 箭头函数
6.3. 模板字符串
6.4. 对象字面量拓展语法
6.5. 变量解构赋值
6.6. 函数参数表达/传参
6.7. Set/WeakSet/Map/WeakMap
6.8. Class
6.9. Generator
6.10. Promise
6.11. Symbol
6.12. Proxy
6.13. 原生的模块化
- es7透析
7.1. async/await
7.2. Decorators
参与资料
Q&A
9.1. 什么时候不能用箭头函数
1. 简介
ECMAScript6(以下简称ES6)是JavaScript语言的下一代标准。因为当前版本的ES6是在2015年6月批准通过发布的,所以又称ECMAScript 2015, 是自从2009年ES5标准化后的第一次重大更新。。虽然目前并不是所有浏览器都能兼容ES6全部特性,但越来越多的程序员在实际项目当中已经开始使用ES6了。
2. 为什么要了解es6
随着越来越多的程序员在实际项目当中已经开始使用ES6了。所以就算你现在不打算使用ES6,但为了看懂别人的你也该懂点ES6的语法了…如:
var sum=0**;
[*2.3,2,2].map(function(elem*){
sum+=elem**;
});
console.log(‘sum=’+sum**);
注:Angular.js、react.js、vue.js都是以es6为标准的,koa框架(node的mvc)致力于全栈开发也全部兼容
3. ES-Checker
ES-Checker用来检查各种运行环境对ES6 的支持情况
$ npm install -g es-checker
$ es-checker
4. Babel
Babel是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在现有环境执行。这意味着,你可以用 ES6 的方式编写程序,又不用担心现有环境是否支持。
Babel的配置文件是.babelrc,存放在项目的根目录下。使用 Babel 的第一步,就是配置这个文件。
在线转换:http://babeljs.io/
工具:
Ø bable-loader
安装bable-loader
$npm install bable-loader –g
安装转换规则
$npm install bable-preset-es2015 –save-dev
Ø babel-cli工具,用于命令行转码;
$ npm install —*g babel-cli*
# 转码结果输出到标准输出
$ babel example.js
# 转码结果写入一个文件
# —out-file 或 -o 参数指定输出文件
$ babel example.js —*out-file compiled.js*
# 或者
$ babel example.js -o compiled.js
# 整个目录转码
# —out-dir 或 -d 参数指定输出目录
$ babel src —*out-dir lib*
# 或者
$ babel src -d lib
# -s 参数生成source map文件
$ babel src -d lib -s
Ø babel-node:babel-cli工具自带一个命令,提供一个支持ES6的REPL环境。它支持Node的REPL环境的所有功能,而且可以直接运行ES6代码;
Ø babel-register:该模块改写require命令,为它加上一个钩子。此后,每当使用require加载.js、.jsx、.es和.es6后缀名的文件,就会先用Babel进行转码;
Ø babel-core:如果某些代码需要调用 Babel 的 API 进行转码,就要使用该模块;
Ø babel-polyfill:Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API
5. 开发工具
5.1. Sublime Text
1) 下载压缩包地址:https://github.com/tanepiper/SublimeText-Nodejs
2) 解压zip文件,并重命名文件夹“Nodejs”
3) 打开sublime,操作”preference”—>”Browse packages”, 把Nodejs复制到该目录
4) 打开Nodejs文件夹,找到文件“Nodejs.sublime-build”,要更改encoding为GB2312或者utf8否则终端显示乱码
5) 要用sublime打开文件“Nodejs.sublime-settings”并修改
“node_command”:”D:\Program\nodejs\node.exe”,
“npm_command”:”D:\Program\nodejs\npm.cmd”,
6) 重启sublime
新建一个test.js文件,按 Ctrl + B 或者F7运行,快速熟悉快捷键。
5.2. WebStorm
绝对是前端开发大型应用的利器之一,后续介绍
6. es6新语法
6.1. Let/const和块级作用域
在ES2015的新语法中,影响速度最为直接范围最大的,恐怕就是 let 和 const 了,它们是继var 之后,新的变量定义方法。与 let 相比,const 更容易被理解:const 也就是 constant 的缩写,跟 C/C++ 等经典语言一样,用于定义常量,即不可变量。但由于在ES6之前的 ECMAScript 标准中,并没有原生的实现,所以在降级编译中,会马上进行引用检查,然后使用 var 代替。
// foo.js
const foo =*‘bar’*
foo =*‘newvalue’*
$ babel foo.js
…
SyntaxError**: test.js**: Line 3:“foo” is read-only
1**|const foo =**‘bar’*
2*|
>*3*| foo =*‘newvalue’*
…
块级作用域
在ES6诞生之前,给JavaScript 新手解答困惑时,经常会提到一个观点:JavaScript 没有块级作用域。这个问题之所以为人所熟知,是因为它引发了诸如历遍监听事件需要使用闭包解决等问题。
<*div id=“output”></div*>
<*script*>
- var buttons *=document.querySelectorAll(‘button’)*
- var output *=document.querySelector(‘#output’)*
- *for(var i =0; i < buttons**.length; i**++){*
buttons[i].addEventListener(‘click’**,function(){*
output.innerText **= buttons**[i].**innerText*
- })
- }
</*script*>
运行时就会报出这样的错误信息:
Uncaught TypeError: Cannot readproperty ‘innerText’ of undefined
出现这个错误的原因是因为buttons[i]不存在,即为undefined。对i的变量引用(Reference)一直保持在上一层作用域(循环语句所在层)上,而当循环结束i 则正好是 buttons.length。而在ES6中,我们只需做出一个小小的改动,便可以解决该问题(假设所使用的浏览器已经支持所需要的特性):
// …
*for(*/\ var */ let i =0; i < buttons**.length; i**++){*
- // …*
}
// …
改进后的代码经过 babel 的编译后的代码:
// …
var _loop *=function(**i**){*
buttons[i].addEventListener(‘click’**,function(){*
output.innerText **= buttons**[i].**innerText*
- })
}
*for(var i =0; i < buttons**.length; i**++){*
- _loop(i*)
}
// …
6.2. 箭头函数
继let和const之后,箭头函数就是使用率最高的新特性。顾名思义便是使用箭头(=>)进行定义的函数,属于匿名函数(Lambda)一类。当然了,也可以作为定义式函数使用,但我们并不推荐这样做,随后会详细解释。
使用(用法)
foo => foo +*‘ world’//means return`foo + ‘ world’`*
(*foo*, bar**)=>** foo + bar
foo *=>{*
- return foo +*‘ world’*
}
(*foo*, bar**)=>{
- return foo + bar
}
其最大的好处便是简洁明了,省略了function关键字,而使用=>代替。
let names *=[‘Will’,‘Jack’,‘Peter’,‘Steve’,‘John’,‘Hugo’,‘Mike’]*
let newSet = names
- .*map((name*, index**)=>{
- *return{*
id*: index**,
name*: name
- }
- })
- .*filter(man **=> man**.*id *%2==0)*
- .*map(man **=>[**man.name*])
- .*reduce((a*, b**)=>** a.concat(b**))
console.log(newSet)//=> [ ‘Will’,’Peter’, ‘John’, ‘Mike’ ]
箭头函数与上下文绑定
参考CoffeeScript 中的定义一般,是用于对函数内部的上下文 (this)绑定为定义函数所在的作用域的上下文。
let obj *={*
hello:‘world’*,
foo**(){*
- let bar *=()=>{*
- *returnthis.**hello*
- }
- return bar
- }
}
window.*hello **=**‘ES6’*
window.*bar **= obj**.foo()*
window.*bar()//=> ‘world’*
上面代码中的 obj.foo 等价于:
// …
foo**(){**
- let bar *=(function(){*
- *returnthis.**hello*
- }).*bind*(this)
*
- return bar
}
// …
注意事项
另外,要注意的是箭头函数对上下文的绑定是强制性的,无法通过 apply 或 call 方法改变其上下文。
let a *={*
init**(){*
- this.*bar **=()=>this.**dam*
- },
dam:‘hei’*,
foo**(){*
- *returnthis.**dam*
- }
}
let b *={*
- dam:‘ha’*
}
a.init**()
console.log(a.foo())//=> hei
console.log(a.foo.bind(b).call(a))//=> ha
console.log(a.bar.call(b))//=> hei
另外,因为箭头函数会绑定上下文的特性,故不能随意在顶层作用域使用箭头函数,以防出错:
// 假设当前运行环境为浏览器,故顶层作上下文为 `window`
let obj *={*
msg:‘pong’*,
ping**:()=>{*
- *returnthis.**msg // Warning!*
- }
}
obj.ping()//=> undefined
let msg =*‘bang!’*
obj.ping()//=> bang!
// 等价于:
let obj *={*
// …*
ping**:(function(){*
- *returnthis.**msg // Warning!*
- }).*bind*(this)
}
// 同样等价于
let obj *={ /* … */**}*
obj.ping *=(function(){*
- *returnthis.**msg*
}).*bind(this/* this -> window */*)
6.3. 模板字符串
模板字符串模板出现简直对 Node.js 应用的开发和发展起到了相当大的推动作用。模板字符串要求使用 ` 代替原本的单/双引号来包裹字符串内容。它有两大特点:
l 支持变量注入
let name =*‘Will Wen Gunn’*
let title =*‘Founder’*
let company =*‘LikMoon Creation’*
let greet =*`Hi, I’m ${name}, I am the${title} at ${company}`*
console.log(greet)//=> Hi, I’m Will Wen Gunn, I am theFounder at LikMoon Creation
l 支持换行
let sql =`
SELECT * FROM Users
WHERE FirstName=’Mike’
LIMIT 5;
`
console.log(sql)
6.4. 对象字面量拓展语法
对象字面量的语法在 ES2015 之前早已挺完善的了。不过,对于聪明的工程师们来说,细微的改变,也能带来不少的价值。
方法属性省略function
let obj *={*
// before*
foo**:function(){*
- return*‘foo’*
- },
// after*
bar**(){*
- return*‘bar’*
- }
}
支持 __proto__ 注入
在 ES2015 中,我们可以给一个对象硬生生的赋予其 __proto__,这样它就可以成为这个值所属类的一个实例了。
class Foo {
- *constructor(){*
- this.*pingMsg **=**‘pong’*
- }
ping**(){*
console.log(this.pingMsg*)
- }
}
let o *={*
- __proto__**:new Foo**()*
}
o.ping()//=> pong
有什么卵用?有一个比较特殊的场景会需要用到:我想扩展或者覆盖一个类的方法,并生成一个实例,但觉得另外定义一个类就感觉浪费了。那我可以这样做:
let o ={
__proto__:new Foo(),*
constructor(){*
this.pingMsg =’alive’*
},*
msg:’bang’,*
yell(){*
console.log(this.msg)*
}*
}
o.yell()//=> bang
o.ping()//=> alive
同名方法属性省略语法
也是看上去有点鸡肋的新特性,不过在做 JavaScript 模块化工程的时候则有了用武之地。
// module.js
*exportdefault{*
- someMethod*
}
function someMethod**(){**
- // …*
}
// app.js
import Module from*‘./module’*
Module.someMethod**()
可以动态计算的属性名称
在上面的两个[…] 中,我演示了动态计算的对象属性名称的使用,分别为对应的对象定义了当前计数器n和n的2次方
let arr *=[1,2,3]*
let outArr = arr.map(n *=>{*
- *return{*
- [ n ]: n**,
- [*`${n}^2`]:Math.pow(n,2*)
- }
})
console.dir(outArr)//=> [{ ‘1’: 1, ‘1^2’: 1 },{ ‘2’: 2, ‘2^2’: 4 },{‘3’: 3, ‘3^2’: 9 }]
6.5. 变量解构赋值
ES6 允许按照一定模式从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
数组
let a =*1*;
let b =*2*;
let c =*3*;
//现在可以这样
*let[a, b**, c**]=[1,2,3];*
*let[foo,[[**bar*], baz**]]=[**1**,[[2],3]];**
foo // 1
bar // 2
baz // 3
- *
*let[,, third**]=[“foo”,“bar”,“baz”];*
third // “baz”
- *
*let[x,,* y**]=[**1,2,3**];
x // 1
y // 3
- *
*let[head,…**tail**]=[**1,2,3,4*];
head // 1
tail // [2, 3, 4]
- *
*let[x, y**,…**z**]=[**‘a’*];
x // “a”
y // undefined
z // []
如果解构不成功,变量的值就等于undefined。
// Matching with object
function search(query**){**
// …*
// let users = [ … ]*
// let posts = [ … ]*
// …*
- *return{*
users*: users**,
posts*: posts
- }
}
*let{ users**, posts }=* search(‘iwillwen’**)
// Matching with array
*let[ x**, y ]=[1,2]*
// missing one
[ x**,,**y *]=[**1,2,3*]
function g**({ name**: x**}){**
- console.log(x*)
}
g**({ name:5**})
// Fail-soft destructuring
*var[a]=[]*
a ===*undefined//=> true*
// Fail-soft destructuring with defaults
*var[*a *=1]=[]*
a ===*1//=> true*
默认值
*let[*foo *=true]=[];*
foo // true
对象
*let{ foo**, bar }={ foo**:“aaa”, bar**:“bbb”};*
foo // “aaa”
bar // “bbb”
*let{ baz }={ foo**:“aaa”, bar**:“bbb”};*
baz // undefined
//数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名
字符串
*const[a, b**, c**, d**, e**]=**‘hello’*;
a // “h”
b // “e”
c // “l”
d // “l”
e // “o”
数值和布尔值
*let{ toString**: s**}=**123*;
s ===*Number.prototype.toString// true*
*let{ toString**: s**}=true;*
s === Boolean.prototype.toString// true
函数参数的解构
function add([x**, y**]){
- return x + y**;
}
add([1,2]);// 3
6.6. 函数参数表达/传参
默认参数值
function noop**(){ returnfalse}**
function readLineInFile(filename**, callback = noop**, complete = noop**){**
}
后续参数
我们知道函数的 call 和 apply 在使用上的最大差异便是一个在首参数后传入各个参数,一个是在首参数后传入一个包含所有参数的数组。如果我们在实现某些函数或方法时,也希望实现像 call 一样的使用方法,在 ES2015 之前,我们可能需要这样做:
function fetchSomethings**(){**
- var args *=[].slice.apply(arguments)*
// …*
}
function fetchSomethings(…args**){**
- // …*
}
要注意的是,…args 后不可再添加
虽然从语言角度看,arguments 和 …args 是可以同时使用,但有一个特殊情况则不可:arguments 在箭头函数中,会跟随上下文绑定到上层,所以在不确定上下文绑定结果的情况下,尽可能不要再箭头函数中再使用 arguments,而使用 …args。虽然 ECMA 委员会和各类编译器都无强制性要求用 …args 代替 arguments,但从实践经验看来,…args 确实可以在绝大部份场景下可以代替 arguments 使用,除非你有很特殊的场景需要使用到 arguments.callee 和 arguments.caller。所以我推荐都使用 …args 而非 arguments。
PS:在严格模式(Strict Mode)中,arguments.callee 和 arguments.caller 是被禁止使用的。
注意事项
默认参数值和后续参数需要遵循顺序原则,否则会出错。
function(…args, last =1){
// This will go wrong
}
6.7. Set/WeakSet/Map/WeakMap
在介绍新的数据结构之前,我们先复习一下在 ES2015 之前,JavaScript 中有哪些基本的数据结构。其中又分为值类型和引用类型,Array 其实是 Object 的一种子类。
l String 字符串
l Number 数字(包含整型和浮点型)
l Boolean 布尔值
l Object 对象
l Array 数组
Set和WeakSet
我们再来复习下高中数学吧,集不能包含相同的元素,我们有很多需要用到集的场景,如搜索、索引建立等。ECMA 委员会为 ECMAScript 增添了集(Set)和“弱”集(WeakSet)。它们都具有元素唯一性,若添加了已存在的元素,会被自动忽略。
let s *=new Set**()*
s.add(‘hello’).add(‘world’).add(‘hello’**)
console.log(s.size)//=> 2
console.log(s.has(‘hello’))//=> true
WeakSet 在 JavaScript 底层作出调整(在非降级兼容的情况下),检查元素的变量引用情况。如果元素的引用已被全部解除,则该元素就会被删除,以节省内存空间。这意味著无法直接加入数字或者字符串。另外 WeakSet 对元素有严格要求,必须是 Object,当然了,你也可以用 new String(‘…’) 等形式处理元素。
let weaks *=new WeakSet**()*
weaks.add(“hello”)//=> Error
weaks.add(3.1415)//=> Error
let foo *=newString(“bar”)*
let pi *=newNumber(3.1415)*
weaks.add(foo**)
weaks.add(pi**)
weaks.has(foo)//=> true
foo *=null*
weaks.has(foo)//=> false
Map和WeakMap
从数据结构的角度来说,映射Map跟原本的Object非常相似,都是Key/Value的键值对结构。但是 Object 有一个让人非常不爽的限制:key 必须是字符串或数字,而Map则解决了这一问题。
let map *=new Map**()*
let object *={ id**:1}*
map.set(object,‘hello’**)
map.set(‘hello’,‘world’**)
map.has(object)//=> true
map.get(object)//=> hello
而 WeakMap 和 WeakSet 很类似,只不过 WeakMap 的键和值都会检查变量引用,只要其一的引用全被解除,该键值对就会被删除。
let weakm *=new WeakMap**()*
let keyObject *={ id**:1}*
let valObject *={ score**:100}*
weakm.set(keyObject**, valObject**)
weakm.get(keyObject)//=> { score:100 }
keyObject *=null*
weakm.has(keyObject)//=> false
6.8. Class
类,作为自 JavaScript 诞生以来最大的痛点之一,终于在 ES2015 中得到了官方的妥协,“实现”了 ECMAScript 中的标准类机制。为什么是带有双引号的呢?因为我们不难发现这样一个现象:
$ node
> class Foo {}
[*Function*: Foo**]
回想一下在 ES2015 以前的时代中,我们是怎么在 JavaScript 中实现类的?
function Foo(){}
var foo = new Foo()
ES6 中的类只是一种语法糖,用于定义原型(Prototype)的。
定义
ES2015 所带来的类语法确实与很多C语言家族的语法相似,与JavaScript中的对象字面量不一样的是,类的属性后不能加逗号,而对象字面量则必须要加逗号。
class Person {
- constructor(*name*, gender**, age**){**
- this.*name **=** name*
- this.*gender **=** gender*
- this.*age **=** age*
- }
isAdult**(){*
- *returnthis.*age *>=**18*
- }
}
let me *=new Person**(‘iwillwen’,‘man’,19)*
console.log(me.isAdult())//=> true
继承
ES2015的类继承总算是为JavaScript类继承之争抛下了一根定海神针
class Animal {
yell**(){*
console.log(‘yell’*)
- }
}
class Person extends Animal {
- constructor(*name*, gender**, age**){**
- super()*// must call `super` before using `this` if this class has a superclass*
- this.*name **=** name*
- this.*gender **=** gender*
- this.*age **=** age*
- }
isAdult**(){*
- *returnthis.*age *>=**18*
- }
}
class Man extends Person {
- constructor(*name*, age**){**
- super(*name,‘man’*, age**)
- }
}
let me *=new Man**(‘iwillwen’,19)*
console.log(me.isAdult())//=> true
me.yell**()
静态方法
class Man {
// …*
- static isMan(obj**){**
- return obj instanceof Man
- }
}
let me *=new Man**()*
console.log(Man.isMan(me))//=> true
遗憾的是,ES2015 的类并不能直接地定义静态成员变量,但若必须实现此类需求,可以用static 加上 get 语句和 set 语句实现。
class SyncObject {
// …*
- static get baseUrl**(){**
- return*‘http://example.com/api/sync'*
- }
}
遗憾与期望
就目前来说,ES2015 的类机制依然很鸡肋:
不支持私有属性(private)
不支持前置属性定义,但可用 get 语句和 set 语句实现
不支持多重继承
没有类似于协议(Protocl)或接口(Interface)等的概念
中肯地说,ES2015的类机制依然有待加强。但总的来说,是值得尝试和讨论的,我们可以像从前一样,不断尝试新的方法,促进 ECMAScript 标准的发展。
6.9. Generator
对于一个纯前端的 JavaScript 工程师来说,可能 Generator 并没有什么卵用,但若你曾使用过 Node.js 或者你的前端工程中有大量的异步操作(ES6提供的一种异步编程解决方案),Generator 简直是你的“贤者之石”。
来龙
Generator 的设计初衷是为了提供一种能够简便地生成一系列对象的方法,如计算斐波那契数列(Fibonacci Sequence)
function* fibo**(){**
- let a =*1*
- let b =*1*
yielda*
yieldb*
- *while(true){*
- let next = a + b
a **=** b*
b **=** next*
yield next*
- }
}
let generator = fibo**()
*for(var i =0; i <10; i**++)*
- console.log(generator.next().value)//=> 1 1 2 3 58 13 21 34 55*
Generator函数
Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
Generator 函数有多种理解角度。从语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
yield表达式
其中支持一种新的语法 yield。它的作用与 return 有点相似,但并非退出函数,而是切出生成器运行时。
function* helloWorldGenerator**(){**
yield’hello’*;
yield’world’*;
- return*‘ending’*;
}
var hw = helloWorldGenerator**();
hw.next**()
// { value: ‘hello’, done: false }
hw.next**()
// { value: ‘world’, done: false }
hw.next**()
// { value: ‘ending’, done: true }
hw.next**()
// { value: undefined, done: true }
即该函数有三个状态:hello,world 和 return 语句(结束执行),必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
事实上,Generator 的用法还是很多种,其中最为著名的一种便是使用 Generator 的特性模拟出 ES7 中的 async/await 特性。而其中最为著名的就是 co 和 koa(基于 co 的 Web Framework) 了。
6.10. Promise
光是 Promise 作为一个老生常谈的话题,作为异步编程的一种解决方案,目前就有多种标准,而目前最为流行的是Promises/A+,而ES2015 中的 Promise 便是基于此。Promise是一种用于解决回调函数无限嵌套的工具(当然,这只是其中一种)。它的作用便是“免去”异步操作的回调函数,保证能通过后续监听而得到返回值,或对错误处理。它能使异步操作变得井然有序,也更好控制。我们以在浏览器中访问一个 API,解析返回的 JSON 数据。
fetch(‘http://example.com/api/users/top'****)**
- .*then(res **=> res**.json())*
- .*then(data **=>{*
vm.data.topUsers **=** data*
*})
// Handle the error crash in the chaining processes*
.catch(err **=> console**.error(err))*
Promise 在设计上具有原子性,即只有两种状态:未开始和结束(无论成功与否都算是结束),这让我们在调用支持 Promise 的异步方法时,逻辑将变得非常简单,这在大规模的软件工程开发中具有良好的健壮性,带有 resolve 和 reject 参数对应的成功和失败。
function fetchData**(){**
- *returnnew Promise**((resolve, reject**)=>{*
// …*
- })
}
弊端
虽说 Promise确实很优雅,但是这是在所有需要用到的异步方法都支持 Promise 且遵循标准。而且链式 Promise 强制性要求逻辑必须是线性单向的,一旦出现如并行、回溯等情况,Promise 便显得十分累赘。所以在目前的最佳实践中,Promise 会作为一种接口定义方法,而不是逻辑处理工具。
6.11. Symbol
ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是ES6 引入Symbol的原因。
ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值,即便我们看不到它的区别在哪里。这就意味著,我们可以用它来保证一些数据的安全性。
console.log(Symbol(‘key’**)==** Symbol(‘key’))//=> false
如果将一个 Symbol 隐藏于一个封闭的作用域内,并作为一个对象中某属性的键,则外层作用域中便无法取得该属性的值,有效保障了某些私有库的代码安全性。Symbol 作为属性名,该属性不会出现在for…in、for…of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有 Symbol属性名。
let privateDataStore *={*
set(val**){*
- let key = Symbol(Math.random().toString(32).substr(2**))
- this[*key**]=** val*
- return key
- },
get(key**){*
- *returnthis[key]*
- }
}
let key = privateDateStore(‘hello world’**)
privateDataStore[key]//=> undefined
privateDataStore.get(key)//=> hello world
黑科技
Symbol 除了带给我们数据安全性以外,还带来了一些很神奇的黑科技,Symbol.iterator,除 Symbol 以外,ES2015 还为 JavaScript 带来了 for…of 语句,这个跟原本的 for…in 又有什么区别?
对象的Symbol.iterator
属性,指向该对象的默认遍历器方法。
var myIterable *={};*
myIterable[Symbol.iterator**]=function\(){*
yield1*;
yield2*;
yield3*;
};
[…*myIterable]// [1, 2, 3]*
对象进行for…of循环时,会调用Symbol.iterator方法,返回该对象的默认遍历器
class Collection {
- \[**Symbol.iterator**](){*
- let i =*0*;
- while(this[*i**]!==undefined){*
yield **this[i];*
- ++*i*;
- }
- }
}
let myCollection *=new Collection**();*
myCollection[0**]=1;**
myCollection[1**]=2;**
for(let value of myCollection**){**
- console.log(value*);
}
// 1
// 2
6.12. Proxy
Proxy 可以在不入侵目标对象的情况下,对逻辑行为进行拦截和处理。
假设我要对 api 这一对象进行拦截并记录下代码行为,我就可以这样做:
var obj *=new Proxy**({},{*
get**:function(**target*, key**, receiver**){**
console.log(`getting ${key}!`*);
- return Reflect.get(target**, key**, receiver**);
- },
set**:function(**target*, key**, value**, receiver**){**
console.log(`setting ${key}!`*);
- return Reflect.set(target**, key**, value**, receiver**);
- }
});
obj.count =*1*
// setting count!
++*obj.count*
// getting count!
// setting count!
// 2
可惜的是,目前 Proxy 的兼容性很差,哪怕是降级兼容也难以实现。
6.13. 原生的模块化
历史小回顾
在 JavaScript 的发展历史上,曾出现过多种模块加载库,如 RequireJS、SeaJS、FIS 等,而由它们衍生出来的 JavaScript 模块化标准有 CommonJS、AMD、CMD 和 UMD 等。其中最为典型的是 Node.js 所遵循的 CommonJS 和 RequireJS 的 AMD。
ES2015 中的模块化机制设计也是相当成熟的。基本上所有的 CommonJS 或是 AMD 代码都可以很快地转换为 ES2015 标准的加载器代码。
import name from*“module-name”*
*import* as name from**“module-name”*
*import{ member }from**“module-name”*
*import{ member as alias }from**“module-name”*
*import{ member1 , member2 }from**“module-name”*
*import{ member1 , member2 as alias2**,[…]}from**“module-name”*
import defaultMember**,{** member *[,[…]]}from**“module-name”*
import defaultMember**,** as alias **from**“module-name”*
import defaultMember from*“module-name”*
import*“module-name”*
全局引入
*import* as name from**‘module-name’*
var name = require(‘module-name’)//CommonJS 类似
局部引入,
与 CommonJS 等标准不同ES2015 的模块引入机制支持引入模块的部份暴露接口
*import{ A**, B**, C }from**‘module-name’*
暴露单独接口
// module.js
*exportfunction method**(){* /\ … */*}
// main.js
import M from*‘./module’*
M.method**()
基本的 export 语句可以调用多次,单独使用可暴露一个对象到该模块外
暴露复合模块
若需要实现像 CommonJS 中的 module.exports = {} 以覆盖整个模块的暴露对象,则需要在 export 语句后加上 default。
// module.js
*exportdefault{*
method1*,
method2*
}
// main.js
import M from*‘./module’*
M.method1**()
降级兼容
在实际应用中,我们暂时还需要使用 babel 等工具对代码进行降级兼容。庆幸的是,babel 支持 CommonJS、AMD、UMD 等模块化标准的降级兼容,我们可以根据项目的实际情况选择降级目标。
*$ babel-m common -d dist/common/ src**/*
*$ babel-m amd -d dist/amd/ src**/*
*$ babel-m umd -d dist/umd/ src**/*
7. es7透析
7.1. async/await
Generator Function与普通的 Function在执行方式上有著本质的区别,在某种意义上是无法共同使用的。对于 ES7 的 Async Function 来说,这一点并不存在!它可以以普通函数的执行方式使用,并且有著 Generator Function 的异步优越性,它甚至可以作为事件响应函数使用。
var fs = require(‘fs’**);
var readFile *=function(**fileName**){*
- *returnnew Promise**(function(**resolve*, reject**){**
fs.readFile(fileName**,function(error, data**){*
- *if(error) reject**(error);*
resolve(data*);
- });
- });
};
var gen *=function*(){*
- var f1 = yield readFile(‘/etc/fstab’**);
- var f2 = yield readFile(‘/etc/shells’**);
console.log(f1.toString*());
console.log(f2.toString*());
};
// async函数就是将 Generator 函数的星号(\)替换成async,将yield替换成await*
var asyncReadFile = async *function(){*
- var f1 = await readFile(‘/etc/fstab’**);
- var f2 = await readFile(‘/etc/shells’**);
console.log(f1.toString*());
console.log(f2.toString*());
};
async函数对 Generator 函数的改进,体现在以下四点:
- 内置执行器
Generator函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。
- 更好的语义
async和await,比起星号和yield,语义更清楚了
- 更广的适用性
co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)
- 返回值是Promise
7.2. Decorators
修饰器(Decorator)是一个函数,用来修改类的行为。这是 ES 的一个提案,目前 Babel 转码器已经支持。
定义如下:
l 是一个表达式
l Decorator 会调用一个对应的函数
l 调用的函数中可以包含target(装饰的目标对象)、name(装饰目标的名称)和 descriptor(描述器)三个参数
l 调用的函数可以返回一个新的描述器以应用到装饰目标对象上
PS:如果你不记得 descriptor 是什么的话,请回顾一下 Object.defineProperty() 方法。
我们在实现一个类的时候,有的属性并不想被 for..in 或 Object.keys() 等方法检索到,那么在 ES5 时代,我们会用到 Object.defineProperty() 方法来实现:
var obj *={*
- foo:1*
}
Object.defineProperty(obj,‘bar’**,{**
enumerable**:false,*
value:2*
})
console.log(obj.bar)//=> 2
var keys *=[]*
*for(var key in obj**)*
- keys.push(key*)
console.log(keys)//=> [ ‘foo’ ]
console.log(Object.keys(obj))//=> [ ‘foo’ ]
/\**
\**ES7中可以这样*
\*/*
class Obj {
- *constructor(){*
- this.*foo **=**1*
- }
@nonenumerable*
getbar**(){ return2}*
}
function nonenumerable(target**, name**, descriptor**){**
descriptor.enumerable **=false*
- return descriptor
}
var obj *=new Obj**()*
console.log(obj.foo)//=> 1
console.log(obj.bar)//=> 2
console.log(Object.keys(obj))//=> [ ‘foo’ ]
黑科技
假如我们要实现一个类似于 Koa的CI的框架,且利用Decorator 特性实现 URL 路由,我们可以这样做。
// 框架内部
// 控制器
class Controller {
- // …*
}
var handlers *=new WeakMap**()*
var urls *={}*
// 定义控制器
@route(‘/‘**)
class HelloController extends Controller {
- *constructor(){*
- super()
*
- this.*msg **=**‘World’*
- }
asyncGET(ctx**){*
ctx.body **=**`Hello ${this.msg}`*
- }
}
// Router Decorator
function route(url**){**
- return target *=>{*
target.url **=** url*
- let urlObject *=newString(url)*
urls[url**]=** urlObject*
*
handlers.set(urlObject*, target**)
- }
}
// 路由执行部份
function router(url**){**
- *if(urls[url]){*
- var handlerClass = handlers.get(urls[url**])
- *returnnew handlerClass**()*
- }
}
var handler = router(‘/‘**)
*if(handler){*
- let context *={}*
handler.GET(context*)
console.log(context.body*)
}
最重要的是,同一个修饰对象是可以同时使用多个修饰器的,所以说我们还可以用修饰器实现很多很多有意思的功能。
8. 参与资料
- 阮一峰《ECMAScript6 入门》
- nodejs学习笔记》入门级教程
- nodejs学习笔记》常用组件(webMVC+orm)
- nodejs学习笔记》演示项目示例
9. Q&A
9.1. 什么时候不能用箭头函数
ref
还没有评论,来说两句吧...