Javascript面向对象(三)——原型继承

Javascript面向对象(三)——原型继承

实际编程中,我们经常需要一些东西并扩展之。例如,我们有user对象,带有属性和方法,现在想要adminguest,和其稍微有些变化,我们最好重用user对象,但不是复制/重新实现它的方法,而是在其基础上构建。原型继承是Javascript重要特性,可以实现之。

[[Prototype]]
在Javascript中,对象有个特殊的隐藏属性[[Prototype]](规范中的名称),其可以为null或引用其他对象,该对象称为原型:
Javascript面向对象(三)——原型继承_第1张图片

这个[[Prototype]]有个魔力的意思,当我们从对象中读取属性,如果没有找到,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

Javascript面向对象(三)——原型继承_第2张图片

这里,我们说animalrabbit的原型,或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

方法自动从原型中带来,如下图:
Javascript面向对象(三)——原型继承_第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)

Javascript面向对象(三)——原型继承_第4张图片

实际上有两个限制:
1、不能循环引用。Javascript抛出错误,如果__proto__循环引用。
2、__proto__的值,只能赋值为对象或null,所有其他值(原始值)被忽略。

另外显而易见,只有有一个[[Prototype]],不支持多继承。

读/写规则

原型仅用于reading属性。
对于数据属性(不是getter/setter访问器),写/删除操作直接通过对象实现。下面的例子,我们给rabbit自己的walk方法赋值:

    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()在自己内部查找方法并立刻调用,没有使用原型方法。
Javascript面向对象(三)——原型继承_第5张图片

对于getter/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"; // (**)

星号()行属性admin.fullName,在原型user中有getter访问器,所以他可以调用,(*)行属性在原型中有setter访问器,所以也可以调用。

this的值

上面的示例可能提出有趣的问题,在setfullName(value)内部this的值是什么? this.namethis.surname是那个对象的属性,useradmin

答案是简单的:this根本不受原型影响。
无论方法出现在哪里,对象或原型。调用方法时,this总是“.”号前面的那个对象。
所以,setter是有admin调用,this是admin,不是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)

结果图示如下:
Javascript面向对象(三)——原型继承_第6张图片

如果我们有其他对象birdsnake等继承自animal,他们也获得animal的方法。但this在每个方法中和调用其对象一致,是运行时确定(.前面的对象),不是animal。所以当我们写数据至this,它实际存在在那些调用的子对象中。
结论是:方法是共享的,但对象状态不是。

总结

  • 在 JavaScript, 所有对象有个隐藏[[Prototype]] 属性,其值只能是其他对象或null.
  • 我们能通过 obj.proto 访问它 (也有其他方法,后继续说明).
  • 被[[Prototype]]引用的对象称为原型.
  • 如果我们想对 obj的属性或调用方法,它不存在,那么JavaScript尝试去原型中查找. Write/delete 属性直接在对象上运行, 他们不使用原型 (除非属性确实是setter访问器).
  • 如果我们调用obj.method(), 并且方法来自原型, this仍然代表当前调用obj.

你可能感兴趣的:(深入理解Javascript,javascript,prototype,继承,对象,扩展)