ES2015学习之class

发表于 2016-11-13
更新于 2024-05-23
分类于 技术专栏
阅读量 3925
字数统计 7714

前言

ES5的原型和原型链、继承的概念是否让你揪心如焚?那么来到ES6,class就能够将你从那些晦涩(其实没有想象中那么难,用心体会自然就明白)的概念中解救出来。class其实是ES5对象和原型的语法糖,将复杂的概念重新封装一遍后提供更加易懂的概念给开发者是每门语言发展壮大的条件。冲着这点就觉得JS以后未来一片光明

最开始接触class用法的是在React项目中,稀里糊涂地按照react的官方文档知道直接简单粗暴地使用了下面的语法:

class Example extends Component

然后就继续开发各种新的功能,从来没时间去理会class3W(What, Why, How),直到最近的一篇博客。于是趁着工作之余的空闲,赶紧补一补这个ES6的新特性。

1、class概念

JS的类的设计本质是为了封装JS之前存在的基于原型的继承方法。class语法并没有给JS带来新的面向对象的继承模型,所以称其为只是一种语法糖。它的定义是使用关键字:class。比如:

class Animal {
  constructor(props){
    this.name = props.name;
    this.type = props.type;
  }
  
  speaking(){
    console.log(`I am a ${this.type}, my name is ${this.name}`);
  }
}

这样一个Animal的类就定义好了,在class里面还有一个叫做constructor的概念,顾名思义它便是构造函数。在执行new操作的时候构造函数便是初始化函数,我们看看下面的代码之后在浏览器的结果:

let dog = new Animal({name:'wangwang',type:'dog'});

从上面图片我们可以看到其dog实例的结构,可以看到之前在JavaScript的原型和原型链的前世今生中那个经典的实例-->原型对象<--构造函数的结构,如下:

class图示

因此我们可以反推出该段class代码如果使用ES5的原型链设计的话应该是:

function Animal(props){
    this.name = props.name;
    this.type = props.type;
}

Animal.prototype.speaking = function(){
  console.log(`I am a ${this.type}, my name is ${this.name}`);
}

let dog = new Animal({name:'wangwang',type:'dog'});

这下子是不是熟悉了很多?

以上便是class的基本使用。接下去介绍class的其他特性。

1.1、变量提升

class实际上是“特殊的函数”,因此它便可以有如同函数定义那样,有两种方式:类表达式和类声明。比如刚才使用的例子就是属于类声明,那么改写成类表达式应该是:

// 匿名类表达式
let Animal = class{
  constructor(props){
    this.name = props.name;
    this.type = props.type;
  }
  
  speaking(){
    console.log(`I am a ${this.type}, my name is ${this.name}`);
  }
}

// 命名的类表达式
let Animal = class Animal{
  constructor(props){
    this.name = props.name;
    this.type = props.type;
  }
  
  speaking(){
    console.log(`I am a ${this.type}, my name is ${this.name}`);
  }
}

无论你使用的哪种方式来定义类,他们都不想函数那样可以自动做到变量提升,也就是你使用之前都必须有其定义,否则会报ReferenceError的错误。

1.2、prototype方法 vs 静态方法

在class中没有加static关键词的方法都是属于原型方法,所以你在上面图中可以看到该方法是挂在原型对象上的。那么有没有static的方法之间有什么区别呢?

static定义的方法是在class没有实例化的时候被调用,如果class实例了那就掉不了静态定义的方法。

改造上面例子的代码:

class Animal {
  constructor(props){
    this.name = props.name;
    this.type = props.type;
  }
  
  speaking(){
    console.log(`I am a ${this.type}, my name is ${this.name}`);
  }
  
  static staticSpeaking(type, name){
    console.log(`I am a ${type}, my name is ${name}`);
  }
}

Animal.staticSpeaking('dog','wangwang');

let dog = new Animal({name:'wangwang',type:'dog'});

dog.speaking();
dog.staticSpeaking('dog','wangwang'); // Uncaught TypeError: dog.staticSpeaking is not a function(…)

可以看到实例化后的dog调用不了staticSpeaking()这个方法,但是请看下面这张图:

我们可以看到staticSpeaking方法在构造函数上,所以其大致的图示如下:

在上面的图中可以直接解释为什么在实例上是访问不到静态方法,而是直接通过构造函数。

那么是不是我们只能在class外部使用呢?不是的。static方法可以在class中另外一个静态方法中调用、另外一个原型方法调用或者构造函数上调用。

1.2.1、静态方法上调用

比如:

class Animal {
  constructor(props){
    this.name = props.name;
    this.type = props.type;
  }
  
  speaking(){
    console.log(`I am a ${this.type}, my name is ${this.name}`);
  }
  
  static staticSpeaking(type, name){
    console.log(`I am a ${type}, my name is ${name}`);
  }
  
