JavaScript 创建对象

本文深入探讨了JavaScript中创建对象的多种方式,包括工厂模式、构造函数模式、原型模式及组合模式,分析了每种模式的优缺点,帮助读者理解对象的继承和工作原理。

创建对象

了解如何创建对象有助于我们理解如何继承,和对象的 prototype 属性
创建对象的方法有许多种,后来的方法解决了前者的缺点,了解这些递进的关系有助于帮助我们更好地了解对象的工作模式

工厂模式

最开始的模式,即通过一个函数,用户可以选择传入一些参数,然后函数返回一个新的对象完成对象的创建

function createPerson (name, age, job) {
  let o = new Object()
  o.name = name
  o.age = age
  o.job = job
  o.sayName = function () {
    console.log('`Hello my name is ${this.name}`')
  }
  return o
}
let person1 = createPerson('Nicholas', 29, 'Software Engineer')
let person2 = createPerson('Greg', 27, 'Doctor')
复制代码

显而易见的是可以多次调用该函数得到多个具有相似的对象,但是其存在一些缺点:

  1. 我们无法知道上述的 person1 到底是不是 createPerson 的实例,因为 person1prototypeObject.prototype
  2. 我们每创建一个对象都要新建一个 sayName 方法属性,即 person1.sayName === person2.sayName // false

构造函数模式

构造函数模式解决了工厂模式的第一个问题,即 可以确认一个实例是否为一个 的对象

function Person (name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.sayName = function () {
    console.log('`Hello my name is ${this.name}`')
  }
}
let person1 = new Person('Nicholas', 29, 'Software Engineer')
let person2 = new Person('Greg', 27, 'Doctor')
复制代码

这里我们使用 new 操作符来实例化一个对象时发生了以下几件事:

  1. 新建一个对象
  2. 将构造函数的 this 指向新对象
  3. 执行构造函数中的代码
  4. 返回新对象

同时 person1prototype 被赋值为 Person.prototype 因此使用 person1 instanceof Person 时其实在判断 Person.prototype 是否出现在了 person1 的原型链之中,即可判断 person1Person 的实例。
但是构造函数模式依然没有解决工厂模式的第二个问题。

原型模式

原型模式的存在即解决了上述所说的 第二个问题 它使得一个类的所有实例共享一些属性。而在那之前我们需要了解什么是原型对象,即上文提到的 prototype

prototype

这里有几个点自己觉得比较重要的,对于我自己理解何为原型比较有帮助

  1. prototype是一个对象
  2. 实例将包含一个属性,这个属性是一个指向了构造函数的原型对象的指针
  3. 实例中可以获取到的属性分两种,一种 实例属性 一种是 原型属性

接下来再讨论原型对象:

  1. 只要创建了一个函数,就会为该函数创建一个 prototype 属性,这个属性指向一个对象即该函数的原型对象
  2. 默认情况下所有的原型对象都会自动获得一个 constructor 属性,这个属性是一个 指向 prototype 所在函数的指针。即 Person.prototype.constructor 指向了 Person
  3. 当调用构造函数创建一个实例后,该实例的内部将包含一个指针,指向了构造函数的原型对象。

代码示例如下:

function Person () {}

Person.prototype.name = 'Nichalos'
Person.prototype.age = 29
Person.prototype.job = 'Software Engineer'
Person.prototype.sayName = function () {
  console.log('Hello my name is ' + this.name)
}

let person = new Person()
复制代码

以上代码表现成原型状态如下,没有上图只有文字描述,多读几遍其实文字也来得精妙啦:

  1. Person.prototype 指向了一个对象(以下简称原型对象,这里时刻牢记是一个对象),即 Person 的原型对象
  2. Person 的原型对象包含一系列属性:第一个即默认情况下被赋予的 constructor 属性,该属性是一个指向构造函数 Person 的指针,所以 Person.prototype.constructor 值为 Person;剩下的属性即代码中定义的各个属性(nameagejobsayName),所以这个原型对象的值为:
{
 constructor: Person,
 name: 'Nichalos',
 age: 29,
 job: 'Software Engineer',
 sayName: Function
}
复制代码
  1. 实例出 person 实例后,person 被赋予了一个内部属性,这个属性指向了第2步中的原型对象,通常这个对象是 __proto__。值得注意的是,虽然我们只是实例化 person,并没有未其添加属性和方法,但是却可以通过搜索原型链 访问 __proto__ 中的属性和方法。我自己将其称作 原型属性

