ES6 ES7

Class语法

1
2
3
4
5
6
7
8
9
// ES5 构造函数生成对象
function Point(x,y)J{
this.x=x;
this.y=y;
}
Point.prototype.toString = function(){
return '('+ this.x+','+this.y+')';
};
var p=new Point(1,2);

ES6则通过Class语法,constructor为构造方法,this关键字为实例对象.ES5的构造函数对应ES6的Point类的构造方法. ES6的类可以看成是构造函数的另一种写法.类的所有方法都定义在类的prototype属性上面.类的内部定义的方法,都是不可枚举的.这一点与ES5的行为不一致.

1
2
3
4
5
6
7
8
9
10
class Point{
constructor(x,y){
this.x=x;
this.y=y;
}
toString(){
return '('+this.x+','+this.y+')';
}
}
var p=new Point(1,2);

类的属性名,可以采用表达式.Square类的方法名,是从表达式得到的.

1
2
3
4
5
6
7
8
9
let methodName = "getArea";
class Square{
constructor(length){
//...
}
[methodName](){
//...
}
}

constructor方法是类的默认方法,通过new命令生成对象的实例时,自动调用该方法.一个类必须有constructor方法,如果没有显示定义,一个空的constructor方法会被默认添加.constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象.

1
2
3
4
5
6
7
// constructor返回一个全新的对象,结果导致实例对象不是Foo实例.
class Foo{
constructor(){
return Object.create(null);
}
}
new Foo() instanceof Foo; //false;

类的构造函数,不使用new是没办法调用的,会报错,这是它跟普通函数的一个主要区别,后者不用new也可以执行.与ES5一样,实例的属性除非定义在其本身(既定义在this对象上),否则都是定义在原型上(即定义在class上).与ES5一样,类的所有实例共享一个原型对象.

1
2
3
4
5
6
7
8
var p1=new Point(2,3);
var p2=new Point(3,2);
p1.__proto__ === p2.__proto__; //true;
p1.__proto__.printName=function(){
return "Oops";
}
var p3=new Point(4,2);
p3.printName();//Oops;

Class不存在变量提升(hoist),这一点与ES5完全不同.

1
2
3
4
5
6
7
// class 不会提升,才可以继承
{
let Foo=class();
class Bar extends Foo{
//...
}
}

class表达式,与函数一样,类也可以使用表达式的形式定义.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 表达式定义了一个类,这个类的名字是MyClass而不是Me,Me只在Class的内部代码可用,指代当前类.
const MyClass=class Me{
getClassName(){
return Me.name;
}
}
let inst=new MyClass();
inst.getClassName();//Me
Me.name; // ReferenceError:Me is not defined
// Me只在Class内部有定义,如果类的内部没有定义的话,可以省略Me,也就是可以写成下面的形式.
const MyClass=class{
//...
}

采用Class表达式,可以写出立即执行的Class.

1
2
3
4
5
6
7
8
9
10
let person=new class{
constructor(name){
this.name=name;
}
sayName(){
console.log(this.name);
}
}('张三');
person.sayName();//'张三';
// person是一个立即执行的类的实例.

利用Symbol值,实现私有方法和私有属性的效果.

1
2
3
4
5
6
7
8
9
10
11
const bar=Symbol('bar');
const snaf=Symbol('snaf');
export default class myClass{
foo(baz){ // 公有方法
this[bar](baz);
}
// 私有方法
[bar](baz){
return this[snaf]=baz;
}
}

this的指向:

类的内部如果有this,它默认指向类的实例.但是必须非常小心,一旦单独使用该方法,很可能报错.
1
2
3
4
5
6
7
8
9
10
11
12
// 将printName方法中的this单独提出来使用,this会指向该方法运行时所在的环境,因为找不到print方法而导致错误.
class Logger{
printName(name='there'){
this.print(`Hello ${name}`);
}
print(text){
console.log(text);
}
}
const logger = new Logger();
const { printName }=logger;
printName();// TypeError: Cannot read property 'print' of undefined

类和模块内部,默认就是严格模式,所以不需要使用’use strict’指定运行模式.只要你的代码写在类或模块中,就只有严格模式可用.考虑到未来所有的代码,其实都是运行在模块中的,所以ES6实际上把整个语言升级到了严格模式.本质上,ES6的类只是ES5的函数的一层包装,所以函数的许多特性都被Class继承,包括name属性.

