JavaScript的面向对象一个复杂的体系, 涉及到非常多, 从最初的类抄写, 函数作为类, 到原型链, 到ES6的Class设计。

这里先挖一个坑 慢慢填, 同时本文也要写上tl, :)

一、早期JavaScript(1.0~1.3)的对象,类, 构造器, new

对象: 对象是0~多个属性的集合,

ECMAScript, an object is a collection of zero or more properties.

  1. 类: 函数作为类, 我们程序员一般约定, 当函数作为类的时候, 首字母大写。
  2. 构造器: 函数作为构造器, 作为new运算的参数, 比如你可以写出new (function(){this.a=1})
  3. new: 接收一个构造器

对象的创建过程:

使用new运算创建一个对象, 由类创建自对象this, 这种创建过程是使用类抄写来实现的, 但是从这一点上来看JavaScript的面向对象是有继承性的。

那么封装呢? 在早期的JavaScript中我们规定一个属性是否能够使用for…in语句列举出来, 如果可以被列举, 那么就是可见的, 否则就是隐藏的。

二、基于原型的对象, 类, 构造器, new

关于原型的部分就不过多的阐述。

基于原型的new运算: 首先这个new运算会使用f.prototype作为原型来创建一个this对象, 然后才是调用f()函数, 使用类抄写的技术来创建一个新的对象。虽然这种方式为JavaScript带来了一定的继承属性, 但是同样这种做法导致的后果就是, 当子类重写父类的方法之后, 那些逻辑就消失了。

基于instanceof的检查语义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Device() {
this.id = 0;
}

function Car() {
this.name = "Car";
this.color = "Red";
}

Car.prototype = new Device();

var x = new Car();

x instanceof Car
x instanceof Device

上面的instanceof运算符号是一个被实现为动态访问原型链的过程, 它将从Car.prototype地在原型链中查找指定的原型链。

  1. 首先JavaScript会从对象x的内部结构中取得它的原型, 这个原型是由new运算在构造对象的时候向对象内部进行添加的。new运算是依据运算时候所使用的构造器来填写这个属性, 所以这意味着它实际实现的时候, 将Car.prototype这个值填写到x对象内部属性上。
  2. instanceof右边操作数是一个类名(Car, Device), 但是实际上是使用class.prototype来进行比对的, 如果右边操作数是Car, 那么就是Car.prototype, 但是上一个例子检查的是Device, 也就是Device.prototype, 但是这两者是不相等的, 所以会让左侧继续向上进行查找, 找到右侧的Device.prototype, 所以这个表达式将返回True值

三、ES6之后的重新设计

ES6之后添加了class, extends, super等关键字完善了JavaScript面向对象系统, 那么创建对象的过程又有什么样的不同?

ES6之后的函数: 类: 首字母大写 方法 一般函数

典型的方法在内部声明时候, 有三个主要特征

  1. 具有一个名为[[homeobject]]的内部属性
  2. 没有名为构造器[[constrcut]]的内部属性
  3. 没有名为prototype的属性

ES6要求所有的子类的构造过程都不得创建这个this实例, 并主动把这个创建的权力交换给父类, 乃至祖先类, 如果在类中通过extends指定了父类, 那么:

  1. 必须在构造器方法中显式的使用super()来调用父类的构造过程
  2. 在上述调用结束之前, 不可以使用this
1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
constructor() {
this.a = 1;
}
}

class B extends A {
constructor() {
super();
this.b = 2;
}
}
var a = new B();

super所解决的问题:

由于早期JavaScript采用类抄写的技术来实现了继承性, 但是一旦子类重写父类的方法, 就会出现逻辑丢失的情况,所以在ES6的时候, JavaScript填上了这个坑, 添加了super这个关键字。而且这个关键字只能在方法中进行使用。因为子类重写父类的方法就相当于扩展父类的方法, 所以只能在方法中使用super关键字也就变得十分合理了。

