js创建对象的几种设计模式

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;
}
const personOne = createPerson('lee', 29, 'Software Engineer')
const personTwo = createPerson('Gred', 24, 'Doctor')

工厂模式虽然解决了创建多个相似对象的问题,但是却没有解决对象识别的问题(即怎样知道一个对象的类型)

2.构造函数模式

构造函数模式在工作中很常见

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        alert(this.name)
    };
}
const personOne = new Person('lee', 29, 'Software Engineer')
const personTwo = new Person('Gred', 24, 'Doctor')

与工厂模式相比构造函数模式

  • 没有显示地创建对象
  • 直接将属性和方法赋值给了this对象
  • 没有return语句
  • 区别于非构造函数,构造函数以大写字母开头

要创建Person的新实例,必须使用new操作符。以这种方式调用构造函数实际上会经历以下4个步骤:

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

 personOne和personTwo分别保存着Person的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person

console.log(personOne.constructor == Person) // true
console.log(personTwo.constructor == Person) // true

以上例子中创建的所有对象既是Object的实例,同时也是Person的实例,这一点通过instanceof操作符可以得到验证。

console.log(personOne instanceof Person) // true
console.log(personOne instanceof Object) // true
console.log(personTwo instanceof Person) // true
console.log(personTwo instanceof Object) // true

personOne和personTwo之所以同时是Object的实例,是因为所有对象均继承自Object

构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通函数也不会有什么两样。

// 当作构造函数使用
const person = new Person('lee', 29, 'Software Engineer');
person.sayName(); // 'lee'

// 作为普通函数调用
Person('Gred', 24, 'Doctor') // 添加到window
window.sayName(); // 'Gred'

// 在另外一个对象的作用域中调用
const o = new Object();
Person.call(o, 'Alice', 27, 'Nurse');
o.sayName(); // 'Alice'

构造函数的问题:每个方法都要在每个实例上重新创建一遍,然而,创建两个完成相同任务的Function实例没有必要。personOne和personTwo中都有一个sayName()的方法,但是这2个方法不是同一个Function的实例。会导致不同的作用域链和标识符解析。因此,不同实例上的同名函数是不相等的。

console.log(personOne.sayName == personTwo.sayName); // false

通过把函数转移到构造函数外解决问题

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName
}
function sayName() {
        alert(this.name)
    };
const personOne = new Person('lee', 29, 'Software Engineer')
const personTwo = new Person('Gred', 24, 'Doctor')

如上,两个实例的sayName共享了一个全局的sayName方法。解决了2个函数做同一件事的问题。但是新问题又来了:

  1. 在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实
  2. 如果对象需要定义很多方法,那么就要定义很多全局函数,导致我们这个自定义的引用类型丝毫没有封装性可言

3.原型模式 

针对以上构造函数的痛点,原型模式可以解决这些问题。

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的作用是包含可以由特定类型的所有实例共享的属性和方法。prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处就是不用在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。 

function Person() {}
Person.prototype.name = 'lee';
Person.prototype.age = 29;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function() {
    alert(this.name)
};
const personOne = new Person();
personOne.sayName(); // 'lee'
const personTwo = new Person();
personTwo.sayName(); // 'lee'
console.log(personOne.sayName == personTwo.sayName); // true

默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针。如上例子,Person.prototype.constructor指向Person。

Person.prototype.constructor === Person // true

当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。这个内部属性[ [ Prototype ] ],虽然无法访问到。但是可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。如果[ [ Prototype ] ]指向调用isPrototypeOf()方法的对象(Person.prototype),那么这个方法就返回true。

console.log(Person.prototype.isPrototypeOf(personOne)); // true
console.log(Person.prototype.isPrototypeOf(personTwo)); // true

虽然在脚本中没有标准的方式访问[ [ Prototype ] ]但是火狐、苹果、谷歌浏览器在每个对象上都支持一个属性__proto__

console.log(personOne.__proto__ == Person.prototype); // true
console.log(personOne.constructor == Person); // true

es6新增了一个方法 Object.getPrototypeOf() 这个方法返回[ [ Prototype ] ]的值

