这算是我个人的一个总结吧……不然总觉得看着看着还是会忘的。今天暂时不说JavaScript里面的数据类型什么的,先来看看JavaScript里面的对象。
首先,众所周知,大家都说写程序有两种思路,一种面向过程,另一种面向对象。面向过程,顾名思义,就是有什么功能你就写什么函数。而面向对象,就是有什么项目你就先把项目封装起来,在里面写函数。
很抽象对吧,那举个例子。有一天产品那边说他们需要你给前端写个JS动画,很简单,就是旋转的那种(←其实用CSS3就好了,但是他们怕兼容问题于是叫你写JavaScript),一共也就只有三种:正方形,长方形,三角形。如果是面向过程:
//生成东西
//旋转起来
如果是面向对象
//正方形
//长方形
//三角
然后产品加需求了,加了很多其他图形,旋转方法也不同……面向过程看了一眼代码瞎了……卧槽这么多,我if/else要写出血啊,而且容易改崩啊(╯‵□′)╯︵┻━┻。
然后面向对象笑了:
//其他图形
而且面向对象还可以继承其他对象的方法,换句话说,相同的旋转方法是不需要重复的,也不需要写那么多的if/else来判断,更不用对其他的对象进行修改。
举了这个例子,OOP设计方法的优越性已经体现出来了,它更自然,而且不会打乱你先前的代码。这点很优秀。但是一个很关键的问题来了,对象这个东西……JavaScript里面没有啊(╯‵□′)╯︵┻━┻。
不,不应该说没有,他只是没有你可以写的类而已……但是对象就是类的实例,你没有类说个蛋的对象啊(╯‵□′)╯︵┻━┻。
而且JavaScript里面也没有传统意义上的继承啊private啊public啊之类的东西,你是要写什么鬼啊(╯‵□′)╯︵┻━┻。
所以JavaScript里面的一个大坑(←我认为的),大概就是对象了吧。
那么接下来,我们来谈论一下创建对象:
- 工厂模式
function createObj(a, b, c) {
var o = new Object();
o.a = a;
o.b = b;
o.c = c;
o.sayHi = function() {
alert("hi");
}
return o;
};
var test1 = createObj(1, 2, 3);
var test2 = createObj(4, 5, 6);
这种模式很好,但是有一个问题,你无法知道该对象的类型。比如
console.log(test1.constructor == createObj)
你会得到false。
卧槽,那如果有朝一日我写了一百多个对象那我怎么分辨哪个是哪个啊(╯‵□′)╯︵┻━┻,还不如面对过程呢混蛋!
- 构造函数模式
在JavaScript中,function实际上都是对象,所以说,我们完全可以用Function这种原生构造函数来创建对象。也就是说先创建自定义的构造函数,然后再来定义对象,比如:
function Obj(a, b, c) {
this.a = a;
this.b = b;
this.c = c;
this.sayHi = function() {
alert("hi");
};
}
var test1 = new Obj(1, 2, 3), test2 = new Obj(4, 5, 6);
然后这些写完之后,你会发现无论是instanceof来检测对象类型还是用constructor来检测,都可以发现test1和test2都指向Obj。
哈哈哈哈大功告成啊哈哈哈哈,看来可以洗洗睡了……等等。
如果有一百个对象,他们都需要用这个构造函数来构造,那么问题来了……每个对象是不是同时都实例了一个不同的sayHi函数呢?
我们来看看:
console.log(test1.sayHi == test2.sayHi);
yooooooooooo是false哟~
卧槽也就是说我每次实例化一个对象都创建了一个作用域链?那如果函数多了呢……卧槽啊崩了啊要!
于是机智的我们想出了一个办法:
function Obj(a, b, c) {
this.a = a;
this.b = b;
this.c = c;
this.sayHi = sayHi();
}
function sayHi() {
alert("hi");
}
var test1 = new Obj(1, 2, 3), test2 = new Obj(4, 5, 6);
这下好了吧,我把sayHi给移到全局环境中总行了吧?
可是这样的话……和面对过程又有什么区别呢亲?
于是我们有了第三种方法:
- 原型模式
每个函数都有一个prototype属性,这个属性是一个指针指向一个对象,而这个对象包含可以有特定类型的所有实例共享的属性和方法,也就是说,我们不用每次都传递进参数了,而是可以直接定义对象实例的信息,比如这样:
function Obj() {
};
Obj.prototype = {
a : 1,
b : 2,
c : 3,
sayHi : function() {
alert("hi");
}
}
var test1 = new Obj(), test2 = new Obj();
而且如果你往对象里面修改一个属性,它也只会屏蔽prototype里面的属性,而不会修改:
test1.a = 4;
console.log(test2.a); //1
不过使用这个模式要注意了:如果你修改了单独的prototype属性,那么无所谓。比如你先实例化再prototype单独属性是无所谓的:
var test1 = new Obj(), test2 = new Obj();
function Obj() {
};
Obj.prototype.a = 1;
console.log(test1.a); //1
但是如果你是给prototype对象的话,那就GG了:
var test1 = new Obj(), test2 = new Obj();
function Obj() {
};
Obj.prototype = {
a : 1,
b : 2,
c : 3,
sayHi : function() {
alert("hi");
}
}
console.log(test1.a) //undefined
因为你重写了整个prototype。
同时如果你重写了整个prototype的话,你会发现constructor属性也被修改了,它并不是Obj而是Object了o(╯□╰)o。如果你在重写里面重设constructor,那又会导致constructor的Enumerable属性被设置为true——也就是说在in操作符里面你可以找到它了。
不过除了这些好像就没什么了吧?作用域链共享了,函数问题解决了,覆盖也可以覆盖了。。。但是等等,如果在prototype里有引用类型怎么办呢?比如array:
function Obj() {
};
Obj.prototype = {
a : 1,
b : 2,
c : 3,
sayHi : function() {
alert("hi");
},
arr : [1, 2, 3]
}
var test1 = new Obj(), test2 = new Obj();
test1.arr.push(4); //hope only test1
console.log(test2.arr); //[1, 2, 3, 4]
很好,不只是覆盖而是修改了。。。boom!
于是有了第四种方法
- 组合使用构造函数模式和原型模式
其实这个很好理解:用构造函数模式放实例属性,用原型模式来装函数就行了:
function Obj(a, b, c) {
this.a = a;
this.b = b;
this.c = c;
this.arr = [1, 2, 3];
};
Obj.prototype.sayHi = function() {
alert("hi");
};
var test1 = new Obj(), test2 = new Obj();
test1.arr.push(4);
console.log(test2.arr); //[1, 2, 3]
好了大功告成!这样的话实例就有自己的独特属性了,函数也不会创建一大堆作用域链,对象识别也搞定了,看来终于搞定了啊哈哈哈哈哈!
不过还有三种创建对象的方法,以及继承对象什么的……明天再说吧……妈蛋明天还有课啊(╯‵□′)╯︵┻━┻