JavaScript创建对象的几种方式

一、工厂模式(字面量)

函数create()能根据接收的参数来构建一个包含所有必要信息的F对象。可无数次调用。
有个问题,怎么知道调用这个函数创建的对象是F对象呢?这个方法无法识别创建出来的对象的类型

function createF(name,age) {
	var tmp = new Object(); //创建一个新对象
	tmp.name = name; 
	tmp.age = age;
	tmp.sayName = function() {
		console.log(this.name);
	}
	return tmp; //返回
}
var t1 = createF('try',19);
t1.sayName();

二、构造函数模式

与上一种方式有什么不同?

  • 没有显示地创建对象;
  • 直接将属性和方法赋给了this对象;
  • 没有return语句。

要创建一个Person新实例,必须使用new操作符。事实上,任何函数只要通过new操作符来调用,那它就可以作为构造函数,构造函数如果不通过new来调用也就是普通函数。
会经历以下4个步骤:

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

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

function Person(name,age) {
	this.name = name;
	this.age = age;
	this.sayName = function() {
		console.log(this.name);
	}
}
var p1 = new Person('try',19); //用new来创建对象
p1.sayName(); //try
var p2 = new Person('try',19);
console.log(p1.constructor == Person); //true
console.log(p2.constructor == Person); //true

//检测对象类型常用的还是instanceof,更可靠
console.log(p1 instanceof Person); //true
console.log(p2 instanceof Person); //true
console.log(p1 instanceof Object); //true
console.log(p2 instanceof Object); //true

 当然,也可以通过call() 或 apply()在某个特殊对象的作用域中调用构造函数。如Person.apply(p1,'try');,这是在对象p1的作用域中调用的,因此调用后
 就拥有量Person的所有属性和sayName方法(并不是所有方法,仅限于在构造函数中出现的,后面会讲原因)。
 构造函数有问题吗?按道理所有Person类型的实例的sayName方法都是一样的呀,但是构造函数里就不一样。

console.log(p1.sayName == p2.sayName); //false

创建两个完成相同任务的Function实例的确没有必要。那就可以向下面这样在构造函数外面定义一个新函数,再让构造函数里的函数等于它。这样一来,由于构造函数里的sayName包含的是一个指向函数的指针,因此p1 p2对象就共享了全局作用于中定义同一个这个函数。那么又有问题了,这样,还有封装性可言?

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

三、原型对象

 首先来解决,什么是原型?
 每创建一个函数,都会同时有一个prototype(原型)属性,这个属性是一个指针,指向prototype(原型)对象。与此同时,所有原型对象都会自动获得一个constructor(构造函数)属性,这也是一个指针,指向那个函数,如下图:

那么也就是,通过这个构造函数,我们可以继续为原型添加其他属性与方法。
​ 当把一个函数作为构造函数 (理论上任何函数都可以作为构造函数) 使用new创建对象的时候,那么这个对象就会存在一个默认的不可见的属性,来指向了构造函数的原型对象。 这个不可见的属性我们一般用 [[prototype]] 来表示,只是这个属性没有办法直接访问到。

一下几点要注意:

  • 从上面的图示中可以看到,创建p1对象虽然使用的是Person构造函数,但是对象创建出来之后,这个p1对象其实已经与Person构造函数没有任何关系了,p1对象的[[ prototype ]]属性指向的是Person构造函数的原型对象。
  • 如果使用new Person()创建多个对象,则多个对象都会同时指向Person构造函数的原型对象。
  • 我们可以手动给这个原型对象添加属性和方法,那么p1,p2,p3…这些对象就会共享这些在原型中添加的属性和方法。
  • 如果我们访问p1中的一个属性name,如果在p1对象中找到,则直接返回。如果p1对象中没有找到,则直接去p1对象的[[prototype]]属性指向的原型对象中查找,如果查找到则返回。(如果原型中也没有找到,则继续向上找原型的原型—原型链。 后面再讲)。
  • 如果通过p1对象添加了一个属性name,则p1对象来说就屏蔽了原型中的属性name。 换句话说:在p1中就没有办法访问到原型的属性name了。但可用delete操作符彻底删除实例属性,从而能够重新访问原型中的属性。
  • 通过p1对象只能读取原型中的属性name的值,而不能修改原型中的属性name的值。 p1.name = “李四”; 并不是修改了原型中的值,而是在p1对象中给添加了一个属性name。
