JavaScript中继承方式详解

继承一直是面向对象语言中的一个最为人津津乐道的概念,在JavaScript中,继承也是难点之一,下面我尽量用通俗的语言来介绍一下实现继承的几种方法。

原型链

ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。这个基本思想说的一点也不基本,那么先说一个在之前博文中提到的概念,原型与实例的关系。我们知道:每一个实例里包含了原型对象中的方法和属性。这是因为任何一个对象都有一个内部属性[[prototype]](相当于ES6中的__proto__属性),我们通常称之为原型,该原型指向的对象叫做原型对象,也就是说实例里的原型指向了该原型对象,因此获得了该原型对象的属性和方法。

下面通过例子来说明:

        this.grade = grade;
    }
    Student.prototype.showGrade = function(){
        alert("grade : " +this.grade);
    };
    var student1 = new Student(2, "sean", 22);
    student1.show();

我们创建一个Student对象,并创建一个Student的实例student1,这样实例student1中的原型指向Student.prototype,如下图:

这里Student的原型对象没有画出来,其实是指向了Object.prototype,所有的引用类型都继承了Object,不用显示的指明。

由上面的分析可知,实例都包含一个指向原型对象的内部指针,那么如果我们让A原型对象等于另一个类型的实例,那么另一个类型实例中的所有属性和方法也将存在这个A原型对象中。还是以例子来说明:

function Person(name, age){
        this.name = name;
        this.age = age;
    }
    Person.prototype.showInfo = function(){
        alert("name:"+this.name+"; age"+this.age);
    };

    function Student(grade){
        this.grade = grade;
    }
    //继承了Person
    Student.prototype = new Person("sean",22);
    Student.prototype.showGrade = function(){
        alert("grade : " +this.grade);
    };

    var student1 = new Student(2);
    student1.showGrade();
    student1.showInfo();

实现继承的本质是重写原型对象,代之以一个新类型的实例。让Student的原型对象由默认的Object.prototype改成Person的一个实例。换句话说,原来存在于 Person 的实例中的所有属性和方法,现在也存在于 SubType.prototype 中了。最终原型链的指向如下:

在上面的代码中,我们没有使用 Student默认提供的原型(默认原型对象为Object.prototype),而是给它换了一个新原型;这个新原型就是Person的实例。于是,新原型不仅具有作为一个Person的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了Person的原型。name和age都在Student.prototype中,这是因为 Student.prototype 现在是Person的实例,而name和age都是Person实例的属性,那么当然也就位于 Student.prototype 中。

这时我们再把构造函数(constructor)加进来:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针。

需要注意的是:Student.constructor 现在指向的是 Person,这是因为原来 Student.prototype 中的 constructor 被重写了的缘故。准确地说:不是 Student 的原型的 constructor 属性被重写了,而是 Student 的原型指向了另一个对象,而这个原型对象的 constructor 属性指向的是 Person。

下面是上述例子完整的原型链(画得挺好的,它自己长歪了O(∩_∩)O~)

如何确定原型和实例的关系

我们可以通过两种方式来确定原型和实例的关系

第一种是使用 instanceof 操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回 true。

alert(student1 instanceof Object); //true
alert(student1 instanceof Person); //true
alert(student1 instanceof Student); //true

我们可以说 instance 是 Object、 SuperType 或 SubType 中任何一个类型

的实例。

第二种是使用 isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型。

alert(Object.prototype.isPrototypeOf(student1)); //true
alert(Person.prototype.isPrototypeOf(student1)); //true
alert(Student.prototype.isPrototypeOf(student1)); //true

不能使用对象字面量创建原型方法

    function Person(name, age){
        this.name = name;
        this.age = age;
    }
    Person.prototype.showInfo = function(){
        alert("name:"+this.name+"; age"+this.age);
    };

    var person1 = new Person("sean", 22);

    function Student(grade){
        this.grade = grade;
    }
    //继承了Person
    Student.prototype = new Person("sean",22);
    //使用字面量添加新方法,会导致上一行代码无效
    Student.prototype = {
        showGrade : function (){
        alert("grade : " +this.grade);
    }
    };

    var student1 = new Student(2, "sean", 22);
    student1.showGrade(); //grade : 2
    student1.showInfo();  // student1.showInfo is not a function

