前言

  不少开发对JavaScript实现面向对象编程存在一知半解,并且不少的在项目实践中写的都是面向过程编程的代码,因此,希望能从零入手介绍面向对象的一些概念到实现简单的面向对象的例子让大家包括我自己加深对面向对象的认知。硬文一篇,希望能对大家有帮助 ^v^

对象基础

  概念

  对象是一个包含相关数据和方法的集合,是通过变量和函数组成,通常被我们说成属性和方法,常用对象字面量的形式表示。

  创建方法

  1.初始化对象

var person={}

  2.添加属性(变量)和方法(函数)

var person={
                name:\'aaron\',
                say:function(){
                    alert(\'hello\')
                }
            }

  3.获取属性和执行方法

person.name
person.say()

  备注:获取属性和执行方法有两种方法,就是说我上面列举的其一:点表示法,还有一种就是括号表示法。如下:

person[\'name\']
person[\'say\']()

  因此,有时对象也被叫做关联数组,即对象做了字符串到值的映射,而数组做的是数字到值的映射。

  4.运行截图

  5.设置对象成员

  备注:有一点需要了解到的是,括号表示法能做到通过定义变量名的方式去设置对象成员,而这一点是点表示法没法实现的。

  6.“this”的含义

  this的指向其实是一直都让开发者头大的问题了,尤其是后端写JS时。其实说白了this就是指向了当前代码运行时的对象。
  例如:


  由于对象字面量执行的是当前对象,所以this指向了person。而像创建的构造函数等this的指向就是构造函数实例对象了   

  优点   

  一目了然,对象字面量创建的对象的好处可以有效的把对象关联的属性和方法统一了起来,也减少了全局变量的污染,得到一定程度的安全(减少了定义全变量覆盖对象属性的危险)。

面向对象–构造函数

  了解OOP思想

  例如从现实世界的某个实例出发,对于一个人(person)来说,我们能在他们身上获取到很多信息(他们的住址,身高,鞋码等等),然后我们会基于这些信息介绍关于他们,并需要他们做出回应。而在面向对象的语言中,我们就可以通过类(class)的概念去描述一个对象,而这个类就是定义对象特质的模板。通过创建的class,我们就可以基于它来创建一些拥有class中属性和方法的对象,即实例对象。而这些实例对象一般是具体的人,例如老师,学生。在OOP中,我们也可以基于这个class,创建其他的新类,而这些新的子类(例如家长)可以继承它们父类的属性(数据)和方法(功能),来使用父对象共有的功能。

  因此,通过对泛指人到具体的某个学生/老师的关系,我们就可以总结到面向对象的三个基本特性:封装,继承,多态

  引入概念

  通过了解面向对象编程(OOP)的基本概念,什么是对象和对象的属性,方法,并了解实现面向对象编程的基本特性。也了解常用的创建对象方法–对象字面量,我们已经对对象的基本概念有了了解。但是,通过对象字面量来创建的只是单一的实体类,并不能实现通用对象(现实模型)的封装,即真正的实现面向对象。
JavaScript是通过构建函数的方式来定义对象和特征的,而构建的实例对象也有通过原型链的方式来继承某些特性。

  从实例中学习构建函数和对象实例

  1.Person()构造函数,创建实例对象并访问属性和方法:

  2.其他创建对象实例的姿势

  1.Object()构造函数

var person1=new Object();
                person1.name=\'ace\';
                person1.age=30;
                person1.greeting=function(){
                    alert(\'Hi! I\\'m \' + this.name + \'.\'\')
                }

  2.使用create():这样就可以基于person1创建与person1具有相同属性和方法的对象。

var person2=Object.create(person1); 

面向对象–对象原型

  引入概念

  JavaScript的继承机制是有别于其他经典的面向对象编程语言的,是通过原型来实现从其他对象继承功能特性的。

  因此,JavaScript常被描述为基于原型的语言–每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性,原型对象也有可能拥有原型,从中继承方法和属性,以此类推。而这种关系被称为原型链。
  我们需要知道的是,这些属性和方法是定义在实例的构造函数上的prototype属性,当然,实例对象也__proto__属性,是从构造函数的prototype属性派生的,即实例对象.__proto===构造函数.prototype。

  理解原型对象


  从截图我们看到,person1实例除了具有Person()构造器中的属性方法外,还具有其他属性和方法,而这些则是Person()构造器原型对象Object上的成员。


  
通过调用valueOf,因此,我们也了解到了调用方法的过程:

    1.浏览器首先检查,person1 对象是否具有可用的 valueOf() 方法。

    2.如果没有,则浏览器检查 person1 对象的原型对象(即 Person)是否具有可用的 valueof() 方法。     

    3.如果也没有,则浏览器检查 Person() 构造器的原型对象(即 Object)是否具有可用的 valueOf() 方法。Object 具有这个方法,于是该方法被调用。

  prototype属性

   通过对valueOf方法的调用过程,我们也就了解到了那些能被继承的属性和方法(对象中存在不能被继承的属性方法,例is()/keys())是定义在prototype属性上的。因此,在构造函数是需要被子类继承的属性方法需要定义在prototype上。

  constructor属性

  每个实例对象都有constructor属性,它是指向创建该实例的构造函数。

  而我们也可以通过在constructor后添加()形式实现创建新实例。

  修改原型


  通过截图我们可以了解到了,虽然已经创建了实例对象person1,当时之后再像构造器Person()prototype中添加方法,person1还是能调用,这就说明了函数调用会通过上溯原型链,从上游对象中调用方法。


  
如图,若在prototype上定义属性的话,则this的当前执行环境为全局,返回的为undefined。并且,在对象继承上看,一般的做法是在构造器中定义属性,在prototype属性中定义方法。

小demo–实例理解JavaScript中的继承

  1.创建构造器Person并在构造器上定义方法

function Person(first, last, age, gender, interests) {
      this.name = {
        first,
        last
      };
      this.age = age;
      this.gender = gender;
      this.interests = interests;
    };
    Person.prototype.bio = function() {
       alert(this.name.first + \' \' + this.name.last + \' is \' + this.age + \' years old. He likes \' + this.interests[0] + \' and \' + this.interests[1] + \'.\');
    };
    Person.prototype.greeting = function() {
      alert(\'Hi! I\\'m \' + this.name.first + \'.\');
    };

  2.创建Teacher类Teacher构造器,并继承Person的所有成员,同时具备自有属性和方法:subject,greeting()

 function Teacher(first, last, age, gender, interests, subject) {
      Person.call(this, first, last, age, gender, interests);
      this.subject = subject;
    }

  3.设置Teacher()的原型和构造器引用


  如图,创建的新的Teacher()构造器只有一个空的原型属性,则需从Person()的原型prototype中继承方法:

Teacher.prototype = Object.create(Person.prototype);


  如图,我们又遇到了个问题,由于我们创建Teacher的prototype的方式,Teacher()构造器的prototype属性执行了Person(),因此,我们需要设置指向Teacher:

Teacher.prototype.constructor = Teacher;

  通过这样,我们就能实现了需要继承的方法都定义在了构造器的prototype属性内,这样才不会打乱了类继承结构。

  4.向Teacher()添加新的greeting方法

Teacher.prototype.greeting = function() {
      var prefix;

      if(this.gender === \'male\' || this.gender === \'Male\' || this.gender === \'m\' || this.gender === \'M\') {
        prefix = \'Mr.\';
      } else if(this.gender === \'female\' || this.gender === \'Female\' || this.gender === \'f\' || this.gender === \'F\') {
        prefix = \'Mrs.\';
      } else {
        prefix = \'Mx.\';
      }

      alert(\'Hello. My name is \' + prefix + \' \' + this.name.last + \', and I teach \' + this.subject + \'.\');
    };

  5.最后运行创建teacher实例,我们就能得到了从Person()构造器继承的属性和方法,并具有只有Teacher()的构造器才有的属性方法。

  6.对象成员

  通过学习,我们可以知道一般对象所具有的对象成员包括三类:

    1.那些定义在构造器函数中的、用于给予对象实例的。一般为对象属性。

    2.那些直接在构造函数上定义、仅在构造函数上可用的。这些通常仅在内置的浏览器对象中可用,并通过被直接链接到构造函数而不是实例来识别。 例如Object.keys()。

    3.那些在构造函数原型上定义、由所有实例和对象类继承的。一般为对象方法。

深入–设计模式原则

  当然,通过上述的方法我们可以实现了基本的面向对象的编程,但是,要实现更高级的类库封装和框架实现,则需要能对设计模式有很好的认知。

  在这,我就列举下设计模式原则,希望大家包括我自己有学习的方向。一般的设计原则为:

    1.单一职责原则

    2.里氏替换原则

    3.依赖倒置原则

    4.接口隔离原则

    5.迪米特原则

    6.开闭原则

  当然,还有关于面向对象的设计模式(23种),则需要深入了解了,其实这已经是有深入到我们自己的代码中了,只是我们对它的认知并不深。这个就后续了解了~~~

实战:构建对象–弹跳球(ES6实现)

  背景

  通过对面向对象基本概念,面向对象的编程思想和构造器,原型实现方法实现一个简单的弹跳球小游戏。

  介绍

  主要通过ES6,class语法糖,通过canvas绘制背景并控制evil的上下左右,吃掉小球。

  上代码

  1.index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Bouncing balls</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
<canvas></canvas>
<script src="mainUpgrade.js"></script>
</body>
</html>

  2.mainUpgrade.js

const canvas=document.querySelector(\'canvas\');
const ctx=canvas.getContext(\'2d\');

const width=canvas.width=window.innerWidth;
const height=canvas.height=window.innerHeight;

//create a random number between min and max.
random=(min,max)=>{
    let num=Math.floor(Math.random()*(max-min+1))+min;
    return num;
};

//create constructor for ball
class Shape{
    constructor(x,y,velX,velY,exists){
        this.x=x;
        this.y=y; //坐标
        this.velX=velX;
        this.velY=velY; //水平和竖直速度
        this.exists=exists; //是否存在
    }
}

class Ball extends Shape{
    constructor(x,y,velX,velY,exists,color,size){
        super(x,y,velX,velY,exists);
        this.color=color;
        this.size=size;
    }

    // draw ball
    draw (){
        ctx.beginPath();
        ctx.fillStyle=this.color;
        ctx.arc(this.x,this.y,this.size,0,2*Math.PI); // arc()绘制圆弧
        ctx.fill();
    }

    //update ball location
    update (){
        if((this.x + this.size)>=width){
            this.velX=-(this.velX)
        }
        if((this.x - this.size)<= 0){
            this.velX=-(this.velX)
        }
        if((this.y + this.size)>= height){
            this.velY=-(this.velY)
        }
        if((this.y - this.size)<= 0){
            this.velY=-(this.velY)
        }
        this.x+=this.velX;
        this.y+=this.velY;
    }

    //spy collision
    collisionDetect (){
        for(let j=0;j<balls.length;j++){
            if(!(this===balls[j])){
                const dx=this.x - balls[j].x;
                const dy=this.y - balls[j].y;
                const distance=Math.sqrt(dx*dx + dy*dy);

                if(distance<this.size + balls[j].size){
                    balls[j].color=this.color=\'rgb(\'+random(0,255)+\',\'+random(0,255)+\',\'+random(0,255)+\')\';
                }
            }

        }
    }
}

//create evil circle
class EvilCircle extends Shape{
    constructor(x,y,exists){
        super(x,y,exists);
        this.color=\'white\';
        this.size=10;
        this.velX=20;
        this.velY=20;
    }
    draw(){
        ctx.beginPath();
        ctx.strokeStyle=this.color;
        ctx.lineWidth=3;
        ctx.arc(this.x,this.y,this.size,0,2*Math.PI);
        ctx.stroke();
    }
    //check evil location
    checkBounds(){
        if((this.x + this.size)>width){
            this.x-=this.size
        }
        if((this.y + this.size)>height){
            this.y-=this.size
        }
        if((this.x - this.size)<0){
            this.x+=this.size;
        }
        if((this.y - this.size)<0){
            this.y+=this.size;
        }
    }
    setControls(){
        window.onkeydown=(e)=>{
            if(e.keyCode===38){
                this.y-=this.velY
            }
            else if(e.keyCode===40){
                this.y+=this.velY;
            }
            else if(e.keyCode===37){
                this.x-=this.velX
            }
            else if(e.keyCode===39){
                this.x+=this.velX
            }
        }
    }
    collisionDetect(){
        for(let i=0;i<balls.length;i++){
            if(balls[i].exists){
                const dx=this.x-balls[i].x;
                const dy=this.y-balls[i].y;
                const distance=Math.sqrt(dx*dx+dy*dy);

                if(distance<this.size+balls[i].size){
                    balls[i].exists=false;
                }
            }
        }
    }
}

let balls=[];

const evil=new EvilCircle(
    random(0,width),
    random(0,height),
    true
);

loop=()=>{
    ctx.fillStyle=\'rgba(0,0,0,0.25)\';
    ctx.fillRect(0,0,width,height);

    while (balls.length < 25){
        const ball=new Ball(
            random(0,width),
            random(0,height),
            random(-7,7),
            random(-7,7),
            true,
            \'rgb(\'+ random(0,255)+\',\'+random(0,255)+\',\'+random(0,255)+\')\',
            random(10,20)
        );
        balls.push(ball);
    }

    for(let i=0;i<balls.length;i++){
        if(balls[i].exists){
            balls[i].draw();
            balls[i].update();
            balls[i].collisionDetect();
        }
    }

    evil.draw();
    evil.checkBounds();
    evil.setControls();
    evil.collisionDetect();

    window.requestAnimationFrame(loop) //执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画
}


loop();

  运行截图

总结

  通过该博文,希望能让大家对了解JavaScript实现面向对象有个基本的了解和概念。如有描述不当的地方望能指出,谢谢。

版权声明:本文为aaron-pan原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/aaron-pan/p/8625338.html