本篇文章通过实例讲解了从原型到原型链。
先来看一个通过构造函数创建实例对象的一个简单示例:
function Person() { // 构造函数
};
Person.prototype.name = "liu";
var person1 = new Person(); // 实例对象
var person2 = new Person(); // 实例对象
console.log(person1.name); // liu
console.log(person2.name); // liu
上述代码中给 Person 构造函数的原型属性上添加了 name 属性,此时通过 Person 构造函数创建的实例对象也都具有了 name 的属性 。
下面切入正题,来详细讲解一下函数的原型。
一、属性1:prototype(译:原型)
prototype 是函数的一个属性,它指向了一个对象 。
这个对象正是调用这个构造函数而创建的实例的原型 。
可以看一下 Person 这个构造函数的打印结果,可以看到是一个对象 。
console.log(Person.prototype); // Object { name: "liu", … }
构造函数与实例原型的关系可以参考下图:

二、属性2:_proto_(译:原型)
_proto_ 也是函数的一个属性,它指向被创建出的实例对象(比如上面说的 person1,person2)的原型。
我们打印一下上边的 person1 或者 person2 的 _proto_ 属性,发现它们都指向了同一个对象。
console.log(person1.__proto__); // Object { name: "liu", … }
console.log(person1.__proto__ === Person.prototype); // true
总结:对于 prototype 和 _proto_ 这两个属性,我个人的理解是:
- 找构造函数的原型就是:构造函数.prototype。
- 找实例对象的原型就是:实例对象._proto_。
此时构造函数与实例原型的关系就可以更新为:

通过上边两个属性我们可以发现构造函数和实例对象都有各自的属性来指向原型,那原型是否有属性来指向构造函数和实例对象呢?
原型指向构造函数的属性是有的,但指向实例对象却没有,因为实例对象是由构造函数创建的,一个构造函数可以创建出多个实例对象,接下来我们看一下第三个属性 constructor 。
三、属性3:constructor(译:构造函数)
每一个原型都有一个constructor 属性指向关联的构造函数
我们可以进行如下验证:
console.log(Person.prototype.constructor); // function Person()
console.log(person1.__proto__.constructor); // function Person()
console.log(person2.__proto__.constructor); // function Person()
console.log(Person === Person.prototype.constructor); // true
console.log(Person === person1.__proto__.constructor); // true
那么构造函数的构造函数呢?我们可以打印一下:
console.log(Person.constructor); //function Function()
可以看到它的构造函数就是一个 Function()。
更新一下关系图:

顺便再说一个获取实例对象原型的方法 Object.getPrototypeOf()。
console.log(Object.getPrototypeOf(person1)); // Object { name: "liu", … }
console.log(person1.__proto__); // Object { name: "liu", … }
了解了构造函数、实例原型和 实例之间的关系,我们再来看一下 实例和原型的关系。
四、实例与原型
当读取实例对象的属性时,如果找不到,就会去与对象关联的原型中查找,如果还查不到,就去找原型的原型,一直查到最顶层位置。
举个例子:
function Students() {
this.name = "Jessica";
};
Students.prototype.name = "Daniel";
var student1 = new Students();
console.log(student1.name); // Jessica
student1.name = "Megan";
console.log(student1.name); // Megan
delete student1.name;
console.log(student1.__proto__); // Object { name: "Daniel", … }
console.log(student1.name); // Daniel
在上边的代码中可以看到,我们给构造函数 Students 添加了一个 name 属性并设置为 Jessica。
接着创建一个名为 student1 的实例对象,它的 name 属性值就是 Jessica。
将它的 name 属性值改为 Megan,此时它的 name 属性值就变为 Megan。
当删掉 student1 的 name 属性值后,它就会去原型 (student1._proto_)上去找,此时 name 值就变成了 Daniel。
但是如果还没有找到呢?我们上边说的原型的原型,最顶层位置又是什么呢?
五、原型的原型
先来回忆一下上边说的原型:
console.log(Person.prototype); // Object { name: "liu", … }
console.log(person1.__proto__); // Object { name: "liu", … }
从上边打印结果可以知道,构造函数和实例对象的原型就是一个 Object 对象。
那这个构造函数和 实例对象的原型的原型又是什么?我们打印来试一下:
console.log(Person.prototype.prototype); // undefined
console.log(Person.prototype.__proto__); // Object { … }
console.log(person1.__proto__.prototype); // undefined
console.log(person1.__proto__.__proto__); // Object { … }
从上面的结果可以看出找构造函数和 实例对象的原型的原型用的属性是 _proto_,也就是说找 Object 的原型用的属性是 _proto_。
现在我们知道了,原型就是一个 对象,既然它是一个 对象,那我们就可以通过最原始的方法创建它:
var obj = new Object();
obj.name = "BMW";
console.log(obj); // Object { name: "BMW" }
console.log(obj.name); // BMW
因为原型就是一个对象,所以我们用 _proto_ 来找一下 obj 的原型:
console.log(obj.__proto__); // Object { … }
所以原型是通过构造函数生成的, 实例的proto 指向构造函数的 prototype。
更新一下关系图:

六、原型链
现在我们知道了构造函数和实例对象的原型的原型是一个 Object 对象,那 Object 的原型的原型又是什么呢?
我们来打印一下:
console.log(Object.prototype.__proto__); // null
结果是 null,我们在第4节实例与原型中说过查找实例对象的属性时,如果原型中没有,就去原型的原型中查找,一直查到最顶层,所以查找到 Object.prototype 就可以停止了。
所以最终的关系图可以更新为:

图中由相互关联的原型组成的链状结构就是原型链,例如下边打印出的结果:
console.log(Person.prototype); // Object { name: "liu", … }
console.log(Person.prototype.__proto__); // Object { … }
console.log(Person.prototype.__proto__.__proto__); // null
console.log(person1.__proto__); // Object { name: "liu", … }
console.log(person1.__proto__.__proto__); // Object { … }
console.log(person1.__proto__.__proto__.__proto__); // null
七、补充
1. constructor
在第3节 constructor 属性中我们讲过,每一个原型都有一个 constructor 属性指向关联的构造函数,看下边这个示例:
function Person() {
};
Person.prototype.name = "Rick";
var person3 = new Person();
console.log(Person.prototype.constructor); // function Person()
console.log(person3.__proto__.constructor); // function Person()
console.log(person3.constructor); // function Person()
前两个打印结果可以理解,他们的构造函数都是 function Person(),但是 person3.constructor 打印的结果为什么也是 function Person()?
person3 中其实并没有 constructor 属性,当不能读取到 constructor 这个属性时,会从 person3 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:
console.log(person3.constructor === Person.prototype.constructor); // true
console.log(person3.constructor === person3.__proto__.constructor); // true
2. 继承?
从上面所讲的来看,每一个对象都会从原型上“继承””属性“,这其实是一种迷惑性的说法,《你不知道的JavaScript》中是这样说的:
继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。

网友评论