JavaScript(四)创建对象总结

119 阅读7分钟

虽然 Object 构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。为解决这个问题,人们开始使用工厂模式的一种变体。

1. 工厂模式

  • 用函数来封装以特定接口创建对象的细节。
function createPerson(name,age){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.sayName = function(){
	    cosnole.log(this.name)
  };
  return o;
}

var person1 = createPerson('qqq946',17);
var person2 = createPerson('lqz',17);
  • 工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

2. 构造函数模式

  • 创建自定义的构造函数。注意:函数名首字母大写。
function Person(name,age){
  this.name = name;
  this.age = age;
  this.sayName = function(){
    console.log(this.name);
  }
}

var person1 = new Person('qqq946',17);
var person2 = new Person('lqz',17);
  • 与工厂模式不同之处:

    • 没有显式地创建对象;
    • 直接将属性和方法赋给了 this 对象;
    • 没有 return 语句。
  • 要创建实例,必须使用 new 操作符。这种方式调用构造函数会经历

    • 创建一个新对象;
    • 将构造函数的作用域赋给新对象(因此 this 指向了这个新对象);
    • 执行构造函数中的代码(为新对象添加属性和方法);
    • 返回新对象。
  • 创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,而这正是构造函数模式胜过工厂模式的地方。

(1)构造函数当做函数

// 当做构造函数
var person = new Person('qqq946',17);
person.sayName(); // 'qqq946'

// 作为普通函数调用
Person('qqq946',17);
window.sayName(); // 'qqq946'

// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, 'qqq946',17);
o.sayName(); // 'qqq946'

(2)构造函数的问题

  • 每个方法都要在每个实例上重新创建一遍。前面例子中,person1 和 person2 中都有一个 sayName( ) 方法,但那两个方法不是同一个 Function 的实例。因为 ES 中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。

  • 所以,以这种方式创建函数,会导致不同的作用域链和标识符解析,但创建 Function 新实例的机制仍然是相同的。因此,不同实例上的同名函数是不相等的console.log(person1.sayName==person2.name);//false

  • 可以像下面这样,通过把函数定义转移到构造函数外部来解决这个问题。这样一来,由于 sayName 包含的是一个指向函数的指针,因此 person1 和 person2 对象就共享了在全局作用域中定义的同一个 sayName( ) 函数。

function Person(name,age){
    this.name = name;
    this.age = age;
    this.sayName = sayName;
}
function sayName(){
    console.log(this.name);
}

var person1 = new Person('qqq946',17);
var person2 = new Person('lqz',17);
  • 但是上述做法也存在问题:在全局作用域中定义的函数实际上只能被某个对象调用,让全局作用域有点名不副实。更让人无法接受的是:如果对象需要定义很多方法,就要定义很多全局函数,那我们的自定义的引用类型就丝毫没有封装性可言了。

3. 原型模式

  • 我们创建的每个函数都有一个 prototype (原型) 属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
function Person(){}
    Person.prototype.name = 'qqq946';
    Person.prototype.age = 17;
    Person.prototype.sayName = function(){
    console.log(this.name);
}

var person1 = new Person();
person.sayName(); // 'qq946'
var person2 = new Person();
console.log(person1.name == person2.name); // true

(1)理解原型对象

  • 无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。

  • 在默认情况下,所以原型对象都会自动获得一个 constructor 属性,这个属性包含一个指向 prototype 属性所在函数的指针。

  • 当调用构造函数创建一个新实例后,该实例内部将包含一个指针 [[Prototype]] (__proto__),指向构造函数的原型对象。

// 通过 isPrototypeOf() 方法确定实例和原型对象之间的关系
console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Person.prototype.isPrototypeOf(person2)); // true

// ES5 新增了一个 Object.getPrototypeOf() 方法,返回__proto__的值
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true
console.log(Object.getPrototypeOf(person1).name); // 'qqq946'
  • 每当代码读取某个对象的某个属性时,都会执行一次搜索。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。
