创建对象
创建对象的方式有以下几种:最简单的就是对象字面量形式。
工厂模式
说起工厂模式这个称呼的由来,可以根据它的书写方式来理解:
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()
这个构造函数生成了person1
和person2
两个对象,也就是两个构造函数的实例,这两个实例的属性和方法都来自于 Person()
构造函数。不过,重点是new
操作符。
首先来看一下new
操作符都干了些啥?
new操作符干了啥
1)最直接的就是通过new
操作符创建了一个新对象
2)将构造函数内部this的作用域指向了创建的这个新的对象
3)将构造函数内部的属性和方法添加到新对象中
4)返回这个新对象,并将这个对象当做值赋给变量person1
和person2
回到工厂模式存在的问题,构造函数创建的对象是否就知道是哪里一类吗?试验一下:
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
,为了更优雅的封装功能,我们可以改变Person
的prototype
。
例如:
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
- 在
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
操作符调用构造函数来创建对象。