JS 中十种创建对象与七种实现继承的方式

1,069 阅读5分钟

「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战

如何创建对象与如何实现继承这两个问题也是面试中经常问到的问题。这里我们列出十种创建对象的方式,以及七种实现继承的方式。

创建对象的十种方式

一、new Object 方式

const xiaoming = new Object({
	name: '小明',
  age: 18
})

可以用下面这种方式简写代替 ​

二、大括号简写方式

const xiaoming = {
	name: '小明',
  age: 18
}

上面这两种方式创建对象的方式是等价的,这两种方式如果在创建结构相同的对象时代码会很冗余。 ​

三、工厂函数方式

工厂函数能解决掉上面两种方式产生的代码冗余的问题

function createPerson(name, age) {
	return {
  	name,
    age,
    getName: function() {
    	console.log(this.name, this.age)
    }
  }
}

const xiaoming = createPerson('小明', 18)

这种方式本质还是new Object,无法根据对象的原型对象准确判断对象的类型 ​

四、构造函数方式

构造函数可以反复创建相同属性结构,不同属性值的多个对象。

function Person(name, age) {
	this.name = name
  this.age = age
  this.getInfo = function() {
  	console.log(this.name, this.age)
  }
}

const xiaoming = new Person('小明', 18)

如果构造函数中包含的方法,则会重复创建,造成内存的浪费 ​

五、原型对象方式

将共同的方法放到原型当中可以避免重新创建相同功能的方法,减少了内存的使用。👉点击看原理

function Person() {}

Person.prototype.name = "这个人很懒,没有名字"
Person.prototype.age = 1
Person.prototype.getInfo = function() {
	console.log(this.name, this.age)
}

const xiaoming = new Person()
// 这里使用的是原型上的属性
xiaoming.getInfo() // "这个人很懒,没有名字", 1

// 添加自身属性
xiaoming.name = '小明'
xiaoming.age = 18
// 使用自身属性
xiaoming.getInfo() // "小明", 18

这种方式同样会存在代码冗余的情况

六、混合模式

function Person(name, age) {
	this.name = name
  this.age = age
}

Person.prototype.getInfo = function() {
	console.log(this.name, this.age)
}

const xiaoming = new Person(name, age)

这种方式虽说解决了代码冗余的问题,但是却又不太符合面向对象封装的思想。 ​

七、动态混合

function Person(name, age) {
	this.name = name
  this.age = age
  if (Person.prototype.getInfo === "undefined") {
  	Person.prototype.getInfo = function() {
    	console.log(this.name, this.age)
    }
  }
}

// 第一次调用时会给 Person.prototype 添加 getInfo 方法
const xiaoming = new Person("小明", 18) 

const xiaoguang = new Person("小光", 15)

这种方式的缺点是语义不符,其实只有第一个对象创建时才会走 if 判断 ​

八、寄生构造函数

通过函数里调用其他构造函数,这种方式其实也是一种继承。

function Person(name, age) {
	this.name = name
  this.age = age
  if (Person.prototype.getInfo === "undefined") {
  	Person.prototype.getInfo = function() {
    	console.log(this.name, this.age)
    }
  }
}

function Programmer(name, age, lang) {
	const p = new Person(name, age)
  p.lang = lang
  return p
}

const xiaoming = Programmer("小明", 18, "JavaScript")
const xiaoguang = Programmer("小光", 15, "C++")

九、class

class 是 ES6 出的一种创建对象的新语法,这种方式的本质也是构造函数,只是一种语法糖。这种语法将我们上面说的那些情况都处理掉了。

class Person {
	constructor(name, age) {
  	this.name = name
    this.age = age
  }
  getInfo() {
  	console.log(this.name, this.age)
  }
}

const xiaoming = new Person("小明", 18)

十、闭包

利用闭包的特性,也可以实现创建对象的方式。 这种方式的优点是不用 this 和 new。缺点就是容易造成内存泄漏。

function Person(name, age) {
  return {
  	getName() {
    	return name
    },
    setName: function(value) {
    	name = value
    },
    getAge: function() {
    	return age
    }
  }
}

const xiaoming = Person("小明", 18)
const xiaoguang = Person("小光", 15)
console.log(xiaoming.getName()) // 小明
console.log(xiaoguang.getName()) // 小光

xiaoming.setName("伪装成小光的小明")
console.log(xiaoming.getName()) // 伪装成小光的小明

实现继承的七种方式

一、原型链式继承

将父类的实例作为子类的原型,这种方式的缺点就是在创建子类实例时,无法向父类构造函数传参

function Animal(name) {
	this.name = name
  this.age = age
  this.say = function() {
  	console.log(this.name)
  }
}

