Skip to main content
 首页 » 编程设计

Javascript面向对象之原型继承

2022年07月19日137java哥

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

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

[[Prototype]]
在Javascript中,对象有个特殊的隐藏属性[[Prototype]](规范中的名称),其可以为null或引用其他对象,该对象称为原型:

这个[[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 

这里,我们说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 

方法自动从原型中带来,如下图:

原型链可以更长:

    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) 

实际上有两个限制:
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()在自己内部查找方法并立刻调用,没有使用原型方法。

对于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) 

结果图示如下:

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

总结

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

本文参考链接:https://blog.csdn.net/neweastsun/article/details/70187756