面向对象
定义
- 内存中同时存储多个数据和功能的存储空间;
- 描述现实中一个具体事物的属性和功能的程序结构;
- 事物的属性,会成为对象中的属性;
- 事物的功能,会成为对象中的方法;
使用
- 在开始写程序前,都要先用对象,描述好要操作的事物的属性和功能;
- 再按需使用对象的功能,访问对象的属性。
本质
- js中一切对象的底层都是关联数组。
- 每个属性/方法都是关联数组中的元素。
- 属性名/方法名是key,属性值/函数对象是value。
三大特点
- 封装
- 继承
- 多态 (抽象)
一、封装(定义对象)
定义:将一个具体事物的属性和功能集中定义在一个对象中。
1、对象字面量的方式
var obj = { name: "张三" }
2、创建 0bject 对象
// 创建对象
var obj = new Object();
// 自定义对象的属性和方法
obj.name = "张三";
obj.showname = function(){
alert(this.name)
};
3、使用构造函数
专门描述一类对象统一结构的函数
- 只要反复创建多个相同结构的对象时,都要先定义构造函数
- 复用对象的结构代码
- 只能复用代码,不能节约内存
创建构造函数
function Parent(name, sex){
this.name = name;
this.sex = sex;
}
Parent.prototype.show = function(){ // 提高性能
return this.name + "-" + this.sex;
};
调用构造函数
new
后面就是构造函数;- 当 new 调用一个函数的时候,这个函数中的
this
就是创建出的对象; - 而且函数的返回值就是
this
(隐式返回)。
var p1 = new Parent('张三', "男");
var p2 = new Parent('李四', "女");
console.log(p1.__proto__ == Parent.prototype); // true
new 做的4件事
- 创建新的空对象;
- 让新对象继承构造函数的原型对象;
- 用新对象去调用构造函数,按需访问对象的属性,调用对象的方法;
- 将新对象的地址保存在变量。
二、继承
定义:子类可以继承父类的一些功能,但不影响父类,以达到代码复用。
1、构造函数绑定
使用 call
| apply
| bind
方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:
function Child(name, sex){
Parent.apply(this, arguments);
this.age = 28;
}
解读:
call
和 apply
的第一个参数都是要绑定的子对象,后面的参数就是要传递给父对象的参数。不同的是 apply 是以数组的形式传递。
Parent.call(this, name, sex)
bind
参数和 call 一样,但是需要手动执行。
Parent.bind(this, name, sex)()
2、原型继承
function Child(){
this.age = 28;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
var c1 = new Child();
解读:
将 Child 的prototype
对象指向一个 Parent 的实例。它相当于完全删除了prototype 对象原先的值,然后赋予一个新值。
Child.prototype = new Parent();
任何一个 prototype 对象都有一个constructor
属性,指向它的构造函数。如果没有 “Child.prototype = new Parent();” 这一行,Child.prototype.constructor 是指向 Child 的;加了这一行以后,Child.prototype.constructor 指向 Parent。
console.log(Child.prototype.constructor === Parent); // true
更重要的是,每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性。
console.log(c1.constructor === Child.prototype.constructor); // true
因此,在运行"Child.prototype = new Parent();"这一行之后,c1.constructor也指向Parent。
console.log(c1.constructor === Parent); // true
这显然会导致继承链的紊乱(c1 明明是用构造函数 Child 生成的),因此我们必须手动纠正,将 Child.prototype 对象的 constructor 值改为 Child。
缺点:该方法需要执行和建立 Parent 的实例了,比较耗内存。
3、利用空对象作为中介
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
uber属性:这个属性直接指向父对象的prototype属性。(uber是一个德语词,意思是"向上"、“上一层”。)这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。
优点:该方法是针对上一方法的不足而改进的, F是空对象,所以几乎不占内存(这个extend函数,就是YUI库如何实现继承的方法。)
4、拷贝继承
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}
三、多态
定义
- 同一个行为具有多个不同表现形式或形态的能力。
- 同一操作(方法)作用于不同的对象上面,可以产生不同的解释和不同的执行结果。
实例
Martin Fowler 在《重构:改善既有代码的设计》里写到:
在电影的拍摄现场,当导演喊出“action”时,主角开始背台词,照明师负责打灯光,后面的群众演员假装中枪倒地,道具师往镜头里撒上雪花。在得到同一个消息时,每个对象都知道自己应该做什么。如果不利用对象的多态性,而是用面向过程的方式来编写这一段代码,那么相当于在电影开始拍摄之后,导演每次都要走到每个人的面前,确认它们的职业分工(类型),然后告诉他们要做什么。如果映射到程序中,那么程序中将充斥着条件分支语句。
下面是使用条件分支语句,如果增加一个角色,就必须修改 action 方法:
var action = function(person){
if (person instanceof p1){
console.log( '背台词' );
}
else if (person instanceof p2){
console.log( '打灯光' );
}
}
var p1 = function(){}
var p2 = function(){}
action(new p1()) // 背台词
action(new p2()) // 打灯光
使用对象的多态性
var action = function(person){
person.do()
}
var p1 = function(){}
p1.prototype.do = function(){
console.log( '背台词' );
};
var p2 = function(){}
p2.prototype.do = function(){
console.log( '打灯光' );
};
var p3 = function(){}
p2.prototype.do = function(){
console.log( '撒雪花' );
};
action(new p1()) // 背台词
action(new p2()) // 打灯光
action(new p3()) // 撒雪花
优点
- 消除类型之间的耦合关系
- 可替换性
- 可扩充性
- 接口性
- 灵活性
- 简化性
四、系统对象
本地对象
非静态对象:Array Object 等可以new实例化的对象
内置对象
静态对象:Math 等不可以实例化的对象
宿主对象:
DOM(document)、BOM(window)等