这是因为刚刚把 Person 的实例赋值给Student的原型,紧接着又将原型替换成一个对象字面量而导致的问题。由于现在的原型包含的是一个 Object 的实例,而非 Person 的实例,因此我们设想中的原型链已经被切断——Student 和 Person 之间已经没有关系了。

原型链存在的问题

通过上面的例子,我们可以发现Person实例中的name和age属性在Student.prototype中,而被所有的Student实例共享。基于上面情况,我们不能在不影响其他实例对象的情况下向Person中传递参数。

还是通过例子来说明;

    function Person(name, age){
        this.name = name;
        this.age = age;
        this.friends = ["lily", "Tom"];
    }
    Person.prototype.showFriends = function(){
        alert(this.friends);
    };

    function Student(grade){
        this.grade = grade;
    }

    Student.prototype = new Person("sean", 22);
    Student.prototype.showGrade =  function (){
        alert("grade : " +this.grade);
    };

    var student1 = new Student(2);
    student1.showGrade(); //grade : 2
    student1.showFriends();  // lily, Tom

    var student2 = new Student(1);
    student2.friends.push("Lucy");
    student1.showGrade(); //grade : 1
    student1.showFriends(); // lily, Tom, Lucy

    student1.showFriends(); // lily, Tom, Lucy

上述代码中,friends属性是Person的实例属性,同时也是Student的原型属性,所以我们改变student2中的friend属性,student1中的属性也会跟着改变。

借用构造函数

借用构造函数(constructor stealing)的技术有时候也叫做伪造对象或经典继承。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。我们可以通过使用 apply()和 call()方法来改变函数的运行环境。

下面是通过apply()方法实现

function Person(name, age){
        this.name = name;
        this.age = age;
        this.showInfo = function(){
            alert("name:"+this.name+"; age"+this.age);
        };
    }

    function Student(grade){
        //继承了Person
        Person.apply(this, ["sean", 22]);
        this.grade = grade;
        this.showGrade = function (){
            alert("grade : " +this.grade);
        };
    }

    var student1 = new Student(2, "sean", 22);
    student1.showGrade(); //grade : 2
    student1.showInfo();  // name: sean; age:22

下面是使用call()方法

function Person(name, age){
        this.name = name;
        this.age = age;
        this.friends = ["lily", "Tom"];
        this.showInfo = function(){
            alert("name:"+this.name+"; age:"+this.age + "; friends:" + this.friends);
        };
    }

    function Student(grade,name, age){
        //继承了Person
        Person.call(this, name, age);
        this.grade = grade;
        this.showGrade = function (){
            alert("grade : " +this.grade);
        };
    }

    var student1 = new Student(2, "sean", 22);
    student1.showGrade(); //grade : 2
    student1.showInfo();  // name: sean; age:22; friends: lily, Tom

    var student2 = new Student(1, "jack", 11);
    student2.friends.push("Lucy");
    student2.showGrade(); //grade : 1
    student2.showInfo();  // name: jack; age:11; friends: lily, Tom, Lucy

    student1.showInfo(); // name: jack; age:11; friends: lily, Tom

通过使用 call()方法(或 apply()方法),我们在 Student 的环境下调用了 Person 构造函数。这样一来,就会在新 Student 对象上执行 Person()函数中定义的所有对象初始化代码。结果,Student 的每个实例就都会具有自己的friends属性的副本了,因此,即使我们改变了student2的friends属性,也不会影响到student1中的friends属性。而且通过 call()方法(或 apply()方法),我们可以向Person 构造函数中传递属性值而不影响其他Student实例中的属性值。

借用构造函数实现的继承,通过上面两种方式来测试原型和实例的关系如下:

    alert(student1 instanceof Object); //true
    alert(student1 instanceof Person); //false
    alert(student1 instanceof Student); //true

    alert(Object.prototype.isPrototypeOf(student1)); //true
    alert(Person.prototype.isPrototypeOf(student1)); //false
    alert(Student.prototype.isPrototypeOf(student1)); //true

说明借用构造函数实现的继承并没有改变子类的原型对象。

构造函数模式存在的问题同样也存在借用构造函数中。因为,属性和方法都在构造函数中定义,没创建一个子类,就会获得一个所有属性和方法的副本,这样很浪费资源,复用性较低。

组合继承

