学习笔记16—对象的创建

虽然使用Object构造函数或对象字面量可以方便地创建对象,但这些方式也有明显的不足:创建具有同样接口的多个对象需要重复编写很多代码。

1 工厂模式

工厂模式是一种众所周知的设计模式,广泛的应用于软件工程领域,用于抽象创建特定对象的过程,下面的例子展示了一种按照特定接口创建对象的方式:

function createPerson(name,age,job){
	let o = new Object();
	o.name = name;
	o.age = age;
	o.job = job;
	o.sayName = function(){
		console.log(this.name);
	}
	return o;
}

let person1 = createPerson("Nicholas",29,"Software Engineer");
let person2 = createPerson("Greg",27,"Doctor");

这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)。

2 构造函数模式

ECMAScript中的构造函数是用于创建特定类型对象的,像Object和Array这样的原生构造函数,运行时可以直接在执行环境中使用,当然也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法:

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

let person1 = new Person("Nicholas",29,"Software Engineer");
let person2 = new Person("Greg",28,"Doctor");
person1.sayName();//Nicholas
person2.sayName();//Greg

在这个例子中,Person()构造函数代替了createPerson()工厂函数,实际上Person()内部的代码跟createPerson()基本是一样的,只是有如下区别:

  • 没有显式地创建对象
  • 属性和方法直接赋值给了this
  • 没有return

另外要注意函数名Person的首字母大写,按照惯例,构造函数名称的首字母都要大写,非构造函数则以小写字母开头。要创建Person的实例,应使用new操作符,以这种方式调用构造函数会执行如下操作:

  1. 在内存中创建一个新对象
  2. 这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性
  3. 构造函数内部的this被赋值为这个新对象(即this指向新对象)
  4. 执行构造函数内部的代码(给新对象添加属性)
  5. 如果构造函数返回非空对象,则返回该对象,否则返回刚创建的新对象

上一个例子的最后person1和person2分别保存着Person的不同实例,这两个对象都有一个constructor属性指向Person:

console.log(person1.constructor == Person); //true
console.log(person2.constructor == Person); //true

constructor本来是用于标识对象类型的,不过一般认为instanceof操作符是确定对象类型更可靠的方式。前面例子中每个对象都是Object的实例,同时也是Person的实例:

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

构造函数不一定要写成函数声明的形式,赋值给变量的函数表达式也可以表示构造函数:

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

let person1 = new Person("Nicholas",29,"Software Engineer");
let person2 = new Person("Greg",27,"Doctor");

person1.sayName();//Nicholas
person2.sayName();//Greg

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

在实例化时,如果不想传参数,那么构造函数后面的括号可加可不加,只要有new操作符,就可以调用相应的构造函数:

function Person(){
	this.name = 'Jake';
	this.sayName = function(){
		console.log(this.name);
	};
}
let person1 = new Person();
let person2 = new Person;

(1)构造函数也是函数
构造函数与普通函数唯一区别就是调用方式不同,除此之外构造函数也是函数,并没有把某个函数定义为构造函数的特殊语法。任何函数只要使用new操作符调用就是构造函数,而不使用new操作符调用的函数就是普通函数。比如前面的例子中定义的Person()可以像下面这样调用:

//作为构造函数
let person = new Person("Nicholas",29,"Software Engineer");
person.sayName();//"Nicholas"

//作为函数调用
Person("Greg",27,"Doctor");//添加到window对象
window.sayName();//'Greg'

//在另一个对象的作用域中调用
let o = new Object();
Person.call(o,"Kristen",25,"Nurse");
o.sayName();//"Kristen"

这个例子一开始展示了典型的构造函数调用方式,即使用new操作符创建一个新对象,然后是普通函数的调用方式,这时候没有使用new操作符调用Person(),结果会将属性和方法添加到window对象。这里要记住,在调用一个函数而没有明确设置this的情况下,this始终指向Global对象。因此在上面的调用之后,window对象上就有了一个sayName()方法,调用它会返回Greg。最后展示的调用方式是通过call()调用函数,同时将特定对象指定为作用域。这里的调用将对象o指定为Person()内部的this值,因此执行完函数代码后,所有属性和sayName()方法都会添加到对象o上面。

