看完还不懂原型继承,我把头拧下来给你

268 阅读4分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

先带着问题思考一下:

  • 原型是什么?
  • 怎么操作原型?

protoType

[[Prototype]] 是对象的隐藏属性,要么是null,要么是对另一个对象的引用,该对象称为原型.

当我们在当前对象上找不到属性时就会在原型对象上查找,这在编程上称为原型继承.

虽然[[Prototype]]是隐藏的,但还是有方法可以设置它: __proto__就是其中一个.

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

当我们在rebbit上找不到的时候会向上查找(自下而上)

image.png

在这我们就可以说 animal 是 rabbit 的原型

因此,在animal中的属性和方法会自动可以被rabbit调用,这种属性称为继承

这里有两个限制

  1. 引用不能形成闭环,如果在闭环中引用__proto__会报错.
  2. __proto__引用只能是对象或者null.

我们经常分不清__proto__和[[Prototype]]的区别,其实__proto__就是[[prototype]]的getter和setter

__proto__可能有点过时,但是他在任何浏览器包括服务端的任何环境都支持,所以很安全.

我们也可以用Object.getPrototypeOf/Object.setProtoTypeOf来代替__proto__取get/set原型

写入不使用原型

原型只用于读取

在对对象写入或者删除时不会操作该对象的原型

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

可以看到 rabbit.walk不会使用和改变animal的walk()

访问器(accessor)属性是一个例外,因为分配(assignment)操作是由 setter 函数处理的。因此,写入此类属性实际上就像调用函数一样。

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,因此它会被调用。

this

在上面这个代码中有个很有意思的地方,在set fullName 的时候this指向的是谁呢? 属性namesurname被写在user还是amdin

答案揭晓:this不受原型的影响

无论在哪里找到方法:在一个对象还是在原型中。在一个方法调用中,this 始终是点符号 . 前面的对象。

这点很重要,因为我们可能会有一个带有很多方法和属性的大对象,并且还有从他继承的对象,当修改继承的对象时,不会改到大对象上的属性.所以方法是共享的,但是状态不是.

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.hasOwnProperty()是从哪里来的呢? 是从Object.prototype.hasOwnProperty()中继承而来的.

那为什么没有打印出来呢? 其实也很简单,for...in会循环可枚举的值,这个属性是不可枚举的. 就像 Object.prototype 的其他属性,hasOwnProperty 有 enumerable:false 标志。并且 for..in 只会列出可枚举的属性。这就是为什么它和其余的 Object.prototype 属性都未被列出。

总结

  • 在所有对象中都有[[Prototype]]的隐藏属性,要么是对象要么是null
  • __proto__[[prototype]]的get/set方法
  • 通过[[prototype]]引用的对象就是原型
  • 在访问一个对象的属性时如果没有就自动去他的原型上查找.
  • 在直接写入属性时不会改变原型的属性,而是在对象上新增一个,访问器属性例外,相当于直接执行get()/set()
  • this始终指向的.前面的对象,方法是共享的,属性不是,修改继承后对象的属性不会改变原对象的属性
  • for...in只会列出可枚举的属性,会列出原型上的属性.