javascript面向对象(二):prototype以及继承详解

在上一篇博客《javascript面向对象(一):object基础以及构造函数详解》中我们了解了各种在js中创建object对象的方法。但是在面向对象语言中,很少会从零创建一个对象,更多的是在现有模板的基础上进行继承和修改。这一节我们就来看看js中obejct对象的继承。

简单继承

在js中,object对象有一个隐藏属性叫[[Prototype]]要么为null或者指向另一个object对象。如果其指向另一个object对象,那么该对象被称为原对象的prototype。

这样做的目的,就是在一个object对象中获取属性的时候,如果没找到,就会去其prototype中查找。有点像其他OOP语言中的继承,子类的属性没有就会去父类中查找,不过这里还没有类的概念。

前面说[[Prototype]]属性是隐藏的,不能直接访问,不过有一种方法可以用来对其读取和赋值,就是object对象的__proto__属性。

__proto__属性是一种已经过时的获取prototype的方法,在最后会讲解为何以及如何改进

看个例子

let a={'name':'xiaofu'};
console.log(a.__proto__);

打印结果如下

{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ,}
constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()

说明我们自己创造的object对象,还是从prototype中获得了很多属性的(方法也是一种属性)。下面我们还会详细讲系统的一些内建prototype,这里先跳过。

我们尝试去新建一个对象,并将a做为其prototype

let a={'name':'xiaofu'};
let b={'age':99};
b.__proto__=a ;
console.log(b.__proto__); 
console.log(b['name']);

打印结果如下

{name: "xiaofu"}
xiaofu

可以看出,当寻找一个b中本来不存在的属性name的时候,确实会去其prototypea中进行查找。

所以如果一个object对象中有很多有用的属性或者方法,就可以直接用这种方式进行继承来使用

a['shout']=function(){
console.log('AH!!!')
}; //方法也可以被继承
b.shout(); //AH!!!

同时还可以进行多重继承,例如再新建一个对象

let c={};
c.__proto__=b;
console.log(c['name']);//xiaofu
c.shout();//AH!!!

这里c继承了b,所以当c中有属性查找不到的时候,先去b中查找,查到不到再去a中查找。

这里有三点要注意一下:

  • prototype的关联不能形成循环,例如上面的a又设置c是其prototype,会报错
  • 对prototype进行赋值的时候,只能是null或者一个object对象,不能是另外的任何数据类型,也会报错
  • 赋值和删除操作不会影响prototype中的内容,而只会对原object对象自身的属性产生影响。只有一个特殊情况就是当prototype中的属性并不是直接的变量,而是通过setget定义的函数

上面提到的第三点中只说了赋值,如果是对object或者Array对象进行修改还是会沿着继承链条进行寻找的

let a={'info':{'name':'xiaofu'}};
let b={};
b.__proto__=a;
b['info']['name']='zhangsan';
console.log(b['info']); //{name: "zhangsan"}
console.log(a['info']); //{name: "zhangsan"}
b['info']={};
console.log(b['info']); //{}
console.log(a['info']); //{name: "zhangsan"}

this关键字

那么问题就来了,如果当prototype中的方法有this关键字的时候,这个this是指代原object对象还是其prototype对象呢?

答案很简单,记住一条规律,不管方法是在object对象还是其prototype中定义的,在这个方法被调用的时候,其中的this关键字永远指向点号前面的那个对象

No matter where the method is found: in an object or its prototype. In a method call, this is always the object before the dot.

看个例子

let a={
    'name':'xiaofu',
    'shout':function(){
        console.log('My name is '+this.name);
    }
};
let b={'name':'zhangsan'};
b.__proto__=a;
b.shout(); //My name is zhangsan
a.shout(); //My name is xiaofu

补充:在DOM操作的时候,异步函数中的this指代引发异步操作的DOM对象

for循环

同时要指出的是,如果用for...in对一个object对象进行循环,prototype中的属性也会被循环到。

let a={
    'name':'xiaofu',
    'shout':function(){
        console.log('My name is '+this.name);
    }
};
let b={'age':99};
b.__proto__=a;
for(let key in b){
    if(b.hasOwnProperty(key)){
        console.log('own: '+key);
    }else{
        console.log('proto: '+key);
    }
}

