2
创建对象

在之前说过通过Object构造函数或者对象字面量的方式可以创建对象。但是这些方式有一个明显的问题,使用同一个接口创建很多对象会产生大量的重复代码。例如:

//如果你要创建三个对象,通过Object构造函数你就要new三个Object构造函数
var obj1 = new Object()
var obj2 = new Object()
var obj3 = new Object()
1. 工厂模式

工厂模式抽象的创建了具体对象的过程,通过用函数封装以特定接口创建对象。

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

var person1 = createPerson('Nicholas', 29, 'software enginner')
var person2 = createPerson('greg', 27, 'doctor')

如上,通过调用createPerson函数,然后传入特定的参数可以创建特定的对象,虽然工厂模式解决了创建多个相似对象的问题,但是没有解决对象识别的问题(即怎样知道一个对象的类型)。

2. 构造函数模式

ECMAScript中的构造函数可以用来创建特定类型的对象,像Object和Array这样的原生构造函数在运行时会自动出现在执行环境中。这样我们也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。

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

var person1 = new Person('Nicholas', 29, 'software enginner')
var person2 = new Person('Grey', 27, 'Doctor')

如上例子,Person构造函数取代了之前的createPerson工厂函数。与之前的工厂函数创建对象的方法有以下几点区别:

1. 没有显示的创建对象
2. 直接将属性和方法赋给了this对象
3. 没有return语句

还有一点可以看到,我们是通过new操作符创建对象,那么使用new操作符发生了什么呢?

1. 创建一个新的对象
2. 将构造函数的作用域赋给新对象(因此this指向了这个新对象)
3. 执行构造函数中的代码
4. 返回一个新对象

在前面例子的最后,person1和person2分别保存着Person的一个不同实例,这两个对象都有一个constructor(构造函数)属性,该属性指向Person。

console.log(person1.constructor) 
//ƒ Person(name, age, job){
//    this.name = name
//    this.age = age
//    this.job = job
//    this.sayName = function() {
//        alert(this.name)
//    }
//}

person1.constructor == Person //true
person2.constructor == Person //true

对象的constructor属性最初是用来标识对象类型的。但是,提到检测对象类型,还是instranceof操作符更可靠点。我们在例子中创建的所有对象既是Object的实例,同时也是Person的实例。

console.log(person1 instanceof Object) //true
console.log(person1 instanceof Person) // true
console.log(person2 instanceof Object) //true
console.log(person2 instanceof Person) // true

创建自定义的构造函数意味着可以将它的实例标识为一种特定的类型,这正是构造函数模式胜过工厂模式的地方。

构造函数也是函数,与普通函数之间的区别就是构造函数和普通函数调用的方式不一样。

function Person(name, age, job){
    this.name = name
    this.age = age
    this.job = job
    this.sayName = function() {
        alert(this.name)
    }
}
//当作构造函数使用
var person = new Person('Nicholas', 29, 'software engineer')

//作为普通函数使用
Person('gray', 29, 'doctor')
window.sayName() //gray

//再另一个对象的作用域中调用
var o = new Object()
Person.call(o, 'kir', 25, 'nurse')
o.sayName() //'nurse'

构造函数创建对象的缺点:构造函数方式创建对象相对于工厂函数的优势就是可以将它的实例作为一种特定类型。但是通过构造函数方式创建的对象,每个方法都要在每个实例上重新创建一次。在之前的例子上person1和person2都有一个名为sayName的方法,但是那两个方法不是同一个Function实例。改写下之前的构造函数方式就会很清楚:

function Person(name, age, job) {
    this.name = name
    this.age = age
    this.job = job
    this.sayName = new Function('alert(this.name)')
}

从改写的例子上可以看到,每个Person实例都包含一个不同的Function实例。然而创建两个完成相同功能的Function实例是没有必要的。况且有this对象的存在,根本不用在执行代码前就把函数绑定到特定对象上面,因此可以像下面这样把函数转移到构造函数外部:

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

function sayName() {
    alert(this.name)
}

