ES6 Class 之类的声明

253 阅读4分钟

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

1. ES5 中定义类

ES5 中没有专门的类的语法,它是用函数(对于一个类来说,肯定要有构造函数,构造函数解决两个问题:第一个是传参数;第二个是实例化,也就是初始化)去模拟的。举个例子:

 // 定义一个 Animal 类
 let Animal = function (type) {
   this.type = type // 定义一个属性并初始化
   this.eat = function () {} // 定义一个 eat() 方法
 }
 
 // 生成一个 dog 实例对象
 let dog = new Animal('dog')
 // 生成一个 monkey 实例对象
 let monkey = new Animal('monkey')
 
 // 打印这个 dog 实例对象
 console.log(dog)
 // 打印这个 monkey 实例对象
 console.log(monkey)
 
 /* 运行结果:
 Animal {type: "dog", eat: ƒ}
 Animal {type: "monkey", eat: ƒ}
 */

运行结果看起来好像没什么问题,那我们修改一下类中的 eat 方法,在上面的 eat 方法中再添加一条语句,同时修改 monkey 对象的 eat 方法,然后分别调用 dogmonkeyeat 方法:

 let Animal = function (type) {
   this.type = type
   this.eat = function () {
     console.log('I am eating food.') // 在 eat 方法中添加一条语句
   }
 }
 
 let dog = new Animal('dog')
 let monkey = new Animal('monkey')
 
 console.log(dog)
 console.log(monkey)
 
 // 修改 monkey 对象的 eat 方法
 monkey.eat = function () {
   console.log('error')
 }
 
 // 分别调用 dog 和 monkey 的 eat 方法
 dog.eat()
 monkey.eat()
 
 /* 运行结果:
 Animal {type: "dog", eat: ƒ}
 Animal {type: "monkey", eat: ƒ}
 I am eating food.
 error
 */

可以看到,修改了 monkey 对象的 eat 方法后,并没有影响到 dog 对象的 eat 方法。dog 成功执行了继承自 Animaleat 方法,而 monkey 继承自 Animaleat 方法被修改后,执行的就不再是 Animaleat 方法,而是自己的 eat 方法了,这就违背了继承的原则,什么是继承?继承就是实例对象都继承了父类的某个方法,如果父类修改了这个方法,那么所有实例对象都会跟着改变。

此外,这样定义一个类还有一个问题,就是生成的每个实例对象都很大,因为每个对象都会有一个 eat 方法。那么该怎么写呢?

其实,非常简单,只要把共有的方法(像这里的 eat 方法)写在这个 function 的原型链(函数都有的一个对象,叫做 prototype,类继承的工作原理就是会沿着原型链往上找)上即可:

 let Animal = function (type) {
   this.type = type
 }
 
 // 使用原型链的方式定义 eat() 方法
 Animal.prototype.eat = function () {
   console.log('I am eating food.')
 }
 
 let dog = new Animal('dog')
 let monkey = new Animal('monkey')
 
 console.log(dog)
 console.log(monkey)
 
 dog.eat()
 monkey.eat()
 
 /* 运行结果:
 Animal {type: "dog"}
 Animal {type: "monkey"}
 I am eating food.
 I am eating food.
 */

运行结果截图如下:

ES5 中定义类

上图中,{type: "dog"}{type: "monkey"} 分别代表两个实例的本身,然后这两个实例都指向了作用域的上一层,也就是原型链上,上面挂载了 eat 方法,这就是用的原型链的方式做了继承(可以理解成一个树根和两个树杈,所有公共方法都放在了树根上,而不是树杈上)。结果就是都输出了相同的内容。

这时,如果我们修改其中一个实例对象原型链上的 eat 方法,另一个实例对象的原型链上的 eat 方法也会跟着改变:

 let Animal = function (type) {
   this.type = type 
 }
 
 Animal.prototype.eat = function () {
   console.log('I am eating food.')
 }
 
 let dog = new Animal('dog')
 let monkey = new Animal('monkey')
 
 console.log(dog)
 console.log(monkey)
 
 // 修改 monkey 对象原型链上的 eat 方法
 monkey.constructor.prototype.eat = function () { // monkey.constructor 指的就是定义 Animal 的 function
   console.log('error')
 }
 
 dog.eat()
 monkey.eat()
 
 /* 运行结果:
 Animal {type: "dog"}
 Animal {type: "monkey"}
 error
 error
 */

可以看到,最后都打印出了“error”。

总结:ES5 中,把定义类的 function 当成构造函数去用,function 内只写实例对象独有的东西,而公有的东西都放到原型链上去。

2. ES6 中定义类

ES6 使用 class 声明类,例如:

 class Animal {
   // 构造函数,传参,初始化
   constructor (type) {
     this.type = type
   }
   // 定义一个 eat() 方法
   eat () {
     console.log('I am eating food.')
   }
 }
 // 生成一个 dog 实例对象
 let dog = new Animal('dog')
 // 生成一个 monkey 实例对象
 let monkey = new Animal('monkey')
 
 console.log(dog)
 console.log(monkey)
 
 dog.eat()
 monkey.eat()
 // 打印 Animal 的类型
 console.log(typeof Animal)
 
 /* 运行结果:
 Animal {type: "dog"}
 Animal {type: "monkey"}
 I am eating food.
 I am eating food.
 function
 */

运行结果截图如下:

ES6 中定义类

可以看到,上面通过 typeof 判断出 class 声明的 Animal 的类型也是 functionES5 中就是用的 function 声明的类),eat 方法位于两个实例对象的原型链上,也就是说和 ES5 中直接写在 function 的原型链上的效果是一样的。换句话说,ES6 中的 class 只是 ES5 中用原型链声明类的语法糖1(语法不一样,但本质是一样的)。

总结:ES6 中的 class 实际上是 ES5 中用原型链声明类的语法糖。

Footnotes

  1. 指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。