JavaScript的原型和原型链

更新时间:2018-12-04 15:43:09点击次数:234次
一、原型(prototype)

prototype属性和[[prototype]]内部属性
1.1. 任何一个函数,都有一个prototype属性,它指向prototype对象。prototype对象中有一个constructor属性,constructor又指向函数本身。

function foo() {
  console.log('hello');
}
foo.prototype.constructor == foo; //true

1.2. 任何一个对象,都有一个内部的[[prototype]]属性,它指向这个对象的构造函数的prototype对象。[[prototype]]是ECMAScript定义的内部属性,在脚本中不可直接访问,但浏览器对每个对象都支持一个__proto__属性,脚本可以访问__proto__,等同于[[prototype]]。

const obj = {};  //等同于const obj = new Object({});
obj.__proto__ == Object.prototype;  //true

构造函数
2.1. 构造函数也是普通的函数,所以它也有prototype属性。prototype的constructor指向构造函数本身。

function Food(name) {
  this.name = name;
}
Food.prototype.constructor == Food; //true

2.2. 构造函数如果不使用new关键字调用,跟普通函数没有区别。如果使用new关键字调用,将按照以下步骤执行。

创建一个新对象。
将this指向这个新对象。
执行构造函数的代码。
返回这个新对象。
function Food(name) {
  this.name = name;
  this.eat = function () {
    console.log('eat!');
  }
}
const food = new Food('apple');
food.__proto__ == Food.prototype; //true

原型对象
原型对象的用途是它可以包含所有实例共享的属性和方法。
如上面的Food构造函数,eat方法是可以被所有实例共享的,所以,可以把它放在原型对象里来定义。

function Food(name) {
  this.name = name;
  this.eat = function () {
    console.log('eat!');
  }
}

const food1 = new Food('apple');
const food2 = new Food('banana');
food1.eat == food2.eat; // false

function NewFood(name) {
  this.name = name;
}
NewFood.prototype.eat = function () {
  console.log('eat!');
}
const newFood1 = new NewFood('apple');
const newFood2 = new NewFood('banana');
newFood1.eat == newFood2.eat; // true

查找属性
当代码读取对象的属性时,会按照特定的顺序来执行搜索。

function Food(name) {
  this.name = name;
}
Food.prototype.name = 'pear';
Food.prototype.eat = function () {
  console.log('eat!');
}
const food = new Food('apple');
console.log(food.name); // apple

结合上图分析,查找food.name的过程,首先在实例本身查找,找到属性name,那么停止查找。查找food.eat(),也是先在实例本身查找,并没有找到,则通过内部的[[prototype]]在构造函数的prototype对象中查找,找到了,则执行food.eat().

二、原型链

链的形成
在上面的图中,food的内部指针[[prototype]]指向了构造函数的原型对象,那么如果又有另一个构造函数的原型,指向了food,那么就形成了一个由原型组成的链。

function Food(name) {
  this.name = name;
}
Food.prototype.eat = function() {
  console.log('eat!');
}
function Vegetable(name, color) {
  this.name = name;
  this.color = color;
}
Vegetable.prototype = new Food(); // 链的形成
const tomato = new Vegetable('tomato', 'red');
console.log(tomato.name); // 输出 tomato
console.log(tomato.color); // 输出 red
tomato.eat(); // 输出 eat!

属性查找
当脚本访问对象的属性时,将沿着原型链向上寻找。具体来说,首先查找对象本身,如果没有找到,则在构造函数的原型中寻找,如果还是没有找到,则继续向上,向构造函数的原型的[[prototype]]所指向的原型对象中去寻找,以此类推。

实现方法
在上面的栗子中,Food的属性值都是基本数据类型,但如果属性值是引用类型,例如在下面代码中新增的regions属性,tomato和potato将会共用属性regions。修改了tomato的regions属性,发现potato的regions属性也跟着变化了。这是因为通过继承,regions出现在了Vegetable的原型对象中,所以,Vegetable的所以实例都共享了regions。

function Food(name) {
  this.name = name;
  this.regions = ['north', 'south']; // 属性值是引用类型
}
Food.prototype.eat = function() {
  console.log('eat!');
}
function Vegetable(name, color) {
  this.name = name;
  this.color = color;
}
Vegetable.prototype = new Food();
const tomato = new Vegetable('tomato', 'red');
const potato = new Vegetable('potato', 'yellow');
console.log(`tomato: ${tomato.regions}`); // tomato: ['north', 'south']
console.log(`potato: ${potato.regions}`); // potato: ['north', 'south']
tomato.regions.push('east');
console.log(`tomato: ${tomato.regions}`); // tomato: ['north', 'south', 'east']
console.log(`potato: ${potato.regions}`); // potato: ['north', 'south', 'east']

假如你的需求场景,确实是要共用regions的,这当然没有问题。但通常情况下,我们都是希望实例间可以保持属性的私有和方法的共享。
在实际工作中,应用最广泛的继承实现方法是组合继承( combination inheritance),也叫做伪经典继承。它综合利用原型和构造函数的优点,实现了实例之间的属性私有和方法共享。

function Food(name) {
  // 把需要私有的属性,都放在构造函数内部
  this.name = name;
  this.regions = ['north', 'south'];
}
// 把需要被共享的方法,放在原型对象中
Food.prototype.eat = function() {
  console.log('eat!');
}
function Vegetable(name, color) {
  // 通过call方法,使得Food的name和regions也成为了Vegetable的属性
  Food.call(this, name);  // 实现了Vegetable对Food的属性的继承
  this.color = color;
}
Vegetable.prototype = new Food();
const tomato = new Vegetable('tomato', 'red');
const potato = new Vegetable('potato', 'yellow');
console.log(`tomato: ${tomato.regions}`); // tomato: ['north', 'south']
console.log(`potato: ${potato.regions}`); // potato: ['north', 'south']
tomato.regions.push('east');
console.log(`tomato: ${tomato.regions}`); // tomato: ['north', 'south', 'east']
console.log(`potato: ${potato.regions}`); // potato: ['north', 'south']

总结
ECMAScript把原型链作为实现继承的最主要方法。
原型链的基本思想是:利用原型,让一个引用类型继承另一个引用类型的属性和方法。
原型链的实现方式:每一个构造函数,都包含一个原型对象,而构造函数的实例也有一个内部指针,指向原型对象。如果让这个实例等于另一个构造函数的原型对象,那么就形成了一个链,是由原型组成的链,也就是原型链。

本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责,本站只提供参考并不构成任何投资及应用建议。本站是一个个人学习交流的平台,网站上部分文章为转载,并不用于任何商业目的,我们已经尽可能的对作者和来源进行了通告,但是能力有限或疏忽,造成漏登,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

  • 项目经理 点击这里给我发消息
  • 项目经理 点击这里给我发消息
  • 项目经理 点击这里给我发消息
  • 项目经理 点击这里给我发消息