原型链

1. 原型链类

1.1 创建对象有几种方法

1.1.1 字面量
1
2
var o1 = {name: 'o1'};
var o2 = new Object({name: 'o2'});

结果:

1
2
{name: "o1"}  
{name: "o2"}
1.1.2 显式的构造函数
1
2
var M = function(){this.name='o3'};
var o3 = new M();

结果:

1
M {name: "o3"}

注:任何函数被 new 运算符计算以后,都可以被称之为构造函数。

1.1.3 Object.create
1
2
var P = {name: 'o4'};
var o4 = Object.create(P);

结果:

1
{}

解释:
Object.create() 方法创建的对象是用原型链来连接的,即把参数(对象)作为原型对象的。

1
o4.__proto__ // p

1.2 原型链、构造函数、实例和原型链

  • 构造函数通过 new 运算符得到实例
  • 用 new 运算符得到的函数为构造函数,相反则是普通函数。任何函数都可以被当作构造函数
  • 函数都有一个 prototype 属性(声明函数时自动添加),该属性指向原型对象
  • 原型对象通过 constructor(构造器) 来获得被引用的构造函数

举个栗子:

1
2
var M = function(){this.name='o3'};
var o3 = new M();

构造器:M
实例:o3

1
M.prototype.constructor === M

输出: true

1
o3.__proto__ === M.prototype

输出: true

原型链:从实例往上找,找到它的原型对象,原型对象再向上找到原型对象,直到 Object 的原型对象。这一过程是通过 __proto__ 向上找的。

多个实例共用一个原型对象,原型对象上的方法被实例共享,举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var M = function(){
this.name='o3';
};
var o3 = new M();

// 增加方法
M.prototype.say = function(){
console.log('say hi');
}

var o3= new M();
var o5= new M();


o3.say() // say hi
o5.say() // say hi

如果实例的某个方法,如果实例本身找不到这个方法时,将会沿着原型链,从它的原型对象上找,如果找到,则停止查找,返回。反之,继续向上一级原型对象上找,直到 Object。

: 函数才会有 prototype,只有对象(函数既是函数也是对象)才有 proto

1
M.__proto__  === Function.prototype;

输出: true

1.3 instanceof 的原理

instanceof 实际判断的是实例对象的 __proto__ 和构造函数的 prototype 是不是同一个引用(原型对象),如果是,则返回 true,反之返回 false。判断时,但原型链上的原型对象都会返回 true。

举个栗子:

1
2
3
4
5
6
7
8
var M = function(){
this.name='o3';
};
var o3 = new M();


o3 instanceof M // true
os instanceof Object // true

因为

1
2
o3.__proto__ === M.prototype // true
M.prototype.__proto__ === Object.prototype // true

问:如果 a 继承 b,b 继承 c,a 生成的实例 instanceof b 或 c 都会返回 true,如何判断它是哪个的实例呢?
答: constructor

1
2
o3.__proto__.constructor === M; // true
o3.__proto__.constructor === Object // false

1.4 new 运算符

new 运算符是用于执行构造函数,创建新实例的。

一些碎碎念:

任何函数都可以通过 函数名() 的方式被执行,但这种执行方式 this 代码不做任何操作,只执行可执行的代码。

以下模拟了新对象被创建的过程(对应上图的是三个步骤):

1
2
3
4
5
6
7
8
9
10
var new2 = function(func){
// 假设 func 就是构造函数
var o = Object.create(func.prototype)'
var k = func.call(o);
if(typeof k === 'object'){
return k
} else {
return o
}
}

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
function Foo(){
this.baz = 'abc';
this.bar = 123;
}

Foo.prototype.foobar = 'foobar'

var foo = new Foo();

foo.baz; // 'abc'
foo.bar; // 123
foo.__proto__.foobar; // 'foobar'

上边的栗子中,由于没有返回对象,因此 new 出来的结果就是 this 指定为新实例的上下文,对象包含 bazbar 属性。

同时,由于新实例继承了 Foo.prototype,所以实例的原型对象上会有 foobar 属性。

如果改成下边这样:

1
2
3
4
5
6
7
8
9
10
11
function Foo(){
this.baz = 'abc';
this.bar = 123;

return {
a: 'a',
b: 'b'
}
}

var foo = new Foo();

由于构造函数中返回了对象,那么返回的对象会代替成为新实例。因此在新实例中并不会包含属性 baz 和 bar,最终的返回结果是:

1
2
3
4
foo.baz; // undefined
foo.bar; // undefined
foo.a; // 'a'
foo.b; // 'b'

特别要注意的一点。新对象被创建继承的是 foo.prototype,而不是 foo!因此,构造函数本身的属性是不会出现在 new 出的对象中的,但会出现在实例对象的构造函数上。

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
function Foo(){
this.bar = 123;
}

// 添加属性
Foo.baz = 2;
// 创建新实例
var foo = new Foo();

foo.bar; // 123
foo.baz; // undefined
foo.__proto__.constructor.baz // 2

创建出的新实例是没有 baz 属性的,只有构造器中的 this 属性和方法会出现在新实例中。

我们把 foo 对象打印出来,会发现 baz 出现在 foo.__proto__.constructor