两类对象:实例对象与函数对象
在了解原型链之前,我们需要先考察一下对象。都说JS一切皆对象,是不是真的一切皆对象还有待商定。但是我们可以抽离出两类对象,分别是实例对象和函数对象。
实例对象就是其值不为null且typeof取值为"object"的一类对象。有两种方式来创建实例对象。第一种是通过对象字面量来创建,比如 {name: “a”};第二种是通过new来创建,比如new Set()。需要注意的是,并不是所有通过new创建的都属于实例对象,比如new Function。
函数对象就是typeof取值为"function"的一类对象。通过function、class或箭头函数创建的就是函数对象。函数对象相比实例对象比较特殊的一点就是,函数对象拥有prototype属性,prototype有什么用,请继续阅读本文剩余部分。创建的函数对象想作为构造函数时,首字母一般大写,比如Promise、Map等,这并不是语法要求,只是约定俗成而已。
__proto__链:所谓的原型链
所有实例对象和函数对象都拥有__proto__属性,__proto__属性下的属性或方法是可以直接被实例对象或函数对象所使用的。通过更改实例对象或函数对象的__proto__属性值就可以实现代码复用,例如:
// base为实例对象,拥有一个say方法
var base = {
say: function(name) {
console.log("hi", name);
}
};
// derived为实例对象,拥有name属性
var derived = {name: "derived"};
// 通过更改derived的__proto__值为base,从而实现复用base的代码
derived.__proto__ = base;
derived.say(derived.name);
实例对象或函数对象下的__proto__属性下也有__proto__属性,而该__proto__属性可能又有__proto__属性,直到最终的__proto__属性值为null。所有这些__proto__属性构成了一条__proto__链,这就是所谓的原型链。处于整条原型链上的属性或方法都是可以直接被实例对象或函数对象使用的,这样更加有利于代码复用。
prototype与new:构造原型链
通过直接更改__proto__属性来实现代码复用,不是很友好,所以JS语法提供了prototype与new来方便构造一条原型链。上文提到过所有函数对象都拥有prototype属性,而实例对象则没有。函数对象的prototype属性配合new才会起到作用。如下代码是new的实现原理:
// 只是简单示范new的实现原理,Type是函数对象
function newImpl(Type, ...args) {
var newObject = {};
// 此处是关键:将Type的prototype赋值给newObject的__proto__
newObject.__proto__ = Type.prototype;
// 调用函数对象Type
var res = Type.apply(newObject, args);
return typeof res === "object" ? res : newObject;
}
从如上new的实现代码就可以得知,新创建的实例对象newObject的__proto__指向了函数对象Type的prototype。一般称newObject是Type的实例,这也是newObject被称为实例对象的原因。现在,我们只需要通过在Type(函数对象)的prototype上增加想要的方法或属性,通过new创建实例对象,这样该实例对象就能复用Type.prototype的代码。
class、extends与new:更便利的构造原型链
通过函数对象的prototype与new构造原型链比直接修改__proto__确实方便了许多,但想实现其他编程语言中的继承,还是复杂了许多。于是ES6提供了class和extends关键字用于实现继承,其实质上仍然是利用了prototype。举例如下:
class Animal {
constructor(type) {
this.type = type;
}
print() {
console.log("我是一只", this.type);
}
}
class Cat extends Animal {
constructor(name) {
super("猫");
this.name = name;
}
say() {
console.log(this.name, "喵喵喵...");
}
}
const tom = new Cat("Tom");
// print方法来至Animal类
tom.print();
// say方法来至Cat类
tom.say();
如上示例中,实例对象tom为什么既能调用print方法,又能调用say方法呢?tom的__proto__指向了Cat.prototype,而say是Cat.prototype的方法,所以tom能够调用say方法。Cat.prototype的__proto__指向了Animal.prototype,而print是Animal.prototype的方法,即print是tom.__proto__.__proto__的方法,所以tom能调用print方法。
instanceof作用及实现原理
如果我们想知道某个实例对象或函数对象是不是某个函数对象的实例,可以通过instanceof关键字来检测。instanceof的用法为,obj instanceof Type,该表达式的值为true或false,其中obj为实例对象或函数对象,而Type必须是函数对象。instanceof的检测原理就是遍历obj的整条原型链,判断链上的某个__proto__值是否等于Type.prototype。instanceof的模拟实现代码如下:
function instanceofImpl(obj, Type) {
// Type必须为函数对象,否则抛出异常
if (typeof Type !== "function") {
if (typeof Type !== "object" || Type === null) {
throw new Error("Right-hand side of 'instanceof' is not an object");
} else {
throw new Error("Right-hand side of 'instanceof' is not callable");
}
}
// obj必须为实例对象或函数对象,否则返回false
if (obj === null || (typeof obj !== "object" && typeof obj !== "function")){
return false;
}
// 遍历obj的整条原型链
var proto = obj.__proto__;
while (proto) {
if (proto === Type.prototype) {
return true;
}
proto = proto.__proto__;
}
return fasle;
}
函数对象Object与Function的关系
JS提供两个函数对象Object和Function (注意:首字母小写的function是用来定义函数对象的),关于它们有2条比较重要的结论:
结论1:所有实例对象和函数对象都是Object的实例
结论2:所有函数对象都是Function的实例
由于Object与Function是函数对象,所以它们也有prototype属性。未完待续