javaScript面向对象编程之对象的继承

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

从上面的例子中看到,a1a2都有eatsleep方法,但是两者却是不一样的。这是由于eatsleep方法是生成在每个实例对象上的,所以两个实例就生成了两次。即每新建一个实例,都会新建一个eatsleep方法。这样就比较浪费系统资源,因为所有eatsleep方法都是同样的行为,每个实例应该共享。

这个就可以通过“原型对象”(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 

上述代码中,构造函数Animalprototype属性指向的对象,就是a1a2的原型对象,在原型对象上面添加一个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也有自己的原型,它的原型是nullnull没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null

Object.getPrototypeOf(Object.prototype); // null

那么完整的原型链就如下图所示:

对象
原型对象
原型对象的原型对象
.....
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属性有什么作用呢?

  1. 通过constructor属性判断实例对象到底是哪个构造函数生成的。
function F(){}
var f1 = new F();

f1.constructor;//F(){}
  1. 通过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运算符只能用于对象,不适用原始类型的值。而且对于nullundefined总是返回false

3. 构造函数的继承

太晚了,顶不住了,有时间再写后面的…

发布了6 篇原创文章 · 获赞 3 · 访问量 1502

猜你喜欢

转载自blog.csdn.net/qq_41863849/article/details/103964869