查看: 793|回复: 0

[JavaScript/JQuery] 深入理解继承

发表于 2017-9-23 08:00:01
尚学堂AD

学习怎样创建对象是理解面向对象编程的第一步,第二步是理解继承。在传统的面向对象编程语言中,类继承其他类的属性。
然而,JS的继承方式与传统的面向对象编程语言不同,继承可以发生对象之间,这种继承的机制是我们已经熟悉的一种机制:原型。

1.原型链接和Object.prototype

js内置的继承方式被称为原型链接(prototype chaining)原型继承(prototypal inheritance)。正如我们在前一天所学的,原型对象上定义的属性,在所有的对象实例中都是可用的,这就是继承的一种形式。对象实例继承了原型中的属性。而原型也是一个对象,所以它也有自己的原型,并且继承原型中的属性。这被称为原型链:对象继承自己原型对象中属性,而这个原型会继续向上继承自己的原型,依此类推。

所有对象,包括我们定义自己的对象,都自动继承自Object,除非我们另有指定(本课后面讨论)。更具体地说,所有对象都继承Object.prototype。任何通过对象字面量定义的对象都有一个__proto__设置为object.prototype,意味着它们都继承Object.prototype对象中的属性,就像这个例子中的book:

  1. var book = {
  2. title: "平凡的世界"
  3. };
  4. var prototype = Object.getPrototypeOf(book);
  5. console.log(prototype === Object.prototype); // true
复制代码

book的原型等于Object.prototype。不需要额外的代码来实现这一点,因为这是创建新对象时的默认行为。这种关系意味着book自动接收来自Object.prototype对象中的方法。

  1.2.从Object.prototype中继承的方法

我们在前几天使用的一些方式实际上是定义在Object.prototype原型对象中,因此所有其他对象也都继承了这些方法。这些方法是:

  • hasOwnProperty():判断对象中有没有某个属性,接受一个字符串类型的属性名作为参数。
  • propertyIsEnumerable():判断对象中的某个属性是否是可枚举的。
  • isPrototypeOf():判断一个对象是否是另个对象的原型。
  • valueOf:返回对象的值表示形式。
  • toString:返回对象的字符串表示形式。
  • toLocaleString: 返回对象的本地字符串表示形式。

这五种方法通过继承所有对象都拥有这6个方法。当我们需要使对象在JavaScript中一致工作时,最后两个是非常重要的,有时我们可能希望自己定义它们。

  1.3:valueOf()

当我们操作对象时,valueof()方法就会被调用时。默认情况下,valueof()简单地返回对象实例。对于字符串,布尔值和数字类型的值,首先会使用原始包装类型包装成对象,然后再调用valueof()方法。同样,Date对象的valueof()
方法返回以毫秒为单位的纪元时间(就像Date.prototype.getTime()一样)。这也是为什么我们可以对日期进行比较,例如:

  1. var now = new Date();
  2. var earlier = new Date(2010, 1, 1);
  3. console.log(now > earlier); // true
复制代码

  1.4修改Object.prototype

默认情况下,所有对象都继承自Object.prototype,因此改变Object.prototype会影响到所有对象。这是非常危险的情况。

  1. Object.prototype.add = function(value) {
  2. return this + value;
  3. };
  4. var book = {
  5. title: "平凡的世界"
  6. };
  7. console.log(book.add(5)); // "[object Object]5"
  8. console.log("title".add("end")); // "titleend"
  9. // in a web browser
  10. console.log(document.add(true)); // "[object HTMLDocument]true"
  11. console.log(window.add(5)); // "[object Window]true"
复制代码

导致的另一个问题:

  1. var empty = {};
  2. for (var property in empty) {
  3. console.log(property);
  4. }
复制代码

解决方法:

  1. for(name in book){
  2. if(book.hasOwnProperty(name)){
  3. console.log(name);
  4. }
  5. }
复制代码

虽然这个方法可以有效地过滤掉我们不需要的原型属性,但是它也限制了使用for-in只能变量的属性,而不能遍历原型属性。建议不要修改原型对象。