注意,hasOwnProperty方法只会对自身定义的属性返回true,而对prototype中的属性返回false。所以打印的结果为

own: age
proto: name
proto: shout

但是有个有趣的问题,就是b的方法hasOwnPrototype为什么没有被for循环操作呢?

这是因为内建的prototype的属性都被放了一个enumerate:false的flag,所以不会被循环。

构造函数的继承

上一篇中说过,除了直接用大括号,还可以用构造函数创建对象。这个时候想指定prototype就不能用__proto__了,而要用函数的prototype属性

let a={
    'name':'xiaofu',
    'shout':function(){
        console.log('My name is '+this.name);
    }
};
function Test(age){
    this.age=age;
};
Test.prototype=a;
let b=new Test(99);
for(let key in b){
    if(b.hasOwnProperty(key)){
        console.log('own: '+key);
    }else{
        console.log('proto: '+key);
    }
}

这里通过Test.prototype=a来将Test创建出来的对象都赋予一个prototype。最后的打印结果为

own: age
proto: name
proto: shout

如果给了一个object对象,在不清楚其构造函数的情况下想要创建一个同样的对象,可以用该对象的constructor方法获取构造函数。不管是用大括号还是构造函数,默认情况下都是可以在prototype中找到constructor的,但是如果人为修改了prototype就不一定了

let a={
    'name':'xiaofu',
    'shout':function(){
        console.log('My name is '+this.name);
    }
};
function Test(age){
    this.age=age;
};
let b=new Test(99);
console.log(b.constructor); //ƒ Test(age){this.age=age;}
Test.prototype=a;
let c=new Test(99);
console.log(c.constructor); //ƒ Object()

可以看到,如果没有人为赋值的情况下,b.constructor指向的是Test函数,所以可以直接用new b.constructor(xx)来创建一个跟b相同的对象。但是一旦将a赋值给Test.prototype,导致新创建的对象的prototype没有了constructor属性,只能继续网上寻找到内建prototype,也就是Object()构造函数。

想要规避这种问题,一是将直接的赋值改为修改

function Test(age){
    this.age=age;
};
let b=new Test(99);
console.log(b.constructor); //ƒ Test(age){this.age=age;}
let c=new b.constructor(33); 
// Test.prototype=a;
Test.prototype['name']='xiaofu'
let d=new Test(99);
console.log(d.constructor); //ƒ Test(age){this.age=age;}

二是人为在赋值的prototype中添加constructor属性

let a={
    'name':'xiaofu',
    'shout':function(){
        console.log('My name is '+this.name);
    }
};
function Test(age){
    this.age=age;
};
let b=new Test(99);
console.log(b.constructor); //ƒ Test(age){this.age=age;}
let c=new b.constructor(33);
Test.prototype=a;
Test.prototype['constructor']=Test;
let d=new Test(99);
console.log(d.constructor); //ƒ Test(age){this.age=age;}

来看下面的一个趣味测试。

首先有如下代码

function Rabbit() {}
Rabbit.prototype = {
  eats: true
};

let rabbit = new Rabbit();

alert( rabbit.eats ); // true

现在分别修改为如下的变式,问最后的输出有什么变化呢?

第一种变式

function Rabbit() {}
Rabbit.prototype = {
  eats: true
};

let rabbit = new Rabbit();

Rabbit.prototype = {};

alert( rabbit.eats ); // ?

Javascript的继承只能来自object对象,构造函数的prototype指向哪个object对象,则创建的对象都指向同一个对象。将prototype重新赋值,但是已经创建出来的对象的prototype还是指向原先的{eats: true}内存地址,只有新创建的对象会受影响,所以结果还是true

js的继承比较死板,不像python中有类属性和实例属性之分,值得一提的是,python中修改了类属性,已经创建的实例中进行取值结果也会改变

第二种变式

function Rabbit() {}
Rabbit.prototype = {
  eats: true
};

let rabbit = new Rabbit();

Rabbit.prototype.eats = false;

alert( rabbit.eats ); // ?

只是修改的话,所有已创建的对象都会受到影响,结果为false

第三种变式

function Rabbit() {}
Rabbit.prototype = {
  eats: true
};

let rabbit = new Rabbit();

