面向对象编程 (一):原型及原型链

  原型链被称为”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指向这个新对象)
执行构造函数中代码(为这个对象添加属性)
返回新对象

  1. 创建一个新对象
  2. 将构造函数作用域赋值给新对象,并执行构造函数。
  3. 如果构造函数没有返回值或者有返回值但返回值不是一个对象时,返回新创建的对象,否则返回构造函数指定的返回值。

  听起来可能会有点难以理解,下面我们来实现一下。
  这里我们写一个构造函数来模拟 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 为止。