再谈 js 的面向对象

1,942 阅读6分钟
原文链接: 1657413883.github.io
为什么需要面向对象编程?

我们以前可能还是熟悉面向过程来进行解决需求,也就是说编写一个个的函数来解决我们项目需求.这种方式不利于团队协作,以及后期维护.

什么是面向对象编程?

面向对象编程:就是一种思维,你把你需要解决的需求抽象为一个对象或者类,再根据这个对象分析它具备什么特性(属性)与动作(方法).

我们熟悉的面向对象编程三大特征,封装,多态,继承.JS虽然不是不像Java是真正意义上的面向对象,但是也是可以利用自身独特的特性,实现上述特征.

##### 面向对象编程-封装

  • 首先学习如何用JS来创建一个类

默认类名我们首字母大写,以示区分.见demo:

//创建一个学生类

var Student = function(name,age,sex){
//在函数内部通过this指向该对象,来给类添加属性与方法.
this.name = name;
this.age = age;
this.sex = sex;

this.display = function(){
    console.log("name is "+ this.name + ",age is " + this.age + ",sex is " + this.sex);        
}    
}

// 也可以通过类的原型来增加属性与方法,这里有两种方式.一个是给原型对象属性赋值,一个是把对象赋值给原型对象.

Student.prototype = {
    job : "study" ,
    display : function(){
    console.log('我是一个学生');
}
}


//上面这段代码等同于
 //Student.prototype.job = "study";
  //Student.prototype.display = function(){
//console.log('我是一个学生');
  // }


    var xiaoM = new Student("xiaoMing" , 20 , "boy");

xiaoM.display(); //我是一个学生
xiaoM.show(); //name is xiaoMing,age is 20,sex is boy

var xiaoH = new Student("xiaoHong" , 25 , "girl");

xiaoH.display(); //我是一个学生
xiaoH.show();  //name is xiaoHong,age is 25,sex is girl
  • 理解通过this给类添加属性与方法与通过prototype添加属性与方法的区别

通过上面那个demo我们应该也明白了,通过this添加的是对象本身拥有的,也就是你每次通过new 一个实例时候生成的(你传入参数),而prototype是你继承过来的.你每个new创建的实例都有的(与你的传参无关)。

  • 这里顺带说下constructor属性

当你创建一个对象或者函数时候,就会创建一个原型对象prototype.在prototype对象里面又会创建constructor属性,可以理解constructor属性就是构造器函数.

var arr1 = new Array(1,2,3);

console.log(Array.prototype.constructor) //function Array() { [native code] }
  • 封装: 就是利用JS的函数级作用域,把对象(类)的一些属性或者方法隐藏在其内部,函数外面不可以访问,JS不像Java有private等关键字,但是也可以实现类似的效果.
//大多数时候我们是借助闭包来实现数据的隐藏
// 闭包: 闭包首先是一个函数,是一个可以访问另外一个函数里面变量的函数,    

var user = (function(){
var name =  'xyz' ;
var password = '123456';
return {
    getName : function(){
        return name;
    },
    getPsd : function(){
        return password;
    },
    setName : function(newName){
        name = newName;
    },
    setPsd : function(newPsd){
        password = newPsd ;    
    }
    } 
}
)();

//获取原始姓名
console.log(user.getName()); // 'xyz'
//获取原始密码
console.log(user.getPsd()); // '123456'

//修改新的名字与密码并打印

user.setName('kobe');
user.setPsd('00000');

console.log("name is " + user.getName() + ",password is " + user.getPsd() ); //name is kobe,password is 00000
面向对象编程-继承

JS由于自身独特的自由性,可以有很多方法来实现继承.

  • 类式继承:把对象的实例赋值给原型
var Person = function(){
    this.info = 'person';
    this.select = [1,2,3];
};
Person.prototype.getInfo  = function(){
    return 'person';
};
var Student = function(){
    this.name = 'student';
};
Student.prototype = new Person();
Student.prototype.getName = function(){
    return 'student';
};
var S1 = new Student();
var name = S1.getName();
var info = S1.getInfo();
console.log(name);//student
console.log(info); //person
console.log(S1.info + " " + S1.name); //person student
console.log(Student.prototype instanceof Person); //true

console.log(S1.select); //[1, 2, 3]
S1.select.push(4);
var S2 = new Student(); 
console.log(S1.select); //[1, 2, 3, 4]
console.log(S2.select); //[1, 2, 3, 4]

类式继承缺点:

  1. 类式继承是通过prototype来实现,如果父类的共有属性是引用类型,那么一个子类的实例改变了它,那么会影响到其他子类.

  2. 子类不是父类的实例,子类的原型才是父类的实例.


  • 构造函数继承: 在子类的环境作用域中执行一次父类的构造函数
//父类
var Person = function(sex){
    this.info = 'person';
    this.select = [1,2,3];
    this.sex = sex;
};
//在父类原型添加方法
Person.prototype.show = function(){
    console.log('i am person');
};