function Person(name,age) {
	this.name = name;
	this.age = age;
	this.colors = ['red'];
}
Person.prototype.sayName = function() {
	console.log(this.name);
}
var p1 = new Person('try',19);
p1.sayName(); //try

p1.name = 'aa';
var p2 = new Person('tt',19); 
p2.sayName(); //tt

Person.prototype.sayAge = function() {
	console.log(this.age);
}
p2.sayAge(); //19

原型对象几种属性与方法

constructor属性:指向构造函数(也就是之前的Person());

但要注意,当原型对象指向另一个新的对象(包括字面量式的重写,因为这也是创建了一个新对象)时,constructor不再指向Person了;如下

console.log(Person.prototype.constructor); //Person
Person.prototype = {
	name: 'ttt',
	age: 30
}
console.log(Person.prototype.constructor); //Object

当然,如果必要(最好写上),可以特意在新对象中包含一个constructor属性并将其值设为Person,确保通过该属性能够访问到适当的值。

console.log(Person.prototype.constructor); //Person
Person.prototype = {
	constructor: Person,
	name: 'ttt',
	age: 30
}
console.log(Person.prototype.constructor); //Person

hasOwnProperty方法:检测一个属性是存在于实例中还是原型中,这个方法(是从Object继承来的)只在给定属性存在于对象实例中才返回true。

与此相类似的

in方法:只要该属性存在于对象中就返回true,无论是实例还是原型都一样;

那么,如何判断一个属性是否存在于原型中:
如果一个属性存在,但是没有在对象本身中,则一定存在于原型中。

原型对象与构造函数方法比较

原型对象 构造函数
属性 共有,一经修改全部实例都会改变 每个实例独有一份
方法 所有实例公用方法 每个实例都有独立的,即便方法相同也要重新复制
缺陷 大家都是访问的同一个对象,如果一个对象对原型的属性进行了修改,则会反映到所有的对象上面 每个对象都有自己独有的一份,大家不会共享,造成内存的浪费和性能的低下。
function Person(name,age) {
	this.name = name;
	this.age = age;
	this.sayAge = function() {
		console.log(this.age);
	}
}
var p1 = new Person('try','12');
Person.prototype.colors = ['red'];
Person.prototype.sayName = function() {
	console.log(this.name);
}

// 原型对象的问题:
p1.colors.unshift('yellow');
var p2 = new Person('ttt','12');
//p1实例中修改的值,在p2中也显现了,共有
console.log(p2.colors); //yellow,red

//构造函数的问题:
console.log(p1.sayAge == p2.sayAge); //false
//p1 p2各有一份

四、组合使用构造函数模式和原型模式

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

function Person(name,age) {
	this.name = name;
	this.age = age;
	this.colors = [];
}
Person.prototype = {
	constructor: Person, //重写原型对象切断了联系 要指回来 不然这个属性没法用了
	sayName: function() {
		console.log(this.name);
	},
	sayAge: function() {
		console.log(this.age);
	}
}
var p1 = new Person('a',19);
var p2 = new Person('b',20);
console.log(p2.colors); //[]
p1.colors.push('yellow');
console.log(p2.colors); //[]

console.log(p1.sayName == p2.sayName); //true

五、动态原型模式

将所有信息都封装在了构造函数中,通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的有点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。但注意使用动态原型时,不能使用对象字面量重写原型,因为如果在已经创建了实例的情况下重写原型,那么就会切断现有实例和新原型之间的联系

function Person() {
	this.name = 't';
	this.age = 19;
	if(typeof this.sayName != 'function') {
		//if不必一一检查所有属性和方法,只需检查初始化后应该存在的任何属性和方法即可
		Person.prototype.sayName = function() {
			console.log(this.name);
		}
		Person.prototype.sayAge = function() {
			console.log(this.age);
		}
	}
}
var p1 = new Person(); //初次调用构造函数
var p2 = new Person(); //不是初次了,if内的语句不会执行,因为这时候原型已完成初始化

参考《JavaScript高级程序设计第3版》。

原文地址:https://www.cnblogs.com/TRY0929/p/11870385.html