javascript 类 继承-Javascript 中的原型继承

2023-09-01 0 5,561 百度已收录

在编程中,我们经常想要获取和扩展某些东西。

例如,我们有一个用户对象及其属性和方法,并且希望将 admin 和 guest 作为 user 的稍微修改的变体。 我们希望重用用户中的内容,而不是复制/重新实现它,而只是在其之上构建一个新对象。

原型继承(Prototypalinheritance)的语言特性可以帮助我们实现这个需求。

1. 原型

在 JavaScript 中,对象有一个特殊的隐藏属性 [[Prototype]](如规范中所命名),该属性要么为 null,要么为对另一个对象的引用。 该对象称为“原型”:

当我们从对象中读取缺失的属性时,JavaScript 会手动从原型中获取该属性。 在编程中,这些行为称为“原型继承”。 很快,我们将通过大量的例子来了解这种继承以及基于它的更耀眼的语言特性。

属性 [[Prototype]] 是内部且隐藏的,但是有很多方法可以设置它。

其中之一是使用特殊名称 __proto__,如下所示:

let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};
rabbit.__proto__ = animal; // 设置 rabbit.[[Prototype]] = animal

现在,如果我们从rabbit中读取它没有的属性,JavaScript将手动从animal中获取它。

例如:

let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};
rabbit.__proto__ = animal; // (*)
// 现在这两个属性我们都能在 rabbit 中找到:
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true

这里的 (*) 行将 Animal 设置为兔子原型。

当alert尝试读取rabbit.eats(**)时,由于rabbit中不存在它,JavaScript将遵循[[Prototype]]引用并在animal中查找它(自下而上):

这里我们可以说“动物是兔子的原型”,或者“兔子的原型继承自动物”。

为此,如果动物具有许多有用的属性和技能,则将在“兔子”中手动提供它们。 这些属性称为“继承”。

假设我们在animal中有一个可以在rabbit中调用的方法:

let animal = {
  eats: true,
  walk() {
    alert("Animal walk");
  }
};
let rabbit = {
  jumps: true,
  __proto__: animal
};
// walk 方法是从原型中获得的
rabbit.walk(); // Animal walk

该方法是从原型中手动获取的,如下所示:

原型链可以很长:

let animal = {
  eats: true,
  walk() {
    alert("Animal walk");
  }
};
let rabbit = {
  jumps: true,
  __proto__: animal
};
let longEar = {
  earLength: 10,
  __proto__: rabbit
};
// walk 是通过原型链获得的
longEar.walk(); // Animal walk
alert(longEar.jumps); // true(从 rabbit)

现在,如果我们从 longEar 中读取不存在的内容,JavaScript 将首先在rabbit 中查找,然后在animal 中查找。

这里只有两个限制:

引用不能创建闭环。 如果我们尝试在闭环中分配 __proto__,JavaScript 将抛出错误。 __proto__ 的值可以是对象或 null。 其他类型将被忽略。

实际上,这可能是显而易见的,应该始终指出:只能有一个[[原型]]。 一个对象不能从其他两个对象继承。

__proto__ 是由于历史原因保留的 [[Prototype]] 的 getter/setter

初学者经常犯的一个常见错误是不知道 __proto__ 和 [[Prototype]] 之间的区别。

请注意,__proto__ 与内部 [[Prototype]] 不同。 __proto__ 是 [[Prototype]] 的 getter/setter。 稍后,我们将看到理解它们何时很重要,因此在理解 JavaScript 语言时请记住这一点。

__proto__ 属性有点过时了。 它的存在是有历史原因的。 现代编程语言建议我们应该使用函数 Object.getPrototypeOf/Object.setPrototypeOf 而不是 __proto__ 来获取/设置原型。 我们稍后会介绍这个功能。

根据规范,__proto__ 必须仅受浏览器环境支持。 但实际上包括服务器在内的所有环境都支持它,所以我们使用它是非常安全的。

因为 __proto__ 标签在外观上更明显,所以我们将在前面的示例中使用它。

2. 不使用原型来编写

原型仅用于读取属性。

可以直接对对象执行写入/删除操作。

在下面的示例中,我们将分配兔子自己的行走:

let animal = {
  eats: true,
  walk() {
    /* rabbit 不会使用此方法 */
  }
};
let rabbit = {
  __proto__: animal
};
rabbit.walk = function() {
  alert("Rabbit! Bounce-bounce!");
};
rabbit.walk(); // Rabbit! Bounce-bounce!

