当前位置: 移动技术网 > IT编程>开发语言>JavaScript > JavaScript的ES3,ES5,ES6三种实现继承方式的教程

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

2018年11月28日  | 移动技术网IT编程  | 我要评论
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;可以实现继承父类的静态属性和方法。

如您对本文有疑问或者有任何想说的,请 点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网