JavaScript面向对象与继承

93 阅读6分钟

1. 创建对象的方式

1、对象字面量的方式

let obj = {
  name: 'jenny',
  age: 18,
  run: function() {
    return `${this.name} is running`
  }
}

字面量的方式本质上与调用 new Object() 构造函数相同

let obj = new Object()
obj.name = 'jenny'

缺点:对于创建大量相似对象的时候,会产生大量的重复代码

2、工厂模式

function createObj(name, aga) {
  return {
    name: name,
    age: age,
    run: function() {
      return `${this.name} is running`
    }
  }
}

const obj1 = createObj('jenny', 18)
const obj2 = createObj('kiki', 20)

工厂模式可以看做是生成对象字面量的机器。主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。

缺点:创建出来的对象获取不到对象最真实的类型(打印出来显示都是Object类型),它只是简单的封装了复用代码,而没有建立起对象和类型间的关系

工厂函数生产的实例带有特定的自定义属性("name", "age"),因此不适合作为原型使用,也不适用于继承

使用案例:使用Object.assign组合对象,来扩展对象的属性或方法

const mvp = {award: "MVP"}
const newObj = Object.assign(obj1, mvp)

3、使用对象字面量作为原型创建实例(寄生?)

使用 Object.create() 方法,新创建出来的对象的__proto__会指向传入的对象

let obj = {
  name: 'jenny',
  age: 18,
  run: function() {
    return `${this.name} is running`
  }
}

let newObj = Object.create(obj)
newObj.height = '1.80'
console.log(newObj);
image.png

这种方式基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。它的一个缺点和工厂模式一样,无法实现对象的识别。

4、构造函数的方式创建对象

// 规范:构造函数的首字母一般是大写
function Person(name, age) {
  this.name = name
  this.age = age

  this.run = function() {
    console.log(this.name + 'is running');
  }
}

let p1 = new Person('jenny', 18)
let p2 = new Person('kiki', 20)

console.log(p1);
// Person {name: 'jenny', age: 18, run: ƒ}
// 可以看到是 Person 类型(实际上是constructor的name属性)
image.png

构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此可以通过原型来识别对象的类型。

但是构造函数存在一个缺点就是,造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次都会新建一个函数对象,浪费了不必要的内存空间,因为相同的方法应该是所有的实例都可以通用的。

5、原型结合构造函数的方式

因为每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。

因此把公用的属性和方法添加在原型上,可以解决不必要的函数对象创建的问题

function Person(name, age) {
  this.name = name
  this.age = age
}
Person.prototype.run = function() {
    console.log(this.name + 'is running');
}

function Person(name, age) {
  this.name = name
  this.age = age
}
Person.prototype.run = function() {
    console.log(this.name + 'is running');
}

let p1 = new Person('jenny', 18)
p1.run() // jennyis running

6、class

  • 在ES6(ECMAScript2015)新的标准中使用了class关键字来直接定义类
  • 但是类的本质上只是构造函数,原型链的语法糖
  • 最终还是会被 babel 工具转换为ES5的代码
class createObj {
    constructor(name, age) {
        this.name = name;
        this.age = age
    }
    run() {
        return this.name + "is running"
    }
}
let obj = new createObj("jenny", 18)

2. 对象继承的方式有哪些

1、原型链的方式实现继承

// 父类
function Person() {
  this.name = "Jackson",
  this.friends = []
}
Person.prototype.eating = function() {
  console.log(this.name + "is eating");
}

// 子类:特有属性和方法
function Student() {
  this.sno = 111
}

// 使用父类构造函数创建实例,赋给子类的原型
let p = new Person()  // p.__proto__ === Person.prototype
Student.prototype = p  
// Student.prototype.__proto__ === Person.prototype

// 创建出来两个stu对象
var stu1 = new Student()
var stu2 = new Student()

// 获取引用,修改引用中的值,会相互影响
// 因为这个fre1被加到p对象,而stu1,stu2的__proto__都指向p
stu1.friends.push("fre1")
console.log(stu1.friends); // [ 'fre1' ]
console.log(stu2.friends); // [ 'fre1' ]

缺点:实例的类型是不能直观看到的;在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱;还有就是在创建子类型的时候不能向父类传递参数

2、使用构造函数结合原型链的方式实现继承

// 父类
function Person(name, age, friends) {
  this.name = name,
  this.age = age
  this.friends = friends
}
Person.prototype.eating = function() {
  console.log(this.name + "is eating");
}
// 子类
function Student(name, age, friends, sno) {
  // 借用构造函数的调用
  // 我们在这里调用Person,并把需要Person处理的参数传过去
  // this 就是Student的实例 
  Person.call(this, name, age, friends) // 这里可以获得父类的属性
  this.sno = sno
}

// 原型链方式
let p = new Person() // 依然需要这里来获得父类的方法
Student.prototype = p 

// 创建student的实例
let stu = new Student('jenny', 18, [], 11)
console.log(stu);

这种方式解决了方式一的弊端,但依然存在问题:由于我们是以父类的实例来作为子类型的原型,所以调用了两次父类的构造函数(Person.call、new Person()),造成了子类型的原型中多了很多不必要的属性。

image.png

3、原型式继承

原型式继承的主要思路就是基于已有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。

这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。

var obj = {
  name: "Jackson",
  age: 18
}

// 方式1:原型式继承函数
function createObject(o) {
  var newObj = {}
  // 这个方法是把o设置为newObj的原型
  Object.setPrototypeOf(newObj, o)
  return newObj
}

// 方式2:Douglas 的实现(当时还没有setPrototypeOf这个方法)
function createObject2(o) {
  function Fn() {}
  Fn.prototype = o
  var newObj = new Fn()
  // 因此 newObj.__proto__ = Fn.prototype = o
  return newObj
}

// 方式3:
let newObj = Object.create(obj)

缺点与原型链方式相同

4、寄生式继承方式

创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象。

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
}

这种继承的优点就是对一个简单对象实现继承,缺点是没有办法实现函数的复用

5、寄生式组合继承

寄生式+构造函数调用的方式,使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。

function inheritPrototype(SubType, SuperType) {
  SubType.prototype = Object.create(SuperType.prototype)
  // 当然子类的prototype还需要有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("running~")
}

// 子类
function Student(name, age, friends, sno) {
  Person.call(this, name, age, friends) // 获取一份Person中的属性和方法
  this.sno = sno // 可扩展自己的属性
}

// Student类还需要获取一份Person.prototype中的属性和方法
inheritPrototype(Student, Person)

var stu = new Student("why", 18, ["kobe"], 111)
stu.running()
 // 打印  Student { name: 'why',age: 18,friends: [ 'kobe' ],sno: 111,}