原型,继承

在编程中,我们经常会想获取并扩展一些东西。
例如,我们有一个 user 对象及其属性和方法,并希望将 admin 和 guest 作为基于 user 稍加修改的变体。我们想重用 user 中的内容,而不是复制/重新实现它的方法,而只是在其之上构建一个新的对象。

[[Prototype]]

在 JavaScript 中,对象有一个特殊的隐藏属性 [[Prototype]](如规范中所命名的),它要么为 null,要么就是对另一个对象的引用。
属性 [[Prototype]] 是内部的而且是隐藏的,但是这儿有很多设置它的方式
其中之一就是使用特殊的名字 proto,就像这样:

  1. let animal = {
  2. eats: true
  3. };
  4. let rabbit = {
  5. jumps: true
  6. };
  7. rabbit.__proto__ = animal; // 设置 rabbit.[[Prototype]] = animal

现在,如果我们从 rabbit 中读取一个它没有的属性,JavaScript 会自动从 animal 中获取。

  1. let animal = {
  2. eats: true
  3. };
  4. let rabbit = {
  5. jumps: true
  6. };
  7. rabbit.__proto__ = animal; // (*)
  8. // 现在这两个属性我们都能在 rabbit 中找到:
  9. alert( rabbit.eats ); // true (**)
  10. alert( rabbit.jumps ); // true

当 alert 试图读取 rabbit.eats (**) 时,因为它不存在于 rabbit 中,所以 JavaScript 会顺着 [[Prototype]] 引用,在 animal 中查找(自下而上)。
在这儿我们可以说 “animal 是 rabbit 的原型”,或者说 “rabbit 的原型是从 animal 继承而来的”。

仍然要强调:只能有一个 [[Prototype]]。一个对象不能从其他两个对象获得继承。

proto 是 [[Prototype]] 的因历史原因而留下来的 getter/setter
请注意,proto 与内部的 [[Prototype]] 不一样proto 是 [[Prototype]] 的 getter/setter。
proto 属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数 Object.getPrototypeOf/Object.setPrototypeOf 来取代 proto 去 get/set 原型。稍后我们将介绍这些函数。

写入不使用原型

原型仅用于读取属性。
对于写入/删除操作可以直接在对象上进行。

“this” 的值

this 不受原型的影响。
无论在哪里找到方法:在一个对象还是在原型中。在一个方法调用中,this 始终是点符号 . 前面的对象。
当继承的对象运行继承的方法时,它们将仅修改自己的状态,而不会修改大对象的状态。
方法是共享的,但对象状态不是。

for…in 循环

我们想排除继承的属性,那么这儿有一个内建方法 obj.hasOwnProperty(key):如果 obj 具有自己的(非继承的)名为 key 的属性,则返回 true。

  1. let animal = {
  2. eats: true
  3. };
  4. let rabbit = {
  5. jumps: true,
  6. __proto__: animal
  7. };
  8. for(let prop in rabbit) {
  9. let isOwn = rabbit.hasOwnProperty(prop);
  10. if (isOwn) {
  11. alert(`Our: ${prop}`); // Our: jumps
  12. } else {
  13. alert(`Inherited: ${prop}`); // Inherited: eats
  14. }
  15. }

几乎所有其他键/值获取方法都忽略继承的属性
几乎所有其他键/值获取方法,例如 Object.keys 和 Object.values 等,都会忽略继承的属性。
它们只会对对象自身进行操作。不考虑 继承自原型的属性。

F.prototype

JavaScript 从一开始就有了原型继承。这是 JavaScript 编程语言的核心特性之一。但是在过去,没有直接对其进行访问的方式。唯一可靠的方法是本章中会介绍的构造函数的 “prototype” 属性。目前仍有许多脚本仍在使用它。
例子:

  1. let animal = {
  2. eats: true
  3. };
  4. function Rabbit(name) {
  5. this.name = name;
  6. }
  7. Rabbit.prototype = animal;
  8. let rabbit = new Rabbit("White Rabbit"); // rabbit.__proto__ == animal
  9. alert( rabbit.eats ); // true

设置 Rabbit.prototype = animal 的字面意思是:“当创建了一个 new Rabbit 时,把它的 [[Prototype]]赋值为 animal”。

默认的 F.prototype,构造器属性

