本文主要对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对象上的方法。如图: