面向对象介绍
什么是对象
对象到底是什么,我们可以从两层次来理解。
-
**(1) 对象是单个事物的抽象。**一本书、一辆汽车、一个人都可以是对象;一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。
-
**(2) 对象是一个容器,封装了属性(property)和方法(method)。**属性是对象的状态,方法是对象的行为。比如可以把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息)。
-
ECMAScript-262把对象定义为:无序属性的集合,其属性可以包含基本值、对象或函数。
什么是面向对象
-
面向对象不是新的东西,它只是过程式代码的一种高度封装,目的在于提高代码的开发效率和可维护性。
-
面向对象编程OOP,是一种编程开发思想。它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
-
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。
-
因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程,更适合多人合作的大型软件项目。
-
面向对象与面向过程:
- 面向过程就是亲力亲为,事无巨细,面面俱到,步步紧跟,有条不紊
- 面向对象就是找一个对象,指挥得结果
- 面向对象将执行者转变成指挥者
- 面向对象不是面向过程的替代,而是面向过程的封装
-
面向对象的特性:封装、继承、多态
程序中面向对象的基本体现
-
在js中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中类Class的概念。
-
我们以一个例子来说明面向过程和面向对象在程序流程上的不同之处:假设我们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个对象表示:
let std1 = {name: 'Michael', score: 98}; let std2 = {name: 'Bob', score: 81};
而处理学生成绩可以通过函数实现,比如打印学生的成绩:
function showScore(stu) { console.log(`姓名:${stu.name}, 成绩:${stu.score}`); }
如果采用面向对象的程序设计思想,我们首选思考的不是程序的执行流程,而是
Student
这种数据类型应该被视为一个对象,这个对象拥有name
和score
两个属性。如果要打印一个学生的成绩,首先必须创建出这个学生对象,然后给对象发一个showScore
消息,让对象把自己的数据打印出来 -
抽象数据行为模板:
function Student(name, score) { this.name = name; this.score = score; } Student.prototype.showScore = function () { console.log(`姓名:${this.name}, 成绩:${this.score}`); }
-
根据模板创建具体实例对象:
let std1 = new Student('meijun', 98); let std2 = new Student('zaichao', 81);
-
实例对象具有自己的具体行为:
std1.showScore(); std2.showScore();
-
面向对象的设计思想是从自然界来的,因为在自然界中,类Class和实例Instance的概念是很自然的。Class 是一种抽象概念,比如我们定义的Student指学生这个概念,而实例Instance则是一个个具体的 Student
-
所以,面向对象的设计思想是:①抽象出Class ②根据Class创建Instance ③指挥Instance得结果
-
面向对象的抽象程度又比函数要高,因为一个Class既包含数据,又包含操作数据的方法
创建对象
new的方式
可以直接通过new Object()
创建:
var person = new Object();
person.name = 'Jack';
person.age = 18;
person.sayName = function () {
console.log(this.name);
}
字面量的方式
每次创建通过new Object()
比较麻烦,可以通过字面量来创建:
var person = {
name: 'Jack',
age: 18,
sayName: function () {
console.log(this.name);
}
}
对于上面的写法固然没有问题,但是假如我们要生成两个 person
实例对象呢?
var person1 = {
name: '在超',
age: 22,
sayName: function () {
console.log(this.name);
}
};
var person2 = {
name: '美君',
age: 22,
sayName: function () {
console.log(this.name);
}
};
不难看出,这样写的代码太过冗余,重复性太高
工厂函数
我们可以写一个函数,解决代码重复问题:
function createPerson(name, age) {
return {
name: name,
age: age,
sayName: function () {
console.log(this.name);
}
}
}
然后生成实例对象:
var p1 = createPerson('zaichao', 23);
var p2 = createPerson('meijun', 23);
这样封装确实爽多了,通过工厂模式解决了创建多个相似对象代码冗余的问题,但却没有解决对象识别的问题,即怎样知道一个对象的类型
构造函数
更优雅—构造函数
一种更优雅的工厂函数就是构造函数:
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
}
}
var p1 = new Person('Jack', 18);
p1.sayName() // => Jack
var p2 = new Person('Mike', 23);
p2.sayName() // => Mike
- 构造函数是根据具体的事物抽象出来的模板
- 实例对象是根据抽象的构造函数模板得到的具体实例对象
- 每一个实例对象都有
constructor
属性,指向创建该实例的构造函数- 注意:
constructor
是实例的属性说法不严谨,具体后面的原型会讲到
- 注意:
- 可以通过实例的
constructor
属性判断实例和构造函数之间的关系- 注意:这种方式不严谨,推荐使用
instanceof
操作符,后面学原型会解释
- 注意:这种方式不严谨,推荐使用
构造函数的执行
在上面的示例中,Person函数取代了 createPerson函数,但是实现效果是一样的。这是为什么呢?
我们注意到,Person()
的代码与createPerson()
有以下几点不同:
- 没有显示的创建对象
- 直接将属性和方法赋给了
this
对象 - 没有
return
语句 - 函数名使用的是大写的
Person
而要创建Person
实例,则必须使用new
操作符。以这种方式调用构造函数会经历以下4个步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(this就指向了这个新对象)
- 执行构造函数中的代码
- 返回新对象
下面是具体的伪代码:
function Person (name, age) {
// 当使用new调用Person()时, 会先创建一个对象
// var instance = {};
// 然后让内部的this指向instance对象
// this = instance;
// 接下来所有针对 this 的操作实际上就是instance
this.name = name
this.age = age
this.sayName = function () {
console.log(this.name)
}
// 在函数的结尾处会将this返回,也就是instance
// return this;
}
构造函数和实例对象的关系
使用构造函数的好处不仅仅在于代码的简洁性,更重要的是可以识别对象的具体类型。在每一个实例对象中的__proto__
中同时有一个 constructor
属性,指向创建该实例的构造函数:
console.log(p1.constructor === Person); // true
console.log(p2.constructor === Person); // true
console.log(p1.constructor === p2.constructor); // true
对象的 constructor
属性最初是用来标识对象类型的。但是如果要检测对象的类型,还是使用 instanceof
更可靠一些:
console.log(p1 instanceof Person); // true
console.log(p2 instanceof Person); // true
构造函数的问题
使用构造函数带来最大的好处就是创建对象更方便,但是其本身也存在浪费内存的问题:
function Person(name, age) {
this.name = name;
this.age = age;
this.type = 'human';
this.sayHello = function () {
console.log('Hello, ' + this.name);
};
}
var p1 = new Person('lpz', 18);
var p2 = new Person('Jack', 16);
在该示例中,表面上好像没什么问题,但是实际上有一个很大的弊端。那就是对于每一个实例对象,type
和 sayHello
都是一样的内容,每次生成一个实例,都必须为重复的内容多占用一些内存,如果实例对象很多,会造成极大的内存浪费
console.log(p1.sayHello === p2.sayHello); // false
对于这种问题可以把需要共享的函数定义到构造函数外部:
var sayHello = function () {
console.log('hello ' + this.name);
};
function Person(name, age) {
this.name = name;
this.age = age;
this.type = 'human';
this.sayHello = sayHello;
}
var p1 = new Person('lpz', 18);
var p2 = new Person('Jack', 16);
console.log(p1.sayHello === p2.sayHello); // true
这样确实可以了,但是如果有多个需要共享的函数,就会造成全局命名空间冲突的问题。可以把多个函数放到一个对象中用来避免全局命名空间冲突的问题:
var fns = {
sayHello: function () {
console.log('hello ' + this.name);
},
sayAge: function () {
console.log(this.age);
}
}
function Person(name, age) {
this.name = name;
this.age = age;
this.type = 'human';
this.sayHello = fns.sayHello;
this.sayAge = fns.sayAge;
}
var p1 = new Person('lpz', 18);
var p2 = new Person('Jack', 16);
console.log(p1.sayHello === p2.sayHello); // true
console.log(p1.sayAge === p2.sayAge); // true
至此基本解决了构造函数的内存浪费问题,但是代码看起来还是格格不入,那有没有更好的方式呢?
原型
更好—prototype
js规定,每一个构造函数都有一个prototype
属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。这也就意味着,可以把所有对象实例需要共享的属性和方法直接定义在 prototype
对象上
function Person(name, age) {
this.name = name;
this.age = age;
}
console.log(Person.prototype);
Person.prototype.type = 'human';
Person.prototype.sayName = function () {
console.log(this.name);
};
var p1 = new Person('meijun', 23);
var p2 = new Person('zaichao', 23);
console.log(p1.sayName === p2.sayName); // true
这时所有实例的 type
属性和 sayName
方法都是同一个内存地址,指向 prototype
对象,因此提高了效率
构造函数、实例、原型之间的关系
任何函数都具有一个prototype
属性,是一个对象
function F() {}
console.log(F.prototype); // object
F.prototype.sayHi = function () {
console.log('hi!');
};
构造函数的 prototype
对象默认都有一个 constructor
属性,指向 prototype
对象所在函数
console.log(F.constructor === F); // true
通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype
对象的指针 __proto__
,它是非标准属性
var f = new F();
console.log(f.__proto__ === F.prototype); // true
实例对象可以直接访问原型对象成员
instance.sayHi(); // hi!
总结
- 任何函数都有
prototype
属性,该属性是一个对象 - 构造函数的
prototype
对象默认都有一个constructor
属性,指向prototype
对象所在函数 - 通过构造函数得到的实例对象内部会包含一个指向构造函数的
prototype
对象的指针__proto__
- 所有实例都直接或间接继承了原型对象的成员
原型链
原型链
-
定义:如果想使用一些属性和方法,并且属性和方法在每个对象中都一样,那么为了共享数据、节省内存空间,可以把属性和方法通过原型的方式进行赋值
-
原型链:在实例对象和原型对象之间、通过
__proto__
联系的一种关系 -
注意:
- 实例对象的原型
__proto__
和构造函数的原型prototype
指向是相同的 - 实例对象中的
__proto__
原型指向的是构造函数中的原型prototype
- 实例对象中
__proto__
原型是浏览器使用的 - 构造函数中的
prototype
原型是程序员使用的
- 实例对象的原型
-
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性
- 搜索首先从对象实例本身开始
- 如果在实例中找到了具有给定名字的属性,则直接返回该属性的值
- 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找
- 如果在原型对象中找到了这个属性,则返回属性值
-
也就是说,在调用
p.sayName()
的时候,会先后执行两次搜索:- 首先解析器会问:实例p有sayName属性吗?答:没有
- 然后继续搜索,再问:p的原型有sayName属性吗?答:有
- 于是读取那个保存在原型对象中的函数
-
当我们调用p2.sayName()时,将会重现相同的搜索过程,得到相同的结果。而这正是多个对象实例共享原型所保存的属性和方法的基本原理
实例对象读写原型对象成员
读取:
- 先在自己身上找,找到即返回
- 自己身上找不到,则沿着原型链向上查找,找到即返回
- 如果一直到原型链的末端还没有找到,则返回
undefined
值类型成员写入(实例对象.值类型成员 = xx
):
- 当实例期望重写原型对象中的某个普通数据成员时,实际上会把该成员添加到自己身上
- 也就是说该行为实际上会屏蔽掉对原型对象成员的访问
引用类型成员写入(实例对象.引用类型成员 = xx
):
- 同上
复杂类型修改(实例对象.成员.xx = xx
):
- 同样会先在自己身上找该成员,如果自己身上找到则直接修改
- 如果自己身上找不到,则沿着原型链继续查找,如果找到则修改
- 如果一直到原型链的末端还没有找到该成员,则报错
实例对象.undefined.xx = xx
更简单的原型语法
我们注意到,前面例子中每添加一个属性和方法就要敲一遍 Person.prototype.xxx
,
为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
type: 'human',
sayHello: function () {
console.log(`我叫:${this.name}, 年龄:${this.age}`);
}
};
在该示例中,我们将 Person.prototype
重置到一个新的对象。这样做的好处就是为 Person.prototype
添加成员更简单,但是也会带来一个问题,那就是原型对象丢失了 constructor
成员。所以,为了保持 constructor
的指向正确,建议的写法是:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
// 手动将constructor指向正确的构造函数
constructor: Person,
type: 'human',
sayHello: function () {
console.log(`我叫:${this.name}, 年龄:${this.age}`);
}
}
原生对象的原型
所有函数都有prototype
属性对象:
- Object.prototype
- Function.prototype
- Array.prototype
- String.prototype
- Number.prototype
- Date.prototype
- …
练习:为数组对象和字符串对象扩展原型方法
原型对象的问题
-
共享数组
-
共享对象
-
如果真的希望可以被实例对象之间共享和修改这些共享数据那就不是问题。但是如果不希望实例之间共享和修改就是问题
-
一个更好的建议是,最好不要让实例之间互相共享这些数组或对象成员,一旦修改的话会导致数据的走向很不明确而且难以维护
原型对象使用建议
- 私有成员(一般是非函数成员)放到构造函数中
- 共享成员(一般就是函数)放到原型对象中
- 如果重置了
prototype
记得修正constructor
的指向
原型的指向可以改变
-
this: 构造函数中的
this
就是实例对象,原型对象中方法中的this
就是实例对象。 -
概述: 实例对象的原型
__proto__
指向的是该对象所在的构造函数的原型对象,构造函数的原型对象prototype
指向如果改变了,实例对象的原型__proto__
指向也会发生改变 -
语法:
Student.prototype = new Person();
-
结果: stu对象可访问
Person
的原型eat()
,但不能访问Student本身的sayHi()
原型的最终指向为null
-
实例对象有
__proto__
原型,构造函数中有prototype
原型,prototype
是对象,prototype
这个对象中也有__proto__
,那么指向了哪里? -
实例对象中的
__proto__
指向的是构造函数的prototype
,所以prototype
这个对象中__proto__
指向的应该是某个构造函数的原型prototype
-
结论:p实例对象
__proto__
->Person.prototype.__proto__
->Object.prototype.__proto__
是null
-
示例:下面是一个div元素对象的原型链
divObj.__proto__ -> HTMLDivElement.prototype.__proto__ -> HTMLElement.prototype.__proto__ -> Element.prototype.__proto__ -> Node.prototype.__proto__ -> EventTarget.prototype.__proto__ -> Object.prototype没有__proto__, 所以object.prototype.__proto__是null
原型指向改变如何添加方法和访问
- 先改变指向后添加方法。比如先让
Student.prototype = new Person()
改变指向,再添加sayHi()
方法
实例对象和原型对象中属性重名问题
-
实例对象访问属性,先从实例对象找,找到了就直接用,找不到去指向的原型对象中找,找到了就使用,找不到就是
undefined
-
原因: JS是一门动态类型语言,对象没有什么,只要点了,那么这个对象就有了这个东西;没有这个属性,只要对象.属性名,对象就有这个属性。但该属性没有赋值,所以结果是
undefined
-
通过实例对象能否改变原型对象中的属性值? 不能
-
就想改变原型对象中属性的值,怎么办?答:直接通过
原型对象.属性=值
console.log(p.fdsfdsfsdfds); // undefined console.log(fsdfdsfds); // 报错