每个函数都有 “prototype” 属性,即使我们没有提供它。默认的 “prototype” 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身。

  1. function Rabbit() {}
  2. // by default:
  3. // Rabbit.prototype = { constructor: Rabbit }
  4. alert( Rabbit.prototype.constructor == Rabbit ); // true

原生的原型

“prototype” 属性在 JavaScript 自身的核心部分中被广泛地应用。所有的内置构造函数都用到了它。

Object.prototype

  1. let obj = {};
  2. alert( obj ); // "[object Object]" ?

生成字符串 “[object Object]” 的代码在哪里?那就是一个内建的 toString 方法,但是它在哪里呢?obj是空的!
……然而简短的表达式 obj = {} 和 obj = new Object() 是一个意思,其中 Object 就是一个内建的对象构造函数,其自身的 prototype 指向一个带有 toString 和其他方法的一个巨大的对象。

其他内建原型

其他内建对象,像 Array、Date、Function 及其他,都在 prototype 上挂载了方法。
按照规范,所有的内建原型顶端都是 Object.prototype。这就是为什么有人说“一切都从对象继承而来”。

  1. let arr = [1, 2, 3];
  2. // 它继承自 Array.prototype?
  3. alert( arr.__proto__ === Array.prototype ); // true
  4. // 接下来继承自 Object.prototype?
  5. alert( arr.__proto__.__proto__ === Object.prototype ); // true
  6. // 原型链的顶端为 null。
  7. alert( arr.__proto__.__proto__.__proto__ ); // null

基本数据类型

最复杂的事情发生在字符串、数字和布尔值上。
这些对象的方法也驻留在它们的 prototype 中,可以通过 String.prototype、Number.prototype 和 Boolean.prototype 进行获取。
特殊值 null 和 undefined 比较特殊。它们没有对象包装器,所以它们没有方法和属性。并且它们也没有相应的原型。

更改原生原型

原生的原型是可以被修改的。例如,我们向 String.prototype 中添加一个方法,这个方法将对所有的字符串都是可用的:

  1. String.prototype.show = function() {
  2. alert(this);
  3. };
  4. "BOOM!".show(); // BOOM!

在开发的过程中,我们可能会想要一些新的内建方法,并且想把它们添加到原生原型中。
但这通常是一个很不好的想法
在现代编程中,只有一种情况下允许修改原生原型。那就是 polyfilling。Polyfilling 是一个术语,表示某个方法在 JavaScript 规范中已存在,但是特定的 JavaScript 引擎尚不支持该方法,那么我们可以通过手动实现它,并用以填充内建原型。

从原型中借用

装饰器模式和转发,call/apply 一章中,我们讨论了方法借用。
那是指我们从一个对象获取一个方法,并将其复制到另一个对象。
一些原生原型的方法通常会被借用。
例如,如果我们要创建类数组对象,则可能需要向其中复制一些 Array 方法。
例如:

  1. let obj = {
  2. 0: "Hello",
  3. 1: "world!",
  4. length: 2,
  5. };
  6. obj.join = Array.prototype.join;
  7. alert( obj.join(',') ); // Hello,world!

另一种方式是通过将 obj.proto 设置为 Array.prototype,这样 Array 中的所有方法都自动地可以在 obj 中使用了。
但是如果 obj 已经从另一个对象进行了继承,那么这种方法就不可行了(译注:因为这样会覆盖掉已有的继承。此处 obj 其实已经从 Object 进行了继承,但是 Array 也继承自 Object,所以此处的方法借用不会影响 obj 对原有继承的继承,因为 obj 通过原型链依旧继承了 Object)。请记住,我们一次只能继承一个对象

原型方法,没有 proto 的对象

设置原型的现代方法:

现代的方法有:

例如:
应该使用这些方法来代替 proto

  1. let animal = {
  2. eats: true
  3. };
  4. // 创建一个以 animal 为原型的新对象
  5. let rabbit = Object.create(animal);
  6. alert(rabbit.eats); // true
  7. alert(Object.getPrototypeOf(rabbit) === animal); // true
  8. Object.setPrototypeOf(rabbit, {}); // 将 rabbit 的原型修改为 {}

Object.create 有一个可选的第二参数:属性描述器。我们可以在此处为新对象提供额外的属性,就像这样:

  1. let animal = {
  2. eats: true
  3. };
  4. let rabbit = Object.create(animal, {
  5. jumps: {
  6. value: true
  7. }
  8. });
  9. alert(rabbit.jumps); // true