面向对象是一种编程思想,咱们经过类(构造函数)和对象实现的面向对象编程,知足下述三个特定:封装、继承和多态。
封装即把实现一个功能的代码封装到一个函数中,之后实现这个功能,只须要执行该函数便可。实现低耦合,高内聚。javascript
如今咱们把属性和方法封装成一个对象:java
//建立一个对象 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返回对象就至关于第三步出厂。这种符合上述一、二、3步骤的函数叫作工厂函数,这种设计函数的思路,叫作工厂设计模式。数组
官方函数建立对象的方法是经过new的方法,当咱们不使用new建立对象的时候,函数内部的this会指向窗口。app
function show(){ alert(this);//[object Window] } show();
因此当咱们在函数内部给this.name赋值为xxxx时,能够经过window.name输出xxxx,由于若是这个函数没有主人的话它的主人就是window对象。iphone
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运算符去调用函数的时候,它首部和尾部会自动的生成如下两步:一、原料操做:强制改变this指向this = new Object();
三、出厂操做:将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对象的引入:全部实例对象须要共享的属性和方法,都放在这个对象中,那些不须要共享的属性和方法,就放在构造函数中。以此来模拟类。
因此想让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
属性,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]); }
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方法,将父对象的构造函数绑定在子对象上,这种继承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
上面是采用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 _ _就指向该构造函数的原型。