JS中面向对象的特性

268 阅读6分钟

面向对象有三个特性:封装、继承、多态

  • 封装:把属性和方法封装到一个类中,可以称之为封装的过程;
  • 继承:继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可;
  • 多态:不同的对象在执行时表现出不同的形态;

本文核心讲继承,首先看看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

弊端

  1. 我们通过直接打印对象 看不到这个属性
  2. 这个属性会被多个对象共享,如果这个对象是个引用类型,那么就会造成问题.
  3. 不能给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最常用的继承模式之一(但基本不用):
  1. 无论什么情况下,都会调用俩次父类构造函数.
    • 一次在创建子类p原型的时候
    • 另一次在子类构造函数内部(也就是每次创建子类实例的时候)
  2. 所有子类实例事实上会拥有两份父类属性
    • 一份在当前的实例自己里面
    • 一份在子类对应的原型对象中(也就是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的显示和隐式原型链结构

20140312171000718.png

  • 值得注意的是 有一个特殊的地方
    1. Function.proto === Function.prototype 因为函数也是由函数创建的 所以函数Function的隐式原型等于自己的显示原型

class定义类

  1. ECMAScript 2015新的标准中使用了class关键字来直接定义类
  2. 类本质上依然是前面所讲的构造函数、原型链的语法糖而已

如何声明一个类

// 声明式
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,并执行如下操作:

  1. 在内存中创建一个新的空对象;
  2. 这个对象内部的[[prototype]]属性会被赋值为该类的prototype属性;
  3. 构造函数内部的this会指向创建出来的新对象;
  4. 执行构造函数的内部代码;
  5. 如果构造函数没有返回对象类型数据,则返回创建出来的新对象

类的访问器方法

对象是可以添加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){
   
}