JavaScript语言中,生成实例对象的传统方法是经过构造函数。下面是一个例子。编程
function Point(x,y){ this.x = x; this.y = y; } Point.prototype.toString = function(){ return '(' + this.x + ', ' + this.y + ')'; }; var p = new Point(1,2);
基本上,ES6的class能够看做只是一个语法糖,它的绝大部分功能,ES5均可以作到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用ES6的class改写。浏览器
//定义类 class Point{ constructor(x,y){ this.x = x; this.y = y; } toString(){ return '(' + this.x + ', ' + this.y + ')'; } }
上面代码定义了一个类,能够看到里面有一个constructor方法,这就是构造方法,而this关键字则表明实例对象。也就是说ES5的构造函数point,对应ES6的point类的构造方法。
Point类除了构造方法,还定义了一个toString方法。注意,定义类的方法的时候,前面不须要加上function这个关键字,直接把函数定义放进去就能够了。另外,方法之间不须要逗号分割,加了会报错。
ES6的类,彻底能够看做构造函数的另外一种写法。函数
class Point{ //... } typeof Point//function Point === Point.prototype.constructor//true;
上面代码代表,类的数据类型就是函数,类自己就指向构造函数。
使用的时候,也就是直接对类使用new命令,跟构造函数的用法彻底一致。this
class Bar{ doStuff(){ console.log('stuff'); } } var b = new Bar(); b.doStuff();
构造函数的prototype属性,在ES6的类上面继续存在。事实上,类的全部方法都定义在类的prototype属性上面。prototype
class Point{ constructor(){} toString(){} toValue(){} } //等同于 Point.prototype = { constructor(){}, toString(){}, toValue(){}, }
在类的实例上面调用方法,其实就是调用原型上的方法。code
class B{} let b = new B{}; b.constructor === B.prototype.constructor;
上面代码中,b是B类的实例,它的constructor方法就是B类原型的constructor方法。
因为类的方法都定义在prototype对象上面,因此类的新方法能够添加在prototype对象上面。Object.assign方法能够很方便地一次向类添加多个方法。
class Point{对象
constructor(){}
}
Object.assign(Point.prototype,{继承
toString(){}, toValue(){}
})
prototype对象的constructor属性,直接指向类的自己,这与ES5的行为是一致的。three
Point.prototype.constructor === Point//true.
另外,类的内部全部定义的方法,都是不可枚举的。ip
class Point{ constructor(x,y){} toString(){} } Object.keys(Point.prototype); Object.getOwnPropertyName(Point.prototype);
上面代码中,toString方法是Point类内部定义的方法,它是不可枚举的。这一点与ES5的行为不一致。
var Point = function(x,y){} Point.prototype.toString=function(){} Object.keys(Point.prototype); //["toString"] Obejct.getOwnPtopertyName(Point.prototype) //["constructor","toString"]
上面代码采用ES5的写法,toString方法就是可枚举的。
类的属性名,能够采用表达式。
let methodName = 'getArea'; class Square{ constructor(length){} [methodName](){} }
上面代码中,Square类的方法名getArea,是从表达式获得的。
constructor方法是类的默认方法,经过new命令生成对象的实例时,自动调用该方法。一个类必须有constructor方法,若是没有显式定义,一个空的constructor方法会被默认添加。
class Point{} //等同于 class Point{ constructor(){} }
上面代码中,定义了一个空的类Point,JavaScript引擎会自动为它添加一个空的constructor方法。
constructor方法默认返回实例对象(即this),彻底能够指定返回另外一个对象。
class Foo{ constructor(){ return Object.create(null); } } new Foo() instanceof Foo //false
上面代码中,constructor函数返回一个全新的对象,结果致使实例对象不是Foo类的实例。
类必须使用new调用,不然会报错。这是它跟普通构造函数的一个主要区别,后者不用new也能够执行。
生成类的实例对象的写法,与ES5彻底同样,也是使用new命令。前面说过,若是忘记加上new,像函数那样调用Class,将会报错。
class Point{} //报错 var point = Point(2,3); //正确 var point = new Point(2,3);
与ES5同样,实例属性除非显式定义在其自己(即this对象上),否者都定义在原型上。
//定义类 class Point{ constructor(x,y){ this.x = x; this.y = y; } toString(){ return '('+this.x+','+this.y+')'; } } var point = new Point(2,3); point.toString();//2,3 point.hasOwnProperty('x')//true point.hasOwnProperty('y')//true point.hasOwnProperty('toString');//false point.__proto__.hasOwnProperty('toString')//true
上面代码中,x和y都是实例对象point自身的属性(由于定义在this变量上),因此hasOwnProperty方法返回true,而toString是原型对象的属性(由于定义在Point类上),因此hasOwnProperty方法返回fasle。这些都与ES5的行为保持一致。
与ES5同样,类的全部实例共享一个原型对象。
var p1 = new Point(2,3); var p2 = new Point(3,2); p1.__proto__ === p2.__proto__ //true
上面代码中,p1和p2都是Point的实例,它们的原型都是Point.prototype,因此__proto__属性是相等的。
这也意味着,能够经过实例__proto__属性为类添加方法。
;__proto__ 并非语言自己的特性,这是各大厂商具体实现时添加的私有属性,虽然目前不少现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,咱们可使用 Object.getPrototypeOf 方法来获取实例对象的原型,而后再来为原型添加方法/属性。
请输入var p1 = new Point(2,3); var p2 = new Point(3,2); p1.__proto__.printName = function () { return 'Oops' }; p1.printName() // "Oops" p2.printName() // "Oops" var p3 = new Point(4,2); p3.printName() // "Oops"
上面代码在p1的原型上添加了一个printName方法,因为p1的原型就是p2的原型,所以p2也能够调用这个方法。并且,此后新建的实例p3也能够调用这个方法。这意味着,使用实例的__proto__属性改写原型,必须至关谨慎,不推荐使用,由于这会改变“类”的原始定义,影响到全部实例。
与函数同样,类也可使用表达式的形式定义。
const MyClass = class Me{ getClassName(){ return Me.name; } }
上面代码中使用表达式定义了一个类。须要注意的是,这个类的名字MyClass而不是Me,Me只在Class的内部代码可用,指代当前类。
let inst = new MyClass(); inst.getClassName()//me; Me.name // ReferenceError: Me is not defined
上面代码表示,Me只在Class内部定义。
若是类的内部没用到的话,能够省略Me,也就是能够写成下面的形式。
const MyClass = class{};
采用Class表达式,能够写出当即执行的Class。
let person = new class{ constructor(name){ this.name = name; } sayName(){ console.log(this.name); } }('张三'); person.sayName();
上面代码中,person是一个当即执行的类实例。
类不存在变量提高,这一点与ES5彻底不一样。
new Foo();// ReferenceError class Foo{}
上面代码中,Foo类使用在前,定义在后,这样会报错,由于ES6不会把类的声名提高到代码头部。这种规定的缘由与下文要提到的继承有关,必须保证子类在父类以后定义。
{ let Foo = class{}; class Bar extends Foo{} }
上面的代码不会报错,由于Bar继承Foo的时候,Foo已经有定义了。可是,若是存在class提高,上面的代码就会报错,由于class会被提高到代码头部,而let命令是不提高的,因此致使Bar继承Foo的时候,Foo尚未定义。
现有的方法
私有方法是常见需求,可是ES6不提供,只能经过变通方法模拟实现
一种作法是在命名上加以区别。
class Widget{ foo(baz){ this._bar(baz); } //私有方法 _bar(baz){ return this.snaf = baz; } }
上面代码中,_bar方法前面的下划线,表示这是一个只限于内部使用的私有方法。可是,这种命名是不保险的,在类的外部,仍是能够调用到这个方法。
另外一种方法就是索性将私有方法移出模块,由于模块内部的全部方法都是对外可见的。
class Widget { foo (baz) { bar.call(this, baz); } // ... } function bar(baz) { return this.snaf = baz; }
上面代码中,foo是公有方法,内部调用了bar.call(this, baz)。这使得bar实际上成为了当前模块的私有方法。
还有一种方法是利用Symbol值的惟一性,将私有方法的名字命名为一个Symbol值。
const bar = Symbol('bar'); const snaf = Symbol('snaf'); export default class myClass{ //公有方法 foo(baz){ this[bar](baz); } //私有方法 [bar](baz){ return this[snaf] = baz; } }
目前有一个提案,为class加了私有属性。方法是在属性名以前,使用#表示。
class Point{ #x; constructor(x=0){ #x=+x;//写成this.#x亦可 } get x(){return #x} set x(value){#x=+value} }
上面代码中,#x就是私有属性,在Point类以外是读取不到这个属性的。因为井号#是属性的一部分,使用时必须带有#一块儿使用,因此#x和x是两个不一样的属性。
私有属性能够指定初始值,在构建函数执行时进行初始化。
class Point{ #x = 0; constructor(){ #x;//0 } }
类的方法内容若是含有this,他默认指向类的实例。可是,必须很是当心,一旦单独使用该方法,极可能报错。
class Logger{ printName(name='three'){ this.print(`Hellow ${name}`); } print(text){ console.log(text); } } const logger = new Logger(); const {printName} = logger; printName();
上面代码中,printName方法中的this,默认指向Logger类的实例。可是,若是将这个方法提取出来单独使用,this会指向该方法运行时所在的环境,由于找不到print方法而致使报错。
一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。
class Logger{ constructor(){ this.printName = this.printName.bind(this); } }
另外一种解决方法是使用箭头函数。
class Logger{ constructor(){ this.printName = (name = 'three')=>{ this.print(`Hellow ${name}`) } } }
还有一种解决方法是使用Proxy,获取方法的时候,自动绑定this。
function selfish(target){ const cache = new WeakMap(); const handler = { get(target,key){ const value = Reflect.get(target,key); if(typeof value !== 'function'){ return value; } if(!cache.has(value)){ cache.set(value,value.bind(target)); } return cache.get(value); } }; const proxy = new Proxy(target,handler); return proxy; } const logger = selfish(new Logger());
class 能够经过extends关键字实现继承,这比ES5的经过修改原型链实现继承,要清晰和方便不少。
class Point{} class ColorPoint extends Point{}
上面代码定义了一个ColorPoint类,该类经过extends关键字,继承了Point类的全部属性和方法。可是因为没有部署任何代码,因此这两个类彻底同样,等于复制了一个Point类。下面,咱们在ColorPoint内部加上代码。
class ColorPoint extends Point{ constructor(x,y,color){ super(x,y);//调用父类的constructor(x,y) this.color = color; } toString(){ return this.color+ '' + super.toString();//调用父类的toString()方法。 } }
上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。
子类必须在constructor方法中调用super方法,不然新建实例时会报错。这是由于子类本身的this对象,必须先经过父类的构造函数完成塑造,获得与父类一样的实例属性和方法,而后再对其进行加工,加上子类本身的实例属性和方法。若是不调用super方法,子类就得不到this对象。
若是子类没有定义constructor方法,这个方法会被默认添加,代码以下。也就是说没有显式定义,任何一个子类都有constructor方法。
class ColorPoint extends Point{ } //等同于 class ColorPoint extends Point{ constructor(...arguments){ super(...arguments) } }
另外一个须要注意的地方是,在子类的构造函数中,只有调用super以后,才可使用this关键字,不然会报错。这是由于子类实例的构建,基于父类实例,只有super方法才能调用父类实例。
class Point { constructor(x, y) { this.x = x; this.y = y; } } class ColorPoint extends Point { constructor(x, y, color) { this.color = color; // ReferenceError super(x, y); this.color = color; // 正确 } }
上面代码中,子类的constructor方法没有调用super以前,就使用this关键字,结果报错,而放在super方法以后就是正确的。
Obejct.getPrototypeOf方法能够用来从子类上获取父类。
Object.getPrototypeOf(ColorPoint) === Point
所以,可使用这个方法判断,一个类是否继承了另外一个类。
super关键字既能够看成函数使用,也能够看成对象使用。在这种状况下,它的用法彻底不一样。
第一种状况,super做为函数调用时,表明父类的构造函数。ES6要求,子类的构造函数必须执行一次super函数。
class A{} class B extends A{ constructor(){ super(); } }
上面代码中,子类B的构造函数之中super(),表明调用父类的构造函数。这是必须的,不然JavaScript引擎会报错。
注意,super虽然表明了父类A的构造函数,可是返回的是子类B的实例,即super内部的this指向的是B,所以super()在这里至关于
A.prototype.constructor.call(this)。
class A { constructor() { console.log(new.target.name); } } class B extends A { constructor() { super(); } } new A() // A new B() // B代码
上面代码中,new.target指向当前正在执行的函数。能够看到,在super()执行时,它指向的是子类B的构造函数,而不是父类A的构造函数。也就是说,super()内部的this指向的是B。
做为函数时,super()只能用在子类的构造函数之中,用在其它地方就会报错。
class A {} class B extends A { m() { super(); // 报错 } }
上面代码中,super()用在B类的m方法之中,就会形成语法错误。
第二种状况,super做为对象时,在普通方法中,指向父类的原型对象;在静态方法中指向父类。
class A{ p(){ return 2; } } class B extends A{ constructor(){ super(); console.log(super.p())//2 } } let b = new B();
上面代码中,子类B当中的super.p(),就是将super看成一个对象使用。这时,super在普通方法之中,指向A.prototype,因此super.p()就至关于A.prototype.p().
这里须要注意,因为super指向父类的原型对象,因此定义在父类实例上的方法或属性,是没法经过super调用的。
class A{ constructor(){ this.p = 2; } } class B extends A{ get m(){ return super.p; } } let b = new B(); b.m//undefined
上面代码中,p是父类A实例的属性,super.p就引用不到它。
若是属性定义在父类的原型对象上,super就能够取到。
class A{} A.prototype.x = 2; class B extends A{ constructor(){ super(); console.log(super.x)//2 } } let b = new B();
上面代码中,水星X是定义在A.prototype上面的,因此super.x能够取到它的值。
ES6规定,在子类普通方法中经过super调用父类的方法时,方法内部的this指向当前的子类实例。
class A{ constructor(){ this.x =1; } print(){ console.log(this.x); } } class B extends A{ constructor(){ super(); this.x =2; } m(){ super.print() } } let b = new B(); b.m()//2
上面代码中,super.print()虽然调用的是A.prototype.print(),可是A.prototype.print()内部的this指向子类B的实例,致使输出的是2,而不是1。也就是说,实际执行的是super.print.call(this).
因为this指向子类实例,因此若是经过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。
class A{ constructor(){ this.x = 1; } } class B extends A{ constructor(){ super(); this.x = 2; super.x = 3; console.log(super.x)//undefined console.log(this.x)//3 } } let b = new B();
上面代码中,super.x赋值为3,这时等同于对this.x赋值为3.而当读取super.x的时候,读的是A.prototype.x,因此返回undefind。
若是super做为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。
class Parent{ static myMethod(msg){ console.log('static',msg) } myMethod(msg){ console.log('instance',msg) } } class Child extends Parent{ static myMethod(msg){ super.myMethod(msg); } myMethod(msg){ super.myMethod(msg); } } Child.myMethod(1);//static 1 var child = new Child(); child.myMethod(2);//instance 2
上面代码中,super在静态方法之中指向父类,在普通方法之中指向父类的原型对象。
另外,在子类的静态方法中经过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。
class A{ constructor(){ this.x =1 } static print(){ console.log(this.x) } } class B extends A{ constructor(){ super(); this.x = 2; } static m(){ super.print(); } } B.x = 3; B.m()//3
上面代码中,静态方法B.m里面,super.print指向父类的静态方法。这个方法里面的this指向的是B,而不是B的实例。
注意,使用super的时候,必须显式指定是做为函数、仍是做为对象使用,不然会报错。
class A{} class B extends A{ constructor(){ super(); console.loog(super)//报错 } }
上面代码中,console.log(super)当中的super,没法看出是做为函数使用,仍是做为对象使用,因此JavaScript引擎解析代码的时候就会报错。这时,若是能清晰地代表super的数据类型,就不会报错。
class A{} class B extends A{ constructor(){ super(); console.log(super.valueOf() instance B)//true } } let b = new B()
上面代码中,super.valueOf()代表super是一个对象,所以就不会报错。同时,因为super使得this指向B的实例,因此super.valueOf()返回的是一个B的实例。
最后,因为对象老是继承其它对象的,因此剋以在任意一个对象中,使用super关键字。
var obj = { toString() { return "MyObject: " + super.toString(); } }; obj.toString(); // MyObject: [object Object]
大多数浏览器ES5实现中,每个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class做为构造函数的语法糖,同时有prototype属性和__proto__属性,所以同时存在两条继承链。
1.子类的__proto__属性,表示构造函数的继承,老是指向父类。
2.子类prototype属性的__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.setPrototypeOf(B,A); const b = new B();
对象的扩展一章给出过Object.setPrototypeOf方法的实现。
Object.setPrototypeOf = function(obj,proto){ obj.__proto__ = proto; return obj; }
所以,就获得了上面的结果
Object.setPrototypeOf(B.prototype,A.prototype); //等同于 B.prototype.__proto__ = A.prototype; Object.setPrototypeOf(B,A); //等同于 B.__proto__ = A;
这两条继承链,能够这样理解:做为一个对象,子类B的原型(__proto__属性)是父类A;做为一个构造函数,子类B的原型对象是父类的原型对象(prototype属性)的实例。