JavaScript中“纠结的类”
正如前文所说,JavaScript中并没有传统意义上的“类”,可能是因为在JavaScript这门脚本语言诞生之初,并没有人会他会有如今的成就,所以就借用了Self语言的基于原型的面向对象设计。具体可以看JavaScript的诞生
面向对象编程(OOP)
面向对象编程是一种编程思想,其重要思维体现在:
- 封装性:讲一组方法,变量封装在一个“模块”中,一般来说是一个类。
- 继承性:继承性是面向对象技术中的另外一个重要特点,是指一个类继承另一个类的方法,类的继承。他们的关系通常是父与子的关系。
- 多态性:是指子类对父类的方法进行重写或重载。
对于传统的OOP语言,比如java,C++。
类意味着复制。
实例化时,他的行为会被复制到实例中;继承时,行为也会被复制到子类中。
JavaScript中的类
JavaScript程序员开始大都是由其它语言程序员没经过系统学习这门语言的精髓就开始编程(因为大家认为JavaScript是一门脚本语言,是如此的简单。)所以从开始到现在,程序员们总是试图使用JavaScript来进行面向对象编程,寻找类这个东西的存在。然而不幸的是,JavaScript没有传统的类。还好我们拥有[[prototype]]这个好东西,我们才能进行所谓的面向对象编程。但这任然不是我们传统观念上的面向对象编程,即使是ES6中ECMA提供的class关键字,任然是[[prototype]]的原法糖。
detail
在许多JavaScript库中都提供了类的语法糖,我们来看一下其内部是如何实现的。
类
1 | function Person(name){ |
我们可以直接用new操作符进行“类的实例化”,但是JavaScript中不存在类。所以我们实际上是新创建了一个空对象,然后将其[[prototype]]设为Person.prototype。然后将该函数的this指向新建对象,然后调用该函数,如果函数没有返回对象,就返回新建对象。
继承
下面是最典型的一种继承实现方式(JavaScript中多种实现继承的方式,但大同小异):
1 | function Person(name){ |
我们可以看出来,所谓的继承,也就是Student.prototype = Object.create(Person.prototype)
这句话,实际上是将子类Student的[[prototype]]设置为一个空对象,该对象的[[prototype]]指向父级类Person的prototype。(Object.create(obj1))的意义是创建一个空对象,该对象的[[proottype]]指向obj1首先我们需要了解的是,我们有4个对象(在JavaScript中函数也是对象,可以拥有自己的属性)来储存数据:
- student0
- student0.proto ([[prototype]])(注意通过Object.create方式创建的
__proto__
并不指向Student.peototype
) - Student
- Student.prototype
根据原型链的查找规则,我们在获取实例的某个属性时,会分别从:student0 —> student0.proto->Student->Student.prototype查找。
看下面的图:
我们可以看到,子类的属性在实例的__proto__中,父类的属性在实例的__proto__的__proto__中,因此我们可以获取子类,父类的所有方法,我们就完成就继承。
ES6的中类
上面提过,ES6中的出现了class关键字,下面我们通过ES6的方法重写一下的例子:
1 | class Person{ |
这个看上去漂亮多了,就像是在写传统的OOP的语言。但是实际上,它的背后任然是混乱的[[prototype]]的链。
ES6的class还有一个常用的特点,静态方法。所谓静态方法,就是直接可以在类上面直接调用的方法。
1 | class Person{ |
想一下class背后的原理,用ES5的语法来实现以下static:
1 | Person.sayme = function(){console.log('me');} |
没错,就是这么简单!只是因为这个方法是类自身的属性,并不是方法的[[prototype]]属性值。
混入(mixin)
正如我们之前所说,传统的OOP意味着复制。
然而我们JavaScript中模拟的类,并不是传统的复制,而是基于原型链的“伪类”。
所以为了实现很真实的“类”,我们有了**混入(mixin)**这一概念。
显示混入
看下面的例子:
1 | function mixin(sourceObj,tragetObj){ |
没错,就是一次复制过程。现在我们也可以用:
1 | object.assign(targetObj,sourceObj); |
因为这个方法的原理就是上面的mixin函数。
隐式混入
思考下面代码:
1 | var something = { |
关键在于**something.cool.call(this)**,将something.cool函数的this指向了Another这个对象。因此,我们把Something的行为“混入”到Another中。
更合理的编程思想:行为委托
类与委托的前世今生
如我们前面所说,JavaScript本身是不存在类的,只是为了适应程序员们的设计习惯,我们使JavaScript中有了“类”的存在,实际上这种设计方式无疑是把吧苹果涂上橙色,在上面插上孔…然后把它装饰成橘子。但他始终都是苹果,无论我们怎么在外部伪装。既然如此,为什么我们不直接把它当成橘子来吃呢?
如何使用行为委托
看下面的例子:
1 | var Task = { |
在上面这段代码中,Task,someWork都不是类(或者函数),而是对象。我们把someWork的[[prototype]]委托给了Task。这很符合Self语言的基于原型的面向对象编程的思想。这也被成为“对象关联(OLOO)”
对象关联风格的代码还有一些不同之处。
- 在代码上,id和label数据成员都是直接存储someWork之上,而不是Task。
- 在类的思想上,我们鼓励方法的重写(多态)。也就上在子类中定义父类同名的函数。但在委托行为中恰恰相反,我们尽量避免在[[prototype]]链上存在同名函数。
Last
不得不说,行为委托从语言底层来说更加适合JavaScript编程,但是越来越多的程序员习惯使用面向对象的编程思想。这迫使JavaScript不得不改变自己,比如在ES6中推出了class的语法糖,但这代表了官方的认可,也许在不久的将来,我们可以看见真正的class出现在JavaScript。