//子类
function P1(sex){
    Person.call(this,sex);
}

//创建实例

var p1 = new P1('boy');
var p2 = new P1('girl');
console.log(p1.info + "," + p1.sex); //person,boy
console.log(p2.info + "," + p2.sex); // person,girl
p1.show(); // p1.show is not a function
p1.select.push(4);
console.log(p2.select); // [1, 2, 3]
console.log(p1.select); // [1, 2, 3, 4] 
console.log(p2.select); // [1, 2, 3]

知识点:

  1. 构造函数继承不涉及prototype,所以父类的原型方法不会共享,每个实例都是独立的.

  • 组合继承: 融合类式继承与构造函数继承的优点,去除两者缺点.
//父类构造函数

function Person(name) {
    this.name = name;
this.lists = [1,2,3];

};

//父类原型方法
Person.prototype.show = function(){
    console.log('i am a person');
};

// 子类构造函数
function Boy (age){
Person.call(this,name);
this.age = age;
};


Boy.prototype = new Person();
// 子类原型方法
Boy.prototype.showInfo = function(){
    console.log('i am a boy');
};
// 实例创建
var b1 = new Boy(30);

var b2 = new Boy(20);
b1.show(); // i am a person
b2.show(); // i am a person
console.log(b1.lists); //[1, 2, 3]
b1.lists.push(4);
console.log(b1.lists); //[1, 2, 3, 4]
console.log(b2.lists); //[1, 2, 3]

优点:

  1. 可以继承父类的原型方法
  2. 实例修改父类中的引用类型属性不会互相干扰

  • 原型式继承: 通过prototype,根据一个已存在的对象来创建一个新的对象.
// 继承函数
function inheritObject(obj) {

// 替代品
function fx(){}
fx.prototype = obj;
return new fx();
};
//已存在对象
var book = {
    page : 30,
    name : ['json','js','java']
};
var b1 = inheritObject(book);
b1.page = 40;
b1.name.push('Python');
var b2 = inheritObject(book);
console.log( b1.name ); // ["json", "js", "java", "Python"]
console.log(b2.page); // 30
console.log(b2.name); // ["json", "js", "java", "Python"]

可见原型式继承对引用类型属性还是公用的.


  • 寄生组合式继承

组合继承存在一个问题就是,子类不是父类的实例,子类的原型才是父类的实例.寄生组合就是来解决这个问题.

// 继承函数
function inheritObject(obj) {

// 替代品
function fx(){}
fx.prototype = obj;
return new fx();
};

// 继承原型
function inheritPrototype(subClass , superClass){

//保存父类原型
var obj = inheritObject(superClass.prototype);
// 设置原型的constructor
obj.constructor = subClass;    

subClass.prototype = obj;

}

// 父类 Person
function Person (){
    this.info = 'person';
    this.lists = [1,2,3];

};
// 给父类添加原型方法
Person.prototype.show = function(){
    console.log('person');
};

// 定义子类

function Boy(){
    Person.call(this);
}

// 子类继承原型
inheritPrototype(Boy , Person);

// 给子类原型添加方法
Boy.prototype.infos = function(){
    console.log('Boy');
}
// 创建实例
var B1 = new Boy();
var B2 = new Boy();
B1.show(); //person
B1.infos();  //Boy
B1.lists.push(4);
console.log(B1.lists);  // [1, 2, 3, 4]
console.log(B2.lists);  // [1, 2, 3]
console.log(B2 instanceof Boy);  // true

寄生组合式继承知识点:

  1. 父类引用类型在子类实例中的不共享.
  2. 子类数父类的实例
  3. 可以继承父类的原型方法

JS的多继承

众所周知,一些传统的面向对象语言是具备多继承的.那么JS呢?理论上JS是基于原型链来实现继承,而原型链是单链不具备多继承特性,但是JS可以通过自身特性模拟多继承.

Object.prototype.extend = function(){
var i = 0,
    len = arguments.length,
    args;    

for(; i < len ; i++){
    args = arguments[i];
    //浅复制
    for(var property in args){
        this[property] = args[property];
    }
}    

};

var Person = {
    info : "person",
    types : 'china'
}
var Boy = {
    sex : "boy" ,
    name : ['kobe','cpul']
}
var Student = [];
Student.extend(Person , Boy);
console.log(Student.sex + ','+ Student.types); //boy,china
多态

在传统面向对象语言中,多态就是同一种方法多种不同调用方式.

    var baiduMap = {  
show: function(){  
    console.log("百度地图API");  
}  
};  
var gooleMap = {  
    show: function(){  
        console.log("谷歌地图API");  
    }  
}  
var rentMap = function( map ){  
    if(map.show instanceof Function ){  
    map.show();  
    }  
}  
rentMap( baiduMap );   //百度地图API
rentMap( gooleMap );  //谷歌地图API

JS的多态更多的是对传参进行判断.