Animal.prototype.eat = function(food) {
	console.log(`${this.name}正在吃:${food}`)
}

function Cat() {}
Cat.prototype = new Animal() // 直接将 prototype 设置为 Animal 的实例对象
Cat.prototype.name = 'cat'

const cat = new Cat()
cat.eat("猫粮")

二、构造函数继承

优点:

  1. 解决了原型链式继承中,修改父类引用属性的问题
  2. 能够向父类构造函数传递参数了

缺点:

  1. 实例并不是父类的实例,只是子类的实例!
  2. 无法实现函数复用,每个子类都有父类实例函数的副本
function Animal(name) {
	this.name = name
  this.say = function(sound) {
  	console.log(`${this.name}说:${sound}`)
  }
}

Animal.prototype.eat = function(food) {
	console.log(`${this.name}${food}`)
}

function Cat(name, age) {
	Animal.call(this, name) // 构建时以 Cat 构建
  this.age = age
}

const cat = new Cat('小猫', 1)
cat.say('喵~') // OK!
cat.eat('小鱼干') // 抛错!!!因为cat实例并不是父类的实例,只是子类的实例,无法调用eat

三、实例继承

缺点也很明显,“子类实例”的结果其实是父类的实例,“子类实例”无法调用自己原型上方法

function Animal(name) {
	this.name = name
  this.say = function(sound) {
  	console.log(`${this.name}说:${sound}`)
  }
}
Animal.prototype.eat = function(food) {
	console.log(`${this.name}${food}`)
}

function Cat(name, age) {
	const p = new Animal(name) // 创建子类型实例
  p.age = age
  return p // 注意这里返回的其实是父类的实例,所以调用 Cat 时有没有 new 结果都一样
}
Cat.prototype.jump = function() {
	console.log('跳!')
}

// 其实不用 new 直接调用
const cat = new Cat('小猫', 1)
cat.eat('小鱼干儿') // 小猫吃小鱼干儿
cat.jump() // 抛错

四、拷贝继承

利用for in可以访问到prototype的特性实现拷贝继承。 这种方式的缺点:

  1. 无法获取父类不可for in遍历的方法。
  2. 每次new子实例时都会重新创建父类实例以及复制一份父类属性
function Animal(name) {
	this.name = name
    this.say = function(sound) {
  	console.log(`${this.name}说:${sound}`)
  }
}

Animal.prototype.eat = function(food) {
	console.log(`${this.name}${food}`)
}

function Cat(name, age) {
	const animal = new Animal(name) // 缺点就是每次调用 Cat 都会 new Animal 和复制操作
  for (let p in animal) { // 利用 for in 可以访问到 prototype 的特性
  	Cat.prototype[p] = animal[p] // 有些不可枚举属性就访问不到
  }
  this.age = age
}

const cat = new Cat('小猫', 1)
cat.eat('小鱼干儿') // 小猫吃小鱼干儿

五、组合继承

function Animal(name) {
	this.name = name
    this.say = function(sound) {
  	console.log(`${this.name}说:${sound}`)
  }
}

Animal.prototype.eat = function(food) {
	console.log(`${this.name}${food}`)
}

function Cat(name, age) {
	Animal.call(this, name) // 构造函数继承
  this.age = age
}

Cat.prototype = new Animal() // 原型链继承
Cat.prototype.constructor = Cat // 将原型的 constructor 指向自身

const cat = new Cat('小猫', 1)

六、寄生组合继承

通过构造函数继承实现内部属性与方法的继承 通过创建一个空类承接父类原型,然后将其实例赋值给子类的原型实现原型上的继承

function Animal(name) {
	this.name = name
    this.say = function(sound) {
  	console.log(`${this.name}说:${sound}`)
  }
}

Animal.prototype.eat = function(food) {
	console.log(`${this.name}${food}`)
}

function Cat(name, age) {
	Animal.call(this, name) // 构造函数继承
  this.age = age
}

// 通过创建一个空类承接父类原型,然后将其实例赋值给子类的原型实现原型上的继承
(function() {
  const Super = function() {} // 创建一个空类
  Super.prototype = Animal.prototype
  Cat.prototype = new Super() // 将实例作为子类的原型
})()

const cat = new Cat('小猫', 1)
cat.eat('小鱼干儿')

七、ES6 class extends 继承

class Animal {
	constructor(name) {
  	this.name = name
  }
  eat(food) {
  	console.log(`${this.name}${food}`)
  }
}

class Cat extends Animal {
	constructor(name, age) {
  	super(name)
    this.age = age
  }
}

const cat = new Cat('小猫', 1)