欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

JavaScript的ES3,ES5,ES6三种实现继承方式的教程

程序员文章站 2022-03-29 10:35:01
es3 继承 在javascript中,所谓的类就是函数,函数就是类。一般情况下,我们在函数的prototype上面定义方法,因为这样所有类的实例都可以公用这些方法;在函数内部(构造函数)中初始化属...

es3 继承

javascript中,所谓的类就是函数,函数就是类。一般情况下,我们在函数的prototype上面定义方法,因为这样所有类的实例都可以公用这些方法;在函数内部(构造函数)中初始化属性,这样所有类的实例的属性都是相互隔离的。 我们定义classa和classb两个类,想让classb继承自classa。 classa代码如下所示:

function classa(name, age) {
    this.name = name;
    this.age = age;
}

classa.prototype.sayname = function () {
    console.log(this.name);
};

classa.prototype.sayage = function () {
    console.log(this.age);
};

classa构造函数内部定义了name和age两个属性,并且在其原型上定义了sayname和sayaage两个方法。 classb如下所示:

function classb(name, age, job) {
    classa.apply(this, [name, age]);
    this.job = job;
}

classb新增了job属性,我们在其构造函数中执行classa.apply(this, [name, age]);,相当于在java类的构造函数中通过super()调用父类的构造函数以初始化相关属性。

此时我们可以通过var b = new classb(“sunqun”, 28, “developer”);进行实例化,并可以访问b.name、b.age、b.job三个属性,但此时b还不能访问classa中定义的sayname和sayaage两个方法。

然后我们新增代码classb.prototype = classa.prototype;,此时classb的代码如下所示:

function classb(name, age, job) {
    classa.apply(this, [name, age]);
    this.job = job;
}
//新增
classb.prototype = classa.prototype;

当执行var b = new classb(“sunqun”, 28, “developer”);时,b.__proto__指向的是classb.prototype,由于通过新增的代码已经将classb.prototype指向了classa.prototype,所以此时b.__proto__指向了classa.prototype。这样当执行b.sayname()时,会执行b.__proto__.sayname(),即最终执行了classa.prototype.sayname(),这样classb的实例就能调用classa中方法了。

此时我们想为classb新增加一个sayjob方法用于输出job属性的值,如下所示:

function classb(name, age, job) {
    classa.apply(this, [name, age]);
    this.job = job;
}
classb.prototype = classa.prototype;
//新增
classb.prototype.sayjob = function(){
    console.log(this.job);
};

此时问题出现了,我们为classb.prototype添加sayjob方法时,其实修改了classa.prototype,这样会导致classa所有的实例也都有了sayjob方法,这显然不是我们期望的。 为了解决这个问题,我们再次修改classb的代码,如下所示:

function classb(name, age, job) {
    classa.apply(this, [name, age]);
    this.job = job;
}
// classb.prototype = classa.prototype;
//修改
classb.prototype = new classa();
classb.prototype.constructor = classb;
classb.prototype.sayjob = function(){
    console.log(this.job);
};

我们通过执行classb.prototype = new classa();将classa实例化的对象作为classb的prototype,这样classb仍然能够使用classa中定义的方法,但是classb.prototype已经和classa.prototype完全隔离了。

我们的目的达到了,我们可以随意向classb.prototype添加我们想要的方法了。有个细节需要注意,classb.prototype = new classa();会导致classb.prototype.constructor指向classa的实例化对象,为此我们通过classb.prototype.constructor = classb;解决这个问题。

关于此处为什么要设置.constructor可参见这篇博客
https://blog.csdn.net/c_kite/article/details/78484191

一切貌似完美的解决了,但是这种实现还是存在隐患。我们在执行classb.prototype = new classa();的时候,给classa传递的是空参数,但是classa的构造函数默认参数是有值的,可能会在构造函数中对传入的参数进行各种处理,传递空参数很有可能导致报错(当然本示例中的classa不会)。于是我们再次修改classb的代码如下所示:

function classb(name, age, job) {
    classa.apply(this, [name, age]);
    this.job = job;
}
//修改
function classmiddle() {

}
classmiddle.prototype = classa.prototype;
classb.prototype = new classmiddle();
classb.prototype.constructor = classb;
classb.prototype.sayjob = function () {
    console.log(this.job);
};

这次我们引入了一个不需要形参的函数classmiddle作为classb和classa之间的中间桥梁。

1. classmiddle.prototype = classa.prototype;: 将classmiddle.prototype指向classa.prototype,这样classmiddle可以访问classa中定义的方法。

2. classb.prototype = new classmiddle();: 将classmiddle的实例化对象赋值给classb.prototype,这样就相当于执行了classb.prototype.__proto__ = classmiddle.prototype;,所以classb就能使用classmiddle中定义的方法,又因为classmiddle.prototype指向了classa.prototype,所以classb.prototype.__proto__也指向了classa.prototype,这样classb能使用classa中定义的方法。

以上思路的精妙之处在于classmiddle是无参的,它起到了classb和classa之间的中间桥梁的作用。 现在我们为classa添加一些静态属性和方法,classa新增如下代码:

...

//为classa添加静态属性
classa.staticvalue = "static value";

//为classa添加静态方法
classa.getstaticvalue = function() {
    return classa.staticvalue;
};
classa.setstaticvalue = function(value) {
    classa.staticvalue = value;
};

静态属性和方法不属于某一个实例,而是属于类本身。classa.prototype上面定义的方法是实例方法,不是静态的。静态属性和方法是直接添加在classa上的。 为了使classb也能继承classa的静态属性和方法,我们需要为classb添加如下代码:

...

//classb继承classa的静态属性和方法
for (var p in classa) {
    if (classa.hasownproperty(p)) {
        classb[p] = classa[p];
    }
}

我们最终可以将上述继承代码的公共部分抽离成一个extendsclass方法,如下所示:

function extendsclass(child, father) {
    //继承父类prototype中定义的实例属性和方法
    function classmiddle() {

    }
    classmiddle.prototype = father.prototype;
    child.prototype = new classmiddle();
    child.prototype.constructor = child;

    //继承父类的静态属性和方法
    for (var p in father) {
        if (father.hasownproperty(p)) {
            child[p] = father[p];
        }
    }
}

我们只需要执行extendsclass(classb, classa);就可以完成大部分继承的逻辑。 最终classa的完整代码如下所示:

function classa(name, age) {
    this.name = name;
    this.age = age;
}

classa.prototype.sayname = function() {
    console.log(this.name);
};

classa.prototype.sayage = function() {
    console.log(this.age);
};

classa.staticvalue = "static value";

classa.getstaticvalue = function() {
    return classa.staticvalue;
};

classa.setstaticvalue = function(value) {
    classa.staticvalue = value;
};

classb的完整代码如下所示:

function classb(name, age, job) {
    classa.apply(this, [name, age]);
    this.job = job;
}

extendsclass(classb, classa);

classb.prototype.sayjob = function() {
    console.log(this.job);
};

es5继承

es5.1规范中新增了object.create()方法,该方法会传入一个对象,然后会返回一个对象,返回的对象的原型指向传入的对象,比如执行代码var output = object.create(input),相当于执行代码output.__proto__ = input;,output的原型是input。我们可以简化之前的代码,不再需要classmiddle,只需要执行classb.prototype = object.create(classa.prototype);即可,相当于执行代码classb.prototype.__proto__ = classa.prototype;。

而且es5.1中新增了object.keys()方法用以获取对象自身的属性数组,我们可以用该方法简化继承父类静态属性和方法的过程。

根据以上两点,我们修改extendsclass方法如下所示:

