面向对象
面向对象的三大特点:封装、继承、多态。
先使用一个个的对象结构集中存储现实中事物的属性和功能。 然后,再按需使用不同对象中的不同属性和功能。这就是面向对象编程。
面向对象使其便于大量数据的管理维护。
一、封装
- 封装就是创建一个对象,集中保存现实中一个事物的属性和功能。
- 将零散的数据封装进对象结构中,极其便于大量数据的管理维护。
- 今后,只要使用面向对象思想开发时,第一步都是先封装各种各样的对象结构备用。
封装对象三种方式:
-
直接使用
{}
。访问时使用对象名.属性名
或对象名.方法名()
var lilei={ sname:"Li Lei",//姓名 sage:11,//年龄 intr:function(){ console.log(`I'm ${this.sname}, I'm ${this.sage}`) } }
-
使用
new Object()
var lilei = new Object(); // 创建空对象 lilei.sname = 'Li Lei'; lilei.intr = function(){ console.log(`I'm ${this.sname}`) }
该方法揭示了js语言底层最核心的原理:js中所有对象底层都是 关联数组
a. 关联数组和对象的存储结构都是 键值对 的形式
b. 访问成员时的方法都是`对象名/数组名['成员名']`,简写`对象名/数组名.成员名`
c. 强行给不存在的位置或属性赋值不会报错,且添加新属性。
d. 强行访问不存在的位置/属性的值不会报错
e. 可以用for...in..循环遍历`for(var 属性名 in 对象名/数组名){...}`
-
使用构造函数
使用前两种方法创建对象,如果想创建多个结构相同的对象时,代码会重复很多,不便维护。解决办法:构造函数-描述同一类型的所有对象的统一结构的函数。
// 使用构造函数步骤1:定义构造函数 function Student(name, age) { this.name = name this.age = age this.intr = function(){ console.log(`I'm ${this.name}, I'm ${this.age}`) } } // 步骤2:使用构造函数反复创建多个对象 var lilei = new Student('lilei', 20) var liumei = new Student('liumei', 18)
总结:
使用new时,此处new做了4件事:①创建一个新的空对象;②设置子对象的
__proto__
属 性,指向构造函数的原型对象(下文详细讨论继承);③调用构造函数时,将构造函数的this指向新new出来的对象;同时在构造函数内通过强行赋值方式,为新对象添加规定的属性和方法。④返回新对象的地址保存到左侧变量中。此处出现了this的2种情况:①obj.fun()调用obj中的fun方法时,fun中的this指向obj对象。②new Fun()时,Fun中的this指向新new出来的对象。
二、继承
在上面的构造函数中,每次new一个新的对象时,都要给新对象赋值一个intr的function,每次定义function时相当于new Function操作,每次都会在内存中开辟新的空间保存这个function,实际上的funtion的内容都是一样的,没必要每次都新创建,造成内存浪费。
解决方法:子对象都需要的相同功能和属性值可以用继承解决。
继承:父对象中的成员,子对象无需重复创建,就可直接使用。
js中继承都是通过原型对象实现的。
原型
原型对象: 替所有子对象集中保存共有的属性和方法的父对象。
何时使用原型对象:当多个子对象有相同属性或功能时,就可以将相同的属性和方法集中定义在原型对象中。
如何创建原型对象:无需额外操作。在定义构造函数时,会自动生成空的原型对象
如何访问原型对象:构造函数中都有一个自带属性prototype,指向自己的原型对象。
如何实现继承:在上面封装模块案例中,通过new构造函数可以实现子对象继承构造函数的原型对象。new使得子对象的
__proto__
属 性,指向构造函数的原型对象。-
如何向原型对象中添加共有属性:直接对其需要的属性赋值:
构造函数.prototype.属性名= 属性值
;构造函数.prototype.方法名 = function(){...}
关于原型和原型链及继承会在这篇文章中详述:js原型、原型链及继承。
使用了原型对象后,在子对象访问属性或方法时,js引擎会优先在子对象内部查找自有属性或方法。当子对象中没有时,js引擎会沿着__proto__
属性去父对象(原型)上查找。若原型上存在该方法或属性,就直接使用,和使用自身的属性方法一样。这个沿着原型查找方法和属性的链路即为原型链。原型链是由多级父对象逐级继承形成的链式结构。
总结:构造函数中方法可以定义在原型对象中,为所有子对象共有。
function Student(name, age) {
this.name = name
this.age = age
}
// 将intr方法定义在构造函数的原型对象上
Student.prototype.intr = function(){
console.log(`I'm ${this.name}, I'm ${this.age}`)
}
var lilei = new Student('lilei', 20)
lilei.intr(); //I'm lilei, I'm 20
var liumei = new Student('liumei', 18)
liumei.intr(); //I'm liumei, I'm 18
此处出现了this的第③种情况:在原型对象共有的方法中使用了this,此处不看定义只看调用,此时谁调用这个公共的方法,this就指向谁。
总结以上三种this情况:谁调用,this就指向谁。
内置类型
- 内置类型:ES标准中规定的,浏览器已实现的,我们可以直接使用的类型。
- 目前已有11种类型:
String
、Number
、Boolean
、Array
、Date
、RegExp
、Function
、Object
、Error
、Math
、global/window
。(在es6种新增了Symbol
类型) - 类型:每种类型需要有2部分组成:①构造函数:负责创建该类型的子对象(可以被new的);②原型对象:负责为该类型所有子对象集中保存公共的方法和属性。
- 所有上面的11种类型中有9种由构造函数和原型对象组成。Math和global/window实际上只是一个内置对象。
三、多态
多态指的是同一个函数在不同情况下表现出不同的状态
一般认为多态包括2种:
- 重载overload:同一个函数,参数列表不同时,执行的逻辑不同,效果不同。
- 重写overrider:在子对象中有与父对象重名的方法。当父对象中的该方法不好用时,可以在子对象中定义同名方法,来覆盖父对象中的该方法
如下例:toString()
的方法调用会产生多种不同的结构
function Student(sname,sage){
this.sname=sname;
this.sage=sage;
}
var lilei=new Student("Li Lei",11);
console.log(lilei); // Student {sname: "Li Lei", sage: 11}
var arr=[1,2,3];
var now=new Date();
console.log(lilei.toString()); // [object Object]
console.log(arr.toString()); // 1,2,3
console.log(now.toString()); // Wed Feb 09 2022 18:17:05 GMT+0800 (中国标准时间)
发生这样的原因在于:①lilei的toString
方法调是用的Object.prototype
上的方法;②数组的toString
方法在Array.prototype
上重新定义了toString
方法,所有没有再往上查找Object.prototype
上的toString方法;③Date类型跟Array同理,对自己类型的原型对象进行了重写,没有继续往上查找toString方法。④以Array类型为例,查找顺序是:arr -> Array.prototype -> Object.prototype -> null。
所以如果Student不想用Object.protoype
上的tostring方法,则可以在自己原型上重写:
Student.prototype.toString=function(){
return `my name is ${this.sname}`
}
console.log(lilei.toString()); // my name is Li Lei
面向对象总结:
- 封装:创建对象两种方式。
- 继承:所有子对象共用的属性值和方法,都要 放在构造函数的原型对象中
- 多态:重写——只要觉得从父对象继承来的成员 不要用,都在子对象中重写同名成员。