原型和原型链

在开始之前,先解释两个概念:构造函数、原型对象

构造函数

定义

构造函数(constructor)属于被实例化的特定类对象。构造函数初始化这个对象,并提供可以访问其私有信息的方法。构造函数的概念可以应用于大多数面向对象的编程语言。本质上,JavaScript 中的构造函数通常在类的实例中声明。

语法

1
2
3
4
5
6
7
// 这是一个通用的默认构造函数类 Default
function Default() {
}

// 这是一个带参数声明的重载构造函数类 Overloaded
function Overloaded(arg1, arg2, ...,argN){
}

要调用 JavaScript 中类的构造函数,请使用 new 操作符将新的对象引用分配给一个变量 —— 使用 new 来创建对象。

1
2
3
4
5
6
function Default() {
/*这就是构造函数*/
}

// 分配给局部变量 defaultReference 的一个新的 Default 对象引用
var defaultReference = new Default();

原型对象

定义

在声明了一个函数之后,浏览器会自动按照一定的规则创建一个对象,这个对象就叫做原型对象。这个原型对象其实是储存在了内存当中。

原型

什么是原型

  • 所有引用类型都有一个__proto__(隐式原型) 属性,属性值是一个普通的对象
  • 所有函数都有一个 prototype (原型) 属性,属性值是一个普通的对象
  • 所有引用类型的__proto__属性指向它构造函数的 prototype

在声明了一个函数后,这个构造函数(声明了的函数)中会有一个属性 prototype,这个属性指向的就是这个构造函数(声明了的函数)对应的原型对象;原型对象中有一个属性 constructor,这个属性指向的是这个构造函数(声明了的函数)。下面一张图可以很简单理解:

构造函数和原型对象
构造函数和原型对象

当我们使用 new 关键字创建的时候,示例代码如下:

1
2
3
4
5
6
7
8
<script>
function Default() {
/*这就是构造函数*/
}

var def = new Default()
console.log(Default);
</script>

此时,def 是构造函数 Default 创建出来的对象,这个 def 对象没有 prototype 属性,prototype 属性只有在构造函数 Default 中才有。

原型
原型

可以看出:

  • 构造函数 Default 中也有 prototype 属性,指向的是其对应的原型对象(Default.phototype)。
  • def 是构造函数 Default 创建出来的对象,它不存在 prototype 属性,所以在调用的时候显示 undefined。
  • 但 def 有一个__proto__属性,def 调用这个属性可以直接访问到构造函数 Default 的原型对象(def.__proto__)。

一般情况下,实例对象.__proto__才能访问到构造函数中的 prototype 的属性或者方法,即 per.__proto__.eat ()。因为__proto__不是标准的属性,所以直接写成 per.eat () 即可,原型是一个属性,而这个属性也是一个对象。

图示如下:

原型2
原型 2

要点

  1. 所有的引用类型(数组、函数、对象)可以自由扩展属性(除 null 以外)。
  2. 所有的引用类型都有一个__proto__属性 (也叫隐式原型,它是一个普通的对象)。
  3. 所有的函数都有一个’prototype’属性 (这也叫显式原型,它也是一个普通的对象)。
  4. 所有引用类型,它的__proto__属性指向它的构造函数的 prototype 属性。
  5. 当试图得到一个对象的属性时,如果这个对象本身不存在这个属性,那么就会去它的__proto__属性 (也就是它的构造函数的 prototype 属性) 中去寻找。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<body>
<script type="text/javascript">
function students () {
}
// 可以使用students.prototype 直接访问到原型对象
//给students函数的原型对象中添加一个属性 name并且值是 "张三"
students.prototype.name = "张三";
students.prototype.age = 20;

var stu = new students();
/*
访问stu对象的属性name,虽然在stu对象中我们并没有明确的添加属性name,但是
stu的__proto__属性指向的原型中有name属性,所以这个地方可以访问到属性name
就值。
注意:这个时候不能通过stu对象删除name属性,因为只能删除在stu中删除的对象。
*/
alert(stu.name); // 张三

var stu1 = new students();
alert(stu1.name); // 张三 都是从原型中找到的,所以一样。

alert(stu.name === stu1.name); // true

// 由于不能修改原型中的值,则这种方法就直接在stu中添加了一个新的属性name,然后在stu中无法再访问到
//原型中的属性。
stu.name = "李四";
alert(stu.name); //李四
// 由于stu1中没有name属性,则对stu1来说仍然是访问的原型中的属性。
alert(stu1.name); // 张三
</script>
</body>

总结相关的几个属性

  1. prototype 属性

    prototype 存在于构造函数中 (其实任意函数中都有,只是不是构造函数的时候 prototype 我们不关注而已) ,他指向了这个构造函数的原型对象。

  2. constructor 属性

    constructor 属性存在于原型对象中,他指向了构造函数

    1
    2
    3
    4
    5
    <script type="text/javascript">
    function students () {
    }
    alert(students.prototype.constructor === students); // true
    </script>

    我们根据需要,可以 students.prototype 属性指定新的对象,来作为 students 的原型对象。但是这个时候有个问题,新的对象的 constructor 属性则不再指向 students 构造函数了。

  3. __proto__ 属性 (注意:左右各是 2 个下划线)

​ 用构造方法创建一个新的对象之后,这个对象中默认会有一个属性 proto, 这个属性就指向了构造方法的原型对象。

原型链

根据要点 5,当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找,即它的构造函数的 prototype,如果还没有找到就会再在构造函数的 prototype 的__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 构造函数
function Foo(name,age){
this.name=name;
this.age=age;
}
Object.prototype.toString=function(){
//this是什么要看执行的时候谁调用了这个函数。
console.log("I'm "+this.name+" And I'm "+this.age);
}
var fn=new Foo('小明',19);
fn.toString(); //I'm 小明 And I'm 19
console.log(fn.toString===Foo.prototype.__proto__.toString); //true

console.log(fn.__proto__ ===Foo.prototype)//true
console.log(Foo.prototype.__proto__===Object.prototype)//true
console.log(Object.prototype.__proto__===null)//true

分析图如下:

原型链
原型链

  • 首先,fn 的构造函数是 Foo ()
  • 所以:fn.__proto__=== Foo.prototype
  • 又因为:Foo.prototype 是一个普通的对象,它的构造函数是 Object
  • 所以:Foo.prototype.__proto__=== Object.prototype
  • 通过上面分析得知:toString () 方法是在 Object.prototype 里面的,当调用这个对象的本身并不存在的方法时,它会一层一层地往上去找,一直到 null 为止。

所以当 fn 调用 toString () 时,JS 发现 fn 中没有这个方法,于是它就去 Foo.prototype 中去找,发现还是没有这个方法,然后就去 Object.prototype 中去找,找到了,就调用 Object.prototype 中的 toString () 方法。

这就是原型链,fn 能够调用 Object.prototype 中的方法正是因为存在原型链的机制。

另外,在使用原型的时候,一般推荐将需要扩展的方法写在构造函数的 prototype 属性中,避免写在__proto__属性里面