以上描述了原型对象的由来、理清楚了 实例构造函数原型对象 的关系:实例和构造函数没什么关系,但是实例有一个属性指向了原型对象,而原型对象是属于构造函数的,所以三者存在了联系。而接下来我们要讨论一些关于原型对象的方法:

hasOwnProperty(propertyName: String) // 返回实例是否具有该实例属性(即实例本身的属性)
in // 操作符,'name' in person,这个操作符只要对象能够访问到该属性就返回true,不论是实例属性还是原型属性
Object.keys(obj: Object) // 该方法会返回一个包含所有可枚举属性的字符串数组
Object.getOwnPropertyNames(obj: Object) // 该方法会的到所有实例的属性不论是实例的属性还是原型属性,也不管是否可枚举
复制代码

接着是原型的特性:

  1. 为实例添加属性会屏蔽原型中的同名属性而不是改写
  2. 原型具有动态性,当我们对原型对象做出修改时,能够立刻从实例上反应出来,就算是先创建了实例,再修改了原型,之前创建的实例的 [[prototype]] 依然会反应出来
  3. 如果先创建了实例,就不能切断本来原型和构造函数之间的联系,即不能重写构造函数的 prototype 属性例如下:
function Person () {}

Person.prototype.name = 'Nicholas'
Person.prototype.age = 22
Person.prototype.sayName = function () {
  console.log(`Hello, my name is ${this.name}`)
}

let person = new Person()
person.sayName() // Hello, my name is Nicholas

person.name = 'Bob'

person.sayName() // Hello, my name is Bob

Person.prototype = {
  constructor: Person,
  name: 'Peter',
  job: 'Software Engineer',
  tellJob: function () {
    console.log(`Hello, my job is ${this.job}`)
  }
}

let anotherPerson = new Person()

anotherPerson.tellJob() // Hello, my job is Software Engineer
anotherPerson.sayName() // type error, no method called sayName

person.sayName() // Hello, my name is Bob
person.tellJob() // type error, no method called tellJob
复制代码

以上代码解释了实例和构造函数的原型对象之间的关系,同时阐明,当我们访问一个对象的实例的时候,会先从属于实例自身的实例的属性搜索,找到了直接返回,若找不到则取原型对象中去找。同时,由于所有的实例都共享一个原型,我们可以将公共的属性和方法都写在原型对象里,让实例共享。解决了文章一开始说的第二个问题。但是这种方法引入了新的一个问题:

function Person () {}

Person.prototype = {
  constructor: Person,
  name: 'Nicholas',
  age: 22,
  job: 'Software Engineer',
  friends: ['Shelby', 'Court'],
  sayName: function () {
    console.log(`Hello, my name is ${this.name}`)
  }
}

let person1 = new Person()
let person2 = new Person()

person1.friends // ['Shelby', 'Court']
person2.friends // ['Shelby', 'Court']

person1.friends.push('Bob')

person1.friends // ['Shelby', 'Court', 'Bob']
person2.friends // ['Shelby', 'Court', 'Bob']

person1.friends === person2.friends // true

复制代码

上述代码说明,原型对象会让实例共享属性,而实际上,我们希望实例拥有自己的属性。

组合使用构造函数模式和原型模式

这个模式时为了解决上述提出的三个问题的,最重要时解决让实例拥有自己的属性,同时所有实例又共享原型上的方法。简而言之为将属性和方法分开设置。

function Person (name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.friends = ['Shelby', 'Court']
}

Person.prototype = {
  constructor: Person,
  sayName: function () {
    console.log(`Hello, my name is ${this.name}`)
  }
}

let person1 = new Person('Nicholas', 22, 'Software Engineer')
let person2 = new Person('Greg', 27, 'Doctor')

person1.friends.push('Bob')

person1.friends // ['Shelby', 'Court', 'Bob']
person2.friends // ['Shelby', 'Court']
person1.friends === person2.friends // false
person1.sayName === person2.sayName true
复制代码

其他

其实还剩下几种创建对象的方法:

  1. 动态原型模型:将对构造函数 prototype 属性书写的位置换到构造函数中来
  2. 寄生构造函数模式和稳妥构造函数模式:这两种方法创建的对象和在构造函数外部直接创建对象没有区别,所以无法判定实例和构造函数之间的关系,这里就不做记载了,有兴趣可以翻看书本进行了解。

持续更新在github

转载于:https://juejin.im/post/5bff5e32e51d45735e4ffa54

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值