console.log(Object.getPrototypeOf(personOne) == Person.prototype); // true
console.log(Object.getPrototypeOf(personOne).name); // 'lee'
console.log(Object.getPrototypeOf(personTwo) == Person.prototype); // true
console.log(Object.getPrototypeOf(personTwo).name); // 'lee'

每当代码读取某个对象的属性时,都会执行一次搜索。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。如上代码,我们调用personOne.sayName()的时候,会先后执行2次搜索。第一次解析器会搜索实例personOne是否有sayName属性,如果没有,继续搜索它的原型中是否有sayName属性。有就读取。

虽然可以通过对象实例访问保存在原型中的值。但是却不能通过实例去重写原型中的值。如果我们给实例添加一个属性,这个属性和原型属性同名,那我们就在实例中创建该属性,该属性会屏蔽原型中的那个属性。如下,获取personOne.name会优先获取到该实例中写入的name属性

personOne.name = 'Alice';
console.log(personOne.name) // 'Alice'  来自实例
console.log(personTwo.name) // 'lee'  来自原型 

使用delete可以删除实例中的属性,从而让我们重新访问原型中的属性。

personOne.name = 'Alice';
console.log(personOne.name) // 'Alice'  来自实例
console.log(personTwo.name) // 'lee'  来自原型 
delete personOne.name;
console.log(personOne.name) // 'lee'  来自原型 

hasOwnProperty()方法可以检测一个属性是存在于实例中还是存在于原型中。如果该属性存在于实例中,则返回true

personOne.name = 'Alice';
console.log(personOne.hasOwnProperty('name')) // true
console.log(personTwo.hasOwnProperty('name')) // false

in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。

personOne.name = 'Alice';
console.log('name' in personOne) // true name在实例中
console.log('name' in personTwo) // true name在原型中

hasPrototypeProperty() 在属性存在于原型中会返回true

personOne.name = 'Alice';
console.log(hasPrototypeProperty(personOne, 'name')) // false name在实例中
console.log(hasPrototypeProperty(personTwo, 'name')) // true name在原型中

getOwnPropertyNames()方法得到所有实例属性,无论它是否可枚举

const keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); // ['constructor','name','age','job','sayName']

更简单的原型语法

前面例子中每添加一个属性和方法就要敲一遍Person.prototype。为减少不必要的输入,也为了从视觉上更好的封装原型功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。

function Person(){}
Person.prototype = {
    name: 'lee',
    age: 29,
    job: 'Software Engineer',
    sayName: function(){
        alert(this.name)
    }
};

将Person.prototype设置为等于一个以对象字面量形式创建的新对象,最终结果相同,但是有一个例外:constructor属性不再指向Person了,指向Object构造函数。

const friend = new Person();
console.log(friend instanceof Person) // true
console.log(friend instanceof Object) // true
console.log(friend.constructor == Person) // false
console.log(friend.constructor == Object) // true

 用instanceof操作符测试Object和Person仍然返回true,但constructor属性则等于Object而不等于Person了。如果constructor的值真的很重要,可以像下面这样特意将它设置回适当的值。

function Person(){}
Person.prototype = {
    constructor: Person,
    name: 'lee',
    age: 29,
    job: 'Software Engineer',
    sayName: function(){
        alert(this.name)
    }
};

现在可以通过该属性访问到适当的值了。但是以这种方式重设constructor属性回导致它的[ [Enumerable] ]特性被设置为true。默认情况下,原生的constructor属性是不可枚举的。我们可以试一下Object.defineProperty();

function Person(){}
Person.prototype = {
    name: 'lee',
    age: 29,
    job: 'Software Engineer',
    sayName: function(){
        alert(this.name)
    }
};
Object.defineProperty(Person.prototype, 'constructor', {
    enumerable: false,
    value: Person
})

如果是定义构造函数以后,重写原型。就相当于把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。

function Person(){}
const friend = new Person();
Person.prototype = {
    name: 'lee',
    age: 29,
    job: 'Software Engineer',
    sayName: function(){
        alert(this.name)
    }
};
friend.sayName(); // error

