JavaScript的原型和原型链

一、原型(prototype)

  1. 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
  1. 构造函数

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

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

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

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

原型对象的用途是它可以包含所有实例共享的属性和方法。

在下面的代码中,当eat方法被包含在构造函数中时,food1和food2被实例化时,将会分别创建一个eat方法。同理,当有很多实例时,那么将会有很多eat方法被创建出来,这无疑会造成资源的浪费。
而将eat方法放在构造函数的prototype对象中,如代码中的NewFood所示,不论创建多少个实例,他们都共用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

在这里插入图片描述

  1. 查找属性

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

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().

二、原型链

  1. 链的形成
    在上面的图中,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!
  1. 属性查找

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

  1. 实现方法

在上面的栗子中,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']

如下图所示,在访问tomato.regions的时候,先在tomato实例本身查找regions,没有找到,则到tomato.__proto__所指的原型对象中去搜索,找到了regions,将它修改为[‘north’, ‘south’, ‘east’]。当访问potato.regions的时候,同理会访问到原型对象的regions,所以得到的结果是[‘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的属性,
  // 这样既实现了Vegetable对Food的属性的继承,
  // 又使这些属性对Vegetable的所有实例来说,是私有的
  Food.call(this, 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']

结合代码和下图分析,当访问tomato.regions的时候,首先查找tomato本身,找到了regions属性,则执行push操作。由于tomato修改的是它本身的regions属性,所以potato.regions不受影响。
得到这个结果的根本原因在于:当访问tomato.regions的时候,根据属性查找的顺序规则,原型对象上的regions属性被屏蔽掉了。

在这里插入图片描述

总结

  1. ECMAScript把原型链作为实现继承的最主要方法。
  2. 原型链的基本思想是:利用原型,让一个引用类型继承另一个引用类型的属性和方法。
  3. 原型链的实现方式:每一个构造函数,都包含一个原型对象,而构造函数的实例也有一个内部指针,指向原型对象。如果让这个实例等于另一个构造函数的原型对象,那么就形成了一个链,是由原型组成的链,也就是原型链。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值