这里是修真院前端小课堂,每篇分享文从
【背景介绍】【知识剖析】【常见问题】【解决方案】【编码实战】【扩展思考】【更多讨论】【参考文献】
八个方面深度解析前端知识/技能,本篇分享的是:
【 JS中的面向对象编程】
1.背景介绍
什么是面向对象编程?
“面向对象编程”(Object OrientedProgramming,缩写为OOP)是目前主流的编程范式。它的核心思想是将真实世界中各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
主要概念为:把一组数据结构和处理它们的方法组成对象(object),把相同行为的对象归纳为类(class),通过类的封装(encapsulation)隐藏内部细节,通过继承(inheritance)实现类的特化(specialization)/泛化(generalization),通过多态(polymorphism)实现基于对象类型的动态分派(dynamicdispatch)。
Javascript是一种基于对象(object-based)的语言,遇到的东西几乎都是对象,但是它不是一种面对对象的语言。像其他语言里面的class(类),它就没办法直接用了。(听说ES 6可以用了,笔者一直学的ES5,6暂未研究,有兴趣的同学可以去看看教程)
2.知识剖析
2.1对象的概念
因为JS是一个基于对象的语言,所以我们遇到的大多数东西几乎都是对象。例如函数就是一个对象,如果你要在js里面新建一个对象,这样写其实就是创建了一个object的实例。对象就是一个容器,封装了属性和方法。属性就是对象的状态,比如下面的name属性。方法就是写在对象里面的函数,也就是对象的行为,比如下面的sayName方法。
var person = new object();
person.name = "Tom";
person.sayNmae = function() {
alert(this.name);
}
2.2 工厂模式
“面向对象编程”的第一步,就是要生成“对象”。但是很多时候我们不得不面临重复生成很多对象的情况,如果我有一千个人要记录他们的信息,像上面这种方法写的话,大大增加了代码的重复量,为了解决这个问题,人们开始使用工厂模式的一种变体,写法如下页。虽然工厂模式解决了代码复用的问题,但是却没办法显示实例(person1)和对象o之间的关系,比如aler(person1 instanceof o);
代码演示:
function Person(name,age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
alert(this.name)
};
}
person1 = new Person("Tom",20,"Engineer");
person2 = new Person("Damon",22,"Waiter");
2.2 构造函数
后来就出现了构造函数,用来创建特定类型的对象,可以将实例和对象联系起来,用到了JS中的“this”,写法如下:
这样对象和实例之间就有关系了,以new这种方式调用构造函数会经历4个步骤:
(1)创建一个新对象。
(2)将构造函数的作用域赋给新对象(这个this就指向了这个新对象)。
(3)执行函数内代码(给对象添加属性)
(4)返回新对象。
代码演示:
function Person(name,age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
alert(this.name)
};
}
person1 = new Person("Tom",20,"Engineer");
person2 = new Person("Damon",22,"Waiter");
构造函数特点:
上面代码中,Persoon就是构造函数,它提供模板,用来生成对象实例。为了与普通函数区别,构造函数名字的第一个字母通常大写。
构造函数的两个特点:
1.函数体内部使用了this关键字,代表了所要生成的对象实例。
2.生成对象的时候,必需用new命令,调用函数。
如果忘了使用new命令,直接调用构造函数会导致构造函数变成普通函数,就不会生成实例对象,并且此时的this这时代表全局对象,将造成一些意想不到的结果。
var Vehicle = function (){
this.price = 1000;
};
var v = Vehicle();
v.price
// Uncaught TypeError: Cannot read property 'price' of undefined
上面代码中,调用Vehicle构造函数时,忘了加上new命令。结果,price属性变成了全局变量,而变量v变成了undefined。
因此必须小心,记得使用new命令。
2.3原型和原型链
原型prototype
JavaScript的每个对象都继承另一个对象,后者称为“原型” (prototype)对象。只有null除外,它没有自己的原型对象。
原型对象上的所有属性和方法,都能被派生对象共享。这就是JavaScript继承机制的基本设计。
通过构造函数生成实例对象时,会自动为实例对象分配原型对象。每一个构造函数都有一个prototype属性,这个属性就是实例对象的原型对象。
原型链
对象的属性和方法,有可能是定义在自身,也有可能是定义在它的原型对象。由于原型本身也是对象,又有自己的原型,所以形成了一条原型链(prototype
chain)。比如,a对象是b对象的原型,b对象是c对象的原型,以此类推。
“原型链”的作用是,读取对象的某个属性时,JavaScript引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回undefined。
需要注意的是,一级级向上,在原型链寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。
利用原型(prototype)的继承特性,我们可以将我们的函数写成
function Person() {
};
Person.prototype.name = "Tom";
Person.prototype.age = "20";
Person.prototype.job = "engineer";
Person.prototype.sayName = function() {
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.sayName == person2.sayName); //true
因为原型的继承,person1和person2的prototype都指向Person的prototype,所以这两个函数其实是相等的。但是用工厂函数或者构造模式, alert(person1.sayName == person2.sayName);就绝对不会为真了。
奇淫巧技1:每次写属性都要加一个prototype是不是很麻烦,其实还有另外一种写法
function Person() {
}
Person.prototype = {
name : "Tom";
age : "20";
job : "engineer";
sayName : function() {
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
alert(person1.sayName == person2.sayName); //true
2.4 构造函数的继承
让一个构造函数继承另一个构造函数,是非常常见的需求。
也有多种方法实现,各有优缺点。比如现在有一个动物对象的构造函数,和一个猫对象的构造函数。
function Animal() {
this.species = “动物”;
};
function Cat(name,color) {
this.name = name;
this.color = color;
}
如何才能使Cat继承Animal呢?
2.4.1 构造函数绑定
第一种方法也是最简单的方法,使用call或apply方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:
function Cat(name,color){
Animal.apply(this, arguments); //加的
this.name = name;
this.color = color;
}
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物
2.4.2 prototype(原型)模式
第二种方法更常见,使用prototype属性。
如果"猫"的prototype对象,指向一个Animal的实例,那么所有"猫"的实例,就能继承Animal了。
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物
代码的第一行,我们将Cat的prototype对象指向一个Animal的实例。相当于将Cat原先的原型对象删除,重新赋一个Animal实例的值。但是任何一个prototype对象都有一个constructor属性,指向它的构造函数。这个时候Cat的构造函数也改变了,变成了Animal。
2.4.2 prototype(原型)模式
所以我们需要“Cat.prototype.constructor = Cat”将Cat的构造函数重新指向为Cat,不然的话会很容易出问题。
这是很重要的一点,编程时务必要遵守。如果替换了prototype对象,
b.prototype = new a();
那么,下一步必然是为新的prototype对象加上constructor属性,并将这个属性指回原来的构造函数。b.prototype.constructor = b;
2.4.3 直接继承prototype(原型)
第三种方法是对第二种方法的改进。由于Animal对象中,不变的属性都可以直接写入Animal.prototype。所以,我们也可以让Cat()跳过 Animal(),直接继承Animal.prototype。现在我们将Animal对象改写
function Animal() {
Animal.prototype.species = "动物";
}
然后,将Cat的prototype对象,指向Animal的prototype对象,这样就完成了继承。
Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物
2.4.3 直接继承prototype(原型)
与前一种方法相比,这样做的优点是效率比较高(不用执行和建立Animal的实例了),比较省内存。缺点是 Cat.prototype和Animal.prototype现在指向了同一个对象,那么任何对Cat.prototype的修改,都会反映到Animal.prototype。所以Animal.prototype的构造函数也变成了Cat。
这个时候我们就需要引入一个空对象作为中转的中介,无论Cat的constructor如何变,只会影响到中转对象F而无法影响到父对象Animal了。
var F = function(){};
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;
2.4.3 直接继承prototype(原型)
然后我们将上述方法封装成为一个函数,使用起来就很方便了
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
2.4.3 直接继承prototype(原型)
使用的时候方法如下:
extend(Cat,Animal);
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物
奇淫巧技2:封装函数的时候怎么方便怎么写不必太过考虑语义化的东西,比如写个状态机,直接将状态用数字表示,这样比字符串的形式好判断多了。但是一点也不语义化。
3.常见问题
必须要声明new来创建实例对象吗?
4.解决方案
1.必须要声明new来创建实例对象吗?
为了保证构造函数必须与new命令一起使用,一个解决办法是,在构造函数内部使用严格模式,即第一行加上use strict。
function Fubar(foo, bar){
'use strict';
this._foo = foo;
this._bar = bar;
}
Fubar();
// TypeError: Cannot set property '_foo' of undefined
上面代码的Fubar为构造函数,use
strict命令保证了该函数在严格模式下运行。由于在严格模式中,函数内部的this不能指向全局对象,默认等于undefined,导致不加new调用会报错(JavaScript不允许对undefined添加属性)。
另一个解决办法,是在构造函数内部判断是否使用new命令,如果发现没有使用,则直接返回一个实例对象。
function Fubar(foo, bar){
if (!(this instanceof Fubar)) {
return new Fubar(foo, bar);
}
this._foo = foo;
this._bar = bar;
}
Fubar(1, 2)._foo // 1
(new Fubar(1, 2))._foo // 1
上面代码中的构造函数,不管加不加new命令,都会得到同样的结果。
5.编码实战
用面对对象编程的思想写状态机
6.扩展思考
面向对象与面向过程的区别?
传统的过程式编程(procedural programming)由一系列函数或一系列指令组成;而面向对象编程的程序由一系列对象组成。
每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。因此,面向对象编程具有灵活性、代码的可重用性、模块性等特点,容易维护和开发,非常适合多人合作的大型应用型软件项目。
7.参考文献
参考一:http://javascript.ruanyifeng....l">阮一峰
参考二:
href="http://www.ruanyifeng.com/blog/search.html?cx=016304377626642577906%3Ab_e9skaywzq&cof=FORID%3A11&ie=UTF-8&q=Javascript+%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A8%8B&sa.x=9&sa.y=8">阮一峰
参考三:《Javascript高级程序设计》chapter 6
8.更多讨论
new命令的原理?
构造函数中的return语句的作用?
面向对象编程的继承原理?
鸣谢
感谢大家观看
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。