javaScript面向对象编程之对象的继承
面向对象编程的重要特点就是继承,对象A继承对象B,就能拥有对象B的行为(方法)和状态(属性),有利于代码的复用。
JavaScript语言的继承通过"原型对象"(prototype
)实现,ES6引入了类(class
),参考ES6入门。
1. 概述原型对象
1.1 构造函数的缺点
再讲原型对象之前,先看一下构造函数。
JavaScript通过构造函数可以生成新的对象,实例对象的属性和方法定义在构造函数内部,通过构造函数生成的每个新对象都具有相同的属性和行为。
function Animal(age, name){
if(!(this instanceof Animal)){
return new Animal(age, name);
}
this.age = age;
this.name = name;
this.print = function(){
console.log(this.name);
}
}
var a1 = new Animal(2, "dog");
var a2 = new Animal(3, "cat");
// 通过构造函数生成的实例对象,都有age,name和print属性
a1; // {age: 2, name: "dog", print: ƒ}
a2; // {age: 3, name: "cat", print: ƒ}
上面的例子看起来没什么问题,每个动物都有自己的年龄跟姓名。我在上面例子的基础上,给动物加上睡觉,吃东西的行为。
function Animal(age, name){
if(!(this instanceof Animal)){
return new Animal(age, name);
}
this.age = age;
this.name = name;
this.print = function(){
console.log(this.name);
};
this.sleep = function(){
console.log("睡觉!");
};
this.eat = function(){
console.log("吃东西!")
};
}
var a1 = new Animal(2, "dog");
var a2 = new Animal(3, "cat");
a1.sleep(); // 睡觉
a2.sleep(); // 睡觉
a1.eat(); // 吃东西
a2.eat(); // 吃东西
// 都具有吃东西和睡觉的行为,那么两者一样吗?
a1.sleep === a2.sleep; // false
a1.eat === a2.eat; // false
从上面的例子中看到,a1
和a2
都有eat
和sleep
方法,但是两者却是不一样的。这是由于eat
和sleep
方法是生成在每个实例对象上的,所以两个实例就生成了两次。即每新建一个实例,都会新建一个eat
和sleep
方法。这样就比较浪费系统资源,因为所有eat
和sleep
方法都是同样的行为,每个实例应该共享。
这个就可以通过“原型对象”(prototype
)得到解决。
1.2 prototype属性
JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。
JavaScript 规定,每个函数都有一个prototype
属性,指向一个对象。
function f(){}
typeof f.prototype; // "object"
上面代码中,函数f
默认具有prototype
属性,指向一个对象。
对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。
function Animal(name){
this.name = name;
}
// 原型对象设置公共的属性sex
Animal.prototype.sex = 1 ;
typeof Animal.prototype; // "object"
var a1 = new Animal("cat");
var a2 = new Animal("dog");
a1.sex; // 1
a2.sex; // 1
上述代码中,构造函数Animal
的prototype
属性指向的对象,就是a1
和a2
的原型对象,在原型对象上面添加一个sex
属性,实例对象都能共享sex
属性。
原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。
// 原型对象修改sex
Animal.prototype.sex = 0 ;
// 每个实例的sex发生改变
a1.sex; // 0
a2.sex; // 0
当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法。
a1.sex = 2;
a1.sex; // 2
a2.sex ; // 0
Animal.prototype.sex; // 0
总结一下,原型对象的作用,就是定义所有实例对象共享的属性和方法。
1.3 原型链
JavaScript 规定,所有对象都有自己的原型对象(prototype
)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”:对象到原型,再到原型的原型……
如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype
,即Object
构造函数的prototype
属性指向的对象。也就是说,所有对象都继承了Object.prototype
的属性。
其实Object.prototype
也有自己的原型,它的原型是null
。null
没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null
。
Object.getPrototypeOf(Object.prototype); // null
那么完整的原型链就如下图所示:
读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype
还是找不到,则返回undefined
。如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”。
注意:在整个原型链寻找某个属性,对性能是有影响的,属性越在原型链的上游,对性能的影响越大。如果寻找一个不存在的属性,将会遍历整个原型链。
1.4 constructor属性
prototype
对象有一个constructor
属性,默认指向prototype
对象所在的构造函数。
function P() {}
P.prototype.constructor === P // true
constructor
属性还定义在prototype
对象上面的,那么意所有实例对象都可以继承。
function F(){}
var f1 = new F();
//prototype对象的constructor属性默认指向prototype对象所在的构造函数
f1.constructor === F; // true
f1.constructor === F.prototype.constructor; // true
那么constructor
属性有什么作用呢?
- 通过
constructor
属性判断实例对象到底是哪个构造函数生成的。
function F(){}
var f1 = new F();
f1.constructor;//F(){}
- 通过
constructor
属性,一个实例对象可以新建另一个实例对象.
function F(){}
var f1 = new F();
// 通过constructor属性新建另一个实例对象
var f2 = new f1.constructor();
f2 instanceof F; // true
constructor
属性表示原型对象与构造函数之间的关联关系,如果修改了原型对象,一般也要修改constructor
属性,防止引用的时候出错。
function F(){};
F.prototype.constructor === F; // true
// 修改原型对象为一个数组对象
F.prototype = new Array();
F.prototype.constructor === F; // false
F.prototype.constructor === Array; // true
上面代码中,修改了F
的原型对象,但是没有修改constructor
属性,导致constructor
属性不再指向F
。
在修改原型对象时,最好是修改constructor
属性。
function F(){}
// 不推荐写法,没有修改contructor属性
F.prototype = {
method1: function (...) { ... },
...
}
// 推荐写法,constructor属性重新指向原来的构造函数
F.prototype = {
constructor: F,
method1: function (...) { ... },
...
}
// 更好的写法,只在原型对象上面添加方法
F.prototype.method1 = function(...){...}
2. instanceof 运算符
instanceof
运算符返回一个布尔值,表示对象是否为某个构造函数的实例。instanceof
运算符的左边是实例对象,右边是构造函数。
var v = new Vehicle();
v instanceof Vehicle // true
instanceof
运算符的实质是:检查右边构造函数的原型对象是否在左边实例对象的原型链上。所以,下面两种写法是等价的。
function F(){}
var f = new F();
f instanceof F; // true
//等价于
F.prototype.isPrototypeOf(f); // true
由于instanceof
会检查整个原型链,因此同一个实例对象,可能会对多个构造函数都返回true
。
var now = new Date();
now instanceof Date; // true
now instanceof Object; // true
// 等价于
Date.prototype.isPrototypeOf(now);
Object.prototype.isPrototypeOf(now);
instanceof
运算符可以用来判断值的类型。
var person = {
name: "jidi",
age: 22
}
var arrays = ["I","love","java"];
person instanceof Object; // true
arrays instanceof Array; // true
instanceof
运算符可以用来判断一个值是否为非null
的对象,因为所有对象都是Object
的实例,除了null
。
var person = {
name: "jidi",
age: 22
}
// 判断一个值是否是非null对象
person instanceof Object; // true
null instanceof Object; // false
注意:instanceof
运算符只能用于对象,不适用原始类型的值。而且对于null
和undefined
总是返回false
。
3. 构造函数的继承
太晚了,顶不住了,有时间再写后面的…