JS面向对象之创建对象模式

时间:2021-04-20 19:51:45

虽然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('alice', 29, 'teacher');

var person2 = createPerson('bob', 11, 'student');

解决了创建多个相似对象代码重复的问题,但是没有解决对象识别的问题,无法知道对象的类型

 

2.构造函数模式

ECMAScript 中的构造函数可以用来创建特定类型的对象,改写之前方法

function Person (name, age, job) {

  this.name = name;

  this.age = age;

  this.job = job;

  this.sayName = function() {

    alert(this.name);

  }

}

var person1 = new Person('alice', 29, 'teacher');

var person2 = new Person('bob', 11, 'student');

使用new操作符调用构造函数的实际过程如下:

(1)创建一个新对象

(2)将构造函数的作用域赋给新对象(此时this就指向这个新对象)

(3)执行构造函数中代码(为新对象添加属性)

(4)返回新对象

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型

 

此处解释一下构造函数:

构造函数也是函数!通过new操作符调用就作为构造函数来使用,而没有new,和普通函数没什么两样,同样可以正常调用,例如:

  Person('alice', 29, 'teacher'); //当做普通函数调用,添加到window

  window.sayName();

// 在另一个对象作用域调用

  var o  = new Object();

  Person.call(o, 'bob', 11, 'student');

  o.sayName();   // 'bob'

构造函数模式的问题:

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

会导致不同的作用域链和标识符解析

alert(person1.sayName == person2.sayName)  // false

可以通过把函数定义转移到构造函数外解决这个问题

写为:

function Person (name, age, job) {

  this.name = name;

  this.age = age;

  this.job = job;

  this.sayName = sayName;

}

function sayName () {

  alert(this.name);

}

在构造函数内部,我们把sayName属性设置成了全局的sayName函数,由于sayName包含的是一个指向函数的指针,因此

person1 和 person2 对象就共享了在全局作用域中定义的同一个sayName()函数。确实解决了两个函数做同一件事的问题,

新的问题又来了,在全局作用域定义的函数只能被某个对象调用,这让全局作用域有点名不副实。而且如果对象需要定义很多方法,就要定义很多个全局函数,那这个自定义的引用类型就没封装性可言了

 

3.原型模式

简单来说就是我们创建的每个函数都有一个prototype(原型)属性,该属性是一个指针,指向一个对象,而这个对象的用途是可以包含由特定类型的所有实例共享的属性与方法。好处是让所有对象实例共享它所包含的属性和方法

例如:

function Person () {}

Person.prototype.name = 'bob';

Person.prototype.age = 29;

Person.prototype.job = 'teacher';

Person.prototype.sayName = function() {

  alert(this.name)

}

var person1 = new Person();

person1.sayName(); // 'bob'

var person2 = new Person();

person2.sayName()  // 'bob'

alert(person1.sayName == person2.sayName)  // true

原型对象

无论什么时候,只要创建了一个新函数,就会为该函数创建一个prototype属性;这个属性指向函数的原型对象

默认下,所有原型对象会获得一个constructor属性,指向prototype属性所在函数的指针

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

alert(Person.prototype.isPrototypeOf(person1));  // true;

alert(Person.prototype.isPrototypeOf(person2));  // true;

alert(Object.getPrototypeOf(person1) == Person.prototype);  // true

alert(Object.getPrototypeOf(person1).name);  // 'bob'

每当代码读取某个对象时,都会进行搜索,从实例对象开始,如果在实例中找到了具有给定名字的属性,则返回该属性的值;

如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找给定名字的属性,一直下去..

!虽然可以通过对象实例访问保存在原型中的值,但是不能通过实例对象重写原型中的值

如果我们在实例中添加了与原型中同名的属性值,那么就在实例中创建该属性,会屏蔽原型中那个属性

通过delete操作符删除实例属性,可以让我们重新访问原型中的属性

person1.name = 'alice';

alert(person1.name);  // 'alice'  来自实例

alert(person2.name)  // 'bob'  来自原型

delete person1.name;

alert(person1.name);  //   'bob'  来自原型

 

我们还可以通过hasOwnProperty()检测一个属性是否存在于实例中,还是存在于原型中

person1.name = 'alice';

alert(person1.hasOwnProperty('name'))  // true   来自实例

delete person1.name;

alert(person1.hasOwnProperty('name'))  // false   来自原型

 