从现在开始,rabbit.walk() 将立即找到对象上的技巧并执行它,而无需使用原型:

访问器属性是一个例外javascript 类 继承,因为赋值操作是由 setter 函数处理的。 因此,编写这样的属性实际上与调用函数相同。

正是因为这个原因,下面代码中的admin.fullName才能正常运行:

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"; // (**)
alert(admin.fullName); // Alice Cooper,admin 的内容被修改了
alert(user.fullName);  // John Smith,user 的内容被保护了

在 (*) 行中,属性 admin.fullName 在原型 user 中有一个 getter,因此它将被调用。 在(**)行中,该属性在原型中有一个setter,因此它将被调用。

3.“这个”的价值

前面的反例中可能会出现一个有趣的问题:setfullName(value)中的this的值是多少? 属性 this.name 和 this.surname 写在那里:在用户还是管理员中?

答案很简单:这根本不受原型的影响。

无论您在哪里找到自己的方法:在对象中还是在原型中。 在方法调用中,这仍然是点表示法。 上面的物体。

因此,setter 调用 admin.fullName= 时使用 admin,而不是 user。

这是一件非常重要的事情,因为我们可能有一个包含很多方法的大对象,而且还有继承它的对象。 当继承的对象以继承的方式运行时,它们只会更改自己的状态,而不会更改较大对象的状态。

比如这里的animal代表的是“方法存储”,兔子就是用的方式。

调用rabbit.sleep()在rabbit对象上设置this.isSleeping:

// animal 有一些方法
let animal = {
  walk() {
    if (!this.isSleeping) {
      alert(`I walk`);
    }
  },
  sleep() {
    this.isSleeping = true;
  }
};
let rabbit = {
  name: "White Rabbit",
  __proto__: animal
};
// 修改 rabbit.isSleeping
rabbit.sleep();
alert(rabbit.isSleeping); // true
alert(animal.isSleeping); // undefined(原型中没有此属性)

结果图:

如果我们有其他继承自动物的对象,如鸟、蛇等javascript 类 继承,它们也可以访问动物方法。 而且,每个方法调用中的 this 是调用时评估的相应对象(点符号之前),而不是animal。 因此,当我们向其中写入数据时,它将存储在这些对象中。

所以,方式是共享的,但对象状态不是。

4. for...in 循环

for..in 循环还迭代继承的属性。

例如:

let animal = {
  eats: true
};
let rabbit = {
  jumps: true,
  __proto__: animal
};
// Object.keys 只返回自己的 key
alert(Object.keys(rabbit)); // jumps
// for..in 会遍历自己以及继承的键
for(let prop in rabbit) alert(prop); // jumps,然后是 eats

如果这不是我们想要的,但我们想排除继承的属性,有一个内置方法 obj.hasOwnProperty(key):如果 obj 有自己的(非继承的)名为 key 的属性,则返回 true。

为此,我们可以过滤掉继承的属性(或对它们执行其他操作):

let animal = {
  eats: true
};
let rabbit = {
  jumps: true,
  __proto__: animal
};
for(let prop in rabbit) {
  let isOwn = rabbit.hasOwnProperty(prop);
  if (isOwn) {
    alert(`Our: ${prop}`); // Our: jumps
  } else {
    alert(`Inherited: ${prop}`); // Inherited: eats
  }
}

这里我们有以下继承链:rabbit继承自animal,animal继承自Object.prototype(因为animal是一个对象字面量{...},这是默认的继承),然后一直到null:

注意,这是一件非常有趣的事情。 rabbit.hasOwnProperty 方式从何而来? 我们没有定义它。 从上图中的原型链我们可以看到这个方法是由Object.prototype.hasOwnProperty提供的。 换句话说,它是遗传的。

...如果 for..in 循环枚举继承的属性,为什么 hasOwnProperty 没有像 eats 和 Jumps 那样出现在 for..in 循环中?

答案很简单:它是不可枚举的。 与 Object.prototype 的其他属性一样,hasOwnProperty 具有 enumerable:false 标志。 而 for..in 只会枚举可枚举的属性。 这就是为什么它和其余的 Object.prototype 属性没有被枚举。

几乎所有其他键/值访问方法都会忽略继承的属性

几乎所有其他键/值检索方法(例如 Object.keys 和 Object.values)也会忽略继承的属性。

它们仅对对象本身进行操作。 不考虑从原型继承的属性。

五、总结

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 javascript javascript 类 继承-Javascript 中的原型继承 https://www.wkzy.net/game/186565.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务