本文系读完winter《重学前端》相关章节并查阅相关文档后的学习总结,特此声明和感谢。
误解
以前总觉得JavaScript中的面向对象是一团乱麻,各种原型和类的概念交织错乱,读过的一些书(如《你不知道的JavaScript》)也对JavaScript中类的写法嗤之以鼻以至于不推荐使用新的class
关键字。种种现象让我过去很长一段时间都认为JavaScript是一门设计得很差劲的语言,所谓的面向对象都是模拟出来的,虽然我还是得靠它吃饭。
两种面向对象的实现思路
面向对象的思想由于其易理解、可扩展、模块化等优势广为流行,其设计思路可分为两个流派:
- 基于类:先定义一个虚拟的分类,如“猫”类,再去实例化具体的猫;分类之间又可形成继承和组合,如继承于“猫”类的“中华田园猫”类,“中华田园猫”类与“公猫”类又可组合为“中华田园公猫”类。
- 基于原型:直接定义一只具体的猫,以这只猫为原型,附加或修改一些特性,就出现了一只独具特色的中华田园猫,还可以“照猫画虎”地根据这只猫来定义出各种猫科动物。
很明显的,JavaScript属于第二种基于原型的流派,它本身是一种非常优秀的方案,但由于一些外部原因,JavaScript被额外添加了许多因素用来模拟基于类的流派,这才让它看起来混乱不堪。
基于原型的面向对象
抛开其模拟类的复杂语法,其实JavaScript基于原型的设计非常简单:
- 将对象的状态和方法统一为“属性”,它们在Java中被分别称为“属性”和“方法”,在C++中被称为“成员变量”和“成员函数”;
- 所有对象都具有私有属性
[[prototype]]
,就是对象的原型; - 访问对象的属性,若对象本身没有,就继续访问其原型、原型的原型等,直到找到此属性或原型为空为止。
ES6中基于原型的语法也非常简明:
Object.create
:根据原型创建新对象,原型可以为null;Object.getPrototypeOf
:获取对象的原型;Object.setPrototypeOf
:设置对象的原型。
基于原型的对象示例:
const cat = { shitOfficer: 'human', say() { console.log('meow~'); }}const chineseCat = Object.create(cat);chineseCat.say = function() { console.log('miao~')}console.log(chineseCat.shitOfficer); // 'human',访问到原型cat的属性chineseCat.say(); // 'miao~'复制代码
JS早期版本中的模拟类
在JS早期版本中,对于类的支持是相当弱的,主要是通过Function
对象和new
操作符来模拟类的,主要知识点如下:
- Function对象的构造器中包含一个
prototype
属性,它指向从这个构造器构造出的所有对象的原型 - 使用
new
操作符时:- 以构造器的
prototype
指向的对象为原型,创建新对象 - 将构造器中的this绑定到上一步创建的对象上,执行构造器(即执行函数)
- 若函数没有返回其他对象,则将第一步创建的对象返回
- 以构造器的
- 需要实现继承时较为复杂且实现方式多样
示例:
// 使用this:直接在构造器中给this添加属性,使用new操作符后this会绑定到新创建的对象上function fun1() { this.p1 = '1'; this.p2 = function() { console.log(this.p1); }}var obj1 = new fun1();obj1.p2(); // '1'// 使用prototype:给构造器的prototype属性指向的原型对象添加属性function fun2() {}fun2.prototype.p1 = '2';fun2.prototype.p2 = function() { console.log(this.p1);}var obj2 = new fun2();obj2.p2(); // '2'复制代码
ES6中的类
ES6中引入了新特性class
,它实际上还是用之前的原型来实现的,使用class
定义的类,其每个实例对象具有同一个原型对象,类中定义的属性都会被写在此原型对象上。 class
从语言标准上统一了类的实现方法,使function
回归原本的函数语意,同时使用extends
关键字可轻松实现继承功能,示例:
class Cat { constructor(name) { this.name = name; } say() { console.log(this.name + ': meow~'); }}class chineseCat extends Cat { constructor(name) { super(name); } say() { console.log(this.name + ': miao~'); }}const myCat = new chineseCat('Mimi');myCat.say(); // 'Mimi: miao~'复制代码
总结
理解了JavaScript基于原型的设计理念后就会明白,JavaScript中的面向对象并非原先想象中那样混乱不堪。在我看来,相较于基于类的面向对象,基于原型的设计摒弃了所有“虚”的、概念上的东西,一切皆为实体对象,万物皆可追溯至源头,且源头仍是一个实体。 当然,由于一些历史原因,JavaScript被赋予了很多打破自身体系的画蛇添足的东西,就像它的名字中很有误导性的“Java”一样。但既然我们使用的是JavaScript,就应该理解并遵循JavaScript本身的设计理念,发挥它独有的特点,对于一名前端来说尤其如此。