前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入理解继承

深入理解继承

作者头像
小周sir
发布2019-09-23 16:07:22
4610
发布2019-09-23 16:07:22
举报
文章被收录于专栏:前端技术开发前端技术开发

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

1.原型链接和Object.prototype

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

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

代码语言:javascript
复制
   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()一样)。这也是为什么我们可以对日期进行比较,例如:

代码语言:javascript
复制
    var now = new Date();
    var earlier = new Date(2010, 1, 1);
    console.log(now > earlier);           // true

  1.4修改Object.prototype

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

代码语言:javascript
复制
 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"

导致的另一个问题:

代码语言:javascript
复制
  var empty = {};
    for (var property in empty) {
        console.log(property);
    }

解决方法:

代码语言:javascript
复制
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()方法参数个格式一样。如下所示:

代码语言:javascript
复制
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。继承自其他对象会比较有趣一点:

代码语言:javascript
复制
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引擎为我们执行以下操作:

代码语言: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属性是可写的,因此通过复写它我们可以改变原型链。例如:

代码语言:javascript
复制
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()方法。

代码语言:javascript
复制
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()。例如:

代码语言:javascript
复制
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就可以了。对于这样简单的操作来所,这种方式可能有点繁琐,但这却是访问父类方法的唯一途径。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-09-16 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.原型链接和Object.prototype
  •   1.2.从Object.prototype中继承的方法
  •   1.3:valueOf()
  • 2:对象继承
  • 3.构造函数继承
  • 4.使用父类的构造函数
  • 5.访问父类的方法
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档