es6: Class语法糖详解

2,180 阅读5分钟

es6 Class语法糖 , 它甜了什么?

如果你还不熟悉 es5 中几种继承方式 , 建议你可以回顾一下高程 或者 我的上一篇文章 js中继承

本文基于此es5的继承方式来讲解 .

继承什么 ?

  • 子类实例继承父类实例的属性和方法

  • 原型链继承

  • 构造函数作为对象, 构造函数的属性, 即静态方法继承

回顾继承方法

寄生组合式继承是结合多种继承方法 的最完美的继承方法 .

function extend(subClass,superClass){
	var prototype = object(superClass.prototype);//创建对象
	prototype.constructor = subClass;//增强对象
	subClass.prototype = prototype;//指定对象
}
function Father(name){
	this.name = name;
}
Father.prototype.sayName = function(){
	alert(this.name);
};
function Son(name,age){
	Father.call(this,name);//继承实例属性,第一次调用Father()
	this.age = age;
}
//Son.prototype = new Father();
//不创建新的父类实例而是用extend完成
extend(Son,Father);

要点

  • Father.call(this,name) 如同在子类混入父类的内容 , 完成了实例上属性的继承
  • extend(Son,Father) 在创建不执行父类构造函数的情况下, 完成了原型链的继承

Class的甜

  • 子类构造器继承时 , 必须先调用 super()(下文会详细分析 , 此时的super 为 父类原型上的 constructor) , 否则报错 . super中的this指向子类的实例 , 聪明的你应该发现了 , 它就是在做 Father.call(this,name) 做的事情

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y);
    this.color = color;
  }
}
  • 自动完成原型链继承

    class A {
        getName(){
        	console.log('xing'); 
        }
    }
    
    class B extend A {
    }
    let me = new B();
    me.getName(); // xing
    
  • 自动完成了静态方法的继承

    //父类的静态方法,也会被子类继承。
    class A {
      static hello() {
        console.log('hello world');
      }
    }
    
    class B extends A {
    }
    
    B.hello()  // hello world
    

    两条原型链

    class不但完成了原型链上的继承 , 也完成了对类静态方法的继承 . 实际上 , 他们都是通过原型链来完成的 .

    • 子类的__proto__属性,表示构造函数的继承,总是指向父类。
    • 子类prototype属性的__proto__属性,表示实例的继承,总是指向父类的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是es6 对象的新api, 可以为对象设置原型 , 如同执行以下代码

  Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
    return obj;
}

es5中的extend函数是生成一个原型是父类的原型对象的对象 , 替换了原来子类的原型对象 .

es6的方法Object.setPrototypeOf就是对原来子类的原型对象进行操作 , 使子类的原型对象的原型是父类的原型对象 所以他们完成的功能是一样 , 只不过Object.setPrototypeOf在原本对象上操作的方式更加优雅 .

super

摘自阮一峰老师 es6入门

我们先了解super的规则,class中会涉及
es6中新增了一个 super关键字 , 在对象的方法中 , super指向对象的原型

const proto = {
foo: 'hello'
};
const obj = {
  foo: 'world',
  find() {
    return super.foo;//super是动态的 执行寻找当前原型
  }
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello"

注意,super关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。

// 报错
const obj = {
  foo: super.foo
}

// 报错
const obj = {
foo: () => super.foo
}

// 报错
const obj = {
  foo: function () {
    return super.foo
  }
}

上面三种super的用法都会报错,因为对于 JavaScript 引擎来说,这里的super都没有用在对象的方法之中。第一种写法是super用在属性里面,第二种和第三种写法是super用在一个函数里面,然后赋值给foo属性。目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。

JavaScript 引擎内部,super.foo等同于Object.getPrototypeOf(this).foo(属性)或Object.getPrototypeOf(this).foo.call(this)(方法)。

const proto = {
  x: 'hello',
  foo() {
    console.log(this.x);
  },
};

const obj = {
  x: 'world',
  foo() {
    super.foo();
  }
}

Object.setPrototypeOf(obj, proto);

obj.foo() // "world"

上面代码中,super.foo指向原型对象protofoo方法,但是绑定的this却还是当前对象obj,因此输出的就是world

const proto = {
};
const obj = {
  setNum() {
	super.num = 1
  }
};

Object.setPrototypeOf(obj, proto);
obj.setNum();
console.log(proto.num,obj.num);//undefined 1

上面代码中, 我们setNum 方法中给super 的num属性 , 但是并非我们所预设的情况 , super指向的是 obj 或者说它在 setNum函数中等于this

//接上面代码
const obj2 = {
};
obj.setNum.call(obj2);
console.log(obj2);//{num: 1}

我们可以看到 , 改变了 setNumthis指向时 , 给对象的属性赋值时的super也随之改变 , 说明此时的super指向的就是this .

class中的super

super在class中有两种形式 . 一种是做对象 , 一种是函数 . 必须明确指定是哪种 , 否则会报错 .

class中的super的行为完全符合super基本规则:

super.foo = xx 等同于 this.foo = xx .

super.foo等同于

Object.getPrototypeOf(this).foo(属性)

Object.getPrototypeOf(this).foo.call(this)(方法) ;

  • constructor(构造函数)中的super

    1. super必须在函数 constructor 函数内部最前面调用一次super() (否则报错) , 因为继承时的 constructor 中的 this 是由 super() 后才生成返回的 , 此时super指向父类的构造函数 , 且this 被绑定为子类实例 . 一般来说 , super作为函数只在此出现 ,在其他地方用作方法会报错 .
    2. 作对象时同类方法中super.
  • 类方法中的super

    1. 类方法实际是在原型对象上 , 所以super 指向的是父类的原型对象;

    2. 调用super上方法时 , this 指向调用者

      class A {
      	a(){
      		console.log(this);
      	}
      }
      class B extends A{
      	m(){
      		super.a();
      	}
      }
      B.prototype.m() // B.prototype对象 
      let b = new B();
      b.m();// b对象
      
      

      这里说明 ,super上的方法中的this 指向调用者

    3. 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,所以返回undefined

  • 类静态方法中的super

    1. 静态方法实际是在子类对象上 , 所以super 指向的是父类对象;
    2. super上方法中的 this指向当前子类
    3. 赋值时指向super等于this , 一般来说 也是子类 .

通过 super 的三种操作 , 我们可以看出 , 它与原本原型链上的读取赋值操作非常相似 , 设计者非常聪明 , 通过对super的限制(或说延伸) , 达到到了与原型链行为统一的效果 .

如果你记不住几种方式, 牢牢记住super的基本行为 ,再去推断 .