上面的这个例子中,调用friend.sayName()时发生错误,因为friend指向的原型中不包含以该名字命名的属性。调用构造函数时会为实例添加一个指向最初原型的指针,而friend的指针指向的是旧的原型对象。新原型对象与其没有关联。但是旧的原型对象和新的原型对象的指针都指向Person 

原型对象的缺点:(1)所有实例在默认情况下都将取得相同的属性值。(上面例子中定义的name,age等)(2)原型中存在引用类型,会导致每一个实例共享这个引用类型。如下例子:每一个实例中的friends会因为其中一个实例的修改,都共享了这个修改后的结果。

function Person(){}
Person.prototype = {
    constructor: Person,
    name: 'lee',
    age: 29,
    friends: ['Jack', 'Bob'],
    job: 'Software Engineer',
    sayName: function(){
        alert(this.name)
    }
};
const personOne = new Person();
const personTwo = new Person();
personOne.friends.push('Ivan');
console.log(personOne.friends); // ['Jack', 'Bob', 'Ivan']
console.log(personTwo.friends); // ['Jack', 'Bob', 'Ivan']
console.log(personOne.friends === personTwo.friends); // true

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

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

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ['Jack', 'Bob'];
}
Person.prototype = {
    constructor: Person,
    sayName: function() {
        alert(this.name);
    }
    
}
const personOne = new Person('lee', 29, 'Software Engineer')
const personTwo = new Person('Gred', 24, 'Doctor')
personOne.friends.push('Van');
console.log(personOne.friends); // ['Jack', 'Bob', 'Ivan']
console.log(personTwo.friends); // ['Jack', 'Bob']
console.log(personOne.friends === personTwo.friends); // false
console.log(personOne.sayName === personTwo.sayName); // true

这种构造函数与原型混成的模式,是目前使用最广泛、认同度最高的一种创建自定义类型的方法。

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);
        }}
}
const personOne = new Person('lee', 29, 'Software Engineer')
personOne.sayName()

这里只在sayName()方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化。不需要再做什么修改了。其中if语句检查的可以是初始化之后应该存在的任何属性或方法 不必用一大堆if语句检查每个属性和每个方法;只检查其中一个即可。

6.寄生构造函数模式

除了使用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)
    };
    return o;
}
const personOne = new Person('lee', 29, 'Software Engineer')
personOne.sayName(); // 'lee'
const personTwo = new Person('Gred', 24, 'Doctor')
personTwo.sayName(); // 'Gred'

上面例子,该模式与工厂模式区别于对Person调用时该模式使用了new,new Person实例化赋值给personOne。而工厂模式直接调用createPerson赋值给personOne。

这个模式可以在特殊情况下用来为对象创建构造函数,假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用这个模式。

function specialArray() {
    // 创建数组
    const values = new Array();
    // 添加值 由于调用new specialArray的时候会传入值 构造函数没有特意处理参数 所以在这里添加
    values.push.apply(values, arguments);
    // 添加方法
    values.toPipedString = function(){
        return this.join("|")
    };
    // 返回数组
    return values;
}
const colors = new specialArray('red', 'blue','green');
alert(colors.toPipedString()); // 'red|blue|green'

缺点:返回的对象与构造函数或者与构造函数的原型属性之间没有关系。不能依赖instanceof操作符来确定对象类型。建议在可以使用其他模式的情况下,不要使用这种模式。

7.稳妥构造函数

 稳妥构造函数遵循与寄生构造函数类似的模式。但有2点不同:一是新创建对象的实例方法不引用this;二是不使用new操作符调用构造函数。适合在一些安全的环境中(这些环境中会禁止使用this和new)

function Person(name, age, job) {
    var o = new Object();
    o.sayName = function() {
        alert(name)
    };
    return o;
}
const personOne = new Person('lee', 29, 'Software Engineer')
personOne.sayName(); // 'lee'

在以这种模式创建的对象中,除了使用sayName()方法之外,没有其他办法访问name的值。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。

你可能感兴趣的:(javascript,javascript,设计模式,开发语言)