function extendsclass(child, father) {
    //继承父类prototype中定义的实例属性和方法
    child.prototype = object.create(father.prototype);
    child.prototype.constructor = child;

    //继承父类的静态属性和方法
    object.keys(father).foreach(function(key) {
        child[key] = father[key];
    });
}

es6 继承

我们之前提到,es6规范定义了object.prototype.proto属性,该属性既可读又可写,通过__proto__属性我们可以直接指定对象的原型。于是在es6中我们将extendsclass修改为如下所示:

function extendsclass(child, father) {
    //继承父类prototype中定义的实例属性和方法
    child.prototype.__proto__ = father.prototype;//暴力直接,利用__proto__属性设置对象的原型

    //继承父类的静态属性和方法
    child.__proto__ = father;
}

直接修改对象的__proto__属性值不是最佳选择,es6规范中还定义了object.setprototypeof()方法,通过执行object.setprototypeof(b, a)会将a对象作为b对象的原型,即相当于执行了b.__proto__ = a;。为此我们利用该方法再次精简我们的extendsclass方法,如下所示:

function extendsclass(child, father) {
    //继承父类prototype中定义的实例属性和方法
    object.setprototypeof(child.prototype, father.prototype);

    //继承父类的静态属性和方法
    object.setprototypeof(child, father);
}

object.setprototypeof(child.prototype, father.prototype);相当于执行代码child.prototype.__proto__ = father.prototype;,使得child能够继承father中的实例属性和方法。

object.setprototypeof(child, father);相当于执行代码child.__proto__ = father;,使得child能够继承father中的静态属性和方法。

由于现代 javascript 引擎优化属性访问所带来的特性的关系,更改对象的 [[prototype]]在各个和 javascript 引擎上都是一个很慢的操作。其在更改继承的性能上的影响是微妙而又广泛的,这不仅仅限于 obj.__proto__ = ... 语句上的时间花费,而且可能会延伸到任何代码,那些可以访问任何[[prototype]]已被更改的对象的代码。如果你关心性能,你应该避免设置一个对象的 [[prototype]]。相反,你应该使用 object.create()来创建带有你想要的[[prototype]]的新对象。

es6中引入了class关键字,可以用class直接定义类,通过extends关键字实现类的继承,还可以通过static关键字定义类的静态方法。

我们用class等关键字重新实现classa和classb的代码,如下所示:

class classa{
    constructor(name, age){
        this.name = name;
        this.age = age;
    }

    sayname(){
        console.log(this.name);
    }

    sayage(){
        console.log(this.age);
    }

    static getstaticvalue(){
        return classa.staticvalue;
    }

    static setstaticvalue(value){
        classa.staticvalue = value;
    }
}

classa.staticvalue = "static value";

class classb extends classa{
    constructor(name, age, job){
        super(name, age);
        this.job = job;
    }

    sayjob(){
        console.log(this.job);
    }
}

es6中不能通过static定义类的静态属性,我们可以直接通过classa.staticvalue = "static value";定义类的静态属性。

需要注意的是,class关键字只是原型的语法糖,javascript继承仍然是基于原型实现的。

并不是所有的浏览器都支持class关键字,在生产环境中,我们可以编写es6的代码,然后用babel或typescript将其编译为es5等主流浏览器支持的语法格式。

es6 class和之前几种继承区别

一个很重要的区别在于, es5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面, 例如上面的说的例子

function classb(name, age, job) {
    classa.apply(this, [name, age]);
    this.job = job;
}

es6 的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

总结

执行var x = new x();时,浏览器会执行x.__proto__ = x.prototype,会将实例化对象的原型设置为对应的类的prototype对象。

实现类继承的关键是child.prototype.__proto__ = father.prototype;,这样会将father.prototype作为child.prototype的原型。object.prototype.__proto__属性是在es6规范中所引入的,为了在es3和es5中需要通过各种方式模拟实现对object.prototype.__proto__进行赋值。

通过执行child.__proto__ = father;可以实现继承父类的静态属性和方法。