1
2
3
4
class Point(){
}
Point.name; // Point
// name属性总是返回紧跟在Class关键字后面的类名.

Class的继承: 类通过extends关键字实现继承,比ES5的通过修改原型链实现继承要清晰和方便很多.

1
2
3
4
5
6
7
8
9
10
11
12
13
// ColorPoint 继承了Point类的所有方法和属性.在ColorPoint内部加上代码
class ColorPoint extends Point{
constructor(x,y,color){
//调用父类的constructor(x,y);
super(x,y);
this.color=color;
}
toString(){
return this.color+""+super.toString(); //调用父类的toString();
}
};
// super关键字,它在这里表示父类的构造函数,用来新建父类的this对象.
// 子类必须在constructor方法中调用super方法,否则新建实例会报错.这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其加工.如果不调用super方法,子类就得不到this对象.

ES6的继承机制与实质是先创造父类的实例对象this,所以必须先调用super方法,然后再用子类的构造函数修改this.(与ES5不一样),如果子类没有定义constructor方法,这个方法会被默认添加.也就是说不管有没有显示定义,任何一个子类都有constructor方法.

1
2
3
4
constructor(...args){
super(...args);
}
//在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错.这是因为子类实例的构建,是基于对父类实例的加工,只有super方法才能返回父类实例.

子类的proto属性,表示构造函数的继承,总是指向父类.
子类的prototype属性的proto属性,表示方法的继承,总是指向父类的prototype属性.
Object.getPrototypeOf()方法可以用来从子类上获取父类.
Object.getPrototypeOf(ColorPoint) === Point; // true
super关键字: 可以当做函数使用,也可以当做对象使用.在这俩种情况下,他的用法完全不同.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 第一种情况
// 子类B的构造函数之中的super(),代表调用父类的构造函数.这是必须的,否则JavaScript引擎会报错.
class A {}
clsss B extends A {
constructor(){
super(); // super内部的this指向B,只能用在子类的构造函数中.
}
}
// 第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类.
class A {
constructor(){
this.x=1;
}
print(){
console.log(this.x);
}
}
class B extends A {
constructor(){
super();
this.x=2;
}
m(){
super.print(); // super.print.call(this);
}
}
let b=new B();
b.m() //2

如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Parent {
static myMethod(msg){
console.log('static',msg);
}
myMethod(msg){
console.log('instace',msg);
}
}
class Child extends Parent{
static myMethod(msg){
super.myMethod(msg);
}
}
Child.myMethod(1); // static 1
var child = new Child();
child.myMethod(2); // instance 2

使用super的时候,必须显示指定是作为函数,还是作为对象使用,否则会报错.

1
2
3
4
5
6
7
class A {}
class B extends A {
constructor(){
super();
console.log(super.valueOf() instanceof B); //true
}
}

最后,由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字.

1
2
3
4
5
6
var obj={
toString(){
return "MyObject:"+super.toString();
}
}
obj.toString(); // MyObject: [object Object]

子类实例的proto属性的proto属性,指向父类实例的proto属性,子类实例的原型的原型,是父类实例的原型.

1
2
3
4
5
6
7
8
9
var p1 = new Point(2,3);
var p2 = new ColorPoint(2,3,'red');
p2.__proto__ === p1.__proto__; // false;
p2.__proto__.__proto__ === p1.__proto__; //true
// 因此,通过子类实例的__proto__.proto__属性,可以修改父类实例的行为.
p2.__proto__.proto__.printName =function(){
console.log('Ha');
}
p1.printName(); // "Ha"

原生构造函数的继承:ES5无法继承原生构造函数.ES6则可以.
Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()

1
2
3
4
5
6
7
8
9
10
11
class MyArray extends Array{
constructor(...args){
super(...args);
}
}
var arr=new MyArray();
arr[0]=12;
arr.length; //1
arr.length=0;
arr[0]; // undefined

