对象继承 VS 类继承

在 class-based 的面向对象的世界里,要出现对象,必须先有类。类之间可以继承,类再使用 new 操作创建出实体,父子对象之间的继承体现在父类和子类上。你不能说 对象 a 继承了对象 b,只能说 class A 继承了 class B,然后他们各自有一个实例a、b。

JS中实现的是原型继承。在 prototype-based 的面向对象的世界里,继承是对象之间的事情,就像现实世界里,儿子继承父亲,没必要非得整出父亲类、儿子类。你有一个对象 a,通过 b = Object.create(a) (或者用类似的polyfill)创建出一个继承了 a 对象的实体 b。a 对象的属性和方法甚至后期对 a 的属性和方法修改,b 对象都直接继承过来。这里,我们说,a对象是b对象的原型。我们发现,这样的继承方式不需要,继承完全在对象间完成。

原型继承的工作机制

在对象属性查找时,如果对象本身存在这个属性,会优先使用它自己的(也就是概念 ownProperty);如果没有,就会查找对象的原型上有没有这个属性;如果原型对象也有自己的原型,递归查找,直到原型的根部,没有原型了停止查找,如果还是不存在,则返回 undefined。这就是传说中的 原型链,这也是 JS 中对象继承的体现方式,原型存在的意思。

这里顺带说一下如何获取一个对象的原型。ES5 提供了 Object.getPrototypeOf(obj) 方法来获取一个对象的原型,在 Chrome 中也可以使用非标准的 obj.__proto__

JS 在 prototype-based 的面向对象的基础上,引入了 构造器 来模拟 class-based 的模式, 配合 new 操作符使用。 构造器和 已有的 prototype 概念如何配合工作呢?

我们知道,JS 中的构造器就是一个普通函数,但是这个函数有一个特殊的属性(函数也是对象,所以也有属性) ———— prototype。此 prototype 用来定义通过构造器构造出来的对象的原型,构造器内部的代码用来给对象初始化。

function Ctor() {}
console.dir(Ctor.prototype);
// 构造器的 prototype 属性,默认值是
// { constructor: Ctor    }

Ctor.prototype.method = function() {
    console.log(1)
}

instance = new Ctor();

instance.constructor // Ctor
instance.method() // console.log(1)

instance 是如何获得 Ctor 的 prototype 属性上的数据的呢?好,还记得 JS 中的继承都是对象之间的继承吗?我们翻译一下 new 操作符到底干了什么。

instance = new Ctor() // 等价于
instance = Object.create(Ctor.prototype) // 用 Ctor 的 prototype 作为原型来创建一个新对象
Ctor.apply(instance) // 执行构造器用来初始化,构造器中的 this 指向 instance

我们称, instance 的原型是 Ctor.prototype, instance 是 Ctor 构造出来的 (new 出来的).

为了让 instance.constructor 能正确指向 instance 的构造器,一个构造器默认的 prototype 上已经存在 constructor 属性,并且指向构造器本身了。在我们覆盖构造器的 prototype 属性时,记得要把 prototype.constructor 属性定义了,让它指回到构造器,否则构造出来的 instance 的 constructor 属性就出问题了。所以我们可以看出,instance.constructor 其实是不是 instance 自己的属性,是原型链上定义的。

这里千万不要把 Ctor.prototype 误理解为是 Ctor 的原型。Ctor 的原型是 Object.getPrototypeOf(Ctor)(非标准写法:Ctor.__proto__),它是 Function.prototype, 因为 Ctor 是一个函数对象,所有函数都构造自 Function,原型是 Function.prototype。Ctor.prototype 是 Ctor 构造出来的实例的原型,不是 Ctor 的原型。

Object & Function 鸡生蛋蛋生鸡

有代码如下:

Object instanceof Function // true
Function instanceof Object // true
// what???

我们来挖掘一下 instanceof 操作符底层逻辑:

instance instanceof Ctor // 等价于

function instanceOf(instance, prototype) {
    var proto = Object.getPrototype(instance); // 取对象原型
    if( proto === null) return false; // 空
    if( proto === prototype) return true; // 原型匹配
    return instanceOf(proto, prototype); // 递归检查原型的原型
}

instance(instance, Ctor.prototype);

JS 中的继承终归是原型的继承,所以 class-based 中的 instanceof 概念最终也需要映射到 prototype 上。但是 JS 中的构造器名称有一个特殊之处,这个名称既表示了构造器这个函数,又表示了 class-based 概念中的 的概念, 而函数本身又是一种特殊的对象。

Object instanceof Function 之所以为 true,我们是把 Object 当做构造器看待,它是一个函数,它是 Function 的实例,所以同时这里我们把 Function 当作类型来看待,它是所有 function 的类。

Function instanceof Object 之所以为 true,我们是把 Function 当作对象看待,它虽然是一个构造函数,但是它也是对象,它是 Object 的实例,所以同时我们又把 Object 当作类型来看待,它是所有对象的类。

从原型角度:

// Object 是一个构造函数,它的原型是 Function.prototype
// Function.prototype 是所有函数的原型,call, apply 就挂在这里
Object.getPrototypeOf(Object) === Function.prototype 

// Function 也是一个构造函数
Object.getPrototypeOf(Function) === Function.prototype
// Function.prototype 本身是一个对象,所有对象的原型根部都是 Object.prototype
Object.getPrototypeOf(Function.prototype) === Object.prototype

这也印证了 JS 中函数是对象的概念。


水不凉
4.9k 声望64 粉丝