本文主要对JS原型相关的问题做一些总结。从一个简单的例子说起:
function Foo() {
...
}
Foo.prototype.constructor === Foo; // true
let foo = new Foo();
foo.constructor === Foo; // true
当声明一个函数的时候,默认会给函数的prototype添加一个.constructor属性,该属性指向该函数。当调用foo.constructor的时候可以看到通用指向Foo函数,是不是foo实例也同样有constructor属性?
答案是否定的!这里foo是通过原型找到该属性的,即foo.__proto__.constructor。那foo.__proto__又是指向哪里呢?
其实foo.__proto__就是指向了Foo.prototype:foo.__proto__ === Foo.prototype。
我们知道,在对象的原型上添加方法,则实例可以通过原型找到对应的方法,譬如:
Foo.prototype.sayHello = function() {
return "hello";
}
foo.sayHello(); // hello
这里foo通过__proto__.sayHello, 将方法调用委托给了Foo.prototype对象,调用Foo.prototype对象上的sayHello方法。同样之前将到的foo.constructor也是委托给了Foo.prototype对象。
如果我们手动修改了Foo.prototype对象, 如:
function Foo() {
...
}
Foo.prototype = {
// 重置该对象为空后没有constructor属性
}
let foo = new Foo();
foo.constructor === Foo; // false
此时foo.constructor时,即foo.__proto__是指向了新的Foo.prototype={}的,自然是找不到constructor属性了!说明无意间对prototype的修改会导致原型链的断裂。
1. __proto__ 与 prototype
__proto__与prototype都是维护原型链重要的部分,但这两种有什么区别呢?
-
__proto__: JS每个对象内部都隐含了一个__proto__属性,用来指向它的原型对象,即xxx.prototype。例如对象a:let a = {name: "example"} -
prototype: 与__proto__不同的是,这个属性只有函数才有。并且添加到prototype上的属性和方法在所有实例中都是共享的。譬如Foo.prototype,可以看到Foo.prototype中也有__proto__属性,指向它的原型对象。
2. 实现继承
通过原型很容易实现对象间的继承,JS中继承是通过委托机制实现的。最常用的一种方式是通过Object.create(..)方法。
function Foo() {
this.name = "Foo";
}
Foo.prototype.sayName = function () {
return "This is " + this.name;
}
function Bar() {
this.name = "Bar";
}
// 让Bar继承Foo
Bar.prototype = Object.create(Foo.prototype);
let bar = new Bar();
bar.sayName(); // This is Bar
通过Object.create(..)方法,让Bar的原型链指向Foo的原型实现继承效果。如图所示:
为了详细了解继承效果,我们也可以自己动手实现。自己实现一个生成子类的方法subClassOf()。
function subClassOf(o) {
function F() {};
F.prototype = o;
return new F(); // new F() 产生一个新对象,该对象内部__proto__关联F.prototype.
}
Bar.prototype = subClassOf(Foo.prototype);
let bar = new Bar();
bar.sayName(); // This is Bar
原理其实很简单,即通过一个中间函数F作为中间桥梁,连接Bar和Foo的原型链,如下图所示:
这里注意的是,如果写成Bar.prototype = subClassof(Foo)这样的话,构造的原型链是不成立的,如图:
这里F.prototype = Foo,并没有指向Foo.prototype, 导致原型链断裂,当调用bar.sayName()时,会报找不到对应方法,因为该方法是在Foo.prototype上的。
JS Object Layout
这张图是该篇博文中的一张JS原型关系图,这张图很清楚的显示了原型中的各种关系。
通过这副图可以总结以下几个要点:
Foo()函数和其实例f1都可以访问到Foo.prototype。因此两者都可以访问原型链上的方法。Foo.prototype的constructor是指向自身函数Foo()的。constructor存在的意义是为了让实例对象f1可以借助constructor访问定义在Foo()函数中的属性和方法。
注意以下这种情况:
Bar.prototype = new Foo();
这种情况下Bar.prototype.constructor的指向是错误的,此时是指向的Foo函数,所以必须要修正constructor:Bar.prototype.constructor = Bar。
3. 原型链获取
获取一个对象的原型链,可以直接通过__proto__获取,如 bar.__proto__。但这是一种非标准的方法,并不是所有浏览器都支持。ES5中的标准方法是Object.getPrototypeOf()。
Bar.prototype = new Foo();
// 修正前
Bar.prototype.constructor === Bar; // false;
// 修正后
Bar.prototype.constructor = Bar;
Bar.prototype.constructor === Bar; // true;
Object.getPrototypeOf(bar) === bar.__proto__ === Bar.prototype
4. 对象关联
如果要使两个对象关联起来,比如让对象bar关联对象foo, 同样使用Object.create()方法
function Foo() {
this.name = "Foo";
this.something = function() {
console.log("This is something");
}
}
Foo.prototype.sayName = function () {
console.log("This is " + this.name);
}
// bar 关联 foo
let bar = Object.create(foo);
这样bar可以调用foo的something()方法和sayName()方法。
这里需要注意的是,bar对象直接关联到foo对象,与上述讲的继承关系又不一样了,继承时是通过Bar.prototype = Object.create(Foo.prototype)将原型链进行关联的。可以看到:
Object.getPrototypeOf(bar) === Bar.prototype; // false
Object.getPrototypeOf(bar) === foo; // true
这里仅仅是两个对象的关联!因此不仅可以调用Foo.prototype原型上的方法,也可以调用foo对象上的方法。如图: