面向对象编程 (一):原型及原型链
原型链被称为”JS 三座大山之一”,初学 JS 的我也是深受其困扰,陆陆续续看了许多博客与文档,终于对这方面的知识有一些自己的理解了。今天就来谈谈原型链。
我们按照下面这些顺序来解释原型链:
- 如何创建一个对象
- 构造函数
- 构造函数使用 new 创建一个对象过程
- 原型
- 原型链
如何创建一个对象?
创建对象大致看来有三种方式:
1. 使用字面量方式
var o1 = { name: 'o1' };
var o2 = new Object({
name: 'o2'
});
2. 使用构造函数方式
var Foo (name) {
this.name = name;
}
var o3 = new Foo('o3');
3. Object.create() 方式
var p = { name: 'o4' }
var o4 = Object.create(p);
其中的原型链就跟第二种方式有关。
构造函数
构造函数其实就是函数,当直接调用时它就是普通的函数,当使用 new 操作符调用时它就是构造函数。我们来看一个例子:
function Foo (name) {
this.name = name;
}
// 此时 Foo 是普通函数
Foo('narmal');
// 此时 Foo 是构造函数
var foo = new Foo('constructor');
这里有一个地方我们要注意,构造函数一般首字母要大写,这是编码规范,为了让别人也清楚这是一个构造函数。
构造函数使用 new 创建一个对象
刚才我们说了,一个普通的函数通过 new 运算符调用它就是构造函数。下面我们来看看构造函数使用 new 操作符创建一个对象过程。
创建一个新对象
将构造函数作用域赋值给新对象(this指向这个新对象)
执行构造函数中代码(为这个对象添加属性)
返回新对象
- 创建一个新对象
- 将构造函数作用域赋值给新对象,并执行构造函数。
- 如果构造函数没有返回值或者有返回值但返回值不是一个对象时,返回新创建的对象,否则返回构造函数指定的返回值。
听起来可能会有点难以理解,下面我们来实现一下。
这里我们写一个构造函数来模拟 new 操作符。
function new2(fn) {
// 1. 创建一个空对象
var obj = Object.create(fn.prototype);
// 2. 调用构造函数,并把上下文指定为新创建的对象
var r = fn.call(obj);
// 判断构造函数返回值是否为一个对象
if(r instanceof Object) {
// 是对象,返回该对象
return r;
} else {
// 不是对象,返回 obj
return obj;
}
}
function Foo () {
this.name = 'Foo';
}
var o1 = new2(Foo);
var o2 = new Foo();
o1.__proto__ === o2.__proto__; // true
原型规则
在讲原型之前,我们先来了解几条原型规则:
1. 所有的引用类型(数组,对象,函数)都可以自由扩展属性(null 除外)
这句话是什么意思呢,我们通过代码来看一下
var arr = [];
var obj = {}
var fun = function () {}
arr.a = 100;
obj.a = 100;
fun.a = 100;
以上这些给引用类型添加属性都是合法行为。
2. 所有的引用类型(数组,对象,函数)都有一个 __proto__(隐式原型) 属性,属性值是一个普通对象
console.log(arr.__proto__)
console.log(obj.__proto__)
console.log(fun.__proto__)
通过以上方法我们就可以访问到引用类型(数组、对象、函数)的隐式原型 __proto__ 属性,
3. 所有的函数都有一个 prototype(显式原型) 属性,属性值也是一个普通对象
console.log(fun.prototype)
通过以上方法可以访问到函数的显式原型 prototype,但是有一点需要注意:prototype 属性只有函数拥有。
4. 引用类型的 __proto__ 属性指向它构造函数的 prototype 属性
function Foo() {
this.name = 'Foo';
}
var foo = new Foo({ name: 'a' });
foo.__proto__ === Foo.prototype // true
上面这个例子,Object 是 obj 的构造函数,obj 是 Object 的一个实例,obj 的 __proto__ 属性指向 Object 的 prototype 属性,也就是说 obj 的 __proto__ 属性和 Object 的 prototype 属性是同一个对象。
5. 当试图得到一个引用类型的某个属性时,如果这个对象本身没有这个属性,那么会去它的 __proto__ 属性(也就是它构造函数的 prototype 属性)中寻找
// 构造函数
var Foo (name) {
this.name = name;
this.alertName = function () {
alert(this.name);
}
}
// 在原型对象上增加一个 printName 方法
Foo.prototype.printName () {
console.log(this.name);
}
// 创建一个实例
var foo = new Foo('foo');
// 实例本身的属性方法
this.alertName();
// 实例本身没有这个方法,在它的 __proto__ 找到的这个方法
this.printName();
看完上面几条规则也许还是一头雾水,没关系,我们来总结一下。上面的规则也就是说所有的引用类型都拥有一个 __proto__ 属性,这个属性和它构造函数的 prototype 属性指向同一个普通对象。
还是不理解?还是没关系,记住,原型对象就是一个普通的对象。
原型
铺垫了这么多,现在,我们来看看到底什么是原型。

我们来看看原型对象、构造函数、实例之间的关系
1. 构造函数-实例
构造函数通过 new 运算符生成可以生成一个实例。
2. 实例-原型对象
实例是一个对象,根据我们的规则 2,它有一个 __proto__ 属性,这个属性就指向它的原型对象。
3. 构造函数-原型对象
我们说过,构造函数就是普通函数,根据我们的规则 3,这个属性也是一个对象。并且构造函数的 prototype 属性和它实例的 __proto__ 指向的是同一个对象。
慢着,还没结束!
我们可以看到上图,原型对象上还有一个属性指向了构造函数,这个属性就是 constructor (构造器)。这点也很重要,记住:每个构造函数原型对象的 consuturtor 属性都指向构造函数本身!这么说起来可能有点难理解,请结合上图理解。
4. 总结
每个函数在创建时都有一个 prototype 属性,这个属性指向一个对象。使用 new 操作符调用构造函数后,生成的实例有一个 __proto__ 属性,这个属性也指向一个对象,这个对象就是该实例的原型对象。
最重要的一点是:实例的 __proto__ 属性和构造函数的 prototype 属性指向的同一个对象。
function Foo() {
this.name = 'name';
}
var foo = new Foo();
foo.__proto__ === Foo.prototype // true
原型链
我们从一个例子开始讲起。
function Foo() {
this.name = 'Foo';
this.alertName = function() {
alert(this.name);
}
}
Foo.prototype.printName = function() {
console.log(this.name);
}
var foo = new Foo();
foo.alertName();
foo.printName();
foo.toString();
alertName() 是 foo 自身的属性,所以访问时直接执行。
printName() 不是 foo 自身的属性,在访问时自身没有找到,就去 foo.__proto__ 的属性上寻找,在这里找到后执行。
toString() 不是 foo 自身的属性,在访问时自身没有找到,就去 foo.__proto__ 的属性上寻找,在这里也没有找到。因为 foo.__proto__ 也是一个对象,所以就去这个对象的 __proto__ 属性上寻找,也就是 foo.__proto__.__proto__ 属性上寻找,在这里找到后执行。
像上面这个例子中,访问一个对象的属性时,对象自身如果没有这个属性,就去它的原型对象上寻找,如果原型对象上还是没有这个属性,就再去这个原型对象的原型对象上寻找,这些原型对象就构成了一个原型链。
关于原型链有这样一个图,便于理解。

Foo 是一个构造函数,f 是 Foo 的实例。
当访问 f 的属性时,如果它自身没有,就会去它的原型对象 f.__proto__ 上找,也就是构造函数的原型 (Foo.prototype) 。如果 Foo.prototype 上没有,会再去它的原型对象 (f.__proto__.__proto__) 上寻找,也就是它构造函数的原型 Object.prototype。如果找到了就返回,如果没有找到,就逐层往上寻找,一直到访问到 null 为止。