上面介绍的原型链和借用构造函数各有优缺点,那么如果把两者组合一下,取各自的优缺点,于是得到了组合继承(combination inheritance)。其思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

    function Person(name, age){
        this.name = name;
        this.age = age;
        this.friends = ["lily", "Tom"];
    }
    Person.prototype.showInfo = function(){
        alert("name:"+this.name+"; age:"+this.age + "; friends:" + this.friends);
    };

    function Student(grade,name, age){
        Person.call(this, name, age);
        this.grade = grade;
    }

    Student.prototype = new Person();
    Student.prototype.constructor = Student;

    Student.prototype.showGrade = function (){
        alert("grade : " +this.grade);
    };

    var student1 = new Student(2, "sean", 22);
    student1.showGrade(); //grade : 2
    student1.showInfo();  // name: sean; age:22; friends: lily, Tom

    var student2 = new Student(1, "jack", 11);
    student2.friends.push("Lucy");
    student2.showGrade(); //grade : 1
    student2.showInfo();  // name: jack; age:11; friends: lily, Tom, Lucy

    student1.showInfo(); // name: jack; age:11; friends: lily, Tom

在这个例子中,Person 构造函数定义了两个属性: name 、age 和 friends。 Person 的原型定义了一个方法 showInfo()。 Student 构造函数在调用 Person 构造函数时传入了 name 参数,紧接着又定义了它自己的属性 grade。然后,将 Person 的实例赋值给 Student 的原型,然后又在该新原型上定义了方法 showGrade。这样一来,就可以让两个不同的 Student 实例既分别拥有自己属性——包括 friends 属性,又可以使用相同的方法了。

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且, instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象。

    alert(student1 instanceof Object); //true
    alert(student1 instanceof Person); //true
    alert(student1 instanceof Student); //true

    alert(Object.prototype.isPrototypeOf(student1)); //true
    alert(Person.prototype.isPrototypeOf(student1)); //true
    alert(Student.prototype.isPrototypeOf(student1)); //true

原型式继承

ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。Object.create(prototype, descriptors) :创建一个具有指定原型且可选择性地包含指定属性的对象。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。

先看一个简单的例子

    var person = {
        say : function(){
            alert("person saying...");
        }
    };
    // 指定新对象原型的对象
    var o = Object.create(person);
    o.say(); // person saying...
    alert(‘say‘ in person); //true
    alert(o.hasOwnProperty(‘say‘)); //false

上面代码中,指定了新对象o的原型的对象为person,因此虽然say不是对象o的方法。而是person中的方法,但是在对象o中也能访问到。

当然,我们也可以为新对象定义额外属性

    var person = {
        say : function(){
            alert("person saying...");
        }
    };
    // 指定新对象原型的对象,并为其添加属性对象
    var o = Object.create(person,{
        name :{ value : "sean" }
    });

    o.say(); // person saying...
    alert(o.name); //sean
    alert(‘name‘ in o); //true
    alert(o.hasOwnProperty(‘name‘)); //true

上面代码在指定了新对象o的原型的对象时,还为新对象增加了name属性对象、这里需要指出的是:为新属性增加的属性都是属性对象。

支持 Object.create()方法的浏览器有 IE9+、 Firefox 4+、 Safari 5+、 Opera 12+和 Chrome。

在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。不过需要注意的是,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。如下:

var person = {
        friends : ["lily", "Tom"]
    };

    var person1 = Object.create(person);
    person1.friends.push("Lucy");
    alert(person1.friends); //lily, Tom, Lucy

    var person2 = Object.create(person);
    person2.friends.push("jack");
    alert(person2.friends); //lily, Tom, Lucy, jack

另外,还有两种继承方式:

  1. 寄生式继承,与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强对象,最后返回对象。为了解决组合继承模式由于多次调用超类型构造函数而导致的低效率问题,可以将这个模式与组合继承一起使用。
  2. 寄生组合式继承,集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式。

感兴趣的读者可以去了解一下,这里我就不详细介绍了。

时间: 2024-12-22 15:07:29

JavaScript中继承方式详解的相关文章

【JavaScript中的this详解】

前言 this用法说难不难,有时候函数调用时,往往会搞不清楚this指向谁?那么,关于this的用法,你知道多少呢? 下面我来给大家整理一下关于this的详细分析,希望对大家有所帮助! this指向的规律 this指向的规律往往与函数调用的方式息息相关:this指向的情况,取决于函数调用的方法有哪些. 我们来看一下姜浩五大定律: 姜浩五大定律: ①通过函数名()直接调用:this指向window: ②通过对象.函数名()调用的:this指向这个对象: ③函数作为数组的一个元素,通过数组下标调用的

