JS基础之面向对象-创建对象

创建对象

创建对象的方式有以下几种:最简单的就是对象字面量形式。

工厂模式

说起工厂模式这个称呼的由来,可以根据它的书写方式来理解:

function factory(name, speed) {
  var obj = new Object()
  obj.name = name
  obj.speed = speed
  return obj
}
var car1 = factory('保时捷', 300)
var car2 = factory('保时捷', 280)
car1.constructor  // function Object() {}
car2.constructor  // function Object() {}

如上代码所示,你可以把factory()当成一个生产保时捷的工厂,你可以给他传入一些零件(即参数),工厂内部就可以根据你传入的零件,在一个车架的基础上(即 new Object()),建造一辆汽车。当你提起这个工厂,自然而然就跟保时捷划等号了。
不过,有可能好几个工厂同时生产同一版本的车,单从外表你是不知道它到底是哪个工厂生产出来的,只知道都长的差不多。

模式存在的问题

这也就是工厂模式的问题,不能确定对象属于哪一类。

构造函数模式

都知道,可以用构造函数来创建对象。可以用原生构造函数也可以用自定义的构造函数。相对于工厂模式来创建对象的方式,构造函数不会显示的创建一个对象,而且不会直接在对象上添加属性和方法,而是通过this对象,最后,并没有return语句。

function Person(name, age, sex) {
  this.name = name
  this.age = age
  this.sex = sex
  this.say = function() {
    console.log(this.name)
  }
}
var person1 = new Person('zhd', 18, 'boy')
var person2 = new Person('zy', 20, 'girl')

如上所示,,通过Person()这个构造函数生成了person1person2两个对象,也就是两个构造函数的实例,这两个实例的属性和方法都来自于 Person()构造函数。不过,重点是new操作符。
首先来看一下new操作符都干了些啥?

new操作符干了啥

1)最直接的就是通过new操作符创建了一个新对象
2)将构造函数内部this的作用域指向了创建的这个新的对象
3)将构造函数内部的属性和方法添加到新对象中
4)返回这个新对象,并将这个对象当做值赋给变量person1person2

回到工厂模式存在的问题,构造函数创建的对象是否就知道是哪里一类吗?试验一下:

person1.constructor  // function Person() {}
person2.constructor  // function Person() {}

从上面代码来看,确实解决了工厂模式创建对象的问题。不过也可以用另一种方式实验:

person1 instanceof Person // true
person1 instanceof Object // true
person2 instanceof Person // true
person2 instanceof Object // true

如果把构造函数当做正常的一个函数来调用呢,那内部的属性和方法都跑哪里去了?实际上,如果不通过new调用构造函数的话,直接运行构造函数,会把构造函数内部的属性和方法放在window上。

工厂模式创建的对象不知道属于哪种类型,那构造函数有什么问题呢?
通过new确实会创建一个对象,不过没创建一个对象,都会把构造函数内部所有的方法都添加到新对象上,这样,如果基于这个构造函数创建很多对象,每个方法都会在实例内部创建一遍,但是每个实例内部的方法干的都是同一件事,不仅造成了多余方法的冗余,也无形增加了内存消耗。

模式存在的问题

每个方法都要在实例上重新创建一遍

那有什么解决的办法呢?实际上,创建的对象内部的方法,对于实例来说干的都是同一件事,那为什么不写成公用的一个方法呢?如下:

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

但是这种解决方案带来了一个无法让人接受的问题:那就是在做封装的时候,通常会定义很多方法,如果这个对象需要定义很多方法,也就是定义很多全局变量,这样既没有封装性可言,而且增加了全局变量。

原型模式

上面提到的构造函数的问题,可以通过第三种模式,原型模式解决它。

function Person() {
 
}
Person.prototype.name = 'zhd'
Person.prototype.age = 18
Person.prototype.sex = 'boy'
Person.prototype.say = function() {
  console.log(this.name)
}
var person1 = new Person()
var person2 = new Person()
person1.say() // 'zhd'
person2.say() // 'zy'

