部署进度
$curl -o https://raw.githubusercontent.com/creationix/nvm//install.sh|bash
激活nvm
$source ~/.nvm/nvm.sh
安装node.js
$nvm install node
切换到该版本
$nvm use node
验证安装
$ node --v8-otions | grep harmony
看本机对es6的支持程度
$ npm install -g es-checker
Babel转码器
//转码前
input.map(item=>item+1);
//转码后
input.map(function (item){
return item+1;
});
后者通用性更强,也就是安装转码器就可以用前者编写方式,会自动转成后者。
命令行环境
$npm install --global babel -cli
$npm install --save babel-preset-es2015
然后在目录下新建配置文件。babelrc.
//.babelrc { "presets":['es2015'] }
Balbel 自带babel-node命令
$babel-node
console.log([1,2,3]).map(x=>x*x)
[1,4,9]
也可以直接运行ES6脚本
$ babel-node es6.js
-o 或 --out-file参数可以重定向文件
$babel es6.js -o es5.js
或者
$babel es6.js --out-file es5.js
-d用于转换整个目录
$babel -d build-dir source-dir
-d后面是目录,如果要生成source map文件,后面加-s
$babel -d build-dir source-dir -s
浏览器环境
1.安装 babel-core模块
$ npm install babel-core@5
2.安装babellify模块
$npm install --save-dev babelify babel-preset-2015
然后用命令行转换ES6脚本
$ browserify script.js -o bundle.js \ -t[babelify --presets[es2015 react]]
在package.json添加代码
{
"browserify:{
“transform”:[[“babellify”,{“presets”:[“es2015”]}]]
}
}
还有Node.js环境的,一般在Cocos上编辑了,这里跳过
let 和 const 命令
let 是局部变量
var 是全局静态变量,不受定义区域影响,一但定议全局可访问
为了增强代码的容错率,建议能用let不要用var,var带来方便,也会带来bug,看情况用
const 常量
跨模块:export const A=1;
全局对象属性
以下这两种方法是一样的
window.a=1; a //1
a=2; window.a //2
a 就是用var定义的变量
变量的解构赋值
数组的解构赋值
正常赋值是这样:
var a=1;
var b=1;
…
ES6可以写成这样
var[a,b,c]=[1,2,3]
下面举例:
let [a,[[b],c]]=[1,[[2,5],3],4]
a //1
b//2
c//3
let [ , ,c]=[“1”,“2”,“3”]
c //3;
let [a, …b]=[1,2,3,4];
a //1
b //2,3,4
let [a,c, …b]=[1,];
a //1
c //undefined
b //[ ]
let [x=1,y=x]=[] //x=1, y=1,
let [x=1,y=x]=[1,2] //x=1, y=2,
let [x=y,y=1]=[] //报错
解构一样适用于const
对象解构赋值
var { a, b}={a:1,b:2}
与数组的规则一样,有对应的键才会有对应的值,否则就是未定义,唯一的区别只是数组是按索引来确认赋,而对象是按成员
字符串的解构赋值
把字符串看成字符数组
const [a,b,c,d]=“hello”
a //“h”
b //“e”
…
数值和布尔值的解构赋值
let { toString: s}=123 //它的意思就是 s = 123.toString()
s===Number.prototype.toString //true
函数参数的解构赋值
下面add的参数不是数组,而是通过解构得到的变量x和y
function add([x,y]){
return x+y;
}
add([1,2]) //3
再来个例子
[[1,2],[3,4]].map([a,b]=>a+b);
[3,7]
还可以这样写
function move({x=0,y=0}={}){
return [x,y];
}
move({x:3,y:2}) //[3,2]
move({}) //[0,0]
move(); //[0,0]
function move({x,y}={x:0,y:0}){
return [x,y];
}
move({x:3,y:2}) //[3,2]
move({}) //[undefined,undefined]
move(); //[0,0]
另外undefined 会触发参数的默认值
[1, undefined,2].map((x=‘yes’)=>x)
[1,‘yes’,2]//这里其实是把前面的数组读了出来。
圆括号问题
1.变量声明语句中,模式不能带有圆括号
var [(a)]=[1];
//报错
2.函数参数中,模式不能带有圆括号
function f([(z)]){return z}
//报错
3.就是模式
({a:b})
={a:32};//报错
只有一种情况可以用:赋值语句的非模式部分可以使用圆括号
什么叫模式?看下面这句
({p:(d)}={})
其中p就是模式,模式不会被赋值
字符串的扩展
字符的Unicode 表示法
”\uD842\uDFB7“
//吉
“\u20BB7”
//" 7"
“u{20BB7}”
//吉
codePointAt()
var s=“吉”
s.charAt(0); //’ ’
s.charCodeAt(0);//55362
s.codePointAt(0).toString(16) //“20bb7”
for (let ch of s){
consold.log(ch.codePointAt(0)).toString(16));
}
//“20bb7”
//""
String.fromCodePoint()
有提取字码,就有需要还原字码
String.fromCodePoint(0x20bb7)
//”吉"
字符串的遍历器接口
就是使用for …of 1
at()
'吉'.at(0); //吉
normalize()
'\u01d1'.normalize()==='\u004F\030C'.normalize() //true
includes(),startWith(),endWith()
看名字都知道干嘛用的
想知道某字符是否被包含在内用 includes()
想知道某字符是否在开头用starWith()
想知道某字符是否在结尾用endWith()
repeat()
看名字
想重复某些字,又懒得打可以这样写
‘x’.repeat(3); //"xxx"
padStart(),padEnd()
字符串补全用的
‘x’.padStart(5,"ab") //'ababx'
‘x’.padEnd(5,"ab") //'xabab'
模版字符串
正常我们拼接字符串是这样
“a”+"b"+"c"
用反引号就可以将字符串变成模版字符串
’abc‘
看起来并不明显,这个主要用来做变量引入
下面来看带变量的
var name ="Bob",time="today"; ''hello ${name}, how are you ${time}?" //同样的道理还可以引入函数${f()}
如果内部也要用反引号也很简单,用反斜杠转义就行了
标签模版
var a=5;
var b=10;
tag ‘Hello ${a +b} world ${a*b}’ //tag相当于函数名,后面接的是参数,会依次接收到多个参数
//“Hello”
//“world”
//""
//15
//50
第一个参数:[‘Hello’,‘world’,’ ']
第二个参数:15
第三个参数:50
String.raw()
String.raw 方法可以作为处理模版字符串的基本方法,它会将所有 变量替换 ,并对反斜线进行转义,方便下一步作为 字符串使用。
String.raw({raw:‘test’},0,1,2);
//‘t0e1s2t’
//等同于
String.raw({raw:[‘t’,‘e’,‘s’,‘t’]},0,1,2);
以上的代码我再解释一下,它就相当于把字符串看成数组,后面的参数是用来跟对应的数组索引的值进行拼接用的,相当于
var s=‘test’;
s.at(0)+0 //t0
s.at(1)+1 //e1
s.at(2)+2 //s2
s.at(0)+0+s.at(1)+1+s.at(2)+2 //t0e1s2t
正则的扩展
/…/ 正则表达式用斜杠包住
RegExp 构造函数
var regex = new RegExp(“xyz”,“i”);
//等价于
var regex=/xyz/i;
ES6可以这样写
var regex= new RegExp(xyz/i);
new RegExp(/abc/ig,'i').flags //"i"
字符串的正则方法
以下都可以使用正则表达式作为参数
String.prototype.match
String.prototype.replace
String.prototype.search
String.prototype.split
u修饰符
含义为“Unicode 模式”,用来正确处理大于\uFFFF的Unicode字符。
/^\uD83D/u.test(’\uD83D\uDC2A’)
//false
/^\uD83D/.test(’\uD83D\uDC2A’)
//ture
点字符
var s = “吉”
/^. / t e s t ( s ) / / f a l s e / . /test(s) // false /^. /test(s)//false/./u.test(s) //true
Unicode 字符表示法
/\u{61}/.test(‘a’) //false 不加u则会表示匹配61个连续的u
/\u{61}/u.test(‘a’) //true
/\u{20BB7}/.test(‘吉’) //true
量词
/a{2}/.test(‘aa’) //true
/a{2}/.u.test(‘aa’)//true
/吉{2}/.test(‘吉吉’) // false
/吉{2}/u.test(‘吉吉’) //true
预定义模式
/^\SKaTeX parse error: Undefined control sequence: \S at position 24: …吉') //false /^\̲S̲/u.test(‘吉’) //true
i 修饰符
/[a-z]/i.test(’\u212A’) //false
/[a-z]/iu.test(’\u212A’) //true
y 修饰符
意为“sticky”(粘连)修饰符
与g修饰符不同,虽然都是全局匹配,但y的匹配从第一个字符开始,而g没有位置要求,而且y是向右原则,匹配后只认右边的,左边的默认为空字符
var s = “aaa_aa_a”;
var r1=/a+/g;
var r2 =/a+/y;
//运行第一次两个都能匹配:
r1.exec(s) //[“aaa”]
r2.exec(s) //[“aaa”]
//运行第二次,注意剩下的是 :“_aa_a”
r1.exec(s) //[“aa”]
r2.exec(s) //null
使用lastIndex可以更好的说明y修饰符
const REGEX=/a/g;
设置从2开始
REGEX.lastIndex=2
const match=REGEX.exec(“xaya”); //下标是2,从y开始,匹配"ya"
match.index=3 //字符串下标从0开始,最大是3
若是/a/y,则会匹配失败
const REGEX=/a/y;
设置从2开始
REGEX.lastIndex=2
const match=REGEX.exec(“xaya”); //下标是2,从y开始,匹配"ya"
match //null
sticky属性
用来确认是否有y修饰的正则
var r=/hello\d/y;
r.sticky //true
flags属性
用来获取正则的内容
abc/ig.source //“abc”
abc/ig.flags //“gi”
RegExp.escape()
字符串必须转义才能作为正则模式
但用RegExp.escape(str),它会将所有正则字符转义。
数值的扩展
二进制和八进制数值的表示法
二进制用0b
八进制用0o(注意大小写)
Number(‘0b111’) //7
Number(‘0o10’) //8
Number.isFinite(),Number.isNaN()
Number.isFinite()用来检测一个数值是否非无穷
Number.isFinite(15); //true
Number.isFinite(‘aaa’); //false
Number.isNaN()用来检测一个值 是否为NaN
Number.isNaN(15); //false
Number.isNaN(‘15’); //false
Number.isNaN(NaN); //true
Number.isNaN(‘true’/‘true’); //true
Number.parseInt(),Number.parseFloat()
字符串转数值方法
Number.parseInt(‘12.5’); //12
Number.parseFloat(‘12.45#’); //12.45
Number.isInteger()
用来断判是否整数
Number.isInteger(25.0); //true
Number.isInteger(25.1); //false
Number.EPSILON
ES6在Number对象上新增了一个极小的常量
Number.EPSILON //2.220116049250313e-16
Number.EPSILON.toFIxed(20); //‘0.0000000000000022204’
引入一个这么小的量,目的在于为浮点数计算设置一个误差范围,我们知道浮点数计算是不精确的
0.1 +0.2; //0.30000000000000004
0.1+0.2-0.3; //5.551115123125783e-17
5.551115123125783e-17.toFixed(20); //‘0.00000000000000055551’
如果这个误差能够小于Number.EPSILON,我们就可认为得到正确的结果
安全整数和 Number.isSafeInteger()
用来判断数值是否落在安全范围,超出安全范围数值就会变成错误的结果
Number.isSafeInteger(null); //false
Math对象的扩展
Math.trunc()
直接砍掉整数后面的小数
Math.trunc(4.9); //4
Math.sign()
用来判断是否带负号的数
Math.sign(5); //1
Math.sign(-0); //-1
Math.cbrt(num:number)
算出一个数的立方根
函数可变参数形式
function(...value)
合并数组
[1,2].concat([3,4,5]);
字典形式
let map=new Map([
[1,‘one’],
[2,‘two’],
[3,‘three’],
]);
let arr=[…map.keys()]; //[1,2,3]
Generator函数
var go =function*(){
yield 1;
yield 2;
yield 3;
}
[…go()] //[1,2,3]
箭头函数
匿名函数的简化写法
(...value)=>{}
Symbol
ES5的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,能够保证每个属性的名字都是独一无二的就好了,这样就能从根本上防止属性名冲突。这就是ES6引入Symbol的原因。
let s=Symbol();
typeof s //“symbol”
注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象。也就是说,由于Symbol值不是对象,所以不能添加属性。基本上它是一种类似于字符串的数据类型
Symbol可以接受一个字符串作为参数,表示对Symbol的描述,主要是为了在控制台显示或转为字符串时比较容易区分
var s1= Symbol(
foo
);
var s2=Symbol(bar
);
s1 //Symbol(foo);
s2 //Symbol(bar);
如果不加参数,toString后都是一样显示Symbol()
不好区分,但s1和s2是不相等的
作为属性名的Symbol
由于每一个Symbol值都是不相等的,这意味着Symbol 值 可以作为 标识符用于对象的属性名,保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。
//(1)
var mySymbol=Symbol();
var a={};
a[mySymbol]=Hello!
;
//(2)
var a ={
[mySymbol]=Hello!
//在对象内使用Symbol值 定义属性时,Symbol值必须放在方括号中
};
//(3)
var a ={};
Object.defineProperty(a,mySymbol,{value:Hello!
});
以上写法都得到同样的结果
a[mySymbol] //Hello!
这个方法会区别每个变量,即使同名也不会冲突,但慎用,会使代码不稳键
魔术字符串
这名取得好,就是个全局通用变量,日常使用,没毛病
属性名的遍历
var obj={}
var a=Symbol.for(b
);
var b=Symbol.for(a
);
obj[a]=“Hello”;
obj[b]=“world”;
var objectSymbols= Object.getOwnPropertySymbols(obj);objectSymbols
//[Symbol(a),Symbol(b)]
var obj={}
var foo = Symbol(foo
);
Object.defineProperty(obj,foo,{
value:“foobar”,
})
for(var i in obj){
console.log(i); //无输出
}
Object.getOwnPropertyNames(obj); //[]
Object.getOwnPropertySymbols(obj); //[Symbol(foo)]
Reflect.ownKeys(obj); //[
value
] 返回属性名
Symbol.for(),Symbol.keyFor()
这个是如果希望使用同一个Symbol值
var s1 =Symbol.for(
foo
);
var s2=Symbol.for(foo
);
s1===s2 //true
内置的Symbol值
Symbol.hasInstance
Foo[Symbol.hasInstance] (foo){
return foo instanceof Array;
}
var o=new MyClass();
o instanceof Arry //false 是否是一个数组的实例
Symbol.isConcatSpreadable()
对象的Symbol.isConcatSpreadable 属性等于一个布尔值,表示该对象使用Array.prototype.conat()时是否可以展开
let arr1=[‘c’,
d
];
[‘a’,b
].concat(arr1,e
) //[‘a’,‘b’,‘c’,‘d’,‘e’]
Symbol.species()
对象的Symbol.species 属性指向一个方法,对象作为构造函数创造实例时会调用 这个方法。即如果this.constructor[Symbol.species]存在,就会使用这个属性作为构造函数来创造新的实例对象
static get [Symbol.species] (){
return this;
}
Symbol.match
对象的Symbol.match 属性指向一个函数,当执行str.match(myObject)时,如果该属性存在。会调用它返回该方法的返回值
String.prototype.match(regexp)
//等同于
regexp[Symbol.match] (this)
class MyMatcher(){
[Symbol.match] (string){
returnhello world
.indexOf(string);
}
}
‘e’.match(new MyMatcher()) // 1 拿返回值匹配字符串
Symbol.replace()
String.prototype.replace(searchValue,replaceValue)
//等同于
searchValue[Symbol.replace] (this.replaceValue)
Symbol.search()
String.prototype.search(regexp)
//等同于
regexp[Symbol.search] (this)
calss MySearch(){
constructor(value){
this.value=value
}
[Symbol.search] (string){
return string.indexOf(this.value);
}
}
foobar
.search(new MySearch(foo
)) //0
Symbol.split
String.prototype.split(separator,limit)
//separator[Symbol.split] (this,limit)
Symbol.interator()
用于返回遍历器的方法,加上后可以做集合对象,然后可以用for…of循环
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
Symbol.toPrimitive()
可以做扩展接口
let obj ={
[Symbol.toPrimitive] (hint){
switch(hint){
case number
:
return 123;l
case string
:
return str;
case default
:
return ‘default’;
default:
throw new Error();
}
}
};
2*obj //246
3+obj // 3default
obj===default
//true
String(obj) //str
Symbol.toStringTag
对象的Symbol.toStringTag 属性指向一个方法,在对象上调用 Object.prototype.toString方法时如果这个属性存在,其返回值 会出现 在toString 方法返回的字符串中,表示 对象的类型。也就是说, 这个属性可用于定制[object Object]或[object Array]中object 后面的字符。
({[Symbol.toStringTag]:foo
}.toString())
//"[object Foo]"
class Collection{
get [Symbol.toStringTag] (){
return xxx
;
}
}
var x = new Collection();
Object.prototype.toString.call(x); //"[object xxx]"
Symbol.unscopables
用于在with环境下排除对象
calss MyCalss{
foo(){return 1;}
get[Symbol.unsopables](){
return {foo:true}; //这里把foo排除了
}
}
var foo=funcation(){return2};
with(MyClass.prototype){ //with环境
foo();//2
}
Proxy 和Refect
Proxy
代理,用于重载对象的方法
var proxy= new Proxy({},{
get: function(target,property){
return 35;
}
});
proxy.time //35
proxy.name //35
proxy.title //35
var target={};
var handler={};
var proxy =new Proxy(target,handler);
proxy.a=b
;
target.a //“b”
get()
get方法用于拦截某个属性的读取操作。前面已经有一个例子,下面是另一个拦截读取操作的例子。
var person={
name:“张三”
};var proxy= new Proxy(person,{
get:function(target,property){
if(property in target){
return target[property];
} else {
throw new ReferenceError(“Property”"+property+"“doesnot exist.”);
}
}
});proxy.name //“张三”
proxy.age //抛出一个错误
上面的代码表示,如果访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性只会返回 undefined
get 方法可以继承。
let proto = new Proxy({},{
get(target,propertyKey,reciver){
console.log(GET
+propertyKey);
return target[propertyKey];
}
});
let obj = Object.create(proto);
obj.xxx //“GET xxx”
上面的代码中,拦截操作定义在Prototype对象上,所以如果读取obj 对象继承的属性,拦截会生效。
下面的例子使用get 拦截实现数组读取负数索引。
function createArray(…elements){
let handler ={
get(target,propKey,receiver){
let index=Number(propKey);
if(index<0){
propKey=String(target.length + index);
}
return Reflect.get(target,propKey,receiver);
}
};
let target =[];
target.push(…elements);
return new Proxy(target,handler);
}
let arr = createArray(a
,b
,c
);
arr[-1] //c
上面的代码中,数组的位置参数是-1, 就会输出数组的最后一个成员。
利用Proxy,可以将读取属性的操作(get)转变为执行某个函数,从而实现属性的链式操作。
var pipe =(function(){
var pipe;
return function (value){
pipe=[];
return new Proxy({},{
get: function(pipeObject,fnName){
if(fnName==“get”){
return pipe.reduce(function(val,fn){
return fn(val);
},value);
}
pipe.push(window[fnName]);
return pipeObject;
}
});
}
}());
var double =n => n2;
var pow=n=>nn;
var reverseInt =n =>n.toString().split().reverse().Join(
)|0;
pipe(3).double.pow.reverseInt.get //get
//63
上面的代码设置Proxy后达到了将函数名链式使用的效果。
set()
set方法用于拦截某个属性的赋值操。
假定Person对象有一个age属性,该属性应该是一个不大于200的整数,那么可以使用Proxy对象保证age的属性符合要求.
对象保证age 的属性值符合要求。
let validator ={
set: function(obj,prop,value){
if(prop===age
){
if(!Number.isInteger(value)){
throw new TypeError(The age isnot an integer
);
}
if(value>200){
throw new RangeError(The age seems invalid
);
}
}
//对于age以外的属性,直接保存
obj[prop]=value;
}
};
let person= new Proxy({},validator);
person.age=100;
person.age //100
person.age =young
//报错
person.age =300 //报错
上面的代码中,由于设置了存值函数set, 任何不符合要求的age 属性赋值都 会抛出一个错误。利用set方法,还可以数据绑定,即每当对象发生变化 时,会自动 更新 DOM
apply()
apply方法拦截函数的调用,call和apply操作。
var handler={
apply(target,ctx,args){
return Reflect.apply(…arguments);
}
}
apply 方法可以接受3个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。
下面是一个例子。
var target= function(){ return I am the target
;};
var handler={
apply:function(){
return I am the proxy
;
}
};
var p=new Proxy(target,handler);
p() ===I am the proxy
;
//true
上面的代码中,变量p是Proxy 的实例,当作函数调用 时(p()),就会被apply 方法拦截。返回上一个字符串。
has()
has 方法可以隐藏某些属性,不被in 操作符发现
var handler ={
has(target,key){
if(key[0]===_
){
reutrn false;
}
return key in target;
}
};
var target = { prop:foo
, prop:foo
};
var proxy =new Proxy(target,handler);
_prop
in proxy //false
上面的代码中,如果原对象的属性名的第一个字符是下画线,proxy,has 就会返回 flase, 从而不会被in 运算符发现
如果原对象不可配置或者禁止扩展,那么这时has 拦截会报错。
var obj={a:10};
Object.preventExtensions(obj);
var p= new Proxy(obj,{
has: function(target,prop){
return false;
}
});
“a” in p; // TypeError is throw
上面代码中,obj 对象禁止扩展,结果使用has 拦截就会报错。
construct()
construct 方法用于拦截new 命令
var handler={
construct(target,args){
return new target(…args);
}
}
下面是一个例子
var p = new Proxy(function(){},{
construct:function(target,args){
console.log(called:
+args.join(,
));
return { value: args[0]*10};
}
});
new p(1).value
//“called: 1”
// 10
如果 constuct 方法返回 的不是对象,就会抛出错误。
var p= new Proxy(function(){},{
constuct: function(target,argumentsList){
return 1;
}
});
new p() // 报错
deleteProperty()
deleteProperty 方法用于拦截delete 操作,如果这个方法抛出错误或者返回fales, 当前属性就无法被 delete 命令删除。
var handler={
deleteProperty(target,key){
invariant(key,delete
);
return true;
}
}
function invariant(key,action){
if(key[0]===_
){
throw new Error(Invalid attempt to ${action} private "${key}" property
);
}
}
var target = { _prop:foo
}
var proxy= new Proxy(target, handler)
delete proxy._prop
//Error: Invalid attempt to delete private “_prop” property
上面的代码中,deleteProperty方法拦截了delete操作符,删除第一个字符为下画线的属性会报错
defineProperty()
defineProperty 方法拦截了Object.defineProperty操作 // 定义变量
var handler ={
defineProperty(target,key,descriptor){
return false
}
}
var target={}
var proxy = new Proxy(target,handler)
proxy.foo=‘bar’
//TypeError:proxy: proxy deifineProperty handler returned false for property "foo"
上面的代码中,defineProperty 方法返回 fase ,导致添加新属性会抛出错误。
enumerate()
enumerate 方法用于拦截for … in 循环。 注意与Proxy 对象的has方法区分,后者用于拦截in 操作符,对for…in 循环无效
var handler= {
enumerate(target){
return Object.keys(target).filter(key=>key[0]!==_
)Symbol.iterator;
}
}
var target={prop:foo
, _bar:bar
,_prop:foo
}
var propxy=new Proxy(target,handler);
for(let key in proxy){
console.log(key);
//“prop”
}
上面的代码中,enumrate 方法取出原对象的所有属性名,将其中第一个字符 等 于下画线的都 过滤掉,然后返回 这些符合 条件 的属性名的一个遍历对象,供for … in 循环消费
下面的另一个例子
var p = new Proxy({},{
enumerate(target){
console.log(“called”);
return [“a”,“b”,“c”] [Symbol.iterator] ();//[Symbol.iterator]被指定成for循环可用于遍历的对象
}
});
for(var x in p){
console.log(x);
}
//“called”
//“a”
//“b”
//“c”
如果enumerate 方法返回 的不是一个对象,就会报错。
var p= new Proxy({},{
enumerate(target){
return 1;
}
});
for(var x in p) {} //报错
getOwnpropertyDescriptor()
getOwnPropertyDescriptor 方法拦截Object.getOwnPropertyDescriptor,返回一个属性描述对象或者undefined.
var handler ={
getOwnPropertyDescriptor(target,key){
if(key[0]===_
){
return
}
return Object.getOwnPropertyDescriptor(target,key)
}
}
var target={_foo: ‘bar’,baz:tar
};
var proxy= new Proxy(target,handler);
Object.getOwnPropertyDescriptor(proxy,wat
);
//undefined
Object.getOwnPropertyDescirptor(proxy,_foo
)
//undefined
Object.getOwnPropertyDescirptor(proxy,baz
)
//{ value: tar
, writable: true, enumerable: true, configurable:true }
上面的代码 中,handler.getOwnPropertyDescirptor方法对于第一个字符为下画线的属性名会返回 undefined.
getPrototyeOf()
getPrototypeOf 方法主要用于拦截Object.getPrototypeOf()运算符,以及其他 一些操作。
var proto= {}
var p=new Proxy({},{
getPrototypeOf(target){
return proto;
}
});
Object.getPrototypeOf§===proto //true;
isExtensible()
isExtensible 方法拦截Object.isExtensible操作。
var p= new Proxy({},{
isExtensible:function(target){
console.log(“called”);
return true;
}
});
Object.isExtensible§;
//“called”
//true
ownKeys()
ownKeys 方法用于拦截Object.keys()操作
let target ={};
let handler ={
ownKeys(target){
return [hello
,world
];
}
};
let proxy=new Proxy(target,handler);
Object.keys(proxy);
//[hello
,world
];
上面的代码拦截了对于target 对象的Object.keys()操作,返回预先设定的数组。
下面的例子则是拦截第一个字符为下画线的属性名;
preventExtensions()
prevenExtensions 方法拦截Object.preventExtensions(). 该方法必须 返回 一个布尔值。
这个方法有一个限制,只有当Object.isExtensible(proxy)为false(即不可扩展)时,proxy,preventExtensions才能返回true,否则会报错。
var p= new Proxy({},{
preventExtensions:funcation(target){
return true;
}
});
Object.preventExtensions§;//报错
上面的代码中,proxy.preventExtensions方法返回 true, 但此时Object.isExtensible(proxy)会返回 true, 因些报错
为了防止 出现这个问题,通常要在proxy.prevenExtensions方法中调用一次Object.preventExtensions.
var p=new Proxy({},{
preventExtensions:function(target){
console.log(“called”);
Object.preventExtensions(target);
return true;
}
});
Object.preventExtensions§;
//“called”
//true
setPrototypeOf()
setPrototypeOf 方法主要用于拦截Object.setPrototypeOf方法
下面是一个例子
var handler={
setPrototypeOf(target,proto){
throw new Error(Changing the prototype is forbidden
);
}
}
var proto={}
var target=function() {};
var proxy=new Proxy(target,handler);
proxy.setPrototypeof(proxyk,proto);
//Error: Changing the prototype is forbidden
上面的代码 中,只要修改target的原型对象就会报错
Proxy.revocable()
Proxy.recocable方法返回 一个可取消的Proxy 实例 。
let target ={}
let handler={};
let {proxy,revoke}=Proxy.revocalbe(target,handler);// 调用 这个方法会返回两个对象,一个是实例,另一个是取消实例的方法
proxy.foo=123
proxy.foo //123
revoke();
proxy.foo // TypeError: Revoked
Proxy.recoable 方法返回一个对象,其proxy 属性是Proxy实例,revoke 属性是一个函数。可以取消Proxy实例。 上面的代码中,当执行revoke 函数后再访问Proxy 实例,就会抛出一个错误。
Reflect
Reflect 对象与Proxy 对象一样,也是ES6 为了操作对象而提供的新API
1.将Object 对象的一些明显 属于语言层面的方法放到Reflect 对象上。 未来的新方法只部署在Reflect对象上
2.修改某些Object方法的返回结果,让其变得更合理。比如在无法定义属性时不报错,而是返回false
3. 让Object操作都 变成函数 行为。某些Object 操作是命令式,比如name in obj 和delete obj[name], 而Reflect.has(obj,name)和Reflect.deleteproperty(obj,name)让它们成了函数行为
4. Reflect 对象的方法与Proxy 对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象找到对应的方法。 这就让Proxy对象可以方便 地调用 对应的Reflect 方法完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect 上获取默认行为 。
Proxy(target,{
set: function(target,name,value,receiver){
var success =Reflect.set(target,name,value,receiver);
if(success){
log(property
+name+on
+target+set to
+value);
}
return success;
}
});
上面的代码中,Proxy方法拦截了target 对象的属性赋值行为。它采用Reflect.set 方法赋值给对象的属性,然后再部署额外的功能。
Reflect 对象的方法
Reflect.get(target,name,receiver)
查找并返回target对象的name 属性,如果没有该属性,则返回undefined.
如果 name 属性部署了读取函数,则读取函数的this 绑定receiver.
var obj={
get foo(){ return this.bar();},
bar:function(){…}
}
//下面的语句会让this.bar() 变成调用wrapper.bar()
Reflect.get(obj,“foo”,wrapper);
Reflect.set(target,name,value,receiver);
设置target对象的name 属性等于value。 如果name 属性设置了赋值函数,则赋值函数的this 绑定 receiver
Reflect.has(obj,name)
等同于name in obj.
Reflect.deleteProperty(obj,name)
等同于delete obj[name].
Reflect.consturct(target,args)
等同于new target(…args), 这提供了一种不使用new 来调用 的构造 函数的方法。
Reflect.getPrototypeOf(obj)
读取对象的_proto__属性,等同于Object.getPrototypeOf(obj).
Reflect.setPrototypeOf(obj,newProto)
设置对象的__proto__属性。注意,Object对象没有与些对应的方法。
Reflect.apply(fun,thisArg,args)
等同于function.prototype.apply.call(fun,thisArg,args).一般说来,如果要绑定一个函数的this 对象,可以写成fn.apply(obj,args),但如果函数定义了自己的apply方法,就只能写成Funcation.prototype.apply.call(fn,obj,args),采用Reflect对象可以简化这种操作。
另外,需要注意的是,Reflect.set(),Reflect.defineProperty()、Reflect.freeze()、Reflect.seal()和Reflect.prevenExtensions() 返回一个布尔值,表示操作是否成功。其对应的Object方法在失败时都会抛出错误
//失败时抛出错误
Object.defineProperty(obj,name,desc);
//失败时返回false
Reflect.defineProperty(obj,name,desc);
上面的代码中,Reflect.defineProperty方法的作用与Object.defineProperty是一样的,都 是为对象定义一个属性。但是,Reflect.defineProperty 方法失败时不会抛出错误,只会返回 false
创建一个Poxy对象就能做这些事,可以用于编写框架
二进制数组
ArrayBuffer对象
var buffer =new ArrayBuffer(12);
var x1=new Int32Array(buffer);
x1[0]=1;
var x2 =new Uint8Array(buffer);
x2[0]=2
x1[0] //2
上面的代码对两只一段内存分别建立两种视图: 32位带符号整数(Int32Array构造函数)和8位不带符号整数(Uint8Array 构造函数)。由于两个视图对应的是同一段内存,因此一个视图修改底层内存会影响 在另一个视图.
TypedArray 视图的构造函数除了接受 ArrayBuffer实例 作为参数,还可以接受普通数组作为参数,直接分配内存生成底层的ArrayBuffer实例,同时完成对这段内在的赋值。
var typedArray =new Uint8Array([0,1,2]);
typedArray.length //3
typedArray[0]=5;
typeArray //[5,1,2]
上面的代码使用TypedArray 视图的Uint8Array 构造函数新建一个不带符号的8倍整数视图。
可以看到,Uint8Array 直接使用普通数组作为参数,对底层内在的赋值同时完成。
ArrayBuffer.prototype.byteLength
要获取ArrayBuffer实例的 byteLength 属性返回所分配 的内存区域的字节长度。
ArryBuffer.prototype.slice()
复制生成一个新的对象
var buffer=new ArrayBuffer(8);
var newBuffer=buffer.slice(0,3);
ArrayBuffer.isView
判断参数是否为TypedArray 实例或DataView实例
var buffer =new ArrayBuffer(8);
ArrayBuffer.isView(buffer); //flase
var v= new Int32Array(buffer);
ArrayBuffer.isView(v) //true
TypedArray 视图
var f64a=new Float64Array(8);
f64a[0]=10;
f64a[1]=20;
f64a[2]=f64a[0]+f64a[1];
上面代码生成一个8 个成员的Float64Array数组共(共64字节), 然后依次对每个成员赋值。
这时,视图构造函数的参数就是成员的个数。可以看到,视图数组的赋值操作与普通数组毫无二致
数组方法
注意,TypeedArray数组没有concat方法。如果想要合并多个TypedArray数组,可以用下面这个函数。
function concatenate(resultConstructor,…arrays){
let totalLength =0;
for(let arr of arrays){
totalLength += arr.length;
}
let result = new resultConstructor(totalLength);
let offset =0;
for(let arr of arrays){
result.set(arr,offset);
offset +=arr.length;
}
return result;
}
concatenate(Uint8Array,Uint8Array.of(1,2),Uint8Array.of(3,4));
//Uint8Array[1,2,3,4]
另外,TypedArray数组与普通数组一样部署了Iterator 接口, 所以可以遍历。
let ui8=Uint8Array.of(0,1,2);
for(letbyte of ui8){
console.log(byte);
}
//0
//1
//2
字节序
字节序指的是数值在内在中的表示方式
var buffer=new ArrayBuffer(16);
var int32View=new Int32Array(buffer);
for(var i=0; i< int32View.length;i++){
int32View[i]=i*2;
}
上面的代码生成 一个16字节的ArrayBuffer对象,然后在其基础上建立了一个32位整数的视图。由于 每个32位整数占据4个字节,所以一共可以写入4个整数,依次为0、2、4、6.
BYTES_PER_ELEMENT属性
每一种视图的构造函数,都有一个BYTES_PER_ELEMENT属性,表示这种数据类型占据的字节数
ArrayBuffer与字符串的互相转换
ArrayBuffer转为字符串,或者字符串转为ArrayBuffer,有一个前提,即字符串的编码方法是确定的。假定字符串采用UTF-16编码(JavaScriptr的内部编码方式),那么可自己编写转换函数。
//ArrayBuffer 转为字符串,参数为ArrayBuffer 对象
function ab2str(buf){
return String.fromCharCode.apply(null,new Uint16Array(buf));
}
//字符串转为ArrayBuffer 对象,参数为ArrayBuffer对象
function str2ab(str){
var buf = new ArrayBuffer(str.length * 2); //每个字符占用2个字节
var bufView =new Uint64Array(buf);
for(var i = 0,strlen= str.length; i <striLen; i++){
bufView[i] = str.charCodeAt(i);
}
return buf;
}
溢出
不同的视图类型所能容纳的数值范围是确定的。超出这个范围就会出现溢出。
TypedArray 数组对于溢出采用的处理方法是求余值。 正向溢出(overflow) 的含义是输入值大于当前数据类型的最大值,最后得到的值 就等于当前数据类型最小值 加上余值,再减去1;负向溢出(underflow)等于当前数据类型的最大值减去余值,再加上1.
var uint8 = new Uint8Array(1);
uint8[0]=256;
uint8[0] //0
uint8[0] =-1;
uint8[0] //255;
TypedArray.prototype.buffer
TypedArray 实例的buffer 属性返回整段内存区域对应的ArrayBuffer 对象。该属性为只读属性。
var a=new Float32Array(64);
var b = new Uint8Array(a.buffer);
以上代码中的a视图对象和b视图对象,对应同一个ArrayBuffer对象,即同一段内存。
TypedArray.prototype.byteLength,TypedArray.prototype.byteOffset
byteLength 属性返回TypedArray 数组占据的内存长度,单位字节。byteOffset 属性返回TypedArray数组从底层ArrayBuffer对象的哪个字节开始。这两个属性都 是只读属性。
var b = new ArrayBuffer(8);
var v1 =new Int32Array(b);
var v2 = new Uint8Array(b,2);
var v3 = new Init16Array(b,2,2);
v1.byteLength //8
v2.byteLength //6
v3.byteLength //4
v1.byteOffset //0
v2.byteOffset //2
v3.byteOffset //2
TypedArray.prototype.length
length 属性表示TypeArray 数组含有多少个成员。注意区分byteLength 属性和length 属性, 前者是字节长度,后者是成员长度。
var a =new Int16Array(8);
a.length //8
a.byteLength//16;
TypeArrray.prototype.set()
TypeArray数组的set 方法用于复制数组(普通数组或TypedArray数组),也就是将一段内存完全复制到另一段内存。
var a = new Uint8Array(8);
var b = new Uint8Array(8);
b.set(a);
上面的代码复制a 数组的内容到b 数组,它是整段内存的复制,比一个个复制成员的那种复制快得多。
set 方法还可以接受第二个参数,表示 从b对象的哪一个成员开始复制a对象。
var a =new Uint16(8);
var b =new Uint16Array(10) ;
b.set(a,2);
以上代码中的b数组比a数组多两个成员,所以从b[2]开始复制。
TypedArray.prototype.subarray()
subarray方法是对于TypedArray 数组的一部分再建立一个新的视图。
var a =new Uint16Array(8);
var b= a.subarray(2,3);
a.byteLength //16
b.byteLength //2
subarray 方法的第一个参数是起始的成员序号,第二个参数是结束的成员序号(不含该成员),如果省略则包含剩余的全部成员。所以上面的a.subarray(2,3)意味着b 只包含a[2]一个成员 字节长度为2.
TypedArray.prototype.slice()
TypedArray实例的slice 方法可以返回一个指定位置的新的TypedArray实例。
let ui8=Uinti8Array.of(0,1,2);
ui8.slice(-1);
//Uint8Array [2]
上面的代码中,ui8是8位无符号整数数组视图的一个实例。它的slice 方法可从当前视图中返回一个新的视图实例。
slice 方法的参数表示原数组的具体位置。负值表示逆向计数的位置,即-1 为倒数第一个位置,-2表示倒数第二个位置,以此类推。
TypedArray.of()
TypedArray 数组的所有构造函数都 有一个静态方法of, 用于将参数转为一个TypedArray实例。
Float32Array.of(0.151,-8,3.7)
// Float32Array[0.151,-8,3.7]
下面3种方法会生成同样的TypedArray数组。
//方法一
let tarr =new Uint8Array([1,2,3]);
//方法二
let tarr=Uint8Array.of(1,2,3);
//方法三
let tarr = new Uint8Array(3);
tarr[0]=0;
tarr[1]=1;
tarr[2]=2;
TypedArray.from()
静态方法from 接受一个可遍历的数据结构(比如数组)作为参数,返回一个基于些结构的TypedArray实例。
Uint16Array.from([0,1,2])
// Uint16Array[0,1,2]
这个方法还可以将一种TypedArray实例转为另一种。
var ui16=Uint16Array.from(Uint8Array.of(0,1,2));
ui16 instanceof Uint16Array //true
from 方法还可以接受一个函数作为第二个参数,用于对每个元素进行扁历,功能类似map方法。
Int8Array.from(127,126,125).map(x=>2*x)
//Int8Array[-2,-4,-6];
Int16Array.from(Int8Array.of(127,126,125),x=>2*x) //相当于遍历, 后面的是内容处理
//Int16Array[-2,-4,-6];
上面的例子中,from 方法没有发生溢出,这说明遍历是针对新生成的16位整数数组,而不是原来的8位整数数组。也就是说,from会将第一个参数指定的TypedArray数组复制到另一段内存中(占用内存从3字节 变为6字节),再进行处理。
复合视图
由于视力的构造函数可以指定起始位置和长度,所以在同一段内在中可以依次存放不同类型的数据,这叫作“复合视图”。
var buffer = new ArrayBuffer(24);
var idView=new Uint32Array(buffer,0,1);
var usernameView=new Uint32Array(buffer,4,16);
var amountDueView=new Float32Array(buffer,20,1);
上面代码将一个24字节的ArrayBuffer对象分成了3个部分:
- 字节0到3:1个32位无符号整数。
- 字节4到字节19: 16个8位整数。
- 字节20到字节23: 1个32位浮点数。
DataView 视图
如果一段数据 包括多种类型(比如服务器传来的HTTP数据),这时除了建立ArrayBuffer 对象的复合视图外,还可以通过 DataView视图进行操作
var buffer = new ArrayBuffer(24);
var dv =new DataView(buffer);
//从第1个字节读取一个8位无符号整数
var v1= dv.getUint8(8);
//从第2个字节读取一个16位无符号整数
var v2=dv.getUint16(1);
//从第4个字节读取一个16位无符号整数
var v3 = dv.getUint16(3)
上面的代码读取了ArrayBuffer 对象的前5个字节,其中有1个8位整数和2个16位整数。
如果一次读取两个或两个以上字节,就必须明确数据的存储方式,到底是小端字节序还是大端字节序。默认情况下,DataView 的get 方法使用大端字节序2解读数据,如果需要使用小端字节序解读,必须在get 方法的第二个参数指定true。
//小端字节序
var v1=dv.getUint16(1,true);
//大端字节序
var v2 = dv.getUint16(3,false);
//大端字节序
var v3 = dv.getUint16(3);
写入内存
//在第1个字节,以大端字节序写入值为25的32位整数
dv.setInt32(0,25,false);
//在第5个字节,以大端字节序写入值为25的32位整数
dv.setInt32(4,25);
//在第9个字节,以大端字节序写入值为2.5的32位整数
dv.setFloat32(8,2.5,true);
如果不确定所使用计算机的字节序,可以采用下面的判断方式。
var littleEndian = {function(){
var buffer=new ArrayBuffer(2);
new DataView(buffer).setInt16(0,256,true);
return new Int16Array(buffer)[0] === 256;
}}();
如果返回true,就是小端字节序; 如果返回 false,就是大端字节序。
Set 和Map 数据结构
Set
ES6提供了新的数据结构——Set. 它类似于数组,但是成员的值。Set本身是一个构造函数,用来生成Set数据结构。
var s =new Set();
[2,3,5,4,5,2,2].map(x =>s.add(x))
for(i of s){ sonsole.log(i)}
//2 3 5 4
上面的代码通过 add方法向Set结构加入成员,结果表明Set结构不会添加重复的值。
Set 函数可以接受一个数组(或类似数组的对象) 作为参数,用于初始化。
var set = new Set([1,2,3,4,4])
[…set]
//[1,2,3,4]
var items = new Set([1,2,3,4,5,5,5,5]);
items.size //5
function divs(){
return […document.querySelectorAll(dev
)]
}
var set =new Set(divs())
set.size //56
//类似于
divs().forEach(div=>set.add(div))
set.size //56
向Set 加入值 时不会发生类型转换,所以5和“5” 是两个不同的值。Set 内部判断丙个值 是否不同使用的算法类似于精确相等运算符(===)。这意味着,两个对象总是不相等的。唯一的例外 是NaN等于自身(精确相等运算符认为NaN不等于自身)。
let set =new Set();
set.add({});
set.size //1
set.add({});
set.size //2
var properties = new Set();
properties.add(“width”);
properties.add(“height”);
if(properties.has(someName){
//do something;
})
Array.from 方法可以将Set 结构转为数组。
var items= new Set([1,2,3,4,5]);
var array = Array.from(items);
这就提供了一种去除数组的重复元素的方法。
function dedupe(array){
return Array.from(new Set(array));
}
dedupe([1,1,2,3]) //[1,2,3]
遍历操作
for ( let item of set.keys()){
console.log(item);
}
WeakSet
WeakSet结构与Set类似,也是不重复的集合。但是,它与Set有两个区别。
首先,WeakSet的成员只能是对象,而不能是其他类型的值。
其次,WeakSet中的对象都 是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用。也就是说, 如果其他对象都不再引用 该对象,那么垃圾回收机制会自动回收该 对象所占用的内存,不考虑该对象还存在于WeakSet中。 这个特点意味着无法引用WeakSet的成员,因此WeakSet是不可遍历的。
var ws=new WeakSet();
ws.add(1);
//TypeError: Invalid value used in weak set
ws.add(Symbol())
//TypeError: invalid value used in weak set
只能是对象
还可以在创建时传入构造函数
var ws = new WeakSet(…);
上面的代码试图向WeakSet添加一个数值和Symbol 值,结果报错。
Map
JavaScript 的对象(Object) 本质上是键值对我集合(Hash结构),但是只能用字符串作为键。
这给它的使用带来了很大的限制
var data = {}
var element = document.getElementById(“myDiv”);
data[element]=metadata;
data["[Object HTMLDivElement]"] //metadata
上面的代码愿意是将一个DOM节点作为对象data 的键,但是由于 对象只接受字符串作为键名,所以element 被自动转为字符串[Object HTMLDivElement].
但Map 的键不一样,它可以用于更多类型
let map = new Map([
[1,one
],
[1,two
],
[1,three
],
]);
[…map.keys()]
//[1,2,3]
let map0 = new Map().set(1,a
).set(2,b
).set(c
);
let map2=new Map([
[…map0].map([k,v]=>[k * 2,_
+v]);
]);
//产生Map 结构{2=>_a
,4=>_b
,6=_c
}
Map转数组
[…myMap]
数组转Map
new Map([[true, 7],[{foo:3},[abc
]]])
Map 转为对象
如果Map 的所有键都 是字符串,则其可以转为对象
funcation strMapToObj(strMap){
let obj =Object.create(null);
for(let [k,v] of strMap){
obj[k]=v;
}
return obj;
}
let myMap= new Map().set(yes
,true).set(no
,false);
strMapToObj(myMap)
// {yes:true,no:false}
对象转为Map
function objToStrMap(obj){
let strMap=new Map()
for(let k of Object.keys(obj)){
strMap.set(k,obj[k]);
}
return strMap;
}
objToStrMap({yes:true, no: false})
//[[yes
,true],[no
,false]];
Map 转为JSON
Map 转为JSON要区分两种情况,一种情况是,Map的键名都 是字符串,这时可以选择转为对象JSON
function strMapToJson(strMap){
return JSON.stringify(strMapToObj(strMap));
}
let myMap =new Map().set(yes
,true).set(no
,false);
strMapToJson(myMap);
//{"yes":true,"no":false}
另一种情况是Map键名有非字符串, 这时可以选择转为数组JSON。
function mapToArrayJson(map){
return JSON.stringify([…map]);
}
let myMap =new Map().set(true,7).set({foo,3}:{abc
});
strMapToArrayJson(myMap);
//``[[true,7],[{“foo”:3}],[“abc”]]`
JSON转为Map
JSON转为Map,正常情况下所有键名都 是字符串
function jsonToStrMap(jsonStr){
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap({"yes":true,"no":false}
)
//Map{yes
=>true,no
=>false}
但是,有一种特殊情况,整个JSON就量个数组,且每个数组成员本身又是一个有两个成员的数组。这时,它可以一一对应地转为Map.这往往是数组转为JSON的逆操作。
function jsonToMap(jsonStr){
return new Map(JSON.parse(jsonStr));
}
jsonToMap([true:7],[{"foo":3},[
abc]]]
);
//Map {true =>7,Object {foo:3}=>[abc
]};
WeakMap
WeakMap 结构与Map 结构基本类似,唯一 的区别 是它只接受对象作为键名(null除外),不接爱其他类型的值 作为键名,而且键名所指向的对象不讲入垃圾回收机制。WeakMap 结构有助于防止 内存泄漏。
WeakMap 应用的典型场合就是DOM节点作为键名。下面是一个例子。
let myElement=document.getElementById(logo
);
let myWeakmap=new WeakMap();
myWeakmap.set(myElement,{timesClicked:0});
myElement.addEventListener(click
,funcion(){
let logoData= myWeakmap.get(myElement);
logoData.timesClicked ++;
myWeakmap.set(myElement.logoData);
},false);
上面的代码中,myElement是一个DOM节点,每当发生click事件,就更新一个状态。我们将这个状态作为键值放在WeakMap里,对应的键名就是myElement.一旦这个SOM节点删除,
该状态就会自动消失,不存在内存泄漏风险。
Iterator和for …of 循环
任何数据结构,只要部署Interator接口,就可以完成遍历操作
let arr = [a
,b
,c
];
let iter = arrSymbol.iterator;
iter.next(); //{value:a
, done : false}
iter.next(); //{value:b
, done : false}
iter.next(); //{value: undefined, done: true}
上面的代码中,变量arr是一个数组,原生具有遍历器接口,部署在arr 的Symbol.iterator属性上.所以,调用 这个属性就会得到遍历器对象。
上面提到,原生部署Iterator 接口的数据结构有3类,对于这3类数据结构,不用自己写遍历器生成函数,for…of 循环会自动 遍历它们。除此之外,其他数据结构(主要是对象)的Iterator 接口,都 需要自己在Symbol.iterator属性上部署,这样才会被 for… of 循环遍历。
对象(Object)之所以没有默认部署Iterator接口,是因为对象的哪个 属性先遍历,哪个 属性后遍历是不确定 的,需要 开发者手动指定。本质上,遍历器是一种线性处理。对于 任何 非线性的数据结构,部署遍历器接口就等于 部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被 当作 Map结构使用,ES5没有Map结构,而ES6原生提供了。、
一个对象如果要有可被 for…of循环调用 的Iterator接口,就必须在Symbol.iterator属性上部署遍历器生成 方法(原型链上的对象具有该 方法也可)
class RangeIterator{
constructor(start,stop){
this.value=start;
this.stop=stop;
}
Symbol.iterator{return this;}
next(){
var value=this.value;
if(value<this.stop){
this.value++;
return (done:false,value:value);
}else{
return {done:true,value:undefined};
}
}
}
function range(start,stop){
return new RangIterator(start,stop);
}
for(var value of range(0,3)){
console.log(value);
}
上面的代码是一个类部署Iterator接口的写法。Symbol.iterator 属性对应一个函数,执行后返回 当前对象的遍历器对象。
下面是通过 遍历器实现 指针结构的例子。
function Obj(value){
this.value=value;
this.next=null;
}
Obj.prototype[Symbol.iterator]=function(){
var iterator={
next:next;
};
var current=this;
function next(){
if(current){
var value= current.value;
var done= current == null;
current = current.next;
return{
done:done,
value:value
}
}else{
return{
done:true;
}
}
}
return iterator;
}
var one =new Obj(1);
var two =new Obj(2);
var three =new Obj(3);
one.next=two;
two.next=three;
for(var i of one){
console.log();
}
//1
//2
//3
// 原意就是自己可以指定一下next是谁,加上Symbol.iterator只是让它可以用for… of 遍历
字符串也有Iterator接口,用法一样,不过是写好的
for … of 循环
它可以用来遍历对象,区别于for…in 只能遍历
for…in循环有几个缺点。
1.数组的键名是数字,但是for…in 循环是以字符串作为 键名,“0”、“1”、“2” 等。
2.for…in 循环不仅遍历数字键名,还会遍历手动添加 的其他键,甚至包括原型链上的键。
3.某些情况下,for…in循环会以任意顺序遍历键名。
总之,for…in 循环主要是为遍历对象而设计的,不适用于遍历数组
for…of 循环相比上面几种做法有一些显著的优点。
for(let value of myArray){
console.log(value);
}
4.有着同for…in 一样的简洁语法,但是没有for…in 那些缺点
5.不同于forEach 方法,它可以与break、continue和return配合使用。
6.提供了遍历所有数据结构的统一操作接口。
下面是一个使用break语句跳出for…of 循环的例子。
for(var n of fibonacci){
if(n>1000)
break;
console.log(n);
}
for…of 会遍历出数组,知道当前的索引是在哪,省去for循环的index操作,这在多重循环可以很有效地规避一些index重复引用问题
上面的例子会输出斐波纳契数列小于等于1000的项。如果当前项大于1000,就会使用break语句跳出for…of循环。
Generator
使用起来特别简单,就是定义一个function函数,注意带上“”号
状态机
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:}
调用next方法会拿到 yield 的值。
Generator.prototype.throw()
var g= function*(){
while(true){
try{
yield;
} catch(e){
if (e!=a
) throw e;
console.log(内部捕获
,e);
}
}
};
var i=g();
i.next();
try{
i.throw(a
);
i.throw(b
);
}catch(e){
console.log(外部捕获
,e);
}
//内部捕获 a
//外部捕获 b
如果Generator 函数内部部署了try…catch 代码块,那么遍历器的throw方法抛出的错误不影响 下一次遍历,否则遍历直接终止。
Generator.prototype.return()
用来终结Generator函数的遍历
function* gen(){
yield 1;
yield 2;
yield 3;
}
var g= gen();
g.next() //{value:1,done:false}
g.return(“foo”) //{value: “foo”, done:true}
g.next() //{value: undefined,done:true};
上面的代码中,遍历器对象g调用return方法后,返回值的value 属性就是return 方法的参数foo。同时,Generator函数的遍历终止,返回值 的done属性为true,以后再调用next方法,done属性总是返回true.
yield* 语句
如果在Generator函数内部调用 另一个Generator函数,默认情况下是没有效果的。
function* foo(){
yield ‘a’;
yield ‘b’;
}
function* bar(){
yield ‘x’;
foo();
yield ‘y’;
}
for(let v of bar()){
console.log(v);
}
//“x”
//“y”
上面的代码中,foo 和bar 都是Genertator函数,在bar 里面 调用foo是不会有效果的。
这时就需要用到yield*语句,用来在一个Generator函数里面执行另一个Generator函数。
function* bar(){
yield ‘x’;
yield * foo();
yield ’y‘;
}
//等同于
function*bar(){
yield ‘x’;
yield ‘a’;
yield ‘b’;
yield ‘y’;
}
作为对象属性的Generator函数
如果一个对象的属性是Generator 函数,那么可以简写成下面的形式。
let obj={
* myGeneratorMethod(){
…
}
}
上面的代码中,myGeneratorMethod 属性前面有一个星号,表示穿上属性是一个Generator函数,其完整形式如下,与上面的写法是等价的。
let obj={
myGeneratorMethod:function*(){
//…
}
};
Generator 函数的this
Generator函数总是返回 一个遍历器,ES6规定这个遍历器是Generator 函数的实例,它也继承了Generator函数的prototype对象上的方法。
function* g(){}
g.prototype.hello = function(){
return ‘hi!’;
}
let obj = g();
obj instanceof g //true
obj.hello() //‘hi!’;
上面的代码表明,Generator函数g返回的遍历器obj是g 的实例。而且继承了g.prototype,但是,如果把g当作普通的构造函数,则并不会生效,因为g返回的总是遍历器对象
function* g(){
this.a=11;
}
let obj =g()
obj.a //undefined
上面的代码中,Generator 函数 g 在 this 对象上添加了一个属性a,但是obj对象拿不到这个属性。
function* F(){
yield this.x =2;
yiled this.y =3;
}
上面的代码中,函数F是一个构造函数,又是一个Generator函数。这时,使用new命令就无法生成F的实例了。因为F返回的是一个内部指针。
’next‘ in (new F())
//true
上面的代码中,由于new F() 返回是一个Iterator对象,具有next方法,所以上面的表达式值 为true。
如果要把Generator 函数当作正常的构造函数使用,可以采用下面的变通方法。首先生成一个空对象,使用bind 方法绑定Generator 函数内部的this.这样,构造函数调用以后,这个空对象就是Generator函数实例对象了。
function* F(){
yield this.x=2;
yield this.y=3;
}
var obj={};
var f=F.bind(obj)();
f.next(); //Object {value:2,done:false}
f.next(); //Object {value:3,done:false}
f.next(); //Object {value:undefined,done:true}
obj //{x:2,y:3}
上面的代码中,首先是F内部的this 对象绑定obj对象,然后调用它,返回一个Iterator对象。这个对象执行了3次next 方法(因为F内部有2条yield语句),完成F内部所有代码的运行。这时,所有内部 属性都 绑定在obj对象上,因而obj 对象也就成了F的实例 。
Generator函数推导
Generator 函数推导是对数组结构的一种模拟,其最大 的优点是惰性求值,即直到真正用到时才会求值,这样可以保证效率。请看下面的例子
let bigArray=new Array(100000);
for(let i = 0;i<100000;i++){
bigArray[i]=i;
}
let first = bigArray.map(n=>nn)[0]
console.log(first);
上面的例子遍历了一个大数组,份量 是在真正遍历之前,这个数组就已经生成,占用了系统资源。
如果改用Generator函数推导,就能避免这一点。下面的代码只在用到时才会生成 一个大数组。
let bigGenerator =function(){
for(let i = 0; i < 100000;i++){
yield i;
}
}
let squared = (for(n of bigGenerator())n*n);
console.log(squared.next());
它可以用于制作协程,单线程异步操作
写好了内部流程,在外面自己调next(),可使本来同步进行的流程中间穿插别的流程,用起来更加灵活。
Promise对象
所谓Promise,就是一个对象,用来传递异步操作的消息。它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的API,可供进一步处理。
Promise 对象有以下两个特点
- 对象的状态不受外界影响。Promise对象代表一个异步操作,有3种状态:Pending(进行中)、Resolved(已完成,又称Fulfilled) 和 Rejected(已失败)。只有异步操作的结果可以决定当前是哪一种状态,任何其它操作都无法改变这个状态。这也是“Promise“ 这个名字的由来,它在英语中意思就是”承诺“,表示其他手段无法改变。
- 一旦状态改变就不会再变,任何时候都 可以得到这 个结果。Promise对象的状态改变只有两种可能:从Pending变为Resolved和从Pending变为Rejected.只其中之一发生,状态就凝固了,不会再变,会一直保持这个结果。就算改变已经发生,你再对Promise对象添加回调函数,也会得到这个结果,这与事件(Event)完全不同。事件的特点是,如果你错过了它,再去监听是得不到结果的
var promise = new Promise(function(resolve,reject)){
//…some code
if(/*异步操作成功*/){
resolve(value);
}else{
reject(error);
}
});
Promise构造函数接受一个函数作为参数,该函数的两具参数分别是resolve和reject 。 它们是两个函数,由JavaScript引擎提供,不用自己部署
下面是一个Promise对象的简单例子。
function timeout(ms){
return enw Promise((resolve,reject)=>{
setTimeout(resolve,ms,‘done’); //这是一个时间延时操作
});
}
//then在这里绑定了一个函数,当执行完timeout后会才会继续执行这个函数
timeout(100).then((value)=>{
console.log(value);
});
上面 的代码中,timeout 方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(ms参数)以后,Promise实例的状态变为Resolved,就会触发then方法绑定的回调函数。
Promise.prototype.then()
Promise实例具有then方法,也就是说then方法是定义在原型对象Promise.prototype上的。
它的作用是为Promise实例添加状态改变时的架设函数。前面说过,then方法的第一个参数是Resolved状态的回调函数,第二参数(可选)是Reject状态的回调函数。
then方法返回的是一个新的Promise实例(注意,,不是原来那个Promise实例)。因此可以采用链式写法,即then 方法后面再调用 另一个then方法。
getJSON("/posts.json").then(function(json){
return json.post;
}).then(function(post){
//…
});
Promise.prototype.catch()
Promise.prototype.catch方法是.then(null,rejetion)的别名,用于指定发生错误时的回调函数。
getJSON("/posts.json").then(function(posts){
//…
}).catch(function(error){
//处理前一个回调函数运行时发生的错误
console.log(‘发生错误!’,error);
});
上面的代码中,getJSON方法返回一个Promise对象,如果该对象状态变为Resolved,则会调用then 方法指定的回调函数;如果异步操作抛出错误,状态就会变为Rejected,就会调用catch方法指定的回调函数处理这个错误。
Promise.all()
Promise.all 方法用于将多个Promise实例包装成一个新的Promise实例。
var p = Promise.all([p1,p2,p3]);
上面的代码中,Promise.all方法接受一个数组作为参数,p1,p2,p3都是Promise对象的实例;如果不是,就会先调 用下面讲到的Promise.resolve,将参数转为Promise实例,再进一步处理。(Promise.all方法的参数不一定是数组,但是必须具有Interator接口,且返回的每个成员都是Promise实例。)
p 的状态由p1、p2、p3决定,分成两种情况。
1.只有p1、p2、p3的状态都变成Fulfilled,p的状态才会变成Fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
2.只要p1、p2、p3中有一个被Rejected,p的状态就变成Rejected,此时第一个被Rejected的实例的返回值会传递给p的回调函数。
下面是一个具体的例子。
//生成一个Promise对象的数组
var promises=[2,3,5,7,11,13].map(function(id){
return getJSON("/prost/"+id+".json");
});
Promise.all((promises).then(function(posts){
//…
}).catch(function(reason){
//…
});
Promise.race()
Promise.race 方法同样是将多个Promise实例包装成一个新的Promise实例。
var p=Promise.race([p1,p2,p3]);
上面的代码中,只要p1、p2、p3中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的回调函数。
Promise.race 方法的参数与Promise.all方法一样,如果不是Promise实例,就会先调用下面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。
下面是一个例子,如果指定时间内没有获行结果,就将Promise的状态变为Rejected,否则变为Resolved.
var p=Promise.race([
fetch(’/resource-that-may-take-a-while’),
new Promise(function(resolve,reject){
setTimeout(()=>reject(new Error(‘request timeout’)),5000);
})
])
p.then(respones=>console.log(response))
p.catch(error=>console.log(error));
上面的代码中,如果5秒之内fetch方法无法返回结果,变量p的状态就会变为Rejected,从而角尺catch方法指定的回调函数。
Promise.resolve()
有时需要将现有对象转为Promise对象,Promise.resolve方法就起到这个作用。
var jsPromise=Promise.resolve($.ajax(’/whatever.json’));
上面的代码将jQuery生成的deferred对象转为新的Promise对象。
Promise.resolve等价于下面的写法 。
Promise.resolve(‘foo’)
//等价于
new Promise(resolve=>resolve(‘foo’))
如果Promise.resolve方法的参数不是具有then方法的对象(又称thenable对象),则返回 var p=Promise.resolve(‘Hello’);
p.then(function(s){
console.log(s)
});
//Hello
上面代码生成了一个新的Promise对象的实例p.由于字符串“Hello”不属于异步操作(判断方法是它不是具有then方法的对象),返回Promise 实例的状态从一生成就是Resolved,所以回调函数会立即执行。Promise.resolve方法的参数会同时传给回调函数。
Promise.resolve方法允许调用时不带参数。所以,如果希望得到一个Promise对象,方便的方法就是直接调用Promise.resolve方法。
var p=Promise.resolve();
p.then(funcetion(){
//…
});
上面 的变量p就是一个Promise对象。
如果Promise.resolve方法的参数是一个Promise实例,则会被原封不动地返回。
Promise.reject()
Promise.reject(reason)方法也会返回一个新的Promise实例,状态为Rejected。Promise.reject方法的参数reason会被传递给实例的回调函数
var p=Promise.reject(‘出错了’);
//等同于
var p=new Promise((resolve,reject)=>reject(‘foo’))
p.then(null,function(s){
console.log(s);
}):
//出错了
上面的代码生成一个Promise对象的实例p,状态为Rejected,回调函数会立即执行。
以上这些方法会省去一些常用 的代码量
两个有用的附加方法
done()
Promise对象的回调链,不管以then方法或catch方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为Promise内部的错误不会冒泡到全局)。为些,我们可以提供一个done方法,总是牌回调链的尾端,保证抛出任何可能出现的错误。
asyncFunc().then(f1).catch(r1).then(f2).done();
其实现代代码相当简单。
Promise.prototype.done=function(onFulfilled,onRejected){
this.then(onFulfilled,onRejected).catch(function(reason){
//抛出一个全局错误
setTimeout(()=>{throw reason},0);
});
};
由上可见,done方法可以像then方法那样用,提供Fuilfilled和Rejected状态的回调函数,也可以不提供任何参数。但不管怎样,done方法都会捕捉到任何可能出现的错误,并向全局抛出。
finally()
finally 方法用于指定不管Promise对象最后状态如何都会执行的操作。它与done方法的最大 区别在于,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。
下面是一个例子,服务器使用Promise处理请求,然后使用finally方法关掉服务器。
server.listen(0).then(function(){
//run test
}).finally(server.stop);
它的实现也很简单。
Promise.prototype.finally=function(callback){
let p =this.constructor;
return this.then(
value =>P.resolve(callback()).then(()=>value),
reson=>P.resolve(callback()).then(()=>{throw reason});
);
};
上面的代码中,不管前面的Promise是Fulfilled还是Rejected,都会执行回调函数callback.
异步操作的async函数、
Generator函数
function asnycJob(){
//…其它代码
var f=yiled readFile(fileA);
//…其它代码
}
上面的函数asnycJob是一个协和,它的奥妙就在于其中的yield命令。它表示执行到此处执行权将交给其他协程。也就是说,yiled命令是异步两个阶段的分界线。
协程遇到yiled命令就软,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点就是代码的写法非常像同步操作。如果去除yield命令,简直一模一样。
下面看看如何使用Generator函数执行一个真实的异步任务。
var fetch=require(‘node-fetch’);
function * gen(){
var url=‘https://api.github.com/users/github’;
var result=yield fetch(url);
console.log(result.bio);
}
上面的代码中,Generator函数封装一个异步操作,先读取一个远程接口,然后从JSON格式的数据解析信息。就像前面说的,这段代码非常像同步操作,只是加上了yield命令。
执行这段代码的方法如下。
var g=gen();
var result=g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.nect(data);
});
上面的代码中,首先执行Generator 函数获取遍历器对象,然后使用nest方法(第二行)执行异步任务的第一阶段。由于Fetch模块返回的是一个Promise对象,因此要用then方法调用下一个next方法
可以看到,虽然Generator函数异步操作表示得很简洁,但是流程管理(即何时执行第一阶段,何时执行第二阶段)却不方便 ,
Thunk函数
参数的求值策略
var x=1;
function f(m){
return m*2;
}
f(x+5)
上面的代码先定义函数f, 然后向它传入表达式x+5.请问,这个表达式应该何时求值?
一种意见是“传值调用”(call by value),即在进入函数体前就计算 x+5的值(等于6),再将这个值传入f.C语言就采用了这种策略。
f(x+5)
//传值调用时等同于
f(6)
另一种意见是“传名调用”(call by name),即直接将表达式x+5传入函数体,只在用到它时求值。Haskell 语言采用了这种策略。
f(x+5)
//传名调用时等同于
(x+5)*2;
传值调用和传名调用,哪一种比较好?回答是各有利弊。传值调用比较简单,但是对参数求值是时实际上尚未用到这个参数,有可能造成性能损失
编译器的“传名调用”实现往往是先将参数放到一个临时函数中,再将这个临时函数传入函数体。这个临时函数就叫作Thunk函数。
function f(m){
return m*2;
}
f(x+5);
//等同于
var thunk = function(){
return x+5;
};
function f(thunk){
return thunk()*2
};
上面的代码中,函数f的参数x+5被一个函数替换了。凡是用到原参数的地方,对Thunk函数求值即可。这就是Thunk函数定义,它是“传名调用”的一种实现策略,用来替换某个表达式
以 读取文件为例,下面的Generator 函数封闭了两个异步操作
var fs=require(‘fs’);
var thunkify=require(‘thunkify’);
var readFile = thunkify(fs.readFile);
var gen = function*(){
var r1=yield readFile(’/etc/fstab’);
console.log(r1.toString())
var r2=yiled readFile(’/etc/shells’);
console.log(r2.toString());
}
上面的代码中,yield命令用于将程序的执行权移出Generator函数。那么就需要一种方法将执行权责地交还给Generator函数。
var g=gen();
var r1=g.next();
r1.value(function(err,data){
if(err)throw err;
var r2=g.next(data);
r2.value(function(err,data){
if(err)throw err;
g.next(data);
});
});
上面的代码中,变量g是Generator函数的内部指针,表示目前执行到哪一步。
co模块
用它可以不用写Generator函数的执行器。
var gen=function*(){
var f1=yield readFile(’/etc/fstab’);
var f2=yield readFile(’/etc/shells’);
console.log(f1.toString());
console.log(f2.toString());
}
var co = require(‘co’);
co(gen);
上面代码中,Generator 函数只要传入co函数就会自动执行。
co 函数返回一个Promise对象,因此可以用then 方法添加回调函数。
co(gen).then(function(){
console.log(‘Generator 函数执行完成’);
})
原理:
1.回调函数。将异步操作包装成Thunk函数,在回调函数中交回执行权。
2.Promise对象。将异步操作包装成Promise对象,用then方法交回执行权
function run(gen){
var g=gen();
function next(data){
var result=g.next(data);
if(result.done) return result.value;
result .value.then(function(data){
next(data);
});
}
next();
}
run(gen);
上面的代码中,只要Generator函数还没有执行到最后 一步,next函数就调用 自身以实现自动执行。
处理并发的异步操作
co 支持并发的异步操作,即允许某些操作同时进行,等到它们全部完成才进行下一步。
这时,要把并发的操作都放在数组或对象里面,跟在yield语句后面。
//数组写法
co(function*(){
var res=yield[
Promise.resolve(1),
Promise.resolve(2),
];
console.log(res);
}).catch(onerror);
//对象的写法
co(function*(){
var res=yield{
1:Promise.resolve(1);
2:Promise.resolve(2);
};
console.log(res);
})catch(onerror);
下面是另一个例子
co(function*(){
var values=[n1,n2,n3];
yield values.map(somethingAsync);
});
function* somethingAsync(x){
//do something async
return y;
}
上面的代码允许并发3个somethingAsync异步操作,等到它们全部完成才会进行下一步。也就是异步中的同步操作
async 函数
async函数就是Generator函数的语法糖。
前文有一个Generator函数,依次读取两个文件
var fs=require(‘fs’);
var readFile= function(fileName){
return new 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 函数就是下面这样。两个关键字 一个async 另一个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函数的星号(*)替换成async,将yield 替换成await,仅此而已。
async 函数对Generator函数的改进体现在以下4点。
1.内置执行器。Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async 函数的执行与普通函数一模一样,只要一行。
var result=asyncReadFile();
2.上面的代码调用 了asyncReadFile函数,然后它就会自动执行,输出最后结果。完全不像Generator函数,需要调用next方法,或者用co模块,才能得到真正执行,从而得到最终结果。
3.更好的语义。async和await 比起星号和yield,语义更清楚。async表示函数里的异步操作,await 表示紧跟在后面的表达需要等待结果。
4更广的适用性。co模块约定,yield命令后面只能是Thunk函数或Promise对象,而async函数的await命令后面可以是Promise对象和原始类型的值(数值、字符串和布尔值,但这等同于同步操作)。
5.返回值是Promise.async函数的返回值 是Promise对象,这比Generator函数的返回值 是Iterator对象方便多了。你可以用then方法指定下一步操作。
进一步说,async函数完全可以看作由多个异步操作包装成的一个Promise对象,而await命令就是内部 then 命令的语法糖。
async 函数的实现
async函数的实现就是将Generator函数和自动执行器包装在一个函数中。
async function fn(args){
//…
}
//等同于
function fn(args){
return spawn(function*(){
//…
});
}
所有的async函数都可以写成上面的第二种形式。其中的spawn 函数就是自动执行器。
下面给出spawn函数的实现,基本就是前文自动执行器的翻版。
function spawn(genF){
return new Promise(function(resolve,reject){
var gen=genF();
function step(nextF){
try{
var next=nextF();
}catch(e){
return reject(e);
}
if(next.done){
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v){
step(function(){ return gen.next(v);});
},function(e){
step(function(){return ten.throw(e);});
});
}
step(function(){return gen.next(undefined);});
});
}
注意点
await 命令后面的Promise对象,运行结果可能是Rejected,所以最好把await命令放在try…catch代码块中。
async function myFunction(){
try{
await somethingThatReturnsAPromise();
}catch(err){
console.log(err);
}
}
//另一种写法
async function myFunction(){
await somethingTahtReturnsAPromise().catch(function(err){
console.log(err);
});
}
await 命令只用用在async函数中,用在普通函数中会报错。
async function dbFuc(db)}{
let docs=[{},{},{}];
//报错
docs.forEach(function(doc){
await db.post(doc);
});
}
上面的代码会报错,因为await用在了普通函数中,但是,即便将forEach方法的参数改成async函数也有问题。
async function dbFuc(db){
let docs=[{},{},{}];
//可能得到错误结果
docs.forEach(async function(doc){
await db.post(doc);
});
}
上面的代码可能不会正常工作,原因是这时3 个db.post 操作将并发执行,也就是同时执行,而不是继发执行。正确的写法是采用for循环。
async function dbFuc(db){
let docs=[{},{},{}];
for(let doc of docs){
await db.post(doc);
}
}
如果确实希望多个请求前功并发执行,可以使用Promise.all 方法。
async function dbFuc(db){
let docs=[{},{},{}];
let promises=docs.map((doc)=>db.post(doc));
let results=await Promise.all(promises);
console.log(results);
}
//或者使用下面的写法
aync function dbFuc(db){
let docs=[{},{},{}];
let promises=docs.map((doc)=>db.post(doc));
let results=[];
for(let promise of promises){
results.push(await promise);
}
console.log(results);
}
因为是封装好的,会比Promise、Generator使用起来更方便,同时灵活性会相对较低
Class
定义变量可以不指定类型,默认为any,最好指定,方便代码编写效率
实例对象用new
name 属性返回 紧跟在class关键字后面的类名
class Point(){}
Point.name //“Point” 老实说,我看不出这个有什么用。都已经知道类名了还要这个干嘛,可能只是为了方便点,和防止手误,拼写错误
Class表达式
const MyClass = class Me{
getClassName(){
return Me.name;
}
};
上面的代码使用表达式定义了一个类。需要注意的是,这个类的名字是MyClass 而不是Me,Me只在Class内部代码可用,指代表当前类,就是this
不存在变量提升
Class 不存在变量提升,名字取得好,就是不能在未定义前使用。
new Foo(); //ReferenceError
class Foo{}
上面代码中,Foo使用在前,定义在后,这样会报错
{
let Foo=class();
class Bar extends Foo{
}
}
如果存在Class的提升 ,上面的代码将报错 let 命令也是不提升的。
Class继承
用 extends
类的属性和_proto_属性
大多数浏览器的ES5实现中,每一个对象都有_proto_属性,指向对应的构造函数的prototype 属性。Class作为 构造 函数的语法糖,同时有prototype属性和_proto_属性,因此同时存在两条承链。
1.子类的_proto_ 属性表示构造函数的继承,总是指向父类。
2. 子类prototpe属性的_proto_属性表示方法的继承,总是指向父类的prototype属性。
class A{
}
class B extends A{
}
B.proto ===A //true
B.prototype.proto===A.prototype //true;
上面的代码中子类B的__proto_属性指向父类A,子类B的prototype属性的_proto_属性指向父类 A的prototype属性
这样的结果是因为类的继承是按照下面的模式实现的
class A{
}
class B{
}
//B的实例继承A的实例
Object.setPrototypeOf(B.prototype,A.prototype);
//B继承A的静态属性
Object.setPrototyeOf(B,A);
Object.setPrototypeOf=function (obj,proto){
Obj.proto=proto;
return obj;
}
因此,就得到了上面的结果。
Extends的继承目标
extends关键字后面可以跟多种类型的值
class B extends A{
}
上面的A 只要是一个有prototype 属性的函数,就能被B继承。由于 函数都有prototype属性,因此A可以是任意函数
下面讨论3种特殊情况
第1种特殊情况,子类继承Object类。
class A extends Object{}
A.proto ====Object //true
A.prototype.proto === Object.prototype //true
这种情况下,A其实就是构造函数Object的复制,A的实例 就是Object的实例
第2种特殊情况,不存在任何继承。
class A{
}
A.proto === Function.prototype //true
A.prototype.proto === Object.prototype //true
这种情况下,A作为一个基类(即不存在任何继承) 就是一个普通函数,所以直接继承Function.prototype.但是,A调用后返回一个空对象(即Object实例),所以A.prototype.__proto__指向构造函数(Object)的prototype属性
第3种情况,子类继承null
class A extends null{
}
A.proto === function.prototype //true
A.prototype.proto=== undefined //true
这种情况下,与第二种情况非常像。Ab也是一个普通函数,所以直接继承Function.prototype. 但是,A调用 后返回的对象不继承任何方法,所以 它的__proto__指向Function.prototype.即实质上执行了下面的代码
class C extends null{
constructor(){return Object.create(null);}
}
Object.getPrototypeOf()
Object.getPrototypeOf方法可用于从子类上获取父类。
object.getPrototypeOf(ColorPoint)===Point
//true
因此,可以使用这个方法判断 一个类是否继承了另一个类。
super关键字
上面进过,在子类中super 关键父类实例。
class B extends A{
get m(){
return this._p * super._p;
}
set m() {
throw new Error(‘这属性只读’)
}
}
上面的代码中,子类 通过 super 关键字调用 了父类的实例。
由于 对象总是继承其他对象的,所以 可以在任意一个对象 中使用super关键字。
var obj ={
toString(){
return “MyObject”+super.toString();
}
}
obj.toString(); //MyObject :[object Object]
实例的 __proto__属性
子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性,也就是说,子类的原型的原型是父类的原型
原生构造函数的继承
class MyArray extends Array{
constructor(…args){
super(…args);
}
}
var arr = new MyArray();
arr[0] =12;
arr.length //1
arr.length =0;
arr[0] //undefined
上面代码定义了一个MyArrary类, 继承了Arrary构造函数,就可以从MyArray生成数组的实例,这意味着,ES6可以自定义原生数据结构(比如Array、String等)的子类
Class 的取值 函数(getter) 和存值 函数(setter)
class MyClass{
constructor(){
//…
}
get prop(){
return ‘getter’;
}
set prop(){
console.log(‘setter:’+value)
}
}
let inst = new MyClass();
inst.prop=123
//setter:123
inst.prop
//‘getter’
上面的代码中,prop 属性有对应的存值 函数和取值函数,因此赋值和读取行为 都被自定义了。
存值 函数和取值函数是设置在属性的descriptor对象上的。
Class的Generator方法
如果在某个方法前加上星号(*),就表示 该 方法是一个Generator函数。
class Foo{
constructor(…args){
this.args=args;
}
*Symbol.iterator{
for(let arg of this.args){
yield arg;
}
}
}
for(let x of new Foo(‘hello’,‘world’)){
console.log(x);
}
//hello
//world
上面的代码中,Foo类的Symbol.iterator方法前有一个星号,表示该方法是一个Generator函数。Symbol.iterator方法返回一个Foo类的默认遍历器,for… of 循环会自动调 用这个遍历器。
Class 的静态
Class的静态属性
用static
new.target属性
new 是从构造函数生成 实例的命令。ES6为new命令引入了new.target 属性,(在构造函数中)返回 new 命令所作用的构造函数。如果构造函数不是通过 new 命令调用 的,那么new.target会返回undefine,因此这个属性可用于确定 构造函数是怎么调用的
这个可以用来限制父类是否能被实例化
Mixin 模式的实现
Mixin模式指的是将多个类的接口“混入”(mix in) 另一个类。它在ES6的实现如下。
function mix(…mixins){
class Mix()
for(let mixin of mixins){
copyProperties(Mix,mixin);
copyProperties(Mix.prototype,mixin.prototype);
}
return Mix;
}
function copyPrototies(target,source){
for(let key of Reflect.ownKeys(source)){
if(key !== “constructor”
&& key !’'prototype"
&& key ! “name”)
{
let desc=Object.getOwnPropertyDescriptor(source,key);
Object.defineProperty(target.key,desc);
}
}
}
上面的mix函数可将多个对象合成为一个类。使用时只需继承这个类即可。
class DistrbutedEdit extends mix(Loggable,Serializable){
//…
}
修饰器
类的修饰
修饰器是一个表达式, 用于修改类的行为。
修饰器的行为的改变,是在代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。
function testable(target){
target.isTestable=true;
}
//事先把对应要做的事都做了,后面只需要加上这个特性
@ testable
class MyTestableClass{};
console.log(MyTexstableClass.isTestable) //true
上面的代码中@testable就是一个修饰器。它修改了MyTestableClass这个类的行为 。为它加上了静态属性isTestable.
基本上修饰器的行为 如下
@decorator
class A{}
//等同于
class A {}
A = decorator(A) || A;
就就是说,修饰器本质上就是能在编译时执行的函数。
修饰器函数可以接受3个参数,依次是目标函数、属性名和该 属性的描述对象。后两个参数可省略。上面的代码中,testable函数的参数target就是所要修饰的对象。如果希望修饰器的行为 能够根据目标对象的不同而不同,就要在外面再封装一层函数
function testable(isTestable){
return function(target){
target.isTestable=isTestable;
}
}
@testable(true)
class MyTestableClass()
MyTestableClass.isTestable //true
@testable(false)
class MyTestableClass()
MyTestableClass.isTestable //false
方法的修饰
修饰器还可以修饰类的属性
class Person{
@readonly
name(){return ‘${this.first} ${this.last}’}
}
上面的代码中,修饰器readonly用来修饰“类”的name 方法
此时,修饰器函数一共可以接受3个参数,第1个参数是所要修饰 的目标对象,第2个参数是所要修饰的属性名,第3 个参数是该 属性的描述对象。
readonly(Person.prototype,‘name’,descriptor);
//可以修改属性的描述对象
function readonly(target,name, descirptor){
//descirptor对象原来的值 如下
//{
// value:specifiedFunction,
// enumerable:false,
// configurable:true,
// writable:true,
//}
descriptor.wirtable=false;
return descriptor;
}
Object.defineProperty(Person.prototype,‘name’,descriptor);
上面的代码说明,修饰器(readonly)会修饰属性的描述对象(descriptor),然后被修改的描述对象再用来定义属性。
为什么修饰器不能用于函数
函数提升
var counter=0;
var add = function(){
counter++;
};
@add
function foo(){
}
上面的代码本意是执行后counter等于1,但实际上结果是counter等于0,因为函数提升。使得实际执行代码如下
var counter;
var add;
@add
function foo(){
}
counter=0;
add= function(){
counter++;
};
core-decorators.js
是一个第三方模块,提供了几个常见的修饰器。通过它可以更好地理解修饰器
@autobind //使方法中的this对象绑定原始对象。
import{autobind} from ‘core -descrators’
class Person{
@autobind
getPerson(){
return this;
}
}
let person =new Person();
let getPerson=person.getPerson;
getPerson===person;
//true
@readonly
@override 修饰器检查子类的方法是否正确覆盖了父类的同名方法,如果不正确会报错
import {override} from ‘core-descorator’;
class Parent{
speak(first,seconds){}
}
class Child extends Parent{
@override
speak(){ }
//SyntaxError :Child# speak() does no properly override Parent #speak(first,second
)
}
在修饰器的基础上可以实现 Mixin模式。
Trait
也是一种修饰器,效果与Minxin类似,但提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等
Module
export 命令
模块功能主要由两个命令构成:export 和import。export 用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
模块的整体加载
除了指定加载某个输出值 ,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值 都加载在这个对象上。
import * as circle from ‘./circle’;
module 命令
可以取代import语句,达到整体输入模块的作用
module circle from ‘./circle’
export defaault 命令
从前面的例子可以看出,使用import 命令时用户需要知道所要加载的变量名或函数名,否则 无法加载。但是,用户肯定希望快速上手,未必愿意总计文档去了解模块有哪些属性和方法,
为了方便用户,使其不用阅读文档就能加载模块,就要用到export default 命令,为模块指定默认输出
export default function(){
console.log(‘foo’)
}
import customName from ‘./export-default’;
customName; //‘foo’
上面的import命令可以用任意名称指向export-default.js 输出的方法,这时就不知道原模块输出的函数名。需要 注意的是,这时import命令后面不使用大括号
export default 42; //输出默认值
export default class {…} //输出类
import MyClass from ‘MyClass’
let o= new MyClass();
模块的继承
模块之间也可以继承
假设有一个circleplus模块继承了circle模块
export *from ‘circle’;
export var e = 2.6.1828182846;
export default function(x){
return Math.exp(x);
}
上面的export 表示输出circle模块的所有属性和方法。注意,export *命令会忽略circle 模块的default 方法。之后 又输出了自定义的e变量和默认方法。
export {area as circleArrea} from ‘circle’;
上面的代码表示,只输出circle模块的area方法,且将其改名为cricleArea.
加载上面的模块的写法如下 。
module math from ‘circleplus’;
import exp from “circleplus”;
console.log(math.E);
上面的imoprt exp 表示,将circleplus模块 的默认方法加载为exp方法
ES6模块加载的实质
ES6模块输出的是值的引用 。动态引用,不会有缓存值,原始值 变,输入值也会跟着变
CommonJS
//lib.js
var counter = 3;
function incCounter(){
counter ++;
}
module.exports={
counter:counter,
incCounter:incCounter,
}
//main.js
var counter = require(’./lib’).counter;
var incCounter=require(’./lib’).incCounter;
console.log(counter) //3
incCounter();
console.log(counter); // 3
ES6
//lib.js
export var counter = 3;
export function incCounter(){
counter ++;
}
module.exports={
counter:counter,
incCounter:incCounter,
}
//main1.js
import {counter,incCounter} from ‘./lib’
console.log(counter) //3
incCounter();
console.log(counter); // 4
循环加载
指的是a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。
//a. js
var b= require(‘b’);
//b.js
var a=require(‘a’);
通常,“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免
ES6 处理“循环加载”是动态引用,遇到模块加载命令import时不会去执行模块,只是生成一个指向被加载模块的引用,需要开发者自己保证真正取值 时能够取到值。只要引用是存在的,就能执行。