delete rabbit.eats;

alert( rabbit.eats ); // ?

前面说到,赋值和删除操作只对对象自己本身的属性有效果,对prototype中的属性无效。所以这里的delete操作不会有任何影响,结果还是true

第四种变式

function Rabbit() {}
Rabbit.prototype = {
  eats: true
};

let rabbit = new Rabbit();

delete Rabbit.prototype.eats;

alert( rabbit.eats ); // ?

将prototype中的值删除,则rabbit指向的prototype中没有了eats属性,找不到于是返回undefined

内建prototype

现在来补前面的一个坑,就是一开始的时候有如下代码

let a={'name':'xiaofu'};
console.log(a.__proto__);

我们用大括号定义了一个对象,这个对象里面并没有什么hasOwnProperty之类的方法,这些方法是从哪来的呢?

JS中一切皆object,所有的对象都是通过Object()这个构造函数创建出来的,而Object函数又被赋予了一个内建prototype

let a={'name':'xiaofu'};
console.log(a.__proto__===Object.prototype); //true

所以很多内建方法都是从这个prototype里面获得的。

值得一提的是Array和Number这种数据类型继承自Object,所以prototype链最终都是指向Object.prototype。当然每种数据类型会创建自己的一些方法去覆盖最原始的方法。

例如想要让所有的函数都加一个接口func.defer(ms),使用这个接口以后会延迟时间执行函数

Function.prototype.defer=function(ms){
    let f = this;
    return function(...args){
        setTimeout(function(){f(...args)},ms);
    };
};
function shout(name){
    console.log('Life is awesome '+name);
}
shout.defer(2000)('xiaofu'); 

这有点像python中的装饰器了,实现的原理也是一样的,只不过js可以方便的在外层函数用this来指代原函数。要注意如果this直接放进内层函数是不能指代原函数的,因为this永远指向调用函数的那个对象。同时setTimeout的第一个参数必须得是一个函数,不能直接放f(...args),达不到延迟的效果。

当然,要强调的是内建的prototype尽量不要修改,以避免不必要的冲突

对简单继承的改进

前面说到直接用__proto__去获取和修改对象的prototype,但是这种方式有个很明显的缺点,就是这个对象不能存储key为__proto__的内容

let a={};
a['__proto__']='xiaofu';
console.log(a['__proto__']);

这样子是无法获取到xiaofu的,能获取到的只是一个内建的prototype。

不过__proto__属性毕竟不是prototype本身,而只是其一个get/set的方法,所以如果有一个完全没有prototype的对象,就不会有__proto__做为key的问题了。

这种创建的时候prototype为null的对象叫做plain object,之前用__proto__赋值和构造函数赋值的方式都无法创造出plain对象,于是有了下面的一种方法

Object.create(proto, [descriptors])

第一个参数是一个object对象或者null,第二个参数是想额外添加的一些放进prototype中间的键值对,例如

let a=Object.create(null);
console.log(a['__proto__']); //undefined
a['__proto__']='xiaofu';
console.log(a['__proto__']); //xiaofu

但是这样子又引入了一个新的问题,就是无法获取和修改prototype了,于是又有个下面连个方法

Object.getPrototypeOf(obj)
Object.setPrototypeOf(obj, proto)

例如

let a=Object.create(null);
console.log(a['__proto__']); //undefined
a['__proto__']='xiaofu';
console.log(a['__proto__']); //xiaofu
console.log(Object.getPrototypeOf(a)); //null
Object.setPrototypeOf(a,{'age':99});
console.log(Object.getPrototypeOf(a)); //{'age':99}
Object.setPrototypeOf(a,{'hobby':'coding'});
console.log(Object.getPrototypeOf(a)); //{'hobby':'coding'}

这样子操作的缺点就是内建的prototype中的一些方法会丢失,但是好处就是得到了一个plain的object,同时要注意一些object的操作并没有丢失,例如Object.keys()

总结

这两节下来就差不多把JS的面向对象了解的差不多了。

JS中的继承只能来自现成的对象,并且所有新的对象的prototype都指向同一内存地址,这种设计确实和其他常见的OOP语言不太一样。于是JS又在这基础上引入了class语法,我们下一节再一起看看。

我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值