(2)构造函数的问题
构造函数虽然有用,但也不是没有问题,构造函数的主要问题在于其定义的方法会在每个实例上都创建一遍,因此对前面的例子而言,person1和person2都有名为sayName()的方法,但这两个方法不是同一个Function实例。因此不同实例上的函数虽然同名却不相等,因为都是做一样的事,所以没必要定义两个不同的Function实例。要解决这个问题,可以把函数定义转义到构造函数外部:

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

function sayName(){
	console.log(this.name);
}

let person1 = new Person("Nicholas",29,"Software Enigineer");
let person2 = new Person("Greg",27,"Doctor");

person1.sayName();//Nicholas
person2.sayName();//Greg

在这里sayName()被定义在了构造函数外部,在构造函数内部,sayName属性等于全局sayName()函数,因为这一次sayName属性中包含的只是一个指向外部函数的指针,所以person1和person2共享了定义在全局作用域上的sayName()函数。这样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,因为那个函数实际上只能在一个对象上调用。如果这个对象需要多个方法,那么就要在全局作用域中定义多个函数。这会导致自定义类型引用的代码不能很好的聚集在一起。这个新问题可以通过原型模式来解决。

3 原型模式

每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上这个对象就是通过调用构造函数来创建的对象的原型。使用原型对象的好处是在它上面定义的属性和方法可以被对象实例共享,原来在构造函数中直接赋值给对象实例的值,可以直接赋给它们的原型:

function Person(){}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
	console.log(this.name);
};

let person1 = new Person();
person1.sayName();//"Nicholas"

let person2 = new Person();
person2.sayName();//"Nicholas"

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

(1)理解原型
只要创建一个函数就会按照特定的规则为其创建prototype属性(指向原型对象)。默认情况下所有原型对象自动获得名为constructor属性,指回与之关联的构造函数。对前面的例子而言 Person.prototype.constructor指向Person。在定义构造函数时原型对象默认只会获得constructor属性,其他所有方法都继承自Object,每次调用构造函数创建一个新实例,这个实例内部[[Prototype]]指针就会被赋值为构造函数的原型对象。关键在于理解这一点:实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有,可以通过下面的代码来理解原型的行为:

/*
*构造函数是函数声明表达式,也可以是函数声明,因此以下两种形式都可以:
function Person(){}
let Person = function(){}
*/
function Person(){}

console.log(typeof Person.prototype);
console.log(Person.prototype);
//{
//	constructor:f Person(),
//  _proto__:Object
//	}

/*
*如前所述,构造函数有一个prototype属性
*引用其原型对象,而这个原型对象也有一个
*constructor属性,引用这个构造函数
*换句话说两者循环引用
*/
console.log(Person.prototype.constructor === Person);//true

/*
*正常的原型链都会终止于Object的原型对象
*Object原型的原型是null
*/
console.log(Person.prototype.__proto__===Object.prototype);//true
console.log(Person.prototype.__proto__.constructor===Object);//true
console.log(Person.prototype.__proto__.__proto__ ===null);//true

console.log(Person.prototype.__proto__);
//{
//	constructor:f Object(),
//	toString: ...
//	hasOwnProperty: ...
//	isPrototypeOf: ...
//	...
//}

let person1 = new Person(),
	person2 = new Person();

/*
*构造函数,原型对象和实例
*是3个完全不同的对象
*/
console.log(person1 !== Person);//true
console.log(person1 !== Person.prototype);//true
console.log(Person.prototype !== Person);//true

/*
*实例通过__proto__链接到原型对象,它实际上指向隐藏特性[[Prototype]]
*
*构造函数通过prototype属性链接到原型对象
*
*实例与构造函数没有直接联系,与原型对象有直接联系
*/
console.log(person1.__proto__ === Person.prototype);//true
console.log(person1.__proto__.constructor === Person);//true

/*
*同一个构造函数创建的两个实例共享一个原型对象
*/
console.log(person1.__proto__ === person2.__proto__);//true