这也意味着ES6可以自定义原生数据结构(比如Array,String)的子类,这是ES5无法做到的.extends关键字不仅可以用来继承类,还可以用来继承原生构造函数.因此可以在原生数据结构的基础上,定义自己的数据结构.下面就是定义了一个带版本功能的数组.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class VersionedArray extends Array{
constructor(){
super();
this.history=[[]];
}
commit(){
this.history.push(this.slice());
}
revert(){
this.splice(0,this.length,...this.history[this.history.length-1])
}
}
var x=new VersionedArray();
x.push(1);
x.push(2);
x //[1,2];
x.history; //[[]]
x.commit();
x.history; //[[],[1,2]]
x.push(3);
x //[1,2,3];
x.revert();
x //[1,2]

下面一个自定义Error子类的例子.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ExtendableErroe extends Error {
constructor(message){
super();
this.message=message;
this.stack=(new Error()).stack;
this.name=this.constructor.name;
}
}
class MyError extends ExtendableError{
constructor(m){
super(m);
}
}
var myerror=new MyError('11'):
myerror.message //11
myerror.instanceof Error // true
myerror.name // "MyError"
myerror.stack

注意 继承Object的子类,有一个行为差异.

1
2
3
4
5
6
7
8
class NewObj extends Object{
constructor(){
super(...args);
}
}
var o=new NewObj({attr:true});
console.log(o.attr===true); // false
//ES6改变了Object构造函数的行为,一旦发现Object方法是new Object()这种形式调用的,ES6规定Object构造函数会忽略参数.

Class的取值函数(getter)和存值函数(setter)
与ES5一样,在
Class内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的行为.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

存值函数和取值函数是设置在属性的descriptor对象上的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CustomHTMLElement{
constructor(element){
this.element=element;
}
get html(){
return this.element.innerHTML;
}
set html(){
this.element.innerHTML=value;
}
}
var descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype,"html");
"get" in descriptor // true
"set" in descriptor // true
// 存值函数和取值函数是定义在html属性的描述对象上面,这与ES5完全一致.

Class的Generator方法:在某个方法前面加(*)号就表示该方法是一个Generator函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Foo{
constructor(...args){
this.args=args;
}
* [Symbol.iterator](){
for(let arg of this.args){
yiled arg;
}
}
}
for(let x of new Foo('hello','world')){
console.log(x);
}
//hello
//world
// Foo 类的Symbol.iterator 方法前有一个星号,表示该方法是一个Generator函数Symbol.iterator方法返回一个Foo类默认遍历器,for...of循环会自动调用这个遍历器.

Class的静态方法.加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调研,这就称为”静态方法”.

1
2
3
4
5
6
7
8
class Foo{
static classMethod(){
return 'hello';
}
}
Foo.classMethod() //'hello'
var foo = new Foo();
foo.classMethod() // TypeError: foo.classMethod is not a function

父类的静态方法可以被子类继承

1
2
3
4
5
6
7
8
9
class Foo{
static classMethod{
return "hello";
}
}
class Bar extends Foo{
}
Bar.classMethod(); // "hello"

静态方法也可以从super对象上调用.

1
2
3
4
5
6
7
8
9
10
11
class Foo{
static classMethod(){
return "hello";
}
}
class Bar extends Foo{
static classMethod(){
return super.classMethod() +",,too";
}
}
Bar.classMethod(); // "hello,,too"

class的静态属性和实例属性

1
2
3
4
5
class Foo{
}
Foo.prop=1;
Foo.prop //1

目前,只有这一种方法可行,因为ES6明确规定,Class内部只有静态方法,没有静态属性.
ES7有一个静态属性的提案,目前babel转码器支持.这个提案对实例属性和静态属性都规定了新的写法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyClass{
myProp=42; //类的实例属性可以用等式,写入类的定义中.
constructor(){
console.log(this.myProp); // 42
}
}
// 对于那些在 constructor 里面已经定义的实例属性,新写法允许直接列出.
class ReactCounter extends React.Component{
constructor(props){
super(props);
this.state={
count:0
}
}
state;
}

类的静态属性只要在上面的实例属性的写法前面加上static关键字就可以了.

1
2
3
4
5
6
class MyClass{
static myStaticProp=42;
constructor(){
console.log(MyClass.myStaticProp);//42
}
}

