深入理解继承

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

1.原型链接和Object.prototype

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

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

   var book = {
        title: "平凡的世界"
    };
    var prototype = Object.getPrototypeOf(book);
    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()一样)。这也是为什么我们可以对日期进行比较,例如:

    var now = new Date();
    var earlier = new Date(2010, 1, 1);
    console.log(now > earlier);           // true

  1.4修改Object.prototype

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

 Object.prototype.add = function(value) {
        return this + value;
    };
    var book = {
        title: "平凡的世界"
    };
    console.log(book.add(5));           // "[object Object]5"
    console.log("title".add("end"));    // "titleend"
    // in a web browser
    console.log(document.add(true));    // "[object HTMLDocument]true"
    console.log(window.add(5));         // "[object Window]true"

导致的另一个问题:

  var empty = {};
    for (var property in empty) {
        console.log(property);
    }

解决方法:

for(name in book){

    if(book.hasOwnProperty(name)){

        console.log(name);
    }
}

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

2:对象继承

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

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

var book = {
        title: "人生"
    };

    // 等价于
    var book = Object.create(Object.prototype, {
        title: {
            configurable: true,
            enumerable: true,
            value: "人生",
            writable: true
        }
    });

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

var person1 = {
    name: '张三',
    sayName: function(){
        console.log(this.name);
    }
};

var person2 = Object.create(person1, {
    name: {
        value: '李四',
        configurable: true,
        enumerable: true,
        writable: true
    }
});

person1.sayName();      // '张三'
person2.sayName();      // '李四'

console.log(person1.hasOwnProperty("sayName"));     // true
console.log(person1.isPrototypeOf(person2));        // true
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.prototypeObject.prototype对象的__proto__内部属性为null

3.构造函数继承

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

  // 这是我们写的
    function YourConstructor() {
        // initialization
    }
    // JavaScript引擎在后台帮我们做的: 
    YourConstructor.prototype = Object.create(Object.prototype, {
        constructor: {
            configurable: true,
            enumerable: true,
            value: YourConstructor
            writable: true
        }
    });

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

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

function Rectangle(length, width) {
        this.length = length;
        this.width = width;
    }
    Rectangle.prototype.getArea = function() {
        return this.length * this.width;
    };
    Rectangle.prototype.toString = function() {
        return "[Rectangle " + this.length + "x" + this.width + "]";
    };

    // 继承 Rectangle
    function Square(size) {
        this.length = size;
        this.width = size;
    }
    Square.prototype = new Rectangle();
    Square.prototype.constructor = Square;
    Square.prototype.toString = function() {
        return "[Square " + this.length + "x" + this.width + "]";
    };
    var rect = new Rectangle(5, 10);
    var square = new Square(6);
    console.log(rect.getArea());                // 50
    console.log(square.getArea());              // 36
    console.log(rect.toString());               // "[Rectangle 5x10]"
    console.log(square.toString());             // "[Square 6x6]"
    console.log(rect instanceof Rectangle);     // true
    console.log(rect instanceof Object);        // true
    console.log(square instanceof Square);      // true
    console.log(square instanceof Rectangle);   // true
    console.log(square instanceof Object);      // true

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

4.使用父类的构造函数

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

function Rectangle(length,width){
                this.length = length;
                this.width = width;
            }
            Rectangle.prototype.getArea = function(){
                return this.length * this.width;
            };
            Rectangle.prototype.toString = function(){
                return '[Rectangle'+this.length+'x'+this.width+']';
            };
            function Square(size){
                Rectangle.call(this,size,size);
            }
            Square.prototype = Object.create(Rectangle.prototype,{
                constructor:{
                    configurable:true,
                    enumerable:true,
                    writable:true
                }
            });
            Square.prototype.toString = function(){
                return '[Square'+this.length+'x'+this.width+']';
            };
            var square = new Square(20);
            console.log(square.getArea()); //400
            console.log(square.toString()); //[Square20x20]

5.访问父类的方法

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

function Rectangle(length,width){
                this.length = length;
                this.width = width;
            }
            Rectangle.prototype.getArea = function(){
                return this.length * this.width;
            };
            Rectangle.prototype.toString = function(){
                return '[Rectangle'+this.length+'x'+this.width+']';
            };
            function Square(size){
                this.length = size;
                this.width = size;
            };
            Square.prototype = Object.create(Rectangle.prototype,{
                constructor:{
                    value:Square,
                    configurable:true,
                    enumerable:true,
                    writable:true
                }
            });
            
            Square.prototype.toString =function(){
                var text = Rectangle.prototype.toString.call(this);
                return text.replace('Rectangle','Square');
            }
            Square.prototype.getBorder = function(){
                return '边数' + Rectangle.prototype.getBorder();
            };
            var square = new Square(20);
            console.log(square);
            console.log(square.getArea());
            console.log(square.toString());
            console.log(square.getBorder());

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

posted @ 2017-09-16 09:14  小周sri的码农  阅读(455)  评论(0编辑  收藏  举报