JavaScript面向对象(ES5篇)
- 一、JavaScript面向对象
- 1. JavaScript创建对象的几种方式
- 1. 1、构造函数模式
- 1. 2、工厂模式
- 1. 3、原型模式
- 1. 4、组合使用构造函数模式和原型模式
- 1. 5、动态原型模式
- 1. 6、寄生构造函数模式
- 1. 7、稳妥构造函数模式
- 二、封装与继承
- 2.1、封装
- 2.2、继承
- 2.1.1、原型继承
- 2.1.2、构造函数继承
- 2.1.3、组合式继承
- 2.1.4、原型式继承
- 2.1.5、寄生式继承
- 2.1.6、寄生组合式继承
- 2.1.7、Prototye.js中实现的继承
- 三、多态
- 3.1、重载
- 3.2、重写
- 3.3、多态
- 四、静态属性和方法
- 4.1、静态属性
- 4.2、静态方法
一、JavaScript面向对象
面向对象(Object Oriented Programming)是一种编程思想。但是JavaScript里面没有类的概念,而是直接使用对象来实现编程的。
1. JavaScript创建对象的几种方式
1. 1、构造函数模式
构造函数模式,即通过给构造函数传递不同的参数,然后调用构造函数来创建不同对象。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.say = function ( something) {
console.log( something );
};
}
var person1 = new Person("小嘉",29,"高级Java开发工程师");
person1.say('卑微小嘉在线重构');
使用构造函数模式来创建不同对象的好处就是它可以标识为一种特定的类型。
例如使用instanceof来检测对象
person1 instanceof Person // true
person1 instanceof Object // true
使用构造函数的缺点是,每个方法都会在每个实例对象上重新创建一遍,即使同名方法或两个方法的作用一样,而不同实例对象上的同名方法其实是不一样的,这样就造成来一种冗余。
person1.sayname != person2.sayname
1. 2、工厂模式
末场模式使用函数来封装创建对象的细节,在函数里面创建对象并返回对象。
function createPerson(name, age, job){
var person = new Object();
person.name = name;
person.age = age;
person.job = job;
person.sayName = function(){
alert(this.name);
};
return person;
}
var person1 = createPerson("小嘉",29,"高级Java开发工程师");
person1.sayName();
但是工厂模式有个缺点,就是对象识别问题,即它不知道是一个对象类型。
1. 3、原型模式
使用原型对象的好处就是,可以让所有对象实例共享它所包含的属性和方法。
构造函数在不返回值的情况下,默认会返回新对象实例。
function Person() {}
Person.propertype.name = "小嘉";
Person.propertype.age = "小嘉";
Person.propertype.job = "高级Java开发工程师";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
这样一来,所有的属性和方法是所有实例共享的,也就是说person1的sayname和person2的sayname是同一个。
在充血原型的时候,切断来现有原型与任何之前已经存在的对象实例之间的联系,它们引用的最初是原型,可以通过重新原型里面添加constuctor来建立联系。
原型模式的问题就是共享问题,在实例或原型上改变一下,在其他实例中都会相应的改变。这样我们引入组合使用构造函数模式和原型模式。
1. 4、组合使用构造函数模式和原型模式
组合使用中,构造函数模式用于定义实例自己的属性,原型模式用于定于方法和共享的属性。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
}
Person.prototype = {
constructor: Person,
sayname: function(){
alert(this.name);
}
}
var person1 = new Person("小嘉",29,"高级Java开发工程师");
person1.sayname();
1. 5、动态原型模式
对于上述都构造函数与原型都组合,经常使人感到困惑,因此动态原型把所有都信息都封装到构造函数中,而通过在构造函数中,而通过在构造函数中初始化原型,又保持了同事使用了构造函数和原型模式都优点。
就是说,我们可以通过检查某个应该存在的方法是否存在来决定是否需要初始化原型。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
if (typeof this.sayName !== "function"){
Person.prototype.sayName = function () {
alert(this.name);
}
}
}
var person1 = new Person("小嘉",29,"高级Java开发工程师");
person1.sayName();
只在sayName()不存在的情况,并且是初次调用构造函数的时候,如果里面的这段代码才会执行
需要注意的是使用动态原型模式时不能使用对象字面量重写原型。
1. 6、寄生构造函数模式
寄生构造函数模式和工厂模式很像
function Person(name, age, job){
var person = new Object();
person.name = name;
person.age = age;
person.job = job;
person.sayName = function(){
alert(this.name);
};
return person;
}
var person1 = new Person("小嘉",29,"高级Java开发工程师");
person1.sayName();
寄生构造函数模式和工厂模式的区别在于
- 寄生构造函数模式在创建对象的时候使用了new关键字,而工厂模式没有使用new关键字。
- 寄生模式的外部包装函数是一个构造函数;
作用:寄生模式可以在特殊的情况下为对象来创建构造函数,原因在于我们可以通过构造函数重写对象的值,并通过return返回 重写调用构造函数(创建的对象的实例)之后的对象实例的新的值。
范例:我们需要一个额外方法的特殊数字,但是我们又不能直接修改Array构造函数,我们可以使用寄生构造模式。
function SpecialArray() {
//创建数组
var array=new Array();
//添加值 arguments获取的是实参,不是形参,所以SpecialArray()并没有形参接收传递过来的参数
array.push.apply(array,arguments);
array.toPipedString=function(){
return this.join("|");
}
return array;
}
var colors=new SpecialArray("red","blue","black");
console.log(colors.toPipedString()); //输出:red|blue|black
1. 7、稳妥构造函数模式
稳妥对象的概念是道格拉斯·克罗克福德提出的,所谓的稳妥对象指的没有公共属性,而且其他方法也不引用this的对象。稳妥对象最适合在一些安全的环境中(这些环境会禁止new和this)或者防止数据被其他应用改动。
稳妥构造函数与寄生构造函数模式类似,区别如下。
- 稳妥构造函数模式不使用new操作符调用构造函数;
- 新创建对象的实例方法不引用this;
function Person (name, age, job) {
var person=new Object();
person.name = name;
person.age = age;
person.job = job;
person.sayName = function() {
alert(name);
}
//返回对象
return person;
}
var person = Person("张三",22);
person.sayName(); //使用稳妥构造函数模式只能通过其构造函数内部的方法来获取里面的属性值
console.log(person.name );// undefined
二、封装与继承
2.1、封装
封装能够将一个实体的信息、功能、响应都封装到一个单独对象中都特性。
从上述的稳妥对象,我们就只能使用方法来获取属性,这也是一种封装的形式。
由于JavaScript并没有public、private,protecteed这些关键字,但是可以利用变量的作用域来模拟public和priviate封装特性。
function Person (name, age, job) {
var _name = name;
var _age = age;
var _job = job;
return {
getName: function () {
return _name;
},
getAge: function () {
return _age;
},
getJob: function () {
return _job;
}
}
}
var person = Person("小嘉", 29, "高级Java开发工程师");
console.log(person.name); //undefine;
console.log(person.getName());
上述只是简单是实现方式。
2.2、继承
在不改变源程序的基础上进行扩充,原功能得以保存,并且对字程序进行扩展,避免重复代码重写。
2.1.1、原型继承
原型继承就是利用原型链来实现继承。需要注意的地方:实现原型继承的时候不要使用对象字面量创建原型方法,因为这样做,会重写原型链。
// 父类定义
function SuperClass() {
this.supername = 'super';
}
SuperType.prototype.getSuperName= function(){
return this.supername;
}
// 子类定义
function SubClass () {
this.subname='subname';
}
// 原型继承
SubClass.prototype = new SuperClass();
SubClass.prototype.getSubName = function (){
return this.subname;
}
var subclass = new SubClass();
console.log(subclass.getSubName());
console.log(subclass.getSuperName());
优点:原型定义的属性和方法可以复用。
缺点:
- 引用类型的原型属性会被所有实例共享;
- 创建子对象时,不能向父对象的构造函数中传递参数;
2.1.2、构造函数继承
使用父类构造函数的call方法来实现构造函数继承。
function SuperClass () {
this.colors = ['red', 'green'];
}
function SubClass () {
// 继承SuperType
SuperType.call(this);
}
var subclass1 = new SubClass();
subclass1.colors.push('blue');
console.log(subclass1.colors);
// red, green, blue
var subclass2 = new SubClass();
console.log(subclass2.colors);
// red, green
需要注意的是,在调用父对象的构造函数之后,再给子类型定义属性,否则会被重写。
缺点:方法都需要在构造函数中定义,难以做到函数的复用,而且父对象在原型上定义的方法,对于子类型是不可见的。
2.1.3、组合式继承
组合式继承,顾名思义就是组合两种模式实现JavaScript的继承,借助原型链和构造函数来实现,这样子在原型上定义方法实现了函数的复用,而且能够保证每个实例都有自己的属性。
function SuperClass (name) {
this.name = name;
this.con = [];
}
SuperClass.prototype.getName = function() {
return this.name;
}
function SubClass (name, age) {
SuperClass.call(this, name);
this.age = age;
}
SubClass.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass;
SubClass.prototype.getAge = function() {
return this.age;
};
var subclass1 = new SubClass('li', 18);
subclass1.con.push('test1');
console.log(subclass1.con); // test1
console.log(subclass1.getAge()); // 18
console.log(subclass1.getName()); // li
var subclass2 = new SubClass('hang', 18);
console.log(subclass2.con); // test1
console.log(subclass2.getAge()); // 18
console.log(subclass2.getName()); // hang
- 优点:弥补了原型继承和构造函数的缺点;
- 缺点:父类构造函数调用了两次;
2.1.4、原型式继承
原型式继承没有严格意义上的构造函数,借助原型可以基于已有的对象创建新的对象。
function createObject(obj) {
function newOrient () {};
newOrient.prototype = obj;
return new newOrient();
}
对上述函数来说,对传入的对象进行了一次前拷贝。在ES5中新增了一个方法,Object.create,它的作用于上面的函数是一样的,但是只支持IE9及以上。
var Person = {
name: '小嘉',
age: 29
}
var Student = Object.create(Person);
Student.name= '卑微小嘉';
Student.job = '高级Java开发工程师';
console.log(Student.job);
其中Object.create还支持传入第二个参数,参数与Object.defineProperties()方法的格式相同,并且会覆盖原型上的同名属性。
2.1.5、寄生式继承
寄生式继承与原型继承很类型,区别在于,寄生式继承创建的一个函数把所有的事情做完了,例如给新的对象增加属性和方法。
function createWorker(obj) {
var clone = Object.create(o);
clone.job = '高级Java开发工程师';
return clone;
}
var Person = {
name: '小嘉',
age: 29
}
var worker = createWorker(person);
console.log(worker.job);
2.1.6、寄生组合式继承
组合继承会调用两次对象父对象的构造函数,并且父类型的属性存在两组,一组在实例上,一组在subClass的原型上。解决这个问题的方法就是寄生式组合继承。
function inheritPrototype(SubClass, SuperClass){
// 继承父类的原型
var prototype = Object.create(SuperClass.prototype);
// 重写被污染的construct
prototype.constructor = SubClass;
// 重写子类的原型
SubClass.prototype = prototype;
}
function SuperClass(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperClass.prototype.sayName = function(){
alert(this.name);
};
function SubClass(name, age) {
SuperClass.call(this, name);
this.age = age;
}
inheritPrototype(SubClass, SuperClass);
SubClass.prototype.sayAge = function(){
alert(this.age);
};
var subclass = new SubClass('hello', 18);
console.log(subclass.__proto__.constructor == SubClass)
可以看到
- 子类可以继承了父类的属性和方法,同时属性没有创建在原型链上,因此多个子类不会共享一个属性;
- 子类可以动态传递参数给父类;
- 父类构造函数只执行了一次;
子类而过在原型上添加方法,必须要在继承之后添加,否则会覆盖原型上的方法,但是如果这两个类是已经存在的类就不行了。
解决方案:
function inheritPrototype(SubClass, SuperClass){
// 继承父类的原型
var prototype = Object.create(SuperClass.prototype);
// 重写被污染的construct
prototype.constructor = SubClass;
// 重写子类的原型
SubClass.prototype = Object.assign(prototype, SubClass.prototype);
}
虽然通过Object.assign来进行copy解决了覆盖原型类型的方法的问题,但是Object.assign只能够拷贝可枚举的方法,如果子类本身就继承了一个类,这个办法也不行。
2.1.7、Prototye.js中实现的继承
Prototye.js通过对Object对象扩展两个静态方法Object.extend(destination, source)实现来JavaScript中继承。从语义角度来看,它事实上仅仅实现来源对象到目标对象的全息拷贝。该方法的本质与JQuery的$.extend(),underscore库_.extend()功能类似。
不过你也可以这样认为:由于目标对象拥有了所有源对象所拥有的特性, 所以看上去就像目标对象继承了源对象(并加以扩展)一样。
Object.extend = function(destination, source) {
for (var property in source) {
destination[property] = source[property]; //利用动态语言的特性,通过赋值动态添加属性和与方法。
}
return destination;
}
三、多态
3.1、重载
重载是多态的一种体现,重载就是同名方法的多个实现,依靠参数的类型和个数来区分和识别,但是在js中,函数的参数是没有类型的,并且参数的个数是任意的。
我们可以使用arguments对象来实现可变参数。
function add(){
var sum = 0;
for(var i=0; i<arguments.length; i++) {
if(typeof arguments[i] === 'number') {
sum += arguments[i];
}
}
return sum;
}
范例重载
function overridable() {
return {
functions: [],
add: function(fn) {
if (typeof fn !== 'function') { return false; }
const functionLength = fn.length;
this.functions[functionLength] = fn;
return {
functions: this.functions,
add: this.add,
result: this.result,
}
},
result: function() {
const functions = this.functions;
return function() {
const functionLength = arguments.length;
if (functions[functionLength]) {
return functions[functionLength].call(this, ...arguments);
} else {
throw new Error('There is no function match ' + functionLength + ' arguments.');
}
};
},
};
};
const foo = overridable()
.add(function(a, b, c) {
return a + b + c;
})
.add(function(a) {
return Math.pow(a, 2);
})
.result();
console.log(foo(1, 2, 3)); // 6
console.log(foo(2)); // 4
上述的代码位严格真正通过测试,请不要在真实生产环境使用。
3.2、重写
在判断引用类型时,我们通常不希望直接用 toString 方法,应该用 Object.prototype.toString.call 方法判断。
正是因为有些类(比如 Array)会重写 toString 方法以方便编程人员获取希望的字符串,而不是枯燥的 [object Object]。
JavaScript的重写机制十分方便,在单页面应用中,我们通常使用window.history.pushState 管理前端路由。不幸的是,这个方法并不会触发任何的事件。但是我们仍然可以使用重写的方式实现路由变换时的监听:
window.history.pushState = function(state, title) {
var pushStateEvent = new Event('pushstate');
window.dispatchEvent(pushStateEvent);
}
window.addEventLisnter('pushstate', function(){
console.log('push state!!');
});
history.pushState('/test', 'test');
3.3、多态
JavaScript本身是不支持多态的,但是我们可以实现多态。多态的思想实际上就是把“想做什么”和“谁去做”分开,多态的最根本的作用就是通过把过程话的条件分支语句转化为对象的多态性,从而消除这些条件分支语句。
范例:使用条件分支
var googleMap = {
show: function () {
console.log('开始渲染谷歌地图');
}
};
var baiduMap = {
show: function () {
console.log('开始渲染百度地图');
}
};
var renderMap = function (type) {
if (type === 'google') {
googleMap.show();
} else if (type === 'baidu') {
baiduMap.show();
}
};
renderMap('google'); // 输出:开始渲染谷歌地图
renderMap( 'baidu' ); // 输出:开始渲染百度地图
虽然renderMap函数保持了一定的弹性,但是这种弹性是很脆弱的,一旦需要替换成其他接口,那么无疑需要改动renderMap。
我们使用了继承就可以保持对象多态性了。
var renderMap = function( map ){
if ( map.show instanceof Function ){
map.show(); }
};
renderMap( googleMap ); // 输出:开始渲染谷歌地图
renderMap( bdMap ); // 输出:开始渲染百度地图
四、静态属性和方法
直接定义在构造函数上的方法和属性是静态属性,定义在构造函数的原型和实例上的方法和属性是非静态的。
4.1、静态属性
function ClassA(){ //定义构造函数
this.name=1;
};
ClassA.funcName='classA'
var instance = new ClassA();
instance.funcName; // undefined
instance.name; // 1
ClassA.funcName; //'classA'
4.2、静态方法
function ClassA(){ //定义构造函数
};
ClassA.func = function(){ //在构造函数上添加一个属性(因为函数也是对象)
console.log("This is a static method");
}
var instance = new ClassA(); //新建一个实例
ClassA.func(); //This is a static method
instance.func(); //Error:instance.func is not a function