JS面向对象
一、什么是对象?
对象是一个整体,对外提供一些功能。
二、面向对象的特性及其优点
面向对象是一种编程思想,它的特性是:封装、继承、多态
- 封装:对象只对外提供与其他对象交互的必要接口,而对自身的属性和实现细节对外隐藏,通过这种办法,实现了对内部数据的保护,防止意外的改变或错误的使用了对象的私有部分。
- 继承:面向对象的一个重要特性是复用性,而继承是实现复用性的重要手段,它可以实现在不重写的前提下,对功能进行复用和拓展。
- 多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈。如下有简单的实现:
//多态
function sayAge(object) {
if ( object instanceof Child ){
console.log( '10' );
}else if ( object instanceof Parent ){
console.log( '30' );
}
}
sayAge(child); // '10'
sayAge(parent); // '30'
它的主要优点是:
- 易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
三、面向对象和面向过程
面向过程,也叫结构化编程,它主要是先确定基础“骨架”即主要函数,然后根据需要在主要函数中实现其他的方法,根据顺序依次调用,难以复用。
面向对象,则是将具体事务按功能细分成各个对象,每个对象包含自己的属性和方法,功能独立,可以按照自己的需要进行组装,易维护且方便复用。
四、对象创建的几种方式
1、实例创建或者字面量创建
实例创建:
let person = new Object();
person.name = 'lee';
person.age = 25;
person.run = function() {
return this.name + this.age + ' is working.'
};
console.log(person.run());//"lee25 is working."
字面量创建:
let person = {
name: 'lee',
age: 25,
run: function() {
return this.name + this.age + ' is working.'
}
};
console.log(person.run());//"lee25 is working."
这两种方式是javascript中最基本的方式,一次只能创建一个对象,所以要创建多个类似对象的时候,会产生大量的代码。
2、工厂模式
为了解决同类对象创建时代码重复的问题,能够在同一个模式下产出类似的对象,出现了工厂模式。
function createPerson(name, age) {
//实例化一个对象
let person = new Object();
//加工 添加属性和方法
person.name = name;
person.age = age;
person.run = function() {
console.log( this.name + this.age + ' is working.' );
};
return person;
}
console.log(createPerson('lee', 25));
函数 createPerson() 能够根据接收的参数来创建一个包含所有必要信息的person对象,每次调用都会反悔一个包含两个属性和一个方法的对象,并可以无数次调用,工厂模式虽然解决了代码重复的问题,但是又面临新的问题:不同的对象,属性和方法都会重复建立,消耗内存,普遍的函数调用不能确定某个对象属于哪一类。
3、构造函数
ECMAscript中的构造函数可以用来创建特定类型的对象,像 Object 和 Array 这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义类型的属性和方法。
构造函数的规范:
- 构造函数命名始终都应该以一个大写字母开头,为了区别ECMAscript中的其他函数
- 通过构造函数创建对象必须使用 new 操作符
function Person(name, age) {
this.name = name;//创建实例属性
this.age = age;//创建实例属性
this.run = function (){//创建实例方法
console.log( this.name + this.age + ' is working.' );
}
}
let lee = new Person('lee', 25);
console.log(lee.run());//lee25 is working.
console.log(lee instanceof Person);//true lee属于Person
构造函数的函数体和上面的工厂模式相比有几个特点:
- 没有显式地创建对象
- 直接将属性和方法赋给了 this 对象
- 没有 return 语句
这种方法调用构造函数会经历以下几个步骤:
- 当使用了构造函数,并且 new 构造函数(),那么就后台执行了new Object();
- 将构造函数的作用域赋给新对象,(即new Object()创建出的对象),而函数体内的 this 指向了 new 出来的对象;
- 执行构造函数中的代码(为这个新对象添加属性和方法);
- 返回新对象(后台直接返回);
lee保存着Person的一个实例,它有一个constructor(构造函数)属性,该属性指向Person,如下代码:
console.log(lee.constructor == Person); // true
对象的constructor属性最初是用来标识对象类型的,但是,目前来说,还是instanceof操作符更可靠一点,在这个例子中,我们创建的对象既是 Object 实例,也是 Person 实例:
console.log(lee instanceof Object); // true
console.log(lee instanceof Person); // true
需要注意的是:
1. 将构造函数当作函数
构造函数与其他函数的唯一区别在于调用的方式不同。任何函数只要通过 new 操作符来调用,都可以作为构造函数,而任何函数,只要不通过 new 来调用,就和普通函数没什么两样。
//当作构造函数使用 使用new操作符来创建一个新的对象
let person = new Person('Lynn', 25);
person.run();//Lynn25 is working.
//作为普通函数调用 不使用 new 属性和方法都添加给了window对象,
//因为在全局作用域调用一个函数时,this 对象总是指向 Global 对象(在浏览器中是 window 对象)
//因此调用完之后,可以通过 window 对象来调用 run() 方法并返回对应的内容;
Person('Rose', 25);//添加到了 window
window.run();//Rose25 is working.
//在另一个对象的作用域中调用
//可以通过使用call() 或者apply() 在某个特殊对象的作用域中调用 Person()
let obj = new Object();
Person.call(obj, 'Kitty', 24);
obj.run();//Kitty24 is working.
2. 构造函数存在的问题
构造函数的主要问题就是:每个方法都要在每个实例上重新创建一遍。假如我们创建了两个对象: person1 和 person2, 这时候,他们都拥有一个 run() 方法,但是他们不是同一个 function 实例。 ECMAscript中的函数是对象,因此,每创建一个对象,我们对其所定义的每一个函数(也就是方法,类似与run() ),都是一个实例化的新对象。所以上面的构造函数 Person() 的每一个 person 实例都包含一个不同的 Function实例来显示 ‘name + age + is working.’。因此不同实例的同名函数是不相等的。
console.log(person1.run == person2.run); // false
但是创建两个完成同样任务的 Function 实例是不必要的,因此我们可以将实例方法放在全局,这样就可以不同的实例共用同一个方法函数。但是全局定义的函数实际上只能被一个对象调用,而且同一个对象可能会存在多个方法,那就需要定义很多全局函数,于是为了满足封装性,我们可以用原型模式来解决。
4、原型模式
我们创建的每一个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,也就是说 prototype 就是通过调用构造函数而创建的新对象实例的原型对象。使用原型对象可以让所有的对象实例共享它所包含的属性和方法。
function Person() {}; //声明一个构造函数
Person.prototype.name = 'lee'; //在原型里添加name属性
Person.prototype.age = 25; //在原型里添加age属性
Person.prototype.run = function (){ //在原型里添加run方法
console.log( this.name + this.age + ' is working.' );
}
上图展示了 Person构造函数、Person原型属性以及 Person 的两个实例之间的关系,其中, Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person。原型对象中除了包含constructor属性之外,还包括后来添加的其他属性和方法。Person的每一个实例都包含一个内部属性,这个属性指向Person.prototype,我们可以通过isPrototypeOf()方法来确定这种关系:
console.log(Person.prototype.isPrototypeOf(person1));// true
console.log(Person.prototype.isPrototypeOf(person2));// true
ECMAscript 5 增加了一个新的方法,叫 Object.getPrototypeOf() ,在所有支持的实现中,这个方法返回 [[prototype]] 的值。
console.log(Object.getPrototypeOf(person1) == Person.prototype);// true
conosle.log(Object.getPrototypeOf(person1).name);// 'lynn'
Object.getPrototypeOf() 可以取得一个对象的原型,这对利用原型实现继承是很重要的,在后续的博客中,我会详细讲到。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是给定名字的属性,搜索会首先从对象实例本身开始,如果存在就会返回该属性的值,如果没找到就会继续搜索指针指向的原型对象,如果在原型对象中找到这个属性,则返回该属性的值。虽然可以通过对象实例访问保存在原型中的值,但是不能通过对象实例重写原型中的值,如果我们在实例中添加一个属性,而该属性与实例原型中的一个属性同名,那这个属性就会在实例中创建,并屏蔽原型中的同名属性。但是原型中的属性值并没有更改。
5、混合模式
组合使用构造函数模式和原型模式,使用构造函数的方式创建实例属性,用原型模式定义方法和共享属性。这样每个实例都会有自己的一份实例属性副本,但同时又共享对方法的引用,最大限度的节省了内存。同时,也支持向构造函数传递参数。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
constructor: Person,
run: function (){
console.log( this.name + this.age + ' is working.' );
}
}
//或者
Person.prototype.run = function (){
console.log( this.name + this.age + ' is working.' );
}