super关键字需要解决的核心问题就是找到方法所属的那一个类, 所以实现的关键就在于为每一个方法添加一个”它所属的类”这样的性质, 这个性质就被称之为主对象。

所以在ES6之后, 通过方法声明语法得到的方法, 虽然仍然是函数类型, 但是与传统的函数类型的属性存在一个根本上的不同, 这些新的方法增加了一个内部槽, 用来存放主对象。

  1. 在类声明中, 如果是类静态声明, 那么主对象就是这个类
  2. 一般声明, 那么这个方法的主对象就是该类所使用的原型, 也就是Aclass.prototype
  3. 对象声明, 那么方法的主对象就是对象本身

关于第三点, 对象的super是什么东西, 我们知道类声明class,的父类也是class, 要弄明白这一点我们就需要回到原型继承的时代,原型是什么? 原型其实就是一个对象, 而所谓的类声明只是一种载体, 真正继承的还是原型对象本身, 所以设置第三条也是非常有必要的了。

super的实现过程

如何找到super?

在ECMAScript约定, 只需要在方法中取出这个主对象HomeObject, 然后对这个HomeObject取出原型, 该原型就一定是所谓的父类。

super总是在一个方法中才能够引用, 如下:

1
2
3
4
const obj = {
foo() {
super.xxx();
}}

super绑定给当前对象

这是因为继承来的行为, 应该是施加给现实中当前对象的, 所以super.xxx()将当前函数中的那个this传递给父类xxx()方法就可以了。 所以当对象调用foo方法的时候, 它总是会将obj传入作为this, (foo()函数中的this就是obj, 我们希望它调用父类的xxx()方法时候, 传入的当前实例就是当前的obj)。

super在语言内核上是一个规范类型的引用, ECMA约定将这个语法标记成为Super引用, 并且为这个引用专门添加了一个thisValue域, 这个域, 其实在函数的上下文中也有一个相同名称, 相同含义, 但是ECMA约定优先取Super引用中的thisValue值, 然后再取函数上下文中的。

说了那么多, 总结起来就是两句话:

  1. 首先找到super关键字所代表的父类对象, 是通过当前方法的[[HomeObject]]的原型链来查找的。
  2. this引用是从当前环境所绑定的this中抄写过来, 并绑定给super的。

在ES6之后创建this的时候, 是将这项权力交给父类的, 也就是刚刚进入构造方法的时候this引用其实是没有任何值的, 必须等this构造出来才行,但是我们在super.xxx()中确需要将这个this绑定给super, 所以ES6就规定只有调用了父类的构造方法之后, 才能够使用super.xxx()的方式引用父类的属性。但是存在一个限制, 就是调用父类构造方法的时候, 也就是super()这样的代码的时候, super其实是不会绑定this值的, 也不会在调用中传入this值, 因为压根没有。

super()中的父类构造方法

如果调用父类构造方法, 那么我们也是找不到super的, 这是为什么了因为构造方法的主对象是对象的原型, 比如你使用new MyClass()创建一个对象, 那么MyClass类的构造器constructor()它的主对象其实是MyClass.prototype,而不是MyClass, 因为MyClass 是静态方法的主对象, 但是constructor()是一般方法。

所以, 在MyClass的构造方法中访问super的时候, 通过HomeObject找到的将是原型的父级对象, 而不是父类的构造器, 但是super()的语义是”调用父类构造方法”, 也就是extends所指定的那个东西, 但是上面我们说到是无法通过[[HomeObject]]来找到父类构造方法。

那么JavaScript如何做到, 那就是JavaScript会从当前调用栈上找到当前函数, 也就是new MyClass()中的当前构造器,并且返回该构造器的原型作为super。

为什么构造不是静态的, 如果我们让构造器编程静态的, 那么我们不就不用费这么大的力气做这些东西了么? 这是因为我们也需要在构造器中使用super.xxx()这样的语法