为了账号安全,请及时绑定邮箱和手机立即绑定

JavaScript继承之原型继承

基本概念

每个函数都有一个prototype属性,这个属性是一个指针,指向原型对象。函数原型的主要用途是:可以使实例共享它所包含的属性和方法。

例子:

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

//函数CreateObj的原型对象CreateObj.prototype
CreateObj.prototype.job = "It";
CreateObj.prototype.getName = function() {
    console.log(this.name);
}

var p1 = new CreateObj("Tom", 20);
var p2 = new CreateObj("Amy", 19);

console.log(p1.age); //输出:20
console.log(p2.age); //输出:19
console.log(p1.getName()); //输出:Tom
console.log(p2.getName()); //输出:Amy

*p1和p2访问的是同一个getName()函数。

console.log(p1.getName === p2.getName); //输出:true

原型与构造函数、实例的关系

1、原型与构造函数的关系

自定义的构造函数,其原型对象默认会取得constructor属性,这个属性指向原型对象所在的函数。

例子:

function CreateObj() {}

console.log(CreateObj.prototype.constructor === CreateObj);
//输出:true

2、原型与实例的关系

当调用构造函数创建一个新实例后,该实例的内部将默认拥有一个属性,指向原型对象。由于这个属性并没有标准的写法,所以一些浏览器支持一个__proto__属性。

例子:

function CreateObj() {};

var p1 = new CreateObj();

console.log(p1.__proto__ === CreateObj.prototype);
//输出:true

我们也可以通过下面的方法来确定原型与实例之间是否存在这种关系。

isPrototypeOf()方法:

例子:

function CreateObj() {};

var p1 = new CreateObj();

console.log(CreateObj.prototype.isPrototypeOf(p1));
//输出:true

Object.getPrototypeOypeOf()方法:

例子:

function CreateObj() {};

CreateObj.prototype.job = "It";

var p1 = new CreateObj();

console.log(Object.getPrototypeOf(p1) === CreateObj.prototype); 
//输出:true
console.log(Object.getPrototypeOf(p1).job); 
//输出:It

3、构造函数与实例的关系

由于原型上的constructor属性是共享的,因此也可以通过实例对象访问。

例子:

function CreateObj() {};

var p1 = new CreateObj();

console.log(p1.constructor === CreateObj);
//输出:true

也可以通过instanceof方法确定构造函数与实例之间的关系。

例子:

function CreateObj() {};

var p1 = new CreateObj();

console.log(p1 instanceof CreateObj);
//输出:true

原型实现继承的原理

每当代码读取对象的某个属性时,就会执行一次搜索:首先从实例本身开始,如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,如果在原型对象中找到了这个属性,则返回该属性的值。

例子:

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

CreateObj.prototype.job = "It";
CreateObj.prototype.getName = function() {
    console.log(this.name);
}

var p1 = new CreateObj("Tom", 20);

console.log(p1.name); //输出:Tom 
console.log(p1.job); //输出:It
console.log(p1.getName()); //输出:Tom

重点解析

虽然可以通过实例访问保存在原型中的值,但却不能通过实例修改原型中的值。如果我们在实例中添加了一个属性,而该属性与原型中的一个属性同名,那我们其实是在实例中新创建了这个属性,这个属性会屏蔽原型对象中保存的同名属性,优先被我们搜索到。

例子:

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

CreateObj.prototype.job = "It";
CreateObj.prototype.getName = function() {
    console.log(this.name);
}

var p1 = new CreateObj("Tom", 20);

p1.job = "worker";
console.log(p1.job); //输出:worker
console.log(Object.getPrototypeOf(p1).job); //输出:It

使用delete操作符可以删除实例属性,从而让我们能够重新访问它原型中的属性。

例子:

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

CreateObj.prototype.job = "It";
CreateObj.prototype.getName = function() {
    console.log(this.name);
}

var p1 = new CreateObj("Tom", 20);

p1.job = "worker";
console.log(p1.job); //输出:worker
delete p1.job;
console.log(p1.job); //输出:It

使用hasOwnProperty()方法可以检测对象的属性是存在于实例中,还是存在于原型中。

例子:

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

CreateObj.prototype.job = "It";
CreateObj.prototype.getName = function() {
    console.log(this.name);
}

var p1 = new CreateObj("Tom", 20);

console.log(p1.hasOwnProperty("name")); //输出:true
console.log(p1.hasOwnProperty("job")); //输出:false

p1.job = "worker";
console.log(p1.hasOwnProperty("job")); //输出:true

in操作符:in操作符不会区分将要访问的属性是存在于实例中还是原型中,只要能够访问到,就会返回true。

例子:

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

CreateObj.prototype.job = "It";
CreateObj.prototype.getName = function() {
    console.log(this.name);
}

var p1 = new CreateObj("Tom", 20);

console.log("job" in p1); //输出:true
console.log("name" in p1); //输出:true

for (key in p1) {
    console.log(key)
}
//输出:name age job getName

ES6中新增的方法
1、Object.setPrototypeOf() 方法

用于重新指定对象的原型。这个方法接受两个参数:第一个参数是需要改变原型的实例对象,第二个参数是替代第一个参数原型的原型对象。

例子:

const P1 = function() {}
P1.prototype.getN = function() {
    console.log(1);
}

const P2 = function() {}
P2.prototype.getN = function() {
    console.log(2);
}

const person1 = new P1();

console.log(Object.getPrototypeOf(person1) == P1.prototype); //输出:true
console.log(person1.getN()); //输出:1