通过以上的方式就可以看出,将所有实例共用的方法添加到构造函数的原型上,这样既减少了定义函数的次数,又没有增加全局变量。

理解原型

这里只简单说几点
1)除了函数,其他所有对象都没有prototype属性。
2)所有对象都有一个_proto_属性,指向构造函数的原型。
3)除了null和undefined,其他所有对象都有constructor属性。

相关的方法

isPrototypeOf():通过这个方法可以确定一个对象是否是另一个对象的原型。
例如:
Object.prototype.isPrototypeOf(person1)
// true

hasOwnProperty():通过这个方法可以检测一个属性是否存在于实例中还是原型中。这个方法接收一个属性名作为参数,由所要查找属性所在的对象调用。
例如:
person1.__proto__.hasOwnProperty('name')
// true
person1.hasOwnProperty('name')
// false

Object.getPrototypeOf():通过这个方法可以返回一个对象的原型对象。
例如:
Object.getPrototypeOf(person1)
// {name:'zhd', age:18, sex:'boy', say:function(){...}}

Object.keys():这个方法返回的一个对象上所有可枚举实例属性,这个方法接收一个对象作为参数,返回一个包含所有属性的字符串数组。
例如:
Object.keys(Person.prototype)
// ["name", "age", "sex", "say"]

Object.getOwnPropertyNames():上面说的Object.keys()方法返回的是所有可枚举的实例属性,相反,这个方法更全面。返回的是所有属性,不管属性是否可枚举
例如:
Object.getOwnPropertyNames(Person.prototype)
// ["constructor", "name", "age", "sex", "say"]

这里有一点需要提醒的是,如果实例和原型存在相同的一个属性或者方法,实例的属性或方法会覆盖原型上的属性或方法。

更简单的原型语法

上面提到的原型模式,如果每添加一个属性或方法,就要写一遍Person.prototype,为了更优雅的封装功能,我们可以改变Personprototype
例如:

function Person() {}
Person.prototype = {
  name: 'zhd',
  age: 18,
  sex: 'boy',
  say: function() {
    console.log(this.name)
  }
}

这样的写法是不是就轻松多了。不过这样方式修改了Person的原型对象,能带来什么问题吗?如下:

var person1 = new Person()
person1.constructor == Person // false

为什么会是false呢?
之前介绍过,一旦创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得一个constructor属性。就像上面一样,一旦声明一个Person构造函数,就同时创建了一个它的原型对象({constructor:function() {}}),同时这个原型对象也会获得一个constructor属性。
所以这种写法,声明一个构造函数以后,又修改了它的原型对象,因此,修改后的原型对象内部的constructor也不再指向Person了,而是指向了原生构造函数function Object() {}
为了解决这种写法带来的问题,我们可以在改变构造函数的原型对象后,手动的在原型对象中添加一个constructor属性,其属性值为Person。如下:

Person.prototype = {
  constuctor: Person,
  name: 'zhd',
  age: 18,
  sex: 'boy',
  say: function() {
    console.log(this.name)
  }
}

我们这样手动添加constructor属性确实解决了写法带来的问题。不过constructor这个属性本身是不可枚举的,我们可以用Object.defineProperty()方法来改变它的特性。如下:

Object.definePropety(Person.prototype, 'constructor', {
  enumerable: false,
  value: Person
})

in 操作符的使用

有两种方式使用in操作符:
1)单独使用,如propertyName in object

  1. for-in循环中使用

不过单独使用in操作符,有一个缺点,如下:

function Person () {}
Person.prototype.name = 'zhd'
console.log('name' in Person) // true

所以,当单独用in操作符检测一个属性是否在目标对象上,结果并不准确,如果属性在目标对象的原型上,也会返回true
所以,搭配hasOwnProperty()方法一起使用,就可以确定要检测的属性是否在对象上还是在原型上。

模式存在的问题

首先,从原型模式的写法上可以看出,省略了构造函数的传参和初始化这一环节,结果是所有实例会默认取得相同的属性和方法,增加了冗余。
但是,原型模式最大的问题是在共享的本质。尤其是对于包含引用类型值得属性。
如下:

function Person () {}
Person.prototype = {
  constructor: Person,
  name: ['person1', 'person2']
}
let person1 = new Person()
let person2 = new Person()
person1.name.push('person3')
console.log(person2.name) // ['person1', 'person2', 'person3']

开发者的意思只是修改一下person1这个实例的name属性,无意修改person2这个实例的name属性。如果说这两个实例共享的是一个name,那就没问题,如果是每个实例独享一个name属性,并做相应的业务逻辑处理,那岂不是翻天了。为了避免这方面的问题,建议构造函数模式和原型模式组合使用。

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

function Person(name, age, sex,fn) {
  this.name = name
  this.age = age
  this.sex = sex
  this.fn = fn
}
Person.prototype.sayName = function() {
  console.log(this.name)
}
let person1 = new Person('zhd','18','boy')

这种组合模式的使用,既照顾到了每个实例私有的属性和方法,又把实例共享的方法或属性放在了原型上面,一举两得。在这先提一下:也推荐借用构造函数和原型链组合的继承方式。这一点会在后面说出。

动态原型模式

其实,动态原型模式很简单,基本上相当于构造函数模式和原型模式的组合使用,区别就是加了一个if判断,而且把原型模式写在了构造函数内部。如下:

function Person(name) {
  this.name = name
  if(typeof this.sayName != 'function') {
    Person.prototype.sayName = function() {
      console.log(this.name)
    }
  }
}

之前提到了构造函数模式和原型模式的组合模式的使用,而动态原型模式其实也是一样,只是判断了一下构造函数内部有没有对象的方法,没有的话直接把方法放到原型上。

其实,讲到这里,项目中常用的方法就已经说的差不多了。创建对象无非就是以上的几种方法。不过还有两种,不常用,但咱们一起来了解一下。

寄生构造函数模式(工厂构造模式)

其实,刚开始看到这一个模式的写法的时候,我感觉与其叫寄生构造函数模式,不如叫工厂构造模式更好理解,为啥这么说呢,我们来看一看它的写法:

function Person(name, age, sex) {
  var o = new Object()
  o.name = name
  o.age = age
  o.sex = sex
  o.joins = function() {
    this.name = '111'
  }
  return o
}
let person1 = new Person('zhd','19','boy')

对工厂模式和构造函数模式特别熟悉的同学一看就知道,这不就是工厂模式和构造函数模式组合使用吗。或者说,在工厂模式的基础上,生成对象的时候,在构造函数前面加了一个new操作符,仅此而已。
然后,我啥也不说,咱们先看一段代码

let person1 = new Person('zhd','19','boy')
let person2 = Person('zhd','19','boy')
console.log(person1) // {name: 'zhd', age:19,sex:'boy',joins:function(){}}
console.log(person2) // {name: 'zhd', age:19,sex:'boy',joins:function(){}}

从上面可以看出,无论是从寄生构造函数模式创建的对象,还是从工厂模式常见的对象,从数据结构上来说,没有什么不同。也就是说,构造函数内部返回的对象与构造函数本身是没有关系的,无论是否用new操作符,创建的对象都是一样的,跟构不构造没关系。所以在此,用 instanceof来确定一个对象从哪个对象来的是不正确的。
如:person1 instanceof Person: // false

稳妥构造函数模式

从这个模式的名称来看,也是依托于构造函数来创建对象的。具体的方式来看下面的代码:

function Person(name, age, sex) {
  let o = new Object()
  o.sayName = function() {
    console.log(name)
  }
  return o
}
let person1 = Person('zhd', '18', 'boy')

从官方定义来看,所谓的稳妥,是指通过参数形式传入到模式内部的实参,没有方法和属性能访问到,除了模式内部存在的方法和属性,你不能人为的添加属性和方法取访问它。他没有公共的属性,而且内部也不会通过this这个对象去访问参数。
从书写方式上来看,同寄生构造函数模式有两点不同:一是不通过this访问数据成员,二是不使用new操作符调用构造函数来创建对象。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容