2:对象继承

最简单的继承方式是对象之间的继承。我们所需要做的就是指定新创建对象的原型应该指向哪个对象。通过Object字面量的形式创建的对象默认将__proto__属性指向了Object.prototype,但是我们可以通过Object.create()方法显示地将__proto__属性指向其他对象。

Object.create()方法接收两个参数。第一个参数用来指定新创建对象的__proto__应该指向的对象。第二个参数是可选的,用来设置对象属性的描述符(特性),语法格式与Object.definedProperties()方法参数个格式一样。如下所示:

  1. var book = {
  2. title: "人生"
  3. };
  4. // 等价于
  5. var book = Object.create(Object.prototype, {
  6. title: {
  7. configurable: true,
  8. enumerable: true,
  9. value: "人生",
  10. writable: true
  11. }
  12. });
复制代码

代码中两个声明的效果是一样的。第一个声明使用对象字面量的方式定义一个带有单个属性:title的对象。这个对象自动继承自Object.prototype,并且属性默认被设置成可配置,可枚举,可写。第二个声明和第一个一样,但是显示使用了Object.create()方法。但是你可能永远不会这样显示地直接继承Object.prototype,没有必要这样做,因为默认就已经继承了Object.prototype。继承自其他对象会比较有趣一点:

  1. var person1 = {
  2. name: '张三',
  3. sayName: function(){
  4. console.log(this.name);
  5. }
  6. };
  7. var person2 = Object.create(person1, {
  8. name: {
  9. value: '李四',
  10. configurable: true,
  11. enumerable: true,
  12. writable: true
  13. }
  14. });
  15. person1.sayName(); // '张三'
  16. person2.sayName(); // '李四'
  17. console.log(person1.hasOwnProperty("sayName")); // true
  18. console.log(person1.isPrototypeOf(person2)); // true
  19. console.log(person2.hasOwnProperty("sayName")); // false
复制代码

这段代码创建了一个对象person1,该对象有一个name属性和一个sayName()方法。person2对象继承了person1,因此它也继承了name属性和sayName()方法。然而,person2是通过Object.create()方法定义的,它也定义了自己的name属性。对象自己的属性遮挡了原型的中同名属性name。因此,person1.sayName()输出'张三',person2.sayName()输出'李四'。记住,person2.sayName()只存在于person1中,被person2继承了下来。

当对象的属性被访问时,JavaScript会首先会在对象的属性中搜索,如果没有找到,则继续在__proto__指向的原型对象中搜索。如果任然没有找到,则继续搜索原型对象的上个原型对象,直到到达原型链的末端。原型链的末端结束于Object.prototype,Object.prototype对象的__proto__内部属性为null。

3.构造函数继承

JavaScript中的对象继承也是构造函数继承的基础。回顾昨天的内容,几乎每一个函数都有一个可以修改或替换的prototype属性。prototype属性自动被赋值为一个新的对象,这个对象继承自Object.prototype,并且对象中有一个自己的属性constructor。实际上,JavaScript引擎为我们执行以下操作:

  1. // 这是我们写的
  2. function YourConstructor() {
  3. // initialization
  4. }
  5. // JavaScript引擎在后台帮我们做的:
  6. YourConstructor.prototype = Object.create(Object.prototype, {
  7. constructor: {
  8. configurable: true,
  9. enumerable: true,
  10. value: YourConstructor
  11. writable: true
  12. }
  13. });
复制代码

因此,不做任何额外的工作,这段代码给我们的构造函数的prototype属性设置了一个对象,这个对象继承自Object.prototype,这意味着通过构造函数YourConstructor()创建的所有实例都继承自Object.prototype。YourConstructor是Object的子类,Object是YourConstructor的超类。

