一起来看看JS的原型继承

396 阅读3分钟

「这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战

你有没有遇到过这种需求,已经有一个 user 对象及其属性和方法,现在希望将 adminguest 作为基于 user 稍加修改的变体。想重用 user 中的内容,而不是复制或者重新实现它的方法,只是在其之上构建一个新的对象。

原型继承(Prototypal inheritance)就可以用来实现这个需求。


[[Prototype]]

在JavaScript中,对象有一个特殊的隐藏属性 [[Prototype]],它要么为 null,要么就是对另一个对象的引用。该对象被称为“原型”:

image-20211115003902595

当从 object 中读取一个缺失的属性时,JavaScript 会自动从原型中获取该属性。在编程中,这种行为被称为“原型继承”。

怎么设置?

属性 [[Prototype]]应该怎么设置呢?

其中之一就是使用特殊的名字 __proto__

示例:

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

可以看到,上面示例中对象animaleatsrabbitjumps,这个时候,如果在rabbit获取eats的值即console.log(rabbit.eats),结果是什么?显而易见,这是不存在的。

但是,如果你通过__proto__设置了 [[Prototype]]

rabbit.__proto__ = animal; 

从执行结果可以看到,即使rabbit自身没有eats属性,它也获取到了值。

image-20211115004526896

过程分析:当我们试图读取 rabbit.eats 时,因为它不存在于 rabbit 中,所以 JavaScript 会顺着 [[Prototype]] 引用,在 animal 中查找(自下而上)

image-20211115004711363

这个时候我们可以说 "animalrabbit 的原型",或者说 "rabbit 的原型是从 animal 继承而来的"。

因此,如果 animal 有许多有用的属性和方法,那么它们将自动地变为在 rabbit 中可用。这种属性被称为“继承”。

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

image-20211115004956065

原型链可以很长

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

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

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

// walk 是通过原型链获得的
longEar.walk();

在这个原型链中,如果我们从 longEar 中读取一些它不存在的内容,JavaScript 会先在 rabbit 中查找,然后在 animal 中查找。

image-20211115005205066

注意:

  • 只能有一个 [[Prototype]]。一个对象不能从其他两个对象获得继承。
  • 注意 __proto__[[Prototype]] 的区别,__proto__[[Prototype]] 的 getter/setter。
  • __proto__ 属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数 Object.getPrototypeOf/Object.setPrototypeOf 来取代 __proto__ 去 get/set 原型。

关于属性的枚举

想要获取属性的key,有Object.keys(obj)for..in两种方式,但是他们是有些不同的

image-20211115010124654

从执行结果,我们可以看到

  • Object.keys 只返回自己的 key
  • for..in 会遍历自己以及继承的键

如果你想要for...in只返回自己的key也是可以的,因为有这样一个内建方法 obj.hasOwnProperty(key):如果 obj 具有自己的(非继承的)名为 key 的属性,则返回 true

代码部分改写:

for(let prop in rabbit) {
  let isOwn = rabbit.hasOwnProperty(prop);

  if (isOwn) {
    console.log(`Our: ${prop}`); // Our: jumps
  } 
}

思考一:

在上面我们用的了rabbit.hasOwnProperty,那么它是怎么来的?

从图中的原型链可以看到,该方法是 Object.prototype.hasOwnProperty 提供的。换句话说,它是从``Object`继承的。

image-20211115010428060

思考二:

for..in 循环会列出继承的属性,为什么 hasOwnProperty 没有像 eatsjumps 那样出现在 for..in 循环中?

因为它是不可枚举的。就像 Object.prototype 的其他属性,hasOwnPropertyenumerable:false 标志。并且 for..in 只会列出可枚举的属性。这就是为什么它和其余的 Object.prototype 属性都未被列出。

总结

  • 通过 [[Prototype]] 引用的对象被称为“原型”
  • for..in 循环在其自身和继承的属性上进行迭代。所有其他的键/值获取方法,如Object.keysObject.values 等仅对对象本身起作用。

参考资料:

Prototypal inheritance


🎨【点赞】【关注】不迷路,更多前端干货等你解锁

往期推荐

👉 产品、技术、设计等各互联网领域的「基础术语」扫盲

👉 Web安全的防御手段都在这里了!

👉 JavaScript的7大类型补缺补漏!

👉 JavaScript深拷贝和浅拷贝看这篇就够了!