• 欢迎访问天天编码网站,Java技术、技术书单、开发工具,欢迎加入天天编码
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏天天编码吧
  • 我们的淘宝店铺已经开张了哦,传送门:https://shop145764801.taobao.com/

7.3 原型继承

JS 教程 tiantian 354次浏览 0个评论 扫描二维码

在编程实践中,我们经常希望获取某些代码,并扩展那些代码。

举例,我们具有一个user对象,它具有自己的属性和方法,而且希望通过一些细微的修改来获得另外的adminguest对象。我们可能会希望可以重用user已有的大部分代码,而且不是通过复制/粘帖的方式来进行,仅仅在其基础上创建出另一个对象。

JavaScript 中的原型继承特性就可以帮助我们完成此功能。

[[Prototype]]

在 JavaScript 中,对象具有一个特殊的隐藏属性 [[Prototype]](规范中的名称),它可能为null,或者引用另一个对象。那个对象就被称为是”一个原型”。

7.3 原型继承

那个 [[Prototype]]具有一个“神奇”的含义。当我们希望从object中读取一个属性时,而且该属性不存在的情况下,JavaScript 会自动从那个原型中读取该属性。在编程中,这样的操作被称作“原型继承”。许多酷的语言特性和编程技术都是基于这个原理。

那个[[Prototype]]属性是内部属性,也是隐藏属性,但是存在多种方法来设置它。

其中的一种方式就是使用__proto__,比如:

let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal;

请注意,那个__proto__并不是完全等同于[[Prototype]]属性。那是该属性的 getter/setter。我们将在后面学习另一个设置该属性的方法,但是,现在__proto__可以正确完成功能。

如果我们在rabbit中查找某个属性,而且该属性并不存在,JavaScript 会自动从animal中获取该属性。

举例:

let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal; // (*)

// we can find both properties in rabbit now:
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true

此处的代码行(*)animal设置为rabbit的原型。

然后,当alert尝试读取属性时rabbit.eats (**),该属性不存在于rabbit中,所以 JavaScript 继续跟随那个[[Prototype]]引用,并在animal对象中发现了该属性(从底往上查找):

7.3 原型继承

此处,我们可以说“animal”是”rabbit”的原型,或者说”rabbit“原型继承自”animal”。

所以,如果animal具有一系列实用的属性或者方法时,那么那些属性和方法自动在rabbit中也变得可访问。那些属性被称作为”可继承“。

如果我们在animal中具有一个方法,它也可以在rabbit上被调用:

let animal = {
  eats: true,
  walk() {
    alert("Animal walk");
  }
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

// walk is taken from the prototype
rabbit.walk(); // Animal walk

该方法自动从原型继承获得,比如:

7.3 原型继承

那个原型链可以变得更长:

let animal = {
  eats: true,
  walk() {
    alert("Animal walk");
  }
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

let longEar = {
  earLength: 10,
  __proto__: rabbit
}

// walk is taken from the prototype chain
longEar.walk(); // Animal walk
alert(longEar.jumps); // true (from rabbit)

7.3 原型继承

实际上,原型继承只存在两个限制:

  1. 那个引用不能具有环。当我们赋值__proto__会导致环时,JavaScript会抛出一个错误。
  2. 那个__proto__的值可以是一个对象或者null。其他所有值(比如原始类型)都被忽略。

此外还有一个很明显的注意点:只存在一个[[Prototype]]属性。一个对象无法原型继承两个对象。

读/写 规则

那个原型仅用于属性读取。

对于数据属性(没有 getters/setters),写/删除操作直接应用于原对象。

在下列的示例中,我们将walk方法赋值给rabbit对象:

let animal = {
  eats: true,
  walk() {
    /* this method won't be used by rabbit */
  }
};

let rabbit = {
  __proto__: animal
}

rabbit.walk = function() {
  alert("Rabbit! Bounce-bounce!");
};

rabbit.walk(); // Rabbit! Bounce-bounce!

现在,rabbit.walk()可以直接在rabbit对象本身获取到walk()方法,并执行该方法,而不会在利用原型:

7.3 原型继承

对于 getters/setters —— 如果我们读取/写入一个属性,它们会在原型链中查找并调用。

举例,检查下面示例代码的admin.fullNam属性:

let user = {
  name: "John",
  surname: "Smith",

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  },

  get fullName() {
    return `${this.name} ${this.surname}`;
  }
};

let admin = {
  __proto__: user,
  isAdmin: true
};

alert(admin.fullName); // John Smith (*)

// setter triggers!
admin.fullName = "Alice Cooper"; // (**)

此处的代码行(*),属性admin.fullNam在原型user中具有一个 getter ,所以被调用。在代码行(**)处,在原型user具有一个 setter,所以被调用。

”this“的值

在上述的示例代码中,你可能会有一个这样的疑问:set fullName(value) 中的this的值是什么?this.namethis.surname属性被写到了那个对象上,user还是admin

问题的答案很简单:this不受原型的影响。

不管该方法是在何处被发现:在一个对象中或者在一个原型中。在方法调用中,this的值总是位于点.符号前面的对象。

所以,那个 setter 使用admin作为this,而不是user

实际上,这是一个非常重要的知识点,因为我们可能具有一个非常大的对象,其具有许多的方法,并被继承下来。那么,我们就可以在继承后的对象上运行那些方法,而且那些方法会修改当前对象的状态,而不是那个被继承的大对象。

举例,此处的animal代表一个”方法存储器“,而rabbit使用该存储器。

那个rabbit.sleep()调用是在rabbit对象设置this.isSleeping

// animal has methods
let animal = {
  walk() {
    if (!this.isSleeping) {
      alert(`I walk`);
    }
  },
  sleep() {
    this.isSleeping = true;
  }
};

let rabbit = {
  name: "White Rabbit",
  __proto__: animal
};

// modifies rabbit.isSleeping
rabbit.sleep();

alert(rabbit.isSleeping); // true
alert(animal.isSleeping); // undefined (no such property in the prototype)

7.3 原型继承

如果我们具有其他的继承自animal的对象,比如birdsnake等等,它们同样可以获取访问animal方法的能力。但在每个方法中的this,都是指向当前的实际调用方法对象,其在运行时才决定下来。所以,如果我们往this中写入数据,数据是存储到了那些对象中。

结果就是,方法是共享的,但是对象状态是独享的。

总结

  • 在 JavaScript 中,所有的对象都具有一个隐藏的[[Prototype]]属性,它可能是一个对象或者为null
  • 我们可以使用obj.__proto__来访问该属性(我们在后面介绍其他的方法)。
  • 那个被[[Prototype]]引用的对象被称为”原型“。
  • 如果我们希望读取obj的属性,或者调用其方法,而且它并不存在,那么 JavaScript 就会到其原型中进行查找。写入/删除是直接在该对象上进行操作,它们不使用原型(除非该属性是一个 setter)。
  • 如果我们调用obj.method(),而且该method来自于原型,this将会引用obj。所以,方法总是在当前对象上进行操作,即使该方法来自于原型继承。

任务


利用原型

下面的代码会创建一对对象,然后修改这对对象。

下面代码的结果是什么?

let animal = {
  jumps: null
};
let rabbit = {
  __proto__: animal,
  jumps: true
};

alert( rabbit.jumps ); // ? (1)

delete rabbit.jumps;

alert( rabbit.jumps ); // ? (2)

delete animal.jumps;

alert( rabbit.jumps ); // ? (3)

搜索算法

这个任务具有两个部分。

我们具有一个对象:

let head = {
  glasses: 1
};

let table = {
  pen: 3
};

let bed = {
  sheet: 1,
  pillow: 2
};

let pockets = {
  money: 2000
};
  1. 请使用__proto__来赋值原型,使得所有的属性查找路径为:pockets -> bed -> table -> head。举例,pockets.pen的结果应该为3(在table中获取)。
  2. 回答问题:为了获得glassespockets.glasseshead.glasses哪个更快?如有必要,请使用基准测试。

写入地址

假设我们的rabbit是继承自animal对象。

如果我们调用rabbit.eat(),那个对象会获得full属性:animal还是rabbit?

let animal = {
  eat() {
    this.full = true;
  }
};

let rabbit = {
  __proto__: animal
};

rabbit.eat();

为什么两只老鼠都饱了?

我们具有两个老鼠:speedylazy,它们都继承自通用的hamster对象。

当我们喂食其中的某只老鼠时,另一只也会被喂饱,为什么?如何修正它?

let hamster = {
  stomach: [],

  eat(food) {
    this.stomach.push(food);
  }
};

let speedy = {
  __proto__: hamster
};

let lazy = {
  __proto__: hamster
};

// This one found the food
speedy.eat("apple");
alert( speedy.stomach ); // apple

// This one also has it, why? fix please.
alert( lazy.stomach ); // apple


天天编码 , 版权所有丨本文标题:7.3 原型继承
转载请保留页面地址:http://www.tiantianbianma.com/prototype-inherence.html/
喜欢 (1)
支付宝[多谢打赏]
分享 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址