面向对象有三大特性:封装、继承、多态
1)封装:将属性和方法封装到一个类中,可以称之为封装的过程;
2)继承:面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中);
3)多态:不同的对象在执行时表现出不同的形态;
一、JavaScript原型链
1.1. 原型链
- 从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取:
// obj对象 var obj = { name: 'yzh', age: 18 }; //第一个{} obj.__proto__ = { } //第二个{} obj.__proto__.__proto__ = { } //第三个{} obj.__proto__.__proto__.__proto__ = { address: '深圳市' } console.log(obj.address); //深圳市
1.2. 原型链尽头
- 打印第三个对象原型__proto__属性
console.log(obj.__proto__.__proto__.__proto__.__proto__); //[Object: null prototype] {} [Object: null prototype] {}这个原型就是我们最顶层的原型了,从Object直接创建出来的对象的原型都是 [Object: null prototype] {}- 1)该对象有原型属性,但是它的原型属性已经指向的是null,也就是已经是顶层原型了;
- 2)该对象上有很多默认的属性和方法;
原型链最顶层的原型对象就是Object的原型对象(Object是所有类的父类);
二、原型链实现继承
// 1. 父类:公共属性和方法;
function Person() {
this.name = 'yzh'
this.friends = [];
}
// 2.
Person.prototype.eating = function () {
console.log(this.name + '吃东西!');
};
// 3. 子类:特有的属性和方法;
function Student() {
this.son = 13456798465;
}
/**
* 内存表现:
* 创建了一个p对象,p对象的__proto__指向Person()的默认原型对象,
* 把 p 对象赋值给Student.prototype。所以当我们在stu找不到属性时就会沿着原型链
* 找到;
*/
// 4.创建父类对象,并且作为子类的原型对象
var p = new Person();
Student.prototype = p;
// 5.
Student.prototype.studying = function () {
console.log(this.name + '在学习!');
};
var stu = new Student();
console.log(stu.son); //13456798465
stu.studying(); //yzh在学习!
console.log(stu.name); //yzh
stu.eating(); //yzh吃东西!
2.1. 原型链继承的弊端
-
1)某些属性其实是保存在p对象上的,输出stu对象看不到这个属性;
console.log(stu); //Person { son: 13456798465 } -
2)创建出来两个stu对象;
var stu1 = new Student(); var stu2 = new Student(); //直接修改对象上的属性,是给本对象添加了一个新属性; stu1.name = "ace"; console.log(stu1, stu1.name); //ace console.log(stu2, stu2.name); //yzh //获取引用,修改引用中的值,会互相影响; stu1.friends.push("luffly"); console.log(stu1.friends) //[ 'luffly' ] console.log(stu2.friends) //[ 'luffly' ]如图:
-
3)在前面实现类的过程中都没有传递参数;
三、借用构造函数实现继承
- 为了解决原型链继承中存在的问题,开发人员提供了一种新的技术: constructor stealing:
- steal是偷窃、剽窃的意思,但是这里可以翻译成借用;
- 借用继承的做法:在子类型构造函数的内部调用父类型构造函数;
- 因为函数可以在任意的时刻被调用;
- 因此通过apply()和call()方法也可以在新创建的对象上执行构造函数;
constructor stealing有很多名称:借用构造函数、经典继承、伪造对象;
// 父类:公共属性和方法;
function Person(name, age, friends) {
this.name = name;
this.age = age;
this.friends = friends;
};
Person.prototype.eating = function () {
console.log(this.name + '吃东西!');
};
// 子类:特有的属性和方法;
function Student(name, age, friends, son) {
Person.call(this, name, age, friends, son); //相当于借用Person()赋值这一操作
this.son = son;
};
var p = new Person();
Student.prototype = p;
Student.prototype.studying = function () {
console.log(this.name + '在学习!');
};
var stu = new Student("yzh", 18, ['long'], 180);
//原型链实现继承解决的弊端:
//第一个弊端:输出stu对象,继承的属性是看不到的;
console.log(stu); //Person { name: 'yzh', age: 18, friends: [ 'long' ], son: 180 }
//第二个弊端:创建出来两个stu对象;
var stu1 = new Student("stu1", 18, ['long'], 181);
var stu2 = new Student("stu2", 20, ['long'], 182);
// 直接修改对象上的属性,是给本对象添加了一个新属性;
stu1.name = "ace";
console.log(stu1, stu1.name); //ace
console.log(stu2, stu2.name); //yzh
// 获取引用,修改引用中的值,会互相影响;
stu1.friends.push("luffly");
console.log(stu1.friends) //[ 'long', 'luffly' ]
console.log(stu2.friends) //[ 'long' ]
3.1. 借用构造函数的弊端
- 1)无论在什么情况下,都会调用两次父类构造函数;
- 一次在创建子类原型的时候;
- 另一次在子类构造函数内部(也就是每次创建子类实例的时候);
- 2)stu的原型对象上会多一些属性,但是这些属性是没有存在的必要;
3.2. 解决方法
四、原型式继承函数
- 继承的目的:重复利用另外一个对象的属性和方法;
- 最终的目的:info对象的原型指向了obj对象;
var obj = { name: "yzh", age: 18 }; //1. function createObject1(o) { var newObj = {}; Object.setPrototypeOf(newObj, o); return newObj }; // var info = createObject1(obj); //2. function createObject2(o) { function Fn() {}; Fn.prototype = o; var newObj = new Fn(); return newObj; }; // var info = createObject2(obj); //3. var info = Object.create(obj); console.log(info); //{} console.log(info.__proto__); //{ name: 'yzh', age: 18 }
Object.setPrototypeOf()方法设置一个指定的对象的原型(即,内部[[Prototype]]属性) 到另一个对象;#语法:Object.setPrototypeOf(obj, prototype);
#参数:
obj要设置其原型的对象;prototype该对象的新原型(一个对象或null);
Object.create()方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype);#语法:Object.create(proto);
#参数:
proto新创建对象的原型对象;
五、寄生式继承函数
- 寄生式(Parasitic)继承是与原型式继承紧密相关的一种思想;
- 寄生式继承的思路是结合 原型类继承 和 工厂模式 的一种方式;
- 即创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再将这个对象返回;
var personObj = { running: function() { console.log('running!'); } }; function createStudent(name) { var stu = Object.create(personObj); stu.name = name; stu.studying= function() { console.log('studying!'); }; return stu; }; var obj2 = createStudent('ace'); console.log(obj2); //{ name: 'ace', studying: [Function (anonymous)] } console.log(obj2.__proto__); //{ running: [Function: running] } obj2.running(); //running! console.log(obj2.name); //ace // var obj3 = createStudent('sabo'); // obj3.running(); // var obj4 = createStudent('luffy'); // obj4.running();
六、寄生组合式继承
- 事实上,现在可以利用寄生式继承将 借用构造函数继承 的两个问题解决掉;
- 当我们在子类型的构造函数中调用父类型.call(this, 参数)这个函数的时候,就会将父类型中的属性和方法复制一份到了子类型中. 所以父类型本身里面的内容,我们不再需要;
- 获取到一份父类型的原型对象中的属性和方法,不能直接让子类型的原型对象 = 父类型的原型对象;
- 因为直接让子类型的原型对象 = 父类型的原型对象,会造成以后修改了子类型原型对象的某个引用类型的时候, 父类型原型对象的引用类型也会被修改,所以我们使用前面的寄生式思想就可以了;
- 即:
Student.prototype = Object.create(Person.prototype);
function inheritPrototype(SubType, SuperType) { SubType.prototype = Object.create(SuperType.prototype); //因为打印的是Person类型,所以我们还是要用属性描述符改变constructor; Object.defineProperty(SubType.prototype, "constructor", { enumerable: false, configurable: true, writable: true, value: SubType }); }; function Person(name, age, friends) { this.name = name; this.age = age; this.friends = friends; }; Person.prototype.running = function() { console.log('在跑步!'); }; Person.prototype.eating = function() { console.log('在吃东西!'); }; function Student(name, age, friends, son) { Person.call(this, name, age, friends); this.son = son; }; inheritPrototype(Student, Person); // Student.prototype = Object.create(Person.prototype); // Student.prototype.constructor = Student; Student.prototype.studying = function() { console.log('在学习!'); }; var stu = new Student('yzh', 18, ['ace'], 1804515); /* * 改变之前:Person { name: 'yzh', age: 18, friends: [ 'ace' ], son: 1804515 } * 改变之后:Student { name: 'yzh', age: 18, friends: [ 'ace' ], son: 1804515 } */ console.log(stu); console.log(stu.name, stu.age); stu.studying(); stu.running(); stu.eating(); - 即:
最后
继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可;