Minxin模式指的是,将多个类的接口”混入”(mixin)另一个类.它在ES6的实现如下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function mix(...mixins){
class Mix{}
for(let mixin of mixins){
copyProperties(Mix,mixin);
copyProperties(Mix.prototype,mixin,prototype);
}
return Mix;
}
function copyProperties(target,source){
for(let key of Reflect.ownKeys(source)){
if(key!=="constructor" && key !== "prototype" && key !== "name"){
let desc = Object.getOwnPropertyDescriptor(source,key);
}
}
}
class DistributedEdit extends mix(Loggable,Serializable){
//...
}

Iterator (遍历器)的概念:
ES6之后又四种数据结构 Array Object Map Set, 需要一种统一的接口机制,来处理所有不同的数据结构.遍历器就是这样一种机制.它是一种接口,为各种不同的数据结构提供访问机制,只要部署iterator接口,就可以完成遍历操作.
iterator的作用有三个:1.为各种数据结构提供统一简便的访问接口;2.使得数据结构的成员能够按照某种次序排列;3.ES6创造了一种新的遍历命令for…of循环,Iterator接口主要供for…of循环.

1
2
3
4
5
6
7
8
9
10
11
12
var it=makeIterator(['a','b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array){
var nextIndex=0;
return {
next: function(){
return nextIndex < array.length ? { value: array[nextIndex++], done: false } : { value: undefined, done: true };
}
}
}

凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口.调用这个接口,就会返回一个遍历器对象.当使用for..of循环遍历某种数据结构时,该循环会自动寻找iterator接口.
ES6规定,默认的iterator接口部署在数据结构的Symbol.iterator属性.只要有这个属性,就可以认为是”可遍历的”,Symbol.iterator属性本身是一个函数,执行这个函数,会返回一个遍历器.至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性.这是一个预定义好的类型为Symbol的特殊值,所以要放在方括号内.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 对象默认不可遍历,因为此对象有Symbol.iterator属性,所以是可遍历的
const obj={
[Symbol.iterator] : function () {
return {
next : function () {
return {
value: 1,
done: true
};
}
};
}
};

ES6中有三类数据结构原生具备iterator接口: 数组\某些类似数组的对象\Set和Map结构.

1
2
3
4
5
6
7
let arr = ['a','b','c'];
let iter = arr[Symbol.iterator]();
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: undefined, done: true }

yield* 后面跟的是一个可遍历的结构,他会调用该结构的遍历器接口.

1
2
3
4
5
6
7
8
9
10
let generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};
var iterator = generator();
iterator.next() //{ value: 1, done: false }
iterator.next() //{ value: 2, done: false }
iterator.next() //{ value: 3, done: false }
...

由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口.下面是一下例子.

1
2
3
4
5
for...of
Array.from()
Map(),Set(),WeakMap(),WeakSet() (比如 new Map([['a','1'],['b','2']]))
Promise.all()
Promise.race()

可以覆盖原生的Symbol.iterator方法,达到修改遍历器行为的目的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var str = new String("hi");
[...str] //["h","i"];
str[Symbol.iterator] = function () {
return {
next: function () {
if (this._first){
this._first = false;
return { value: "bye", done: false }
}else{
return { done: true };
}
}
_first: true
}
}
[...str] //["bye"]
str // "hi"

Generator函数是ES6提供的一种异步编程解决的方案,语法行为与传统函数完全不同.从语法上理解,Generator函数是一个状态机,封装了多个内部状态.执行Generator函数会返回一个遍历器对象.Generator函数除了是状态机,还是一个遍历器生成函数.返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态.形式上,Generator函数是一个普通函数,但是有俩个不同的特征.一是,function关键字与函数名之间有一个星号,二是函数内部使用yield语句,定义不同的内部状态.

1
2
3
4
5
6
7
// 该函数有三个状态: hello world return(结束执行)
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();

Proxy 用于修改某些操作的默认行为,等同于在语言层面上做出修改,属于一种’元编程’(meta programming),即对编程语言进行编程.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 对一个空对象架设一层拦截,重新定义了属性的读取和设置行为.Proxy实际上重载了点运算符,即用自己的定义覆盖了语言的原始定义.
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

