相信但凡作为一个前端工程师,都被面试到过这个面试题目,HR考察的就是对oop思想的理解。
作为一个从后端转过来的怂逼,oop一直是心中的永远的痛啊。
这几天一直在通读js高级程序设计,重复理解js创建对象的几种方式,以及原型链和constructor等关键点。
谈到创建对象,我们要从最原始的方式说起,也是最简单的方式,就是直接创建一个Object实例,然后添加属性和方法。
1、简单方式
var o=new Object();
o.name='Lucy';
o.age="20";
o.job = "doctor";
o.showName=function(){
console.log(this.name);
}
简单方式不仅有这种Object构造函数形式,还有对象字面量
var o = {
name:"Lucy",
age:20,
job:"doctor",
sayName: function () {
console.log(this.name);
}
}
这种写法看上去没啥问题,但是在创建多个p,就需要重复的代码,给p添加属性和方法,所以这种写法终究还是不算通用。所以后面就干脆把创建对象的过程抽象出来了,就有了工厂模式。
2、工厂模式
function createPerson(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.showName = function(){
console.log(this.name);
}
return o;
}
var p = createPerson('wly','23','前端工程师');
p.showName();
alert(p.constructor == createPerson); //false
alert(p instanceof createPerson); //false
现在可以通过调用这个函数,无数次创建对象了,但是却并不能确切地知道对象的类型。因为ECMAScript的构造函数可用来创建特定类型的对象,所以构造函数模式就出现了。
3、构造函数模式
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayInfo= function(){
console.log(this.name +"," + this.age + "," + this.job);
}
}
var p1 = new Person('lily','23','前端工程师');
var p2 = new Person('lucy','24','前端工程师');
alert(p1.constructor == Person); //true
alert(p2.constructor == Person); //true
alert(p1 instanceof Person); //true
alert(p2 instanceof Person); //true
alert(p1 instanceof Object); //true
alert(p2 instanceof Object); //true
alert(p1.sayInfo == p2.sayInfo); //false
构造函数模式最大的问题:同一构造函数的不同实例的相同方法是不一样的。

p1,p2都有个constructor属性,该属性指向的就是person。而对象的constructor属性本来就是用来标识对象类型的。不过检测对象类型,还是用instanceof比较好些。至于其中的_prototo__,constructor以后另做详解。
那构造函数又有什么缺点呢,p1和p2都有sayInfo方法,但是是不同实例的同名函数,所以是不相等的( alert(p1.sayInfo == p2.sayInfo) 是false),也就是说构造函数的每个方法事实上是在每个实例中重新重建了一遍,所以难免有点浪费内存。
![]() |
后面就干脆把sayInfo拎出来,成为下面这个样子。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayInfo= sayInfo;
}
function sayInfo(){
console.log(this.name +"," + this.age + "," + this.job);
}
var p1 = new Person('lily','23','前端工程师');
var p2 = new Person('lucy','24','前端工程师');
alert(p1.sayInfo == p2.sayInfo); //true
![]() |
这儿其实就是p1和p2共享了在全局作用域的sayInfo函数。但是如果我们定义这个全局函数只是为了给某个对象调用,是不是有点奇怪。假设这个Person对象有多个方法,那岂不是要定义多个全局函数。
4、原型模式
function Person(){}
Person.prototype.name = "lily";
Person.prototype.age = 12;
Person.prototype.job = "doctor";
Person.prototype.friends = ["Tom","Lucy"];
Person.prototype.sayName = function(){
console.log(this.name);
};
var a = new Person();
var b = new Person();
a.friends.push("Jack");
console.log(a.friends); //"Tom","Lucy","Jack"
console.log(b.friends); //"Tom","Lucy","Jack"
console.log(a.friends == b.friends) //true
我们的初衷是像这样在所有实例共享属性和方法,我们无话可说,但是一般很少会这样用,因为实例应该拥有自己的全部的属性,这也是为啥很少有人用原型模式的原因。
5、构造模式和原型模式组合使用
创建对象最常见的方式,就是组合使用构造函数模式和原型模式。构造函数用来定义实例属性,原型模式用来定义方法和共享属性。每个实例都会有自己的一份实例属性的副本,但同时又可以共享对方付的引用。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Tom","Lucy"];
}
Person.prototype = {
constructor : Person,
sayName:function(){
alert(this.name);
}
}
var a = new Person("lily",12,"doctor");
var b = new Person("lucy",23,"teacher");
a.friends.push("Jack");
console.log(a.friends); //"Tom","Lucy","Jack"
console.log(b.friends); //"Tom","Lucy"
console.log(a.friends == b.friends) //false
console.log(a.sayName == b.sayName) //true
独立的friends属性,共享的sayName,这是定义引用类型的一种默认模式。
还有两种方式:寄生构造函数模式 、稳妥构造函数模式,我认为有需要可以去了解下,感觉这两种模式只是在特定情况才会去使用,一般是很少会用到的,着重点应该前面5个的演变历程。

