【手撕JS】面对对象设计和原型链-设计模式(二)

195 阅读5分钟

往期回顾:

  1. 【手撕JS】面对对象设计和原型链-属性(一)

设计模式

虽然Object构造函数和字面变量都可以创建一个对象,但是不能服用代码

工厂模式

这里用工厂模式创建一个对象

function createPerson(name, age, job){
  var o = new Object()
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function(){
     console.log(this.name)
  }
  return o
}

var person1 = createPerson("小明", 20 ,"学生")
var person2 = createPerson("小红", 20 ,"学生")

console.log(person1.sayName(),person2.sayName())
// 输出 小明 小红

工厂模式的出现解决了复用的问题,但是没有解决这个到底是什么类型的问题

构造函数模式

为了解决工厂模式出现的问题,慢慢发展出了构造函数模式,对比工厂模式,更加简洁并且可以知道类型。

举个栗子

// 同样还是用人这个类
function Person(name, age, job){
  this.name = name
  this.age = age
  this.job = job
  this.sayName = function(){
     console.log(this.name)
  }
}

var person1 = new Person("小明", 20 ,"学生")
var person2 = new  Person("小红", 20 ,"学生")
console.log(person1.sayName(),person2.sayName())
// 输出 小明 小红
// 这里我们判断一下 person1 是不是 Persion 类型的
console.log(person1 instanceof Person) // true
console.log(person1 instanceof Object) // true

person1和person2的constructor都分别保存着不同的Person实列,所以可以被instanceof判断出类型,那么为啥又是Object类型呢?因为每一个对象都继承自Object。

我们用构造函数模式创建出了一个Person类型,使用的时候只需要new一下,能创建一个新的实例,复用了很多代码

欸有小伙伴会问了,这个也是函数我直接用可以吗?

var person3 = Persion('小刚',23,"码农")
console.log(person3)

小伙伴想想 person3 会是啥? Person实例? 函数? undefined? 答案是undefined 因为我们并没有在Person里面返回一个对象。 那这个函数运行以后 小刚去哪里了? 难道和小红私奔了?

构造函数模式直接运行在window下面,那么就挂载在window下面

console.log(window.name) // "小刚"

小明:好险,还好,还好! 如果在另一个对象的作用域中调用,那么就挂载在那个作用域下

var home = {};
Person.call(home,"小肖",16,"中学生") 
// 也可以用 apply
home.sayName() // 小肖

使用call或者apply后,传入的对象中就有了Person的属性。

那么构造函数模式就已经完美了吗?你看既可以指定属性,又可以复用方法,这也太棒了吧,但是它还不是完美的,每一个实例中的方法都是不同的,哪怕它们有着同样的名字。

person1.sayName == person2.sayName // false

原型模式

如果我们把方法放到原型链上,那么是不是就减少了function的开销,并且可以共享所有的方法和属性。当然也从这里开始,变得难以理解了。

小贴士:我们创建的每一个函数都有一个原型属性,而原型属性是一个指针,指向了一个对象。

我们用原型模式创建一个Person类型

function Person(){
}
Person.prototype.name = "小明"
Person.prototype.age = "20"
Person.prototype.job = "学生"
Person.prototype.sayName = function(){
  console.log(this.name)
}

var person1 = new Person()
person1.sayName()
// 小明
var person2 = new Person()
person2.name = "小红"
person2.sayName()
// 小红

这个时候得理一理小红和小明之间的关系,让我们看看小刚还有没有机会。

image.png

上图展示了 Person 构造函数、Person的原型属性和Person两个实例之间的关系,Person.prototype 指向了原型对象,而prototype里面的constructor又指回了Person。person1和person2的prototype( __ proto __ )都指向了Person.prototype。这也是为什么person1和person2能够调用sayName方法的原因。 拓展一下:解释器问person1:喂,你有sayName吗? person1说:我没有。然后解释器打了个电话给person1的原型:喂,你有sayName吗?person1的原型看了看自己:有的,我给你。

下图展示了Person的原型和constructor的指向

image.png

基于上面的拓展,我们可以知道在person1中定义和原型上相同的属性的时候,检查器会优先得到person1上面的值,当没有的时候就会去查找原型。现在我们就知道小明和小红是啥关系了。

delete person2.name
console.log(person2.name) // 小明 原型上一开始定义的name 为小明
// 小红漏出了马脚,原来她是女装的小明。

组合模式

小红大呼冤枉,小红说着从裙子里面掏出了构造函数模式+原型模式证明了自己的清白。

构造函数模式让每个实例有自己的属性,而原型模式又分享了方法,这样组合起来就等于赛亚人+龟派气功。每一个实例都会有一份属于自己的实例属性副本,但是又共享着方法、最大限度了节省了内存,可以说,这是定义类型默认的一个模式

顶个栗子

function Person(name, age, job){
  this.name = name
  this.age = age
  this.job = job
  this.friends = []
}
Person.prototype.constructor = Person
Person.prototype.sayName = function(){
  console.log(this.name)
}

var person1 = new Person("小明", 20 ,"学生")
var person2 = new  Person("小红", 20 ,"学生")
person1.friends.push("小红")
person2.friends.push("小明")

console.log(person1.friends == person2.friends) // false
console.log(person1.sayName == person2.sayName) // true

寄生构造模式

在前面都不适合的情况下,我们可以使用寄生构造模式。基本思路就是创建一个函数,这个函数作用仅仅是封装对象,然后返回对象。

顶个栗子:

function Person(name, age, job){
  var o = new Object()
  o.name = name
  o.age = age
  o.job = job
  o.sayName = function(){
    console.log(this.name)
  }
  retrun o
}
// 这个模式其实和工厂模式一摸一样 然后我就去翻红宝书了

// **引用 红宝书第三版 161页例子**
function SpecialArray(){
  // 创建数组
  var values = new Array()
  // 添加值
  values.push.apply(values, arguments)
  // 添加方法
  values.toPipedString = function(){
     return this.join('|')
  }
  return values;
}
var colors = new SpecialArray('red','blue')
colors.toPipedString() // "red|blue"

下一篇 继承