【好文收藏】javascript中event对象详解

event代表事件的状态,例如触发event对象的元素.鼠标的位置及状态.按下的键等等. event对象只在事件发生的过程中才有效. event的某些属性只对特定的事件有意义.比如,fromElement 和 toElement 属性只对 onmouseover 和 onmouseout 事件有意义. 例子 下面的例子检查鼠标是否在链接上单击,并且,如果shift键被按下,就取消链接的跳转. <HTML> <HEAD><TITLE>Cancels Links</T

javascript中event对象详解

event代表事件的状态,例如触发event对象的元素.鼠标的位置及状态.按下的键等等. event对象只在事件发生的过程中才有效. event的某些属性只对特定的事件有意义.比如,fromElement 和 toElement 属性只对 onmouseover 和 onmouseout 事件有意义. 例子 下面的例子检查鼠标是否在链接上单击,并且,如果shift键被按下,就取消链接的跳转. 前端UI资源I分享 <HTML> <HEAD><TITLE>Cancels L

JavaScript中的this详解

  前  言  this  JavaScript中的this详解 this详解 This的指向有几种情况?如何人为控制? [谁调用this,this指向谁!!] [this的指向,不关心this写在哪!!只关心包含this的函数,由谁调用!!] ①   通过()直接调用,this指向window  func(); ②   对象.函数调用,this指向当前对象. Obj.func()    div.onclick = function(){} ③   数组下标调用,this指向当前数组   [fu

JavaScript正则表达式详解(二)JavaScript中正则表达式函数详解

二.JavaScript中正则表达式函数详解(exec, test, match, replace, search, split) 1.使用正则表达式的方法去匹配查找字符串 1.1. exec方法详解 exec方法的返回值 exec方法返回的其实并不是匹配结果字符串,而是一个对象,简单地修改一下execReg函数,来做一个实验就可以印证这一点: function execReg(reg, str) { var result = reg.exec(str); alert(typeof result

javascript中addEventListener(attachEvent)详解

addEventListener 有三个参数:第一个参数表示事件名称(不含 on,如 "click"):第二个参数表示要接收事件处理的函数:第三个参数为 useCapture.例子如下:  <button type="button">点击我</button> <img src="11 (6).jpg" alt="" style="display:block"> <s

JavaScript 继承方式详解

js继承的概念 js里常用的如下两种继承方式: 原型链继承(对象间的继承)类式继承(构造函数间的继承) 由于js不像java那样是真正面向对象的语言,js是基于对象的,它没有类的概念.所以,要想实现继承,可以用js的原型prototype机制或者用apply和call方法去实现 在面向对象的语言中,我们使用类来创建一个自定义对象.然而js中所有事物都是对象,那么用什么办法来创建自定义对象呢?这就需要用到js的原型: 我们可以简单的把prototype看做是一个模版,新创建的自定义对象都是这个模版

JavaScript继承方式详解

js继承的概念 js里常用的如下两种继承方式: 原型链继承(对象间的继承) 类式继承(构造函数间的继承) 由于js不像java那样是真正面向对象的语言,js是基于对象的,它没有类的概念.所以,要想实现继承,可以用js的原型prototype机制或者用apply和call方法去实现 在面向对象的语言中,我们使用类来创建一个自定义对象.然而js中所有事物都是对象,那么用什么办法来创建自定义对象呢?这就需要用到js的原型: 我们可以简单的把prototype看做是一个模版,新创建的自定义对象都是这个模

轻松学习JavaScript二十九:JavaScript中的this详解

这几天在看很多的JS的代码,多次出现this关键字,有时候表示不理解,就仔细看了这一方面的知识. 在JavaScript语言中,this的定义是:this是包含它的函数作为方法被调用时所属的对象.说明:这句话有点咬 嘴,但一个多余的字也没有,定义非常准确,我们可以分3部分来理解它:1包含它的函数.2作为方法被调用时.3所 属的对象.随着函数使用场合的不同,this的值会发生变化.但是有一个总的原则,那就是this指的是,调用函数的那 个对象. this是Javascript语言的一个关键字,它代