ES6提供原生的Proxy构造函数,用来生成Proxy实例.
var proxy = new Proxy(target,handler);
Proxy对象的所有用法,都是上面这种形式,不同的只是handler参数的写法.其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 要使得Proxy起作用,必须对Proxy实例进行操作,而不是对目标对象进行操作.
var proxy = new Proxy({},{
get: function(target,property){
return 35;
}
});
proxy.time; //35
proxy.name; //35
// 如果handler没有设置任何拦截,那就等同于直接通向原对象
var target = {};
var handler = {};
var proxy = new Proxy(target,handler);
proxy.a = 'b';
target.a; // "b"

Proxy实例也可以作为其他对象的原型对象.

1
2
3
4
5
6
7
var proxy = new Proxy({},{
get: function(){
return 35;
}
})
let obj = Object.creat(proxy);
obj.time; // 35

同一个拦截器,可以设置多个操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var handler = {
get : function(target,name){
if(name==='prototype'){
return Object.prototype;
}
return 'Hello, '+name;
},
apply: function(target,thisBinding,args){
return args[0];
}
constructor: function(target,args){
return {value: args[1]};
}
};
var fproxy = new Proxy(function(x,y){
return x + y;
},handler);
fproxy(1,2) //1
new fproxy(1,2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo // "Hello, foo"

下面是Proxy支持的拦截操作一览.
对于可以设置但是没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果.

1
2
3
4
5
6
get(target,propKey,receiver) // 拦截对象属性的读取. 比如 proxy.foo 和 proxy['foo'].
set(target,propKey,value,receiver) // 拦截对象属性的设置,比如 proxy.foo = v 或者 proxy['foo'] = v,返回一个布尔值.
has(target,propKey) // 拦截 propKey in proxy 操作,返回一个布尔值
deleteProperty(target,propKey) // 拦截delete proxy[propKey]的操作,返回一个布尔值
ownKeys(target) // 拦截Object.getOwnPropertyNames(proxy) Object.getOwnPropertySymbol(proxy) Object.keys(proxy), 返回一个数组,返回目标对象所有自身的属性和属性名.
...

Reflect 对象与Proxy对象一样.也是ES6为了操作对象而提供的新API.Reflect对象的设计目的:

1 将Object对象的一些明显属于语言内部的方法(Object.defineProperty),放在Reflect对象上.现阶段,某些方法同时在Object和Reflect对象上部署,未来新的方法将只部署在Reflect对象上,也就是说,从Reflect对象上可以拿到语言内部的方法.
2 修改某些Object方法返回结果,让其变得合理.比如,Object.defineProperty(obj,name,descJ)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj,name,desc)则会返回false.
1
2
3
4
5
6
7
8
9
10
11
12
13
// 老写法
try{
Object.defineProperty(target,property,attributes);
// success
} catch(e){
// failure
}
// 新写法
if (Reflect.defineProperty(target,property,attributes)){
// success
}else{
// failure
}
3 让Object操作都变成函数行为.某些Object操作是命令式,比如 name in obj 和 delete obj[name], 而Reflect.has(obj,name) 和  Reflect.deleteProperety(obj,name)让他们变成函行为
1
2
3
4
// 老写法
'assign' in Object // true
// 新写法
Reflect.has(Object,'assign') // true

使用Proxy实现观察者模式
观察者模式(Observe mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行.

1
2
3
4
5
6
7
8
9
10
11
12
// 数据对象person是观察者目标,函print是观察者.一旦数据对象发生变化,print就会自动执行.
const person = observable({
name: '张三',
age: 20
})
function print(){
console.log(`${person.name),${person.age}`);
}
observe(print);
person.name = '李四';
// 输出
// 李四, 20

下面,使用Proxy写一个观察者模式的最简单实现,既实现observable和observe这俩个函数.思路是observable函数返回一个原始对象的Proxy代理,拦截赋值操作,触发充当观察者的各个函数.

1
2
3
4
5
6
7
8
const queuedObservers = new Set();
const observe = fn => queueObservers.add(fn);
const observable = obj => new Proxy(obj,{set});
function set(target,key,value,receiver){
const result = Reflect.set(target,key,value,receiver);
queueObservers.forEach(observer => observer());
return result;
}

Recommended Posts