面向对象有三个特性:封装、继承、多态
- 封装:把属性和方法封装到一个类中,可以称之为封装的过程;
- 继承:继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可;
- 多态:不同的对象在执行时表现出不同的形态;
本文核心讲继承,首先看看JS原型链的机制.
我们知道从一个对象上获取属性,如果当前对象中没有获取到就回去它的的原型上面获取直到Object的原型对象(原型属性指向null就是顶层原型)
通过原型链实现继承
function Person(){
this.name = "luo"
}
Person.prototype.eating = function (){}
function Student(){
this.sno = "01"
}
let p = new Person()
Student.prototype = p
let s = new Student()
console.log(s.sno); //01
console.log(s.name); //luo
弊端
- 我们通过直接打印对象 看不到这个属性
- 这个属性会被多个对象共享,如果这个对象是个引用类型,那么就会造成问题.
- 不能给Person传递参数,因为这个对象时一次性创建的(无法定制化)
组合借用构造函数继承
为了解决原型链继承中存在的问题,开发人员提供了一种新的技术:constructor stealing(伪造对象)
借用继承的做法非常简单::在子类型构造函数的内部调用父类型构造函数
function Person(name, age){
this.name =name
this.age = age
}
Person.prototype.eating = function (){}
function Student(name, age){
Person.call(this,name, age)
}
let p = new Person()
Student.prototype = p
let s1 = new Student('luo',18)
console.log(s1);
组合借用继承的问题
- 组合继承是JS最常用的继承模式之一(但基本不用):
- 无论什么情况下,都会调用俩次父类构造函数.
- 一次在创建子类p原型的时候
- 另一次在子类构造函数内部(也就是每次创建子类实例的时候)
- 所有子类实例事实上会拥有两份父类属性
- 一份在当前的实例自己里面
- 一份在子类对应的原型对象中(也就是s1.__proto__里面)
原型式继承函数->对象继承
原型式继承函数创始人也是JSON的创立者道格拉斯·克罗克福德(Douglas Crockford)
let obj = {
a:1
}
let o1 = object(obj)
//方式一
function object(obj){
function Func(){}
Func.prototype = obj
return new Func()
}
console.log(o1.__proto__=== obj) //true
//方式二
function object(obj){
let newobj = {}
Object.setPrototypeOf(newobj, bj)
return newobj
}
//方式三
let student = Object.create(person, {
address:{
value:'深圳',
enumerable:true
}
})
寄生式继承函数
寄生式(Parasitic)继承是与原型式继承紧密相关的一种思想, 并且同样由道格拉斯·克罗克福德(Douglas Crockford)提出和推广的
// es5
function object(obj){
function Func(){}
Func.prototype = obj
return new Func()
}
function createStudent(person,name){
let newObj = object(person)
newObj.name = name
newObj.studying = function() {
console.log('studying');
}
return newObj
}
function person(){
this.age = 11
}
let s1 = createStudent(person,'luo')
let s2 = createStudent(person,'haha')
寄生组合式继承
之前提出比较理想的组合继承,我们可以利用寄生式继承将它的俩个问题给解决掉.
- 首先明确一点,当我们在子类型的构造函数中调用父类型.call(this, 参数)这个函数的时候,就会将父类型中的属性和方法复制一份到子类型中,所以父类型本身里面的内容我们不再需要;
- 那我们还需要获取到一份父类型的原型对象中的属性和方法;
- 不能让子类型的原型对象 = 父类型的原型对象,因为这么做意味着以后修改子类型对象的某个引用类型时,父类型原生对象的引用类型也会被修改.
//ES5
function createObject(o) {
function Fn() {}
Fn.prototype = o
return new Fn()
}
function inheritPrototype(SubType, SuperType) {
// ES6
SubType.prototype = Object.create(SuperType.prototype)
// ES5
// SubType.prototype = createObject(SuperType.prototype)
Object.defineProperty(SubType.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: SubType
})
Object.setPrototypeOf(Student,Person) //继承静态方法
}
function Person(name, age, friends) {
this.name = name
this.age = age
this.friends = friends
}
Person.prototype.eating = function() {
console.log("eating~")
}
function Student(name, age, friends, sno, score) {
Person.call(this, name, age, friends)
this.sno = sno
this.score = score
}
inheritPrototype(Student, Person)
Student.prototype.studying = function() {
console.log("studying~")
}
var stu = new Student("luo", 18, ["kobe"], 111, 100)
console.log(stu)
console.log(stu.constructor.name)
一个张图清晰说明javascript的显示和隐式原型链结构
- 值得注意的是 有一个特殊的地方
- Function.proto === Function.prototype 因为函数也是由函数创建的 所以函数Function的隐式原型等于自己的显示原型
class定义类
- ECMAScript 2015新的标准中使用了class关键字来直接定义类
- 类本质上依然是前面所讲的构造函数、原型链的语法糖而已
如何声明一个类
// 声明式
class Person{
constructor(name,age){
this.name = name
this.age = age
}
eating(){ //在前面我们说过对于实例的方法,我们是希望放到原型上的,这样可以被多个实例来共享
console.log('222');
}
}
// 类表达式
const Student = class {
}
// 你会发现它和我们的构造函数的特性其实是一致的;
let p = new Person
console.log(Person) //[class Person]
console.log(Person.prototype) //{}
console.log(Person.peorotype.constructor) //[class Person]
console.log(p.__proto__ === Person.prototype) // true
console.log(typeof Person) // function
类的构造函数
- 每个类都可以有且仅有一个自己的构造函数,这个构造函数的名称是固定的叫constructor
当我们通过new操作符,操作一个类的时候就会调用这个类的构造函数constructor,并执行如下操作:
- 在内存中创建一个新的空对象;
- 这个对象内部的[[prototype]]属性会被赋值为该类的prototype属性;
- 构造函数内部的this会指向创建出来的新对象;
- 执行构造函数的内部代码;
- 如果构造函数没有返回对象类型数据,则返回创建出来的新对象
类的访问器方法
对象是可以添加setter和getter函数的,那么类也是可以的
class Person{
constructor(name,age){
this._name = name
this.age = age
}
set name (newName){
this._name = newName
}
get name (newName){
return this._name
}
}
let p1 = new Person('luo',18)
console.log(p1.name); //luo
console.log(p1.name = 'haha');
console.log(p1.name); //haha
类的静态方法
静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用static 关键字来定义
//es5
function Person(name){
this.name = name
}
Person.haha=function haha(){
console.log('111');
}
//es6
class Student{
constructor(name,age){
this._name = name
this.age = age
}
static fn(){
return 999
}
}
类的继承 extents
class Person{
}
class Student extends Person{
}
//继承内置类
class myArray extends Array{
lastItem(){
return this[this.length - 1]
}
}
let array = new myArray(0, 1, 2)
console.log(array.lastItem()) //2
console.log(array.push(3)) //4
super关键字
- 在子类的构造函数中使用this或者返回默认对象之前,必须先通过super调用父类的构造函数!
- super可以在:子类的构造函数、实例方法、静态方法使用
class Person{
constructor(name,age){
this.name = name
this.age = age
}
eating(){
console.log('eat1');
}
static runing(){
console.log('run1');
}
}
class Student extends Person{
constructor(name, age , sno){
super(name)
this.sno =sno
}
eating(){
super.eating()
console.log('2');
}
static runing(){
super.runing()
console.log('2');
}
}
let s1 = new Student('luo',18,5)
console.log(s1.eating()); // eat1 2
console.log(Student.runing()); // run1 2
类的混入 自定义mixin
class Person{
}
function mixin(BaseClass){
return class extends BaseClass{
running(){
console.log('run');
}
}
}
class Student extends mixin(Person){
}