// 当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。
function Person(){}
Person.prototype.name = 'qqq946';
var person1 = new Person();
person1.name = 'lqz';

var person2 = new Person();
console.log(person1.name); // 'lqz'
console.log(person2.name); // 'qqq946'

// 使用 delete 操作符可以完全删除实例属性,从而访问到原型属性
delete person1.name;
console.log(person1.name); // 'qqq946'

// 使用 hasOwnProperty() 方法可以检测一个属性存在于实例中(true)还是原型中(false)
console.log(person1.hasOwnProperty('name')); // false

(2)更简单的原型语法

function Person(){}
Person.prototype = {
  // constructor: Person
  name: 'qqq946',
  age: 17,
  sayName: function(){
    console.log(this.name);
  }
}

// 此时本质上完全重写了 prototype 对象,因此 constructor 属性也改变了
var friend = new Person();
console.log(friend.constructor == Person); // false
console.log(friend.constructor == Object); // true

// 需要的话可以将第3行代码解除注释,可使 constructor 属性指向构造函数
  • 注意:调用构造函数会为实例添加一个指向最初原型的 __proto__ 指针,而吧原型修改为另外一个对象就等于切断了构造函数和最初原型之间的联系。
function Person(){}
var friend = new Person();
Person.prototype = {
  name: 'qqq946',
  sayName: function(){
    console.log(this.name);
  }
}
friend.sayName(); // error

(3)原型对象的问题

function Person(){}
Person.prototype = {
  name: 'qqq946',
  friends: ['a','b']
}
var person1 = new Person();
var person2 = new Person();

person1.friends.push('lqz');
console.log(person1.friends); // 'a','b','lqz'
console.log(person2.friends); // 'a','b','lqz'

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

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

  • 这样每个实例都会有自己的一份实例属性副本,同时又共享着方法的引用,最大限度地节省了内存。此外,这种混成模式还支持向构造函数传递参数。

  • 这种模式是目前使用最广泛的的一种模式。
function Person(name,age){
  this.name = name;
  this.age = age;
  this.friends = ['a','b'];
}
Person.prototype = {
  constructor: Person,
  sayName: function(){
    console.log(this.name);
  }
}
var person1 = new Person('qqq946',17);
var person2 = new Person('lqz',17);
person1.friends.push('c');
console.log(person1.friends); // 'a','b','c'
console.log(person2.friends); // 'a','b'

5. 动态原型模式

  • 使用动态原型模式时,不能使用对象字面量重写原型。
function Person(name,age){
  this.name = name;
  this.age = age;
  if(typeof this.sayName != "function"){
  	Person.prototype.sayName = function(){
    	console.log(this.name);
  	}
	}
}

6. 寄生构造函数模式

  • 在前几种模式都不适用的情况下,可以使用寄生构造函数模式。
  • 基本思想:创建一个函数,仅仅用来封装创建对象的代码,然后再返回创建的对象。
function Person(name,age){
  var o = new Object();
  o.name = name;
  o.age = age;
  o.sayName = function(){
    console.log(this.name);
  };
  return o;
}
var friend = Person('qqq946',17);
friend.sayName(); // 'qqq946'
  • 除了使用 new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实一模一样。

  • 返回的对象与构造函数或者与构造函数的原型之间没有关系。因此,我们建议在可以使用其他模式的情况下,不要使用这种模式。

7. 稳妥构造函数模式

  • 道格拉斯·克罗克福德发明了 JavaScript 中的稳妥对象这个概念。

  • 与寄生构造函数有两点不同:一是创建对象的实例方法不引用 this;二是不使用 new 操作符调用构造函数。
function Person(name,age){
  var o = new Object();
  o.sayName = function(){
    console.log(name);
  };
  return o;
}

参考资料

  • 《JavaScript 高级程序设计》(第三版)