乐趣区

从一道题解读JS原型链

之前对 js 原型和原型链的理解一直觉得很绕,绕来绕去的,在看了《JavaScript 高级程序设计》和各种文章之后,终于对原型和原型链有了初步的了解,可是还是没有很深入的了解,今次通过以前段时间遇到的一道题,分析一下,用自己的想法进行解读,加深自己对原型和原型链的理解。
一、题目
下面程序运行结果是什么?
function Animal() {
this.name = ‘Animal’;
}

Animal.prototype.changeName = function (name) {
this.name = name;
}

function Cat() {
this.name = ‘Cat’;
}

var animal = new Animal();

Cat.prototype = animal;
Cat.prototype.constructor = Cat;

var cat = new Cat();

animal.changeName(‘Tiger’);

console.log(cat.name)
A. AnimalB. Cat C. Tiger D. 都不是
答案是 B Cat
二、解读
1. 原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。在默认情况下,所有的原型对象都会自动获得一个 constructor(构造函数)属性,这个属性是一个指向 prototype 属性所在函数的指针。下面用图来说明
function Animal() {
this.name = ‘Animal’;
}
Animal.prototype.changeName = function (name) {
this.name = name;
}

首先创建了一个 Animal 函数,Animal 中含有一个 prototype 属性,指向 Animal Prototype,而 Animal.prototype.constructor 指向 Animal。这个时候由于 name 属性是在函数中定义的,所以不在 Animal Prototype 中,而 changeName 函数是通过 Animal.prototype.changeName 定义的,所以我们可以通过这种方式,在实例化多个对象时,共享原型所保存的方法。同理,当创建了 Cat 函数时,也是一样。
function Cat() {
this.name = ‘Cat’;
}

2. 创建实例
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。在 ECMA-262 第 5 版中管这个指针叫 [[Prototype]]。虽然在脚本中没有标准的方式访问 [[Prototype]],但 Firefox、Safari 和 Chrome 在每个对象上都支持一个属性__proto__。明确重要的一点,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。

// 将 Cat 的原型对象指向 animal 实例,获得 animal 中的属性,原有的属性丢失
Cat.prototype = animal;

这一部分相当于是把 Cat 的原型对象的指针指向了 animal 实例,所以原来 Cat 原型对象中的 constructor 属性丢失,替换成了 animal 实例中的属性,包括 name 属性以及__proto__内部属性,同时__proto__属性也指向 Animal.prototype,因此 Cat 也可以通过原型链查找调用到 Animal 中的属性和方法。
// 相当于重新创建了 constructor,指向 Cat 构造函数
Cat.prototype.constructor = Cat;

这一部分相当于是重新在原型对象中创建了一个 constructor 属性,同时指向 Cat 构造函数。
var cat = new Cat(); // 实例化一个 Cat 对象,跟实例化 Animal 相似

3. 调用方法
animal.changeName(‘Tiger’);
当 var animal = new Animal(); 实例化了一个 Animal 对象后,animal 都包含一个内部属性,该属性指向了 Animal.prototype;换句话说,animal 与构造函数 Animal 没有直接的关系。可是,可以看到虽然在实例中不含 changeName,但我们却可以调用 animal.changeName(name),这是通过查找对象属性的过程来实现的,即:
首先查找实例中实例中 animal 是否有 changeName 方法,如果没有则继续寻找,去到 Animal.prototype 寻找是否有 changeName 方法,如果有则调用,没有则继续寻找,到 Object.prototype 中寻找,最后没找到则会返回一个 null。
很明显,在这里实例 animal 中没有 changeName 方法,所以需要到 Animal.prototype 寻找 changeName 方法,并调用成功修改了实例 animal 中的 name 属性,为 Tiger。
这个时候由于 Cat.prototype 是指向实例 animal 的,因此 Cat.prototype 中的 name 属性也变为 Tiger。

console.log(cat.name) // Cat
最后,获取 cat.name,与查找方法同样,也是先去实例中 cat 查找是否含有 name 属性,在这里很明显是存在的,因此直接结束寻找,此时 cat.name = ‘Cat’。
三、总结
通过这道题,加深了我对原型和原型链的理解,其实这道题也可以扩展到关于继承的知识点,在 JavaScript 中实现继承主要是依靠原型链来实现。之后等我再搞的更清楚一点再继续写吧。
此文章是本人自己对于原型和原型链的一点小小的理解,中间可能存在偏差或者错误的,请多多指点!!!

退出移动版