由于prototype属性是可写的,因此通过复写它我们可以改变原型链。例如:

  1. function Rectangle(length, width) {
  2. this.length = length;
  3. this.width = width;
  4. }
  5. Rectangle.prototype.getArea = function() {
  6. return this.length * this.width;
  7. };
  8. Rectangle.prototype.toString = function() {
  9. return "[Rectangle " + this.length + "x" + this.width + "]";
  10. };
  11. // 继承 Rectangle
  12. function Square(size) {
  13. this.length = size;
  14. this.width = size;
  15. }
  16. Square.prototype = new Rectangle();
  17. Square.prototype.constructor = Square;
  18. Square.prototype.toString = function() {
  19. return "[Square " + this.length + "x" + this.width + "]";
  20. };
  21. var rect = new Rectangle(5, 10);
  22. var square = new Square(6);
  23. console.log(rect.getArea()); // 50
  24. console.log(square.getArea()); // 36
  25. console.log(rect.toString()); // "[Rectangle 5x10]"
  26. console.log(square.toString()); // "[Square 6x6]"
  27. console.log(rect instanceof Rectangle); // true
  28. console.log(rect instanceof Object); // true
  29. console.log(square instanceof Square); // true
  30. console.log(square instanceof Rectangle); // true
  31. console.log(square instanceof Object); // true
复制代码

这段代码中有两个构造函数:Reactangle和Square。Square构造函数的原型对象被重新赋值为Reactangle的对象实例。在创建Reactangle对象实例的时候没有传递参数,因为它们没有用,如果传递参数了,所有的Square对象实例都会共享相同的尺寸。以这种方式改变原型链之后,要确保constructor属性的指向正确的构造函数。

4.使用父类的构造函数

如果你想要在子类的构造函数中调用父类的构造函数,那么我们就需要利用call()方法或apply()方法。

  1. function Rectangle(length,width){
  2. this.length = length;
  3. this.width = width;
  4. }
  5. Rectangle.prototype.getArea = function(){
  6. return this.length * this.width;
  7. };
  8. Rectangle.prototype.toString = function(){
  9. return '[Rectangle'+this.length+'x'+this.width+']';
  10. };
  11. function Square(size){
  12. Rectangle.call(this,size,size);
  13. }
  14. Square.prototype = Object.create(Rectangle.prototype,{
  15. constructor:{
  16. configurable:true,
  17. enumerable:true,
  18. writable:true
  19. }
  20. });
  21. Square.prototype.toString = function(){
  22. return '[Square'+this.length+'x'+this.width+']';
  23. };
  24. var square = new Square(20);
  25. console.log(square.getArea()); //400
  26. console.log(square.toString()); //[Square20x20]
复制代码
5.访问父类的方法

在上一个例子中,Square类型有自己的toString()方法,该方法遮挡了原型中的toString()方法。但有时候我们仍然想要访问父类的方法该怎么办?我们可以直接访问原型对象中的属性,如果是访问方法的话,可以call()或apply()。例如:

  1. function Rectangle(length,width){
  2. this.length = length;
  3. this.width = width;
  4. }
  5. Rectangle.prototype.getArea = function(){
  6. return this.length * this.width;
  7. };
  8. Rectangle.prototype.toString = function(){
  9. return '[Rectangle'+this.length+'x'+this.width+']';
  10. };
  11. function Square(size){
  12. this.length = size;
  13. this.width = size;
  14. };
  15. Square.prototype = Object.create(Rectangle.prototype,{
  16. constructor:{
  17. value:Square,
  18. configurable:true,
  19. enumerable:true,
  20. writable:true
  21. }
  22. });
  23. Square.prototype.toString =function(){
  24. var text = Rectangle.prototype.toString.call(this);
  25. return text.replace('Rectangle','Square');
  26. }
  27. Square.prototype.getBorder = function(){
  28. return '边数' + Rectangle.prototype.getBorder();
  29. };
  30. var square = new Square(20);
  31. console.log(square);
  32. console.log(square.getArea());
  33. console.log(square.toString());
  34. console.log(square.getBorder());
复制代码

在这个版本的代码,使用Square.prototype.toString()和call()方法一起调用Rectangle.prototype.toString()。这个方法只需要在返回结果之前将Rectangle替换成Square就可以了。对于这样简单的操作来所,这种方式可能有点繁琐,但这却是访问父类方法的唯一途径。



回复

使用道具 举报