在上一篇博客《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
中查找。
这里有三点要注意一下:
a
又设置c
是其prototype,会报错set
和get
定义的函数上面提到的第三点中只说了赋值,如果是对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"}
那么问题就来了,如果当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...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
。
现在来补前面的一个坑,就是一开始的时候有如下代码
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上关注我,如果有问题欢迎在底下的评论区交流,谢谢。