/*
*instanceof检查实例的原型中是否包含指定构造函数的原型
*/
console.log(person1 instanceof Person);//true
console.log(person1 instanceof Object);//true
console.log(Person.prototype instance Object);//true

ECMAScript的Object类型有一个方法叫Object.getPrototypeOf(),返回参数的内部特性[[Prototype]]的值:

console.log(Object.getPrototypeOf(person1)===Person.prototype);//true
console.log(Object.getPrototypeOf(person1).name);//"Nicholas"

使用Object.getPrototypeOf()可以方便地取得一个对象的原型,而这在通过原型实现继承时显得尤为重要。Object类型还有一个setPrototypeOf()方法,可以向实例的私有特性[[Prototype]]写入一个新值,这样就可以重写一个对象的原型继承关系:

let biped = {
	numLegs:2
};
let person = {
	name:'Matt'
};
Object.setPrototypeOf(person,biped);

console.log(person.name);//Matt
console.log(person.numLegs);//2
console.log(Object.getPrototypeOf(person)===biped);//true

为了避免使用Object.setPrototypeOf()可能造成的性能下降,可以通过Object.create()来创建一个新对象,同时指定原型:

let biped = {
	numLegs:2
};
let person = Object.create(biped);
person.name = 'Matt';

console.log(person.name);//Matt
console.log(person.numLegs);//2
console.log(Object.getPrototypeOf(person)===biped);//true

Object.create()用指定对象充当新建对象的prototype值。
(2)原型层级
虽然可以通过实例读取原型对象上的值,但不可能通过实例重写这些值,如果在实例上添加了一个与原型对象中同名的属性,那就会在实例上创建这个属性,这个属性会遮住原型对象上的属性:

function Person(){}

Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
	console.log(this.name);
};

let person1 = new Person();
let person2 = new Person();

person1.name = 'Greg';
console.log(person1.name);//"Greg"来自实例
console.log(person2.name);//"Nicholas"来自原型

当console.log()访问person1.name时,会先在实例上搜索这个属性,因为这个属性在实例上存在,所以就不会搜索原型对象了,而在访问person2.name时,并没有在实例上找到这个属性,所以会继续搜索原型对象并使用定义在原型对象上的属性。

只要给对象实例添加一个属性,这个属性就会遮蔽原型对象上的同名属性,也就是虽然不会修改它,但会屏蔽对它的访问,即使在实例上把这个属性设为null,也不会恢复它和原型的联系。不过使用delete操作符可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索原型对象。

function Person(){}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
	console.log(this.name);
};

let person1 = new Person();
let person2 = new Person();

person1.name = "Greg";
console.log(person1.name);//"Greg"
console.log(person2.name);//"Nicholas"

delete person1.name;
console.log(person1.name);//"Nicholas"

hasOwnProperty()方法用于确定某个属性是在实例上还是在原型对象上,这个方法继承自Object的,会在属性存在于调用它的对象实例上时返回true:

function Person(){}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
	console.log(this.name);
};

let person1 = new Person();
let person2 = new Person();
console.log(person1.hasOwnProperty("name"));//false

person1.name = ”Greg";
console.log(person1.name);//"Greg"
console.log(person1.hasOwnProperty("name"));//true

console.log(person2.name);//"Nicholas"
console.log(person2.hasOwnProperty("name"));//false

delete person1.name;
console.log(person1.name);//"Nicholas"
console.log(person1.hasOwnProperty("name"));//false

(3)原型和in操作符
有两种方式使用in操作符:单独使用和在for-in循环中使用,在单独使用时,in操作符会在可以通过对象访问指定属性时返回true,无论该属性是在实例上还是在原型上:

function Person(){}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
	console.log(this.name);
};

let person1 = new Person();
let person2 = new Person();

console.log(person1.hasOwnProperty("name"));//false
console.log("name" in person1);//true

如果要确定某个对象是否在原型上,可以通过这种方式:

function hasPrototypeProperty(object,name){
	return !object.hasOwnProperty("name") && (name in object);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值