这是我参与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 方法,然后分别调用 dog 和 monkey 的 eat 方法:
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 成功执行了继承自 Animal 的 eat 方法,而 monkey 继承自 Animal 的 eat 方法被修改后,执行的就不再是 Animal 的 eat 方法,而是自己的 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.
*/
运行结果截图如下:
上图中,{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
*/
运行结果截图如下:
可以看到,上面通过 typeof 判断出 class 声明的 Animal 的类型也是 function(ES5 中就是用的 function 声明的类),eat 方法位于两个实例对象的原型链上,也就是说和 ES5 中直接写在 function 的原型链上的效果是一样的。换句话说,ES6 中的 class 只是 ES5 中用原型链声明类的语法糖1(语法不一样,但本质是一样的)。
总结:
ES6中的class实际上是ES5中用原型链声明类的语法糖。
Footnotes
-
指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。 ↩