in 操作符

(1)单独使用时,in操作符会在通过对象能够访问给定属性时返回true,不管属性存在于实例还是原型中

function hasPrototypeProperty(object,name) {

  return !object.hasOwnProperty(name) && (name in object);

}

上述方法返回true 说明属性存在于原型中

(2)在使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的属性,既包括实例中的,也包括原型中的

  屏蔽了原型中不可枚举属性的实例属性也会在for-in循环时返回,所有开发人员定义的属性都是可枚举的。

  IE中不显示,是一个bug

Object.keys()

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

var p1 = new Person();

p1.name = 'asd';

p1.age = 31;

var p1keys = Object.keys(p1);

alert(p1keys); // ['name', 'age']

如果还要取得不可枚举的属性,可以用Object.getOwnPropertyNames()方法

var keys = Object.getOwnPropertyNames(Person.prototype);

alert(keys) // "constructor,name,age,job,sayName"

 

重写原型对象

function Person() {

}

Person.prototype = {

  name: 'bob';

  age: 29;

  job: 'teacher';

  sayName: function () {

    alert(this.name)

  }

}

上述将原型对象设置成等于一个以对象字面量形式创建的新对象,最终结果一样

但有一个例外:constructor 不再指向Person了

每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性;这里本质上完全重写了默认的prototype对象,因此constructor属性也变成了新的constructor属性,指向Object构造函数

var friend = new Person();

alert(friend instanceof Object);  //true

alert(friend instanceof Person);  //true

alert(friend.constructor == Person); //false

alert(friend.constructor == Object); // true

 

原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象的任何修改都能够立即从实例上反映出来,即使是先创建了实例后修改原型也照样如此

var friend = new Person();

Person.prototype.sayHi = function() {

  alert('Hi')

};

friend.sayHi();  'hi'  因为实例与原型之间的连接不过是一个指针

但如果是重写整个原型对象就不一样了

function Person() {

}

var friend = new Person();

Person.prototype = {

  name: 'bob';

  age: 29;

  job: 'teacher';

  sayName: function () {

    alert(this.name)

  }

}

friend.sayName(); // error

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


原型对象的问题

由共享的本质导致,对于基本值来说,修改实例属性会覆盖原型属性,

但对于引用类型值的属性就不一样了,如下:

function Person() {

}

Person.prototype = {

  constructor: Person,

  name: 'bob',

  age: 29,

  job: 'teacher',

  friends: ['a','b'],

  sayName: function() {

    alert(this.name)

  }

}

var person1 = new Person();

var person2 = new Person();

person1.friends.push('Van');

alert(person1.friends);// 'a,b,Van'

alert(person2.friends); //  'a,b,Van'

alert(person1.friends === person2.friends)  //true

因为friends数组存在于Perosn.prototype上,所有修改后,person2去调用也是同样的结果,

可是我们希望的是实例一般要有自己的全部属性的,所以也很少有人单独使用原型模式

 

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

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性

每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存

function Person(name, age, job) {

  this.name = name;

  this.age = age;

  this.job = job;

  this.friends = ['a', 'b', 'c']

}

Person.prototype = {

  constructor: Person,

  sayName: function() {

    alert(this.name)

  }

}

5.动态原型模式

function Person(name, age, job) {

  this.name = name;

  this.age = age;

  this.job = job;

  //方法,这里只在sayName方法不存在的情况下加到原型上

  if(typeof this.sayName != 'function') {

    Person.prototype.sayName = function () {

      alert(this.name)

    }

  }

}

if 语句检查的可以是在初始化之后应该存在的任何属性或方法

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('bob',29,'teacher');

friend.sayName()  // 'bob'

除了使用new操作符并把使用的包装函数叫做构造函数之外,这其实和工厂模式没有区别。

构造函数在默认情况下,会返回新对象实例,而通过return语句,可以重写返回值

注意: 寄生构造函数模式,返回的对象与构造函数以及其原型属性没有任何关系,不能依赖instanceof操作符来确定对象类型

JS面向对象之创建对象模式

 

7.稳妥构造函数模式

特点:

(1)实例方法不引用this

(2)不使用new操作符调用构造函数

function Person(name, age, job) {

  var o = new Object();

  o.sayName = function () {

    alert(name)

  };

  return o;

}

在这种模式创建的对象里,除了使用sayName()方法外,无法访问name的值,安全性很强