面向对象
内容介绍:
- 类的声明
- 生成实例
- 继承的几种方式
类的声明与实例化
/** * 类的声明 */ function Animal(){ this.name = 'name'; } /** * ES6中类的声明 */ class Animal2{ constructor(){ this.name = 'name'; } }
实例化
用new运算符来实例化
/** * * 实例化 */ new Animal(); new Animal2();
继承的几种方式
一、借助构造函数实现继承
代码实现如下:
function Parent(name){ this.name = name;//实例基本属性 this.like = ['apple','banana'];//实例引用属性 this.eat = function(){ //实例函数(引用属性) console.log(this.name + ' like ' + this.like.join(' ')); }; } //原型方法 Parent.prototype.say = function(){ console.log(this.name + 'say hi'); }; function Child(name){ Parent.call(this,name); //核心 call也可以用apply代替 this.type = 'child1'; } var child1 = new Child('张三'); var child2 = new Child('李四'); child2.like.push('orange'); child1.eat(); //张三 is like apple banana child2.eat(); //李四 is like apple banana orange child1.say(); //child1.say is not a function
实现继承的原理:在子类构造函数Child中执行 Parent.call(this) 这句代码;即在子类的构造函数Child中执行父类构造函数Parent,同时改变运行的上下文(this指向),使this指向了Child。从而父类的实例属性会挂载到子类上去,如此便实现了继承。等于是把父类的实例属性和方法复制一份给子类实例装上。
该继承方法的缺点:
1.只能继承父类的实例属性和方法,不能继承父类的原型属性和方法。所以上面例子中child1就获取不到say方法
2.无法实现函数共享,每new一次,Child的实例属性和方法都会开辟内存空间,比较浪费内存,缺乏效率。
为了解决内存消耗问题,下面介绍一下原型链继承
二 、原型链继承
实现继承的原理:每个构造函数都有prototype属性指向该构造函数的原型(原型对象),这个原型(原型对象)上的属性和方法都会被该构造函数的实例所继承。
代码如下:
function Parent(name){ this.name = name;//实例基本属性 this.like = ['apple','banana'];//实例引用属性 } //原型方法 Parent.prototype.eat = function(){ console.log(this.name + ' like ' + this.like.join(' ')); }; function Child(name){ this.type = 'child1'; } Child.prototype = new Parent();//核心 Child.prototype.constructor = Child;//修复构造函数的指向 var child1 = new Child('张三'); var child2 = new Child('李四'); child2.like.push('orange'); child1.eat(); //undefined like apple banana orange child2.eat(); //undefined like apple banana orange
原型链继承解决了构造函数继承的内存消耗问题,但也出现了新的问题:
该继承方法的缺点:
1. 创建子类的实例时,无法向父类构造函数传递参数(上例子中Child中的name 无法传给Parent)。
2. 如果在实例中修改了原型上的属性,那么原型的属性就会被修改,所有继承自该原型的类的属性都会一起改变。
上面代码中,只改变了child2这个实例的like属性,因为该属性是它原型的属性,即把原型的属性更改了,所以实例child1的值也改变了。
三、组合方式
借鉴上面两种方式的优缺点,采用构造函数和原型链结合方式
代码如下:
function Parent(name){ this.name = name;//实例基本属性 this.like = ['apple','banana'];//实例引用属性 } //原型方法 Parent.prototype.eat = function(){ console.log(this.name + ' like ' + this.like.join(' ')); }; function Child(name){ Parent.call(this,name); //核心 this.type = 'child1'; } Child.prototype = new Parent();//核心 子类的原型指向父类 Child.prototype.constructor= Child;//修复构造函数的指向 var child1 = new Child('张三'); var child2 = new Child('李四'); child2.like.push('orange'); child1.eat(); //张三 like apple banana child2.eat(); //李四 like apple banana orange
实现继承的原理:
通过Parent.call(this,name); 可以继承父类的基本属性和引用属性 ,并且可以传参数;
通过 Child.prototype = new Parent(); 将子类的原型指向父类的实例,这样子类的实例(即new child())就继承了父类的所有属性和方法(包括实例属性/方法和原型属性/方法)。
优点:
1. 修改子类实例的属性时,不会引起父类的属性的变化
2. 可以向父类传参数了
缺点:
1. 创建子类的实例的时候,父类构造函数执行了两次
上面代码中,在构造函数继承(即Parent.call(this,name))的时候已经执行了一次Parent,父类Parent的属性已经在子类Child里运行了(Child已经有了Parent的属性和方法,不包括原型上的属性和方法)。外面原型链继承的时候(即Child.prototpe= new Parent())就没有必要再执行一次Parent了,只需要把原型上的属性和方法继承了就可以了。
四、组合方式的优化1(推荐)
组合方式中为了解决构造函数继承的缺点(即父类的原型中的属性继承不了),所以把子类的原型指向了父类的实例。但是父类的属性在构造函数继承时子类中就已经存在了,子类只缺少原型上的属性和方法。所以,我们进一步优化,把子类的原型指向父类的原型。这样就避免了父类构造函数的二次执行
代码如下:
function Parent(name){ this.name = name;//实例基本属性 this.like = ['apple','banana'];//实例引用属性 } //原型方法 Parent.prototype.eat = function(){ console.log(this.name + ' like ' + this.like.join(' ')); }; function Child(name){ Parent.call(this,name); //核心 this.type = 'child1'; } Child.prototype = Parent.prototype;//核心 子类的原型指向父类的原型 Child.prototype.constructor= Child;//修复构造函数的指向 var child1 = new Child('张三'); var child2 = new Child('李四'); child2.like.push('orange'); child1.eat(); //张三 like apple banana child2.eat(); //李四 like apple banana orange
缺点:
上例中的代码Child.prototype = Parent.prototype;即把子类的原型指向了父类的原型,此时子类的原型的构造函数就是父类的原型的构造函数,而父类的原型的构造函数是Parent,所以Child.prototype和Parent.prototype共用一个构造函数Parent。显然不是我们想要的,所以我们执行了Child.ptototype.constructor = Child;即把子类的构造函数指向自己。但是这样做同时也把父类的构造函数指向了子类。如下面的代码:
console.log(child1.__proto__.constructor === Child); //true console.log( new Parent().__proto__.constructor === Child);//true console.log( new Parent().__proto__.constructor === Parent);//false
通过上面代码可以看出,子类Child的实例的构造函数是Child;父类Parent的实例的构造函数也是Child。
五、组合方式优化2(推荐)
实现的原理:通过Object.cerate(obj)方法,该方法创建了一个新的对象,并且这个新的对象继承了指定的对象obj。类似于:
//将Object.create()的实现原理 封装成一个函数 function create(obj){ function F(){}; //创建一个构造函数 F.prototype = obj; //对象obj 赋给 F的原型 return new F(); //返回构造函数的实例,这样F的实例就会继承obj的所有属性和方法 }
代码实现如下:
function Parent(name){ this.name = name;//实例基本属性 this.like = ['apple','banana'];//实例引用属性 } //原型方法 Parent.prototype.eat = function(){ console.log(this.name + ' like ' + this.like.join(' ')); }; function Child(name){ Parent.call(this,name); //核心 this.type = 'child1'; } Child.prototype = Object.create(Parent.prototype);//核心 子类的原型指向Object.create 创建的中间对象 Child.prototype.constructor = Child;//把Child的原型的构造函数的指向Child var child1 = new Child('张三'); var child2 = new Child('李四'); child2.like.push('orange'); child1.eat(); //张三 like apple banana child2.eat(); //李四 like apple banana orange console.log(child1.__proto__.constructor === Child); //true console.log(new Parent().__proto__.constructor === Parent);//true
代码Child.prototype = Object.create(Parent.prototype); 即把子类的原型指向了Object.create(Parent.prototype)创建的新对象,该对象继承了Parent.prototype的属性和方法。这样子类和父类构造函数就分离开了。然后再把子类的原型的构造函数指向自己。
有上面的代码可以看出,子类实例的构造函数指向了子类Child,父类的实例的构造函数指向了父类Parent。这正是我们想要的结果。