如上,通过Person构造函数创建的对象都共享全局作用域中的sayName函数。但是这样的写法总感觉有点不合适,如果对象有很多方法,那么就要在全局作用域中创建很多函数。接下来介绍一个原型模式可以解决这个问题。

3. 原型模式

我们创建的每个函数都有一个prototype属性,这个属性时一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
通过字面意思了解,prototype就是通过调用构造函数而创建的那个对象实例的原型对象,使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。

function Person() {}
Person.prototype.name = 'Nicholas'
Person.prototype.age = 29
Person.prototype.job = 'software engineer'
Person.prototype.sayName = function() {
    alert(this.name)
}

var person1 = new Person()
person1.sayName() //'Nicholas'

var person2 = new Person()
person2.sayName() //'Nicholas'

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

如上可以看到,创建的对象的这些属性和方法是由所有实例共享的,换句话说,person1和person2访问的都是同一组属性和同一个sayName函数。

首先我们理解下什么是原型对象?
当我们创建任意一个新函数的时候,就会根据特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。默认情况下,原型对象会有一个constructor属性,该属性指向prototype属性所在函数的指针。

function Person() {}
//Person.prototype.constructor 指向的就是Person构造函数, 通过这个构造函数我们可以继续为原型对象添加属性和方法

创建了自定义的构造函数之后,其原型对象默认有constructor属性,至于其他方法都是从Object继承而来。当调用构造函数创建一个新实例后,该实例内部将包含一个指针(内部属性)指向构造函数的原型对象。通常我们是用(__proto__)来表示的。这个连接是存在于对象实例和构造函数原型对象之间的,而不是对象实例和构造函数之间。

  • isPrototypeOf( )
    我们可以通过isPrototypeOf( )方法来判断对象实例的__proto__是否指向对应的原型对象。
Person.prototype.isPrototypeOf(person1) //true
Person.prototype.isPrototypeOf(person2) //true
  • Object.getPrototypeOf( )
    该方法会返回传入的实例对象的原型对象的值。
Object.getPrototypeOf(person1) == Person.prototype //true
Object.getPrototypeOf(person1).name = 'Nicholas'

每当代码读取某个对象属性的时候就会执行一次搜索。搜索是首先从对象实例本身开始,如果在实例中找到了具有给定名字的属性,则返回该属性的值,如果没有找到则继续搜索指针指向的原型对象,在原型对象中找具有给定名字的属性。

function Person() {
    this.name = 'bob'
}
Person.prototype.name = 'jim'
var p = new Person()
console.log(p.name) // 'bob'

可以看到上面输出的"bob"因为在每个实例对象上面有具有一个name为bob的属性。

虽然我们可以通过对象实例访问到原型对象中的值,但是我们并不能通过实例对象修改原型对象中的值。

  • hasOwnProperty()
    通过hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。该方法是从Object继承来的。当属性存在于对象实例中时返回true。
function Person() {}

Person.prototype.name = 'Nicholas'
Person.prototype.age = 29
Person.prototype.job = 'soft ware'

var p1 = new Person()
var p2 = new Person()

console.log(p1.hasOwnProperty('name')) //false
p1.name = 'bob'
console.log(p1.hasOwnProperty('name')) //true

通过使用hasOwnProperty()方法,什么时候访问的是实例属性,什么时候访问的是原型属性就可以一清二楚了。

原型对象与in操作符
有两种方式使用in操作符,单独使用和在for-in循环中使用。in操作符会在通过对象能够访问给定属性时返回true。(不管在对象实例上还是在原型对象上的属性)

function Person() {}
Person.prototype.name = 'Nicholas'
Person.prototype.age = 29
Person.job = 'software engineer'

var p1 = new Person()
console.log("name" in p1) //true

要取得对象上所有可枚举的实例属性,可以使用Object.keys()方法,这个方法接受一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。

function P() {
    this.a = 'a'
}
P.prototype.b = 'b'

var o = new P()
console.log(Object.keys(o)) //['a']
  • Object.getOwnPropertyNames()
    如果你想获得所有的实例属性,不管是否可枚举的,可以通过Object.getOwnPropertyNames().