//改变实例对象的原型
Object.setPrototypeOf(person1, P2.prototype);

console.log(Object.getPrototypeOf(person1) == P2.prototype); //输出:true
console.log(person1.getN()); //输出:2

2、Super对象

super是绑定在函数内部的对象,可以正确的指向当前对象的原型。实际上,也可以看作是Object.getPrototypeOf(this)方法的简写。

例子:

const F = function(name, age) {
    return {
        name,
        age,
        getName: function() {
            return this.name;
        }
    }
}

const S = {
    fn() {
        console.log(super.name); //输出:Tom
        console.log(super.age); //输出:19
        console.log(super.getName()); //输出:Tom
        console.log(super.name == Object.getPrototypeOf(this).name); //输出:true
    }
};
Object.setPrototypeOf(S, new F("Tom", 19));
S.fn();

注意,super表示原型对象时,只能用在对象的方法之中,而且必须是ES6的简写方法,否则报错。

例子:

const F = function(name, age) {
    return {
        name,
        age,
        getName: function() {
            return this.name;
        }
    }
}

const S = {
    fn_1() {
        console.log(super.name); //输出:Tom
    },
    fn_2: () => { //箭头函数内部没有super对象的绑定
        console.log(super.name); //输出:报错
    },
    fn_3: function() { //不是简写方法
        console.log(super.name); //输出:报错
    }
};
Object.setPrototypeOf(S, new F("Tom", 19));
S.fn_1();
S.fn_2();
S.fn_3();

重写原型

将构造函数的prototype属性的值设置为一个以对象字面量形式创建的新对象,这样就可以重写原型。重写原型的好处是:不用每添加一个属性或方法就敲一遍构造函数的prototype属性。

例子:

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

CreateObj.prototype = {
    job: "It",
    getName: function() {
        console.log(this.name);
    }
}

var p1 = new CreateObj("Tom", 20);

console.log(p1.constructor == CreateObj);
console.log(CreateObj.prototype.constructor == CreateObj);
console.log(p1.constructor == Object);
console.log(CreateObj.prototype.constructor == Object);
//输出:false,false,true,true

通过上面的例子可以发现:原型对象和实例的constructor属性不再指向构造函数CreateObj,因为我们完全重写了prototype对象,并且没有指定constructor属性的值,所以constructor属性默认指向Object构造函数。如果constructor的值真的很重要,可以像下面这样特意将它设置回适当的值。

例子:

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

CreateObj.prototype = {
    constructor: CreateObj,
    job: "It",
    getName: function() {
        console.log(this.name);
    }
}

var p1 = new CreateObj("Tom", 20);

console.log(p1.constructor == CreateObj);
console.log(CreateObj.prototype.constructor == CreateObj);
console.log(p1.constructor == Object);
console.log(CreateObj.prototype.constructor == Object);
//输出:true,true,false,false

初始原型与重写原型的区别

原型与实例之间的关系属于松散的连接关系,也就是:我们对原型对象所做的任何修改都能够立即从实例上反映出来,即使是先创建了实例,后修改原型也照样如此。

例子:

function CreateObj(name) {
    this.name = name;
}

var p1 = new CreateObj("Tom");

CreateObj.prototype.getName = function() {
    console.log(this.name);
}

console.log(p1.getName());
//输出:Tom

而重写原型时,如果先创建实例,后修改原型,会导致创建实例的时候,实例的__proto__属性不能正确指向最初的原型对象,从而找不到属性名发生错误。

例子:

function CreateObj(name) {
    this.name = name;
}

var p1 = new CreateObj("Tom");

CreateObj.prototype = {
    constructor: CreateObj,
    getName: function() {
        console.log(this.name);
    }
}

console.log(p1.getName());
//输出:error

原生构造函数的原型

所有的引用类型(Object、Array、String等)都可以看作原生的构造函数,在其构造函数的原型上都默认定义了很多方法。例如数组方法:Array.prototype.slice()

例子:

var a = [1, 2, 3];
var b = a.slice(0, 2);

console.log(b);
console.log(a.constructor == Array);
console.log(a.__proto__ == Array.prototype);
//输出:[1, 2],true,true

也可以在原生构造函数的原型上定义新方法。

例子:

String.prototype.firstBigA = function() {
    var str, a = this.split("");
    a[0] = "A";
    str = a.join("");
    return str;
}

console.log("aaa".firstBigA());
//输出:Aaa

原型继承的不足

原型中所有属性是被很多实例共享的,这种共享对于函数非常合适,但对于包含引用类型值的属性来说,问题比较大。

例子:

function Family() {};
Family.prototype = {
    constructor: Family,
    names: ["爸爸", "妈妈"]
}

var p1 = new Family();
var p2 = new Family();

p1.names.push("爷爷");
p2.names.push("姥姥");

console.log(p1.names);
console.log(p2.names);
//p1 输出:["爸爸", "妈妈", "爷爷", "姥姥"]
//p2 输出:["爸爸", "妈妈", "爷爷", "姥姥"]

上面的例子中,p1与p2输出一样的结果,修改一个实例属性,却影响了另一个实例属性,这并不我们想要看到的结果。因此不建议单独使用原型模式,而需要组合使用构造函数与原型。具体参考:JavaScript创建对象之构造函数


文中的代码部分,带有“例子”和“测试代码”字样的,只是用来学习或测试某一功能用的代码,不可以直接用于项目的开发中。带有“代码如下”字样的,都是经过本人测试,简单修改即可用于项目开发中的代码,如有错误,欢迎指出。

点击查看更多内容
2人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消