  static anotherStaticSpeaking(){
    this.staticSpeaking('dog', 'wangwang');
  }
}

Animal.anotherStaticSpeaking()

1.2.2、在原型方法上调用

比如:

class Animal {
  constructor(props){
    this.name = props.name;
    this.type = props.type;
  }
  
  speaking(){
    console.log(`I am a ${this.type}, my name is ${this.name}`);
    Animal.staticSpeaking('cat', 'kitty');
    this.constructor.staticSpeaking('duck', 'gaga');
  }
  
  static staticSpeaking(type, name){
    console.log(`I am a ${type}, my name is ${name}`);
  }
}

let dog = new Animal({name:'wangwang',type:'dog'});

dog.speaking();

1.2.3、在构造函数上调用

这个方法和在原型方法上调用是一致,就不多说了。

静态方法的设计是何用意呢?勤思考的童鞋们可以好好想一想哈!

1.3、get和set方法

getter和setter的语法和ES5的对象字面量一样,比如:

class Animal {
  constructor(props){
    this.name = props.name;
    this.type = props.type;
  }
  
  speaking(){
    console.log(`I am a ${this.type}, my name is ${this.name}`);
  }
  get animalName(){
    return this.name;
  }
  
  set animalName(name){
    this.name = name
  }
}

let dog = new Animal({name:'wangwang',type:'dog'});

dog.speaking();

console.log(dog.animalName);
dog.animalName = 'huahua';
console.log(dog.animalName);

1.4、子类

class的继承是使用extends关键词,比如:

class Animal {
  constructor(props){
    this.name = props.name;
    this.type = props.type;
  }
  
  speaking(){
    console.log(`I am a ${this.type}, my name is ${this.name}`);
  }
}

class Cat extends Animal {
  speaking() {
    super.speaking();  
    console.log('but i inherit from the animal');
  }
  
  walking() {
    console.log('I can walking 10 km');
  }
}

let cat = new Cat({name:'kitty', type:'cat'})

cat.speaking();
cat.walking();

使用extends继承了Animal类之后,cat将拥有Animal所有的属性和方法,并且可以重新改写父类的所有方法,其中的super方法可以访问到父类的所有方法和属性。其结构如图:

其原型链模型如下:

那么如果使用ES5的原型来实现的话,代码大致是这样:

function Animal(props){
    this.name = props.name;
    this.type = props.type;
}
Animal.prototype.speaking = function(){
  console.log(`I am a ${this.type}, my name is ${this.name}`);
}

function Cat(props){
    Animal.call(this, props);
}

Cat.prototype = new Animal({name:'',type:''});
Cat.prototype.constructor = Cat; //为什么?
Cat.prototype.speaking = function(){
  Animal.prototype.speaking.call(this);
  console.log('but i inherit from the animal');
}
Cat.prototype.walking = function(){
  console.log('I can walking 10 km');
}
let cat = new Cat({name:'kitty', type:'cat'})

cat.speaking();
cat.walking();

这种实现可以参考JavaScript的原型和原型链的前世今生(二)的组合继承方式写法。

至于为什么需要Cat.prototype.constructor = Cat这行赋值留给童鞋们去好好思考哈!(给个提示:答案在之前的博客中有提及到的哦^<>^)

2、Binding问题

剩余一个要讲的问题便是class的自动绑定this问题,ES6中的Class默认是不会自动绑定this的。什么意思呢?举个例子:

class Animal {
  constructor(props){
    this.name = props.name;
    this.type = props.type;
  }
  
  speaking(){
    console.log(`I am a ${this.type}, my name is ${this.name}`);
  }
}

let cat = new Animal({name:'kitty', type:'cat'});
cat.speaking();

let globalSpeaking = cat.speaking;
globalSpeaking();

在执行到最后的globalSpeaking()是会报错:”Uncaught TypeError: Cannot read property 'type' of undefined(…)“,因为此时的globalSpeaking是属于全局对象,而全局对象没有typename属性,那么解决自动绑定的问题可以是这样的:

class Animal {
  constructor(props){
    this.name = props.name;
    this.type = props.type;
    this.speaking = this.speaking.bind(this)
  }
  
  speaking(){
    console.log(`I am a ${this.type}, my name is ${this.name}`);
  }
}

let cat = new Animal({name:'kitty', type:'cat'});
cat.speaking();

let globalSpeaking = cat.speaking;
globalSpeaking();

这个问题到时候会牵扯到另外一篇文章Eslint背后那些我们应该知道的为什么中。比较重要的!

参考

  1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
  2. JavaScript的原型和原型链的前世今生

公众号关注一波~

微信公众号

关于评论和留言

如果对本文 ES2015学习之class 的内容有疑问,请在下面的评论系统中留言,谢谢。

网站源码:linxiaowu66 · 豆米的博客

Follow:linxiaowu66 · Github