1_class的由来与使用
class是es6提供的一种新的写法,其目的只是让js的写法更加倾向于传统的,面向对象的写法罢了。
js构造函数
//构造函数
function Point (x , y){
this.x = x;
this.y = y;
}
//给构造函数原型上添加方法
point.prototype.toString = function(){
return '(' + this.x + ',' + this.y + ')';
}
let p = new Point('x','y');
//对象p上有x,y属性,其原型上有toString方法
通过class类实现上述代码
class Point {
constructor(x,y){
this.x = x;
this.y = y;
}
toString(){
return '(' + this.x + ',' + this.y + ')';
}
}
let p = new Point('a','b');
p.toString();//(a,b)
typeOf Point === 'function'//true
Point === Point.prototype.constructor;//类本身指向构造函数
-
上述类定义了一个 构造函数constructor,和 toString方法;并新建一个p实例对象
ES6的类,完全可以看成构造函数的另一种写法。
class类的原型
- 每一个定义在类里面的方法,都放在了类的prototype里面。每一个由类新建的对象,它的prototype指向类的prototype。
- 使用时:p.toString() === Point.prototype.toString()
- 类上的cunstructor属性直接指向类本身(Point.prototype.cunstructor === Point)
不可枚举
- 类上的方法是不可枚举的(即类的属性不可以遍历出来)
class Point {
cunstructor(){}
toString(){}
}
Object.keys(Point.prototype) //枚举实例对象自身的属性
//[]
Object.getOwnPropertyNames(Point.prototype) //枚举自身对象的属性名(包括不可枚举的)
//['constructor','toString']
- ES5写法(es5添加在prototype上的的方法是可以枚举的)
var Point = function(){}
Point.prototype.toString(){}
Object.keys(Point.prototype)
//['toString']
Object.getOwnPropertyNames(Point.prototype)
//['constructor','toString']
2_constructor方法与实例
cunstructor方法
- cunstructor是类的默认方法,会在new命令的时候自动调用这个方法。而每个类都必须要有constructor方法,如果没有显示定义的话,就会自动生成一个空的constructor方法,并默认返回实例对象this(也可以指定返回另一个对象)
class Foo(){
constructor(){
return Object.create(null)
}
}
new Foo() instanceof Foo
//false
//说明后者类的prototype 不在 前者实例对象上的原型链
//object instanceof constructor 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
实例对象
实例上的属性,一般都定义在原型上(即定义在class上),除非显式定义在其本身上的(即定义在this上)、
class Point{
constructor(x,y){
this.x = x;
this.y = y;
}
toString(){
console.log('toString')
}
}
let p = new Point(1,2)
p.hasOwnProperty('x');//true
p.hasOwnProperty('y');//true
p.hasOwnProperty('toString');//false
p.__proto__.hasOwnProperty('toString');//true
//x和y是显示定义在实例对象上的,是实例对象自身的属性
//而toString属性是定义在原型上的:p.__proto__ === Point.prototype
所有实例共享一个原型对象
class Point{
constructor(x,y){
this.x = x;
this.y = y;
}
toString(){
console.log('toString')
}
}
let p1 = new Point('1','2');
let p2 = new Point('3','4');
let p3 = new Point('5','6');
p1.__proto__ === p2.__proto__ === p3.__proto__ //true
//因为实例上的__proto__都指向Point.prototype,所以三者相等
//由此也可得,可以通过修改实例上的__proto__,从而更改到原型上的prototype
proto 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。
3_取值、存值、表达式
取值函数(getter)和存值函数(setter)
对某个属性设置存值函数和取值函数,拦截该属性的存取行为
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'
属性表达式
类里面的属性名,还可以写成表达式;需要给表达式添加中括号[ ]
let methodName = 'toString';
class Point {
constructor(){}
[methodName](){}
}
class表达式
类也可以和函数一样,用表达式的形式定义。
const MyClass = class Me{
constructor(){}
getClassName(){
return Me.name
}
}
let m = new MyClass();
m.getClassName();//Me
Me.name;//Me is not defined
//上面代码用表达式定义了一个叫MyClass的类,而Me不是定义的类名,Me只在class内部的代码里起作用
class表达式可以写出立即执行的class
let person = new class {
constructor(name) {
this.name = name
}
sayName() {
console.log(this.name);
}
}('张三')
person.sayName()//'张三'
4_注意点
不存在变量提升
类不存在变量提升
let p = new Point()
class Foo{}
//报错
//es6不会把类的定义提到使用前,原因与继承有关,保证子类在父类之后定义。
{
let Foo = class{}
class Bar extends Foo{}
}
//上述不会报错,因为Bar继承Foo的时候,Foo已经有定义了。但是,如果存在class的提升,上面代码就会报错,因为class会被提升到代码头部,而let命令是不提升的,所以导致Bar继承Foo的时候,Foo还没有定义。
name属性
class Point {}
Point.name//'Point'
//由于本质上,ES6 的类只是 ES5 的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name属性。
//name属性总是返回紧跟在class关键字后面的类名
generator方法(todo)
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
this指向
类的方法内部如果有this,他会默认指向实例。但单独使用该方法容易报错
class Logger{
printName(name = 'there'){
this.print(`hello ${name}`)
}
print(text){
console.log(text)
}
}
const logger = new Logger();//创建一个实例
const {printName} = logger;//将logger里面的printName函数赋值
printName();//报错
logger.printName()//正常执行:hello there
上面代码中,
printName方法中的this,默认指向Logger类的实例。但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境,因为找不到
一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。
class Logger {
constructor() {
this.printName = this.printName.bind(this);
//这里将printName方法添加到实例上,同时绑定了this的指向,为创建的实例
}
// ...
}
另一种解决方法是使用箭头函数。因为使用箭头函数使,函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
class Logger {
constructor() {
this.printName = (name = 'there') => {
this.print(`Hello ${name}`);
};
}
// ...
}
5_静态方法和实例属性的新方法
静态方法(static)
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上
static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。静态方法会被直接定义在类上,而不是类的prototype上
class Foo{
static classMethod(){
return 'Hello'
}
}
Foo.calssMethod();//hello
let f = new Foo()
f.classMethod()//报错:f.classMethod is not a function
如果静态方法包含
this关键字,这个this指的是类,而不是实例。
class Foo{
static bar(){
this.baz();
}
static baz(){
console.log('static baz')
}
baz(){
console.log('baz')
}
}
Foo.bar()//'static baz'
父类的静态方法,可以被子类继承。
class Foo{
static classMethod(){
return 'Hello'
}
}
class child extends Foo{}
child.classMethod()//'Hello'
静态方法也是可以从
super对象上调用的。
class Foo{
static classMethod(){
return 'Hello'
}
}
class child extends Foo{
static classMethod(){
return super.classMethid()+',too'
}
}
child.classMethod()//'Hello'
实例属性的新方法
实例属性除了定义在
constructor()方法里面的this上面,也可以定义在类的最顶层。
class Foo {
count = 0;//直接写在类的最顶层
increment(){
this.count++;
}
}
//因为属性count和函数increment处于同一层级,所以定义的时候不需要写成this.count = 0
这种写法好处是能让实例上的属性定义起来清晰明了,一目了然,也比较简洁
6_静态属性、私有方法、new.target属性
静态属性
class Foo{}
Foo.prop = 1;
console.log(Foo.prop);//1
//新写法
class Foo{
static prop = 1
}
console.log(Foo.prop);//1
静态属性是直接定义在类上面的属性,而非在prototype上,实例上也不会有这个属性
私有方法和私有属性
私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。
在命名上加以区分
class Widget {
// 公有方法
foo (baz) {
this._bar(baz);
}
// 私有方法:将私有的方法名字前添加下划线
_bar(baz) {
return this.snaf = baz;
}
// ...
}
这种命名是不保险的,在类的外部,还是可以调用到这个方法。
另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的
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;
}
// ...
};
bar和snaf都是Symbol值,一般情况下无法获取到它们,因此达到了私有方法和私有属性的效果。但是也不是绝对不行,
Reflect.ownKeys()依然可以拿到它们。
const inst = new myClass();
Reflect.ownKeys(myClass.prototype)
// [ 'constructor', 'foo', Symbol(bar) ]
//静态方法 Reflect.ownKeys() 返回一个由目标对象自身的属性键组成的数组。
new.target属性
ES6为new命令引入的新的属性,返回的是new命令作用于的那个构造函数;如果构造函数没有通过new命令调用,则会返回undefined。所以这个属性可以用来确定构造函数是怎么被调用的
//构造函数的写法
function Person(name) {
if (new.target !== undefined) {
//通过new调用
this.name = name
console.log('通过new调用');
} else {
throw new Error('没有使用new命令调用')
}
}
let person = new Person('name')//'通过new调用'
let person2 = Person('name')//报错
//判断条件也可以写成
if(new.target === Person)
- 在class类里面,则是在constructor函数里面可以使用new.target属性
class Person {
constructor(x, y) {
if (new.target === Person) {
console.log('true');
} else {
throw new Error('false')
}
}
}
let person1 = new Person();//'true'
let person2 = Person();// 'Class constructor Person cannot be invoked without 'new''
子类继承父类的时候,new.target会返回子类
class Person {
constructor(x, y) {
if (new.target === Person) {
console.log('true');
} else {
// throw new Error('false')
}
}
}
class Child extends Person {
constructor(x, y) {
super(x, y)
console.log(new.target);
}
}
let person1 = new Person()
let child1 = new Child()
//会打印Child类
利用这个特点,可以写出不能独立使用、必须继承后才能使用的类
当父类的new.target属性是其本身的时候,抛出错误
class Person {
constructor(x, y) {
if (new.target === Person) {
throw new Error('本类不可以实例化')
} else {
//
}
}
}
class Child extends Person {
constructor(x, y) {
super()
console.log(new.target);
}
}
let person1 = new Person()//报错
let child1 = new Child()
console.log(child1);
7_继承简介和Object.getPrototypeOf()
继承简介
子类在继承父类的时候,如果没重新定义constructor函数,则默认是父类的constructor函数;
如果要重新定义constructor函数,则需要在函数内调用super方法,先通过父类加工出子类的this,然后使子类的this拥有和父类一样的实例属性与方法,然后加上子类自己的实例属性和方法。
父类的静态方法也会被子类继承
es5和es6区别
ES5 的继承,实质是先创造子类的实例对象
this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
Object.getPrototypeOf
该方法用来获取子类的父类
class Person{}
class Child extends Person;
Object.getPrototypeOf(Child);//Person
8_继承之super
super可以当作函数使用,也可以当作对象使用
super()函数
super关键字作为函数被调用的时候,表示的是父类的构造函数。
当class类通过new关键字调用时,class内部会调用new.target属性,返回当前的class;而子类继承父类的时候,也会调用new.target属性,此时返回的是子类。
也能说,super()相当于A.prototype.constructor.call(this),(A指父类)
(作为函数的时候,super只能在构造函数里调用)
super对象
super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class A{
p(){
console.log('p')
}
}
class B extends A {
constructor(){
super();
super.p()//这里的super指的是父类的prototype
}
}
需要注意,由于
super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。
class A{
constructor(){
this.a = '1'//这里是给实例添加属性
}
}
class B extends A {
get m(){ //这里对m方法添加get,即获取到的时候会调用
return super.a; //这里尝试返回super.a,但由于a是实例上的属性,而不是原型prototype上的属性,所以没法访问
}
}
let b = new B()
b.m//undefined
class A{
constructor(){}
}
A.prototype.x = 2;//这里将属性定义在了原型上,这时候super就可以访问到
子类普通方法里调用super的方法的this指向
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'
//实际上执行的是A.prototype.print.call(this)
所以通过super去给某个属性赋值的时候,super就是this,赋值的属性会变成子类实例的属性
class A{
constructor(){
this.x = 1;
}
}
class B extends A{
constructor(){
super()
this.x = 2; //修改实例上的属性
console.log(this.x); //2
super.x = 3; //相当于this.x=3,修改了实例上的属性
console.log(this.x); //3
console.log(super.x); //undefined,因为super.x相当于读取的是A.prototype.x,所以返回的是undefined
}
}
let b =new B()
*如果
super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。
super需要显示指定
//super需要显示指定为对象或者是函数,否则会报错
class A{}
class B extends A{
constructor(){
super();
console.log(super)//会报错,因为没法看出是对象还是函数,所以一定要写成super()或者super.xx
}
}
class A{}
class B extends A{
constructor(){
super();
console.log(super.valueOf() instanceof B);//true
//这里的super可以看出来是对象,其中 valueOf() 方法可返回 Boolean 对象的原始值;所以super.valueOf()返回的是一个B的实例
}
}
由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字。
let obj = {
toString(){
return super.toString();
}
}
obj.toString();[object object]
9_类的prototype属性和__proto__属性
大多数浏览器的 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;
}
\