共计 10703 个字符,预计需要花费 27 分钟才能阅读完成。
面向对象是一种编程思想,我们通过类(构造函数)和对象实现的面向对象编程,满足下述三个特定:封装、继承和多态。
封装
封装创建对象的函数
封装即把实现一个功能的代码封装到一个函数中,以后实现这个功能,只需要执行该函数即可。实现低耦合,高内聚。
现在我们把属性和方法封装成一个对象:
// 创建一个对象
var person = new Object();
// 添加属性和方法
person.name = "钢铁侠";
person.sex = "男";
person.showName = function(){alert("我的名字叫" + this.name);// 我的名字叫钢铁侠
}
person.showSex = function(){alert("我的性别是" + this.sex);// 我的性别是男
}
person.showName();
person.showSex();
如果我们想创建一个不同性别不同姓名的对象,就需要再写一遍上述代码:
// 创建一个对象
var person2 = new Object();
// 添加属性和方法
person2.name = "猩红女巫";
person2.sex = "女";
person2.showName = function(){alert("我的名字叫" + this.name);// 我的名字叫猩红女巫
}
person2.showSex = function(){alert("我的性别是" + this.sex);// 我的性别是女
}
person2.showName();
person2.showSex();
如果我们想要创建多个对象的话,写起来就非常麻烦,所以要去封装创建对象的函数解决代码重复的问题。
function createPerson(name, sex){var person = new Object();
person.name = name;
person.sex = sex;
person.showName = function(){alert("我叫" + this.name);
}
person.showSex = function(){alert("我是" + this.sex + "的");
}
return person;
}
然后生成实例对象,就等于是在调用函数:
var p1 = createPerson("钢铁侠", "男");
p1.showName();// 我叫钢铁侠
p1.showSex();// 我是男的
var p2 = createPerson("猩红女巫", "女");
p2.showName();// 我叫猩红女巫
p2.showSex();// 我是女的
上述过程可以类比为开工厂生产酸奶:第一步:需要原料;第二步:加工酸奶;第三步:出厂售卖;我们通过 var 声明空对象的这一步就相当于第一步原料,添加属性和函数就相当于第二步加工,通过 return 返回对象就相当于第三步出厂。这种符合上述 1、2、3 步骤的函数叫做工厂函数,这种设计函数的思路,叫做工厂设计模式。
通过 new 调用函数
官方函数创建对象的方法是通过 new 的方法,当我们不使用 new 创建对象的时候,函数内部的 this 会指向窗口。
function show(){alert(this);//[object Window]
}
show();
所以当我们在函数内部给 this.name 赋值为 xxxx 时,可以通过 window.name 输出 xxxx,因为如果这个函数没有主人的话它的主人就是 window 对象。
function show(){alert(this);//[object Window]
this.name = "xxxx";
}
show();
alert(window.name);//xxxx
但是如果这个函数通过 new 运算符去调用,那么这个函数中的 this,就会指向新创建的对象。
function show(){alert(this);//[object Object]
}
var obj = new show();
当我们通过 new 运算符去调用函数的时候,它首部和尾部会自动的生成以下两步:1、原料操作:强制改变 this 指向this = new Object();
3、出厂操作:将 this 返回return this;
。
function show(){// this = new Object();
alert(this);//[object Object]
this.name = "xxxx";
// return this;
}
var obj = new show();
alert(obj.name);//xxxx
所以现在我们改造一下之前创建的函数,调用的时候全部都通过 new 去调用,并且将函数中的 person 改成 this。
function createPerson(name, sex){
this.name = name;
this.sex = sex;
this.showName = function(){alert("我叫" + this.name);
}
this.showSex = function(){alert("我是" + this.sex + "的");
}
}
var p1 = new createPerson("钢铁侠", "男");
p1.showName();// 我叫钢铁侠
p1.showSex();// 我是男的
var p2 = new createPerson("猩红女巫", "女");
p2.showName();// 我叫猩红女巫
p2.showSex();// 我是女的
构造函数
我们把这种可以创建对象的函数,叫做构造函数。(功能就是用来构造对象)
function Person(name, sex){
this.name = name;
this.sex = sex;
}
为了和别的函数,进行区分,我们把构造函数首字母大写。官方的构造函数:Array、Object、Date。
我们通过 typeof 可以看到官方通过 new 创建的 Object、Array、Date 本质上都是 function 函数。而且所有被该函数,创建的对象,对象的方法都是一套,arr1.push === arr2.push
返回值是 true。
var arr = new Array();
var obj = new Object();
var d = new Date();
alert(typeof Array);// 类型 function 函数
但是通过调用函数生成的对象方法,彼此之间没有联系,不能反映出它们是同一个原型对象的实例。alert(p1.showName === p2.showName);
返回值为 false。
var arr1 = new Array(10, 20, 30);
var arr2 = new Array(40, 50, 60);
alert(arr1.push === arr2.push); //true
我们声明两个数组
var arr1 = [10, 20, 30, 40, 50];
var arr2 = [60, 70, 80, 90, 100];
给数组添加求和的函数
arr1.sum = function(){
var res = 0;
for(var i = 0; i < this.length; i++){res += this[i];
}
return res;
}
调用 arr1.sum 可以输出 arr1 的和为 150,但是调用 arr2.sum 会系统报错,提示 arr1.sum 不是一个函数。因为 arr1 和 arr2 是单独的两个对象,给 arr1 添加一个方法,arr2 并不会拥有这个方法。所以我们之前通过 new 调用函数生成对象后,他们的方法是相互独立的。
alert(arr1.sum == arr2.sum);//false
每一个实例对象,都有自己的属性和方法的副本。这不仅无法做到数据共享,也是极大的资源浪费。
原型 prototype
prototype 对象的引入:所有实例对象需要共享的属性和方法,都放在这个对象中,那些不需要共享的属性和方法,就放在构造函数中。以此来模拟类。
所以想让 arr2 也拥有求和函数就需要再重新写一个 arr2.sum,这样就会造成浪费,我们想让对象共用一个方法,这时候就需要引入原型 prototype。在 JS 中一切皆对象,函数也是对象。每一个被创建的函数,都有一个官方内置的属性,叫做 prototype(原型)对象,我们输出一下 show.protoype,得到结果[object Object]
。
function show(){}
alert(show.protoype);//[object Object]
所有实例对象需要共享的属性和方法,都放在这个对象里面;那些不需要共享的属性和方法,就放在构造函数里面。
如果,我们想要让该函数创建出来的对象,公用 一套函数,那么我们应该将这套函数,添加该函数的 prototype 原型。所以我们如果想让两个数组都拥有求和的方法,就需要将这个方法添加在 Array 的原型上。
Array.prototype.sum = function(){
var res = 0;
for(var i = 0; i < this.length; i++){res += this[i];
}
return res;
}
现在 arr1 和 arr2 都可以使用这个函数,并且 arr1.sum == arr2.sum,他们使用的这个函数都是原型上的同一个方法。
alert(arr1.sum());//150
alert(arr2.sum());//400
alert(arr1.sum == arr2.sum);//true
我们可以通过混合法,让用户自定义构造函数,封装一个可以创建对象的函数,并且调用的是同一个方法。
function Person(name, sex){//this = new Object();
this.name = name;
this.sex = sex;
//return this;
}
// 函数,必须,添加在这个函数的 prototype 原型
Person.prototype.showName = function(){alert("我叫" + this.name);
}
Person.prototype.showSex = function(){alert("我是" + this.sex + "的");
}
var p1 = new Person("钢铁侠", '男');
var p2 = new Person("猩红女巫", "女");
p1.showName();// 我叫钢铁侠
p1.showSex();// 我是男的
p2.showName();// 我叫猩红女巫
p2.showSex();// 我是女的
alert(p1.showName == p2.showName); //true
面向对象编程案例
现在我们要测试 100 辆不同品牌的汽车,记录他们在道路上行驶的性能指数。
创建一个可以构造各式各样车的构造函数
function Car(type, name, speed){
this.type = type;
this.name = name;
this.speed = speed;
}
在 Car 的原型上添加功能:让车跑在路上,计算时速。
Car.prototype.run = function(road){alert(` 一辆 ${this.type}品牌的 ${this.name}系列,时速为 ${this.speed}km/ h 的车,跑在长度为 ${road.length}km 的 ${road.name},最终的成绩是 ${road.length / this.speed}小时 `);
}
创建一个可以构造各式各样马路的构造函数
function Road(name, length){
this.name = name;
this.length = length;
}
添加第一个测试用例 car1:
var kuahaidaqiao = new Road("跨海大桥", 1000);
var car1 = new Car("大众", "PASSAT", 100);
car1.run(kuahaidaqiao);// 一辆大众品牌的 PASSAT 系列,时速为 100km/ h 的车,跑在长度为 1000km 的跨海大桥,最终的成绩是 10 小时
这就是面向对象编程,只写一遍代码,之后再要创建对象,只需要调用封装好的函数。类和对象是面向对象编程的两个语法,是面向对象实现的基础,但是在 JS 中没有类的概念,一切皆对象,所有的实例都是由 Object 构造函数构造出来的。所以当我们有类的需求的时候,我们自创了一个构造函数,来替代类的存在,所以构造函数的本质就是类。
Prototype 模式的验证方法
为了配合 prototype
属性,Javascript 定义了一些关键字,帮助我们使用它。
-
instanceof
格式:对象 instanceof 构造函数
功能:判断这个对象是否是后面这个构造函数构造的。如果是,返回 true;否则,返回 false。
alert(car1 instanceof Car);//true alert(car1 instanceof Road);//false alert(car1 instanceof Object);//true
-
isPrototypeOf()
格式:构造函数.prototype.isPrototypeOf(对象)
功能:判断某个
proptotype
对象是否拥有某个实例,如果是,返回 true;否则,返回 false。alert(Car.prototype.isPrototypeOf(car1)); //true alert(Road.prototype.isPrototypeOf(kuahaidaqiao)); //true
-
hasOwnProperty()
格式:对象.hasOwnProperty(“ 属性 ”)
功能:实例对象一旦创建,将自动引用 prototype 对象的属性和方法。也就是说,实例对象的属性和方法,分成两种,一种是本地的,另一种是引用的。每个实例对象都有一个
hasOwnProperty()
方法,用来判断某一个属性到底是本地属性,还是继承自prototype
对象的属性。Car.prototype.color = "white"; alert(car1.hasOwnProperty("name")) //true alert(car1.hasOwnProperty("color")) //false
-
in 运算符
格式:” 属性 ”in 对象
功能:
in
运算符可以用来判断,某个实例是否含有某个属性,不管是不是本地属性。alert("type" in car1); // true alert("length" in car1); // false
in 运算符还可以用来遍历某个对象的所有属性,包括添加在它身上的方法。
for(var prop in car1) {alert("car1["+prop+"]="+car1[prop]); }
ECMA6 语法糖 class 类
ES6 提供了简单的定义类的语法糖 class
构造函数的写法:
function Iphone(size, color){
this.size = size;
this.color = color;
}
Iphone.prototype.show = function(){alert(` 您选择了一部 ${this.color}颜色的, 内存大小是 ${this.size}GB 的手机 `);
}
var iphone1 = new Iphone(64, "玫瑰金");
iphone1.show();// 您选择了一部玫瑰金颜色的, 内存大小是 64GB 的手机
类的写法:
class Iphone{constructor(size, color){
this.size = size;
this.color = color;
}
show(){alert(` 您选择了一部 ${this.color}颜色的, 内存大小是 ${this.size}GB 的手机 `);
}
}
var iphone2 = new Iphone(256, "黑色");
iphone2.show();// 您选择了一部黑色颜色的, 内存大小是 256GB 的手机
继承
由于所有的实例对象共享同一个 prototype 对象,那么从外界看起来,prototype 对象就好像是实例对象的原型,而实例对象则好像 ” 继承 ” 了 prototype 对象一样。这就是 Javascript 继承机制的设计思想。
继承一方面是为了实现面向对象,另一方面为了帮助大家更高效的编写代码,可以让一个构造函数继承另一个构造函数中的属性和方法。
首先我们定义一个 People 类
function Person(name, sex){
this.name = name;
this.sex = sex;
}
Person.prototype.showName = function(){alert("我叫" + this.name);
}
Person.prototype.showSex = function(){alert("我是" + this.sex);
}
现在我们要在 Person 类的基础上创建一个 Worker 类,拥有 Person 类的全部属性和方法,同时添加它自己的属性 job,我们可以通过以下几种方法实现继承。
function Person(name, sex){
this.name = name;
this.sex = sex;
}
Person.prototype.showName = function(){alert("我叫" + this.name);
}
Person.prototype.showSex = function(){alert("我是" + this.sex);
}
call/apply
第一种方法也是最简单的方法,使用 call 或 apply 方法,将父对象的构造函数绑定在子对象上,这种继承 Person 的方式叫做构造函数的伪装,因为 Person 对象原本应该只为 new 的 Person 对象服务,但是它现在还可以被 Worker 对象使用。
function Worker(name, sex, job){
// 继承 Person 的属性
Person.call(this, name, sex);
this.job = job;// 添加自己的属性
}
var w1 = new Worker("小明", "男", "程序员");
call 和 apply 的区别在于参数形式不同,call(obj, pra, pra)后面是单个参数。apply(obj, [args])后面是数组,作用都是强制改变 this 的指向。
function Worker(name, sex, job){
// 继承 Person 的属性
// 构造函数的伪装
Person.apply(this, arguments);
this.job = job;
}
现在 Worker 想继承 Person 上的方法,Person 方法都放在 prototype 中,prototype 本质是对象,存储的是引用数据类型,所以我们不能直接将父级的方法赋值给子,继承只能是单向的,子继承父,但是不能影响父。
每一个构造函数身上都会有一个 prototype 原型,我们可以将 Person 身上的 prototype 遍历,在遍历的过程中将 Person 身上的函数取出,放入 Worker 的 prototype 中,这样它们就不会互相影响了。
for(var i in Person.prototype){Worker.prototype[i] = Person.prototype[i];
}
同时通过 prototype 来拓展自己的方法
Worker.prototype.showJob = function(){alert("我是干" + this.job + "工作的");
}
直接调用父级的构造函数继承
第二种方法更常见,使用 prototype 属性。如果 ”Worker” 的 prototype 对象,指向一个 Person 的实例,那么所有 ”Worker” 的实例,就能继承 Person 了。
Worker.prototype = new Person();
每一个 prototype 对象都有一个 constructor 属性,指向它的构造函数。如果没有 ”Worker.prototype = new Person();” 这一行,Worker.prototype.constructor 是指向 Worker 的;加了这一行以后,Worker.prototype.constructor 指向 Person。
alert(Worker.prototype.constructor == Person) //true
更重要的是,每一个实例也有一个 constructor 属性,默认调用 prototype 对象的 constructor 属性。因此,在运行 ”Worker.prototype = new Person();” 这一行之后,w1.constructor 也指向 Person。
var w1 = new Worker("小明", "男", "程序员");
alert(w1.constructor == Worker.prototype.constructor) //true
alert(w1.constructor == Person.prototype.constructor) //true
alert(w1.constructor == Person) //true
这显然会导致继承链的紊乱(w1 明明是用构造函数 Worker 生成的),因此我们必须手动纠正,将 Worker.prototype 对象的 constructor 值改为 Worker。这是很重要的一点,编程时务必要遵守。
Worker.prototype.constructor = Worker
alert(w1.constructor == Worker.prototype.constructor) //true
alert(w1.constructor == Person.prototype.constructor) //false
alert(w1.constructor == Person) //false
Object.create()拷贝继承
上面是采用 prototype 对象,实现继承。我们也可以换一种思路,纯粹采用 ” 拷贝 ” 方法实现继承。简单说,如果把父对象的所有属性和方法,拷贝进子对象。Object.create()类似于数组中的 concat 方法,可以创建一个新对象。这样就可以将父对象的 prototype 对象中的属性,一一拷贝给 Child 对象的 prototype 对象。
// 拷贝原有对象, 创建新对象
Worker.prototype = Object.create(Person.prototype);
多态
创建一个父亲的构造函数
function Father(name, sex, age){
this.name = name;
this.sex = sex;
this.age = age;
}
Father.prototype.sing = function(){alert(` 父亲的民歌唱得非常好 `);
}
再创建一个儿子构造函数,继承父亲的属性和方法
function Son(name, sex, age, degree){
// 构造函数的伪装
Father.call(this,name, sex, age);
// 拓展自己的属性
this.degree = degree;
}
// 继承方法
// 原型链
for(var i in Father.prototype[i]){Son.prototype[i] = Father.prototype[i];
}
通过 new 创建一个儿子对象,执行 son1.sing()调用父亲原型上的方法。
var son1 = new Son("小明", "男", 20, "本科");
son1.sing();// 父亲的民歌唱得非常好
现在我们重写父级继承的函数
Son.prototype.sing = function(){alert(` 唱摇滚 `);
}
再次调用 son1.sing(),执行的是 son1 自己添加的 sing 方法。
var son1 = new Son("小明", "男", 20, "本科");
son1.sing();/ 唱摇滚
对于父亲自身的 sing 方法没有影响,在子级重写的方法只在子级生效。
var f2 = new Father("大明", "男", 40);
f2.sing();// 父亲的民歌唱得非常好
现在我们再来看继承和多态的概念,其实它们都是继承某一部分,是同一件事情的两个侧重。继承侧重于从父级继承到属性和方法,而多态侧重于自己拓展的属性和方法,也就是重写的内容。简单来说凡是跟父级一样的部分叫继承,不一样的部分叫多态。
注意:虽然 Son 继承了 Father 的属性和方法,但是通过 Son 构造函数 new 出来的对象不属于 Father。
alert(son1 instanceof Son) //true
alert(f2 instanceof Father) //true
alert(son1 instanceof Father) //false
alert(son1 instanceof Object) //true
同样的案例使用 ECMA6 class 类的方法来实现继承和多态,对比原来的方法更简单,更形象。
class Father{constructor(name, sex, age){
this.name = name;
this.sex = sex;
this.age = age;
}
// 声明方法
sing(){alert("会唱民歌");
}
}
/*
继承 Father 创建一个子类 Son
通过 extends 继承
*/
class Son extends Father{constructor(name, sex, age, degree){super(name, sex, age);
}
sing(){alert("唱摇滚");
}
}
var son1 = new Son("小明", "男", 30, "本科");
alert(son1.name);// 小明
son1.sing();// 唱摇滚
var f1 = new Father("大明", "男", 40);
alert(f1.name);// 大明
f1.sing();// 会唱民歌
原型链
在 javascript 中,每个对象都有一个指向它的 原型 (prototype) 对象 的内部链接。每个原型对象又有自己的原型,直到某个对象的原型为 null 为止,组成这条链的最后一环。
在构造函数有一个 prototype 的属性,通过构造函数构造出来的对象有_ _proto _ _属性,指向构造该对象的构造函数的原型,它还有一个名字叫魔术变量。
也就是说通过 Son 构造函数构造出来的对象 son1 的_ _proto _ _完全等于 Son 的原型 prototype。
alert(son1.__proto__ === Son.prototype);//true
alert(f2.__proto__ === Father.prototype);//true
这也就解释了为什么通过同一个构造函数构造出来的对象,使用的都是同一套函数,因为通过该构造函数构造出来所有对象的_ _proto _ _就指向该构造函数的原型。