第三十章.面向对象编程
1.创建对象
-
创建单个对象
var afei = { name : "皮卡", age : 18, university : "外经贸", sayName : function(){ alert(this.name); } }
-
工厂模式
假设我们要创建多个统一类别的对象,比如:
var pika = { name : "皮卡", age : 18, university : "外经贸", sayName : function(){ alert(this.name); } }; var ayuan = { name : "阿远", age : 19, university : "外经贸", sayName : function(){ alert(this.name); } }; var weige = { name : "伟哥", age : 20, university : "外经贸", sayName : function(){ alert(this.name); } };
此时一个一个定义有点麻烦,我们可以封装一下:
/* 通过这个方法我们可以创建多个具有相同属性的对象,由于这个函数内部类似于 原料->加工->产出 的形式,所以我们称之为工厂模式。 */ function createPerson(name,age){ var obj = {}; obj.name = name; obj.age = age; obj.university = "外经贸"; obj.sayName = function(){ alert(this.name); } return obj; } var afei = createPerson("皮卡",18); var yanxin = createPerson("阿远",19); var yinshi = createPerson("伟哥",20);
-
构造函数模式
在我们执行函数的时候,如果在函数前加上关键词
new
,那么会对函数产生以下影响:函数内部默认生成一个空对象
函数默认返回这个对象
函数内部 this 指向这个对象
从而我们可以将上述代码写为:
/* 此时这个函数单独执行没有太大意义(相当于给window加属性),只有在new执行的时候才能发挥其作用,我们称这个函数为 构造函数。ES5里面没有 类 的概念,我可以将构造函数认为是 类。 按照惯例,构造函数第一个字母大写(但不是必须),以便于区分。 */ function Person(name,age){ this.name = name; this.age = age; this.university = "外经贸"; this.sayName = function(){ alert(this.name); } } /* 通过new得到对象的过程,我们称之为 实例化 。 而得到的每个对象我们称之为实例,比如:对象afei是构造函数Person的一个实例。 */ var afei = new Person("皮卡",18); var yanxin = new Person("阿远",19); var yinshi = new Person("伟哥",20);
-
原型模式
每个函数都拥有
prototype
(原型)属性,该属性是一个对象,里面所存储的各种属性和方法是可以直接被该函数的实例使用的。比如上述例子中,所有对象都拥有
university和sayName
属性,而这两个属性所有实例都是相同的,所以没有必要每个实例都创建一份。(检测 pika.sayName === ayuan.sayName)我们可以将它们放置到 原型 中,让所有实例都使用同一个方法,这样就可以节省资源。
//实例所需的不同属性放置于构造函数 function Person(name,age){ this.name = name; this.age = age; } //实例所需的相同属性放置于 prototype Person.prototype.company = "潭州教育"; Person.prototype.sayName = function(){ alert(this.name); }; var afei = new Person("皮卡",18); var yanxin = new Person("阿远",19); var yinshi = new Person("伟哥",20);
(再次检测 pika.sayName === ayuan.sayName)
(标准浏览器为每个对象添加了
__proto__
属性,该属性指向其对应的原型)
2.包装对象
基础数据类型是具有 . 操作的(比如 字符串.charAt()
),很显然,基础数据类型本身不可能拥有属性,所以他们使用的都是其原型上的方法。而原型是针对于 object 类型的数据而言的,基础数据类型是如果使用到原型上的各种方法的呢?答案是 包装对象。
每个基础数据类型在执行 . 操作的时候,js会产生一个其对应的包装对象以供使用,比如 "123".charAt(1)
,代码运行时,内部其实是 new String("123").charAt(1)
,这里创建的 new String("123")
就是"123"
的包装对象。.charAt()
这个方法,其实是定义在该包装对象对应的原型上的,也就是定义在String.prototype
上的,所以我们能对基础数据类型进行 . 操作。
需要注意的是,每次 . 操作都会产生一个新的包装对象,也就是说每次 . 操作产生的包装对应并不是同一个指向,这也就是为什么基础数据类型能存值但是没法取到值的原因。
3.原型链
实例可以使用原型上的各种属性,而原型本身也是一个对象,它是Object构造函数的实例,所以在寻找属性的时候,如果自身没找到,会去原型里面找,而如果原型里面没找到,会继续去Object.prototype里面找,这构成了一个最基础的原型链。
如果将实例的原型指向另外一个构造函数的实例,那么就可以将上述的链型结构变长。从而导致:当使用某个对象的属性的时候,会先从对象自身查找,如果没有找到,会去该对象对应的原型对象上查找,如果还没有找到,会继续去原型对象对应的原型对象查找,直到Object.prototype为止,这就构成了一个实例与原型之间的链条,我们称之为原型链。
(代码演示、画图演示……)
4.继承
在一个类的基础上,想要扩展一些新功能,但是又不影响之前的实例,就需要用到继承。也就是,子类拥有父类的所有属性和方法,子类的扩展不会影响父类。
-
构造函数内部的继承
构造函数内部定义了实例的私有属性,子类需要拥有父类的这些定义,并且还能扩展新的定义:
//父类 Person function Person(name,age){ this.name = name; this.age = age; } //子类 Teacher function Teacher(name,age,id){ //继承父类的所有私有属性定义 Person.call(this,name,age); //添加子类新的私有属性定义 this.id = id; } //子类 Student function Student(name,age,regTime){ //继承父类的所有私有属性定义 Person.call(this,name,age); //添加子类新的私有属性定义 this.regTime = regTime; } var p = new Person("张三",30); var t = new Teacher("Jam",18,"201801624"); var s = new Student("皮卡",20,new Date(2020,6));
我们可以看到在父类 Person 的基础上,继承出了两个新的子类 Teacher 和 Student,新子类拥有各自独特的私有属性定义,并且也保留了父级的私有属性定义,同时,子类的新增不会影响父类。
-
原型的继承
子类需要继承父类原型上的所有定义,同样的,可以新增定义,但是不会影响父级。
//用于继承原型的辅助函数 function extend(CLASS){ function Fn(){} Fn.prototype = CLASS.prototype; return new Fn(); } //父类 Person function Person(name,age){ this.name = name; this.age = age; } Person.prototype.showInfo = function(){ console.log("姓名:"+this.name+",年龄"+this.age); }; //子类 Teacher function Teacher(name,age,id){ Person.call(this,name,age); this.id = id; } //继承父类的原型属性 Teacher.prototype = extend(Person); //新增自己的原型属性 Teacher.prototype.showID = function(){ console.log("ID:"+this.id); } //其他子类同上
结合这两种方式,我们就可以将一个父类从私有属性和原型属性两个方便进行继承,达到我们的目的。需要注意的是,每个类的原型上有一个默认属性 constructor
,它的值默认是指向构造函数本身,而在我们继承的时候,这个属性的指向会被我们改变,所以在继承之后,最好要将该属性的指向恢复一下 构造函数.prototype.constuctor = 构造函数
。
以上这些是面向对象编程的基础知识,我们可以将以前的面向过程的思维转换为面向对象的思维,也就说将各个功能抽象为对象,然后再在其基础上定义各种方法,将我们关注的侧重点放到一个一个的类上,而不是一步一步的流程。这样的方式在解决某些问题的时候能节省大量的代码,并且更便于维护。这也就是JavaScript中面向对象编程的两个重大特点 封装性 和 继承性。