原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们队原型对象所做的任何修改都能够立即从实例上反映出来,即使是先创建实例对象后修改原型也是一样。因为实例与原型对象之间的连接只不过是一个指针,而非一个副本。
注意,我们可以随时为原型对象添加属性和方法,但是如果重写了整个原型对象,那么情况就不一样了。

function Person() {}

var friend = new Person()

Person.prototype = {
    constructor: Person,
    name: 'nike',
    sayName: function() {
        alert(this.name)
    }
}

friend.sayName() //error

重写了原型对象切断了现有原型和任何之前已经存在的对象实例之间的关系,它们引用的任然是之前最初的原型。

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

通过使用这两种结合的方式创建对象是很常见的方式,构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。结果每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度的节约了内存。

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

Person.prototype = {
    constructor: Person,
    sayName: function() {
        alert(this.name)
    }
}
5. 动态原型模式

动态原型模式把所有的信息都封装在了构造函数中,而通过在构造函数中初始化原型,又保持了同时使用构造函数和原型的优点。可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。

function Person(name, age, job) {
    this.name = name
    this.age = age
    this.job = job
    
    if(typeof this.sayName != "function") {
        Person.prototype.sayName = function() {
            alert(this.name)
        }
    }
}
6. 寄生构造函数模式

寄生构造函数模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后在返回新创建的对象。但从表面上看,这个函数又很像是典型的构造函数。

function Person(name, age, job) {
    var o = new Object()
    o.name = name
    o.age = age
    o.job = job
    o.sayName = function() {
        alert(this.name)
    }
    return o
}

var friend = new Person("Nicholas", 29, "software engineer")
friend.sayName() //'Nicholas'

如上可以看到,Person函数创建了一个新对象,并初始化该对象的属性和方法,然后返回该对象。其实这个方式和工厂模式是几乎一样的,构造函数在不返回值的情况下,默认会返回新对象实例。这里通过return语句,重写了构造函数的返回值。

//工厂模式, 可以看到寄生构造函数模式和工厂模式不同的地方就是使用new操作符
function Person(name, age, job) {
    var o = new Object()
    o.name = name
    o.age = age
    o.job = job
    o.sayName = function() {
        alert(this.name)
    }
}

var person1 = Person('Nicholas', 29, 'software enginner')

那么这个方法的使用场景是什么呢?(其实我也没弄明白)这个模式可以在特殊的情况下来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用这个模式。

function SpecialArray() {
    var values = new Array()
    
    values.push.apply(values, arguments)
    
    values.toPipedString = function() {
        return this.join("|")
    }
    
    return values
}
var c = new SpecialArray("red", "blue", "green")
console.log(c.toPipedString()) // "red|blue|green"

关于寄生构造函数模式,返回的对象与构造函数或者和构造函数的原型属性没有关系,也就是说构造函数返回的对象与在构造函数外部创建的对象没什么不同。所以通过instanceof操作符来确定对象类型是没有意义的。

7. 稳妥构造函数

稳妥对象指的是没有公共属性,而且方法也不引用this对象。稳妥对象醉适合在一些安全的环境中(禁止使用this和new),或者在防止数据被其他应用程序改动时使用。

function Person(name, age, jbo) {
    var o = new Object()
    
    o.sayName = function() {
        alert(name)
    }
    
    return o
}

注意,这里看着也很像工厂函数,但是和工厂函数有区别的就是没有使用this。这里只有通过sayName方法来访问name。

var friend = Person('Nicholas', 29, 'software Enigneer')
friend.sayName()

这样,变量friend中保存了一个稳妥对象,除了调用sayName方法外,没有其他的方式可以访问其数据成员。即使可以通过其他办法给这个对象添加方法或数据成员,但也没有办法进行访问,稳妥构造函函数模式提供的这种安全性,令他非常适合在某些安全执行环境。

总结:关于创建对象的几种方式基本上都在这里了,个人觉得常用的还是构造函数模式和原型模式的结合版本。


fsrookie
2.9k 声望256 粉丝

目前很多文章都是摘抄记录其他教程。见谅。