JavaScript笔记之面向对象

时间:2022-12-01 12:07:27

了解构造函数原型对象的语法特征,掌握 JavaScript 中面向对象编程的实现方式,基于面向对象编程思想实现 DOM 操作的封装。

  • 了解面向对象编程的一般特征
  • 掌握基于构造函数原型对象的逻辑封装
  • 掌握基于原型对象实现的继承
  • 理解什么原型链及其作用
  • 能够处理程序异常提升程序执行的健壮性

一、面向对象

学习 JavaScript 中基于原型的面向对象编程序的`语法实现,理解面向对象编程的特征。

面向对象编程是一种程序设计思想,它具有 3 个显著的特征:封装、继承、多态。

1.1 封装

封装的本质是将具有关联的代码组合在一起,其优势是能够保证代码复用且易于维护,函数是最典型也是最基础的代码封装形式,面向对象思想中的封装仍以函数为基础,但提供了更高级的封装形式。

命名空间

先来回顾一下以往代码封装的形式:

<script>
  // 普通对象(命名空间)形式的封装
  let beats = {
    name: '狼',
    setName: function (name) {
      this.name = this.name;
    },
    getName() {
      console.log(this.name);
    }
  }

  beats.setName('熊');
  beats.getName();
</script>

以往以普通对象(命名空间)形式封装的代码只是单纯把一系列的变量或函数组合到一起,所有的数据变量都被用来共享(使用 this 访问)。

构造函数

对比以下通过面向对象的构造函数实现的封装:

<script>
  function Person() {
    this.name = '佚名';
    // 设置名字
    this.setName = function (name) {
      this.name = name;
    }
    // 读取名字
    this.getName = () => {
      console.log(this.name);
    }
  }

  // 实例对像,获得了构造函数中封装的所有逻辑
  let p1 = new Person();
  p1.setName('小明');
  console.log(p1.--name);// 小明

  // 实例对象
  let p2 = new Person();
  console.log(p2.name); // 佚名
</script>

构造函数相当于一个"模子",能够像字面量那样创建出对象来,所不同的是借助构造函数创建出来的实例对象之间是彼此不影响的。

总结:

  1. 构造函数体现了面向对象的封装特性
  2. 构造函数实例创建的对象彼此独立、互不影响
  3. 命名空间式的封装无法保证数据的独立性

注:可以举一些例子,如女娲造人等例子,加深对构造函数的理解。

原型对象

实际上每一个构造函数都有一个名为 prototype 的属性,译成中文是原型的意思,prototype 的是对象类据类型,称为构造函数的原型对象,每个原型对象都具有 constructor 属性代表了该原型对象对应的构造函数。

目标:能够利用原型对象实现方法的共享的

  1. JavaScript规定,每一个构造函数都有一个prototype属性,指向另一个对象,所以我们也称为原型对象
  2. 构造函数通过原型分配的函数是所有对象所共享的
  3. 这个对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存
  4. 我们向以把那些不变的方法,直接定义在prototype对象上,这样所有对象的实例就可以共享这些方法。
  5. 构造函数和原型对象中的this都指向实例化的对象
<script>
  function Person() {
    
  }

  // 每个函数都有 prototype 属性
  console.log(Person.prototype);
</script>

如下图所示:

JavaScript笔记之面向对象

了解了 JavaScript 中构造函数与原型对象的关系后,再来看原型对象具体的作用,如下代码所示:

<script>
  function Person() {
    // 此处未定义任何方法
  }

  // 为构造函数的原型对象添加方法
  Person.prototype.sayHi = function () {
    console.log('Hi~');
  }
	
  // 实例化
  let p1 = new Person();
  p1.sayHi(); // 输出结果为 Hi~
</script>

其结构如图所示:

JavaScript笔记之面向对象

构造函数 Person 中未定义任何方法,这时实例对象调用了原型对象中的方法 sayHi,接下来改动一下代码:

<script>
  function Person() {
    // 此处定义同名方法 sayHi
    this.sayHi = function () {
      console.log('嗨!');
    }
  }

  // 为构造函数的原型对象添加方法
  Person.prototype.sayHi = function () {
    console.log('Hi~');
  }

  let p1 = new Person();
  p1.sayHi(); // 输出结果为 嗨!
</script>

JavaScript笔记之面向对象

构造函数 Person 中定义与原型对象中相同名称的方法,这时实例对象调用则是构造函中的方法 sayHi

通过以上两个简单示例不难发现 JavaScript 中对象的工作机制:当访问对象的属性或方法时,先在当前实例对象是查找,然后再去原型对象查找,并且原型对象被所有实例共享。

<script>
  function Person() {
    // 此处定义同名方法 sayHi
    this.sayHi = function () {
      console.log('嗨!' + this.name);
    }
  }

  // 为构造函数的原型对象添加方法
  Person.prototype.sayHi = function () {
    console.log('Hi~' + this.name);
  }
  // 在构造函数的原型对象上添加属性
  Person.prototype.name = '小明';

  let p1 = new Person();
  p1.sayHi(); // 输出结果为 嗨!
  
  let p2 = new Person();
  p2.sayHi();
</script>

JavaScript笔记之面向对象

什么是原型对象??

答:是构造函数的一个属性,它的数据类型是对象

原型对象有啥用??

答:原型对象对应的构造函数的实例方法或属性不存在时会去查找原型对象

总结:结合构造函数原型的特征,实际开发重往往会将封装的功能函数添加到原型对象中。

1.4给数组扩展方法

<script>
const arr = [1, 2, 3, 4, 56, 67]
Array.prototype.sum = function () {
  return this.reduce((prev, item) => prev + item, 0)
}
console.log(arr.sum()) // 133
Array.prototype.max = function () {
  return Math.max(...this) // this指向实例对象arr
}

console.log(arr.max()) //67
</script>

1.5 constructor属性

function Star(){   
}
const ssx = new Star()
//每个原型对象上都有一个constructor属性,指向创建出来的构造函数
Star.prototype.constructor === Star    //  

-----------------------------------------
用法:
function Star(){   
}
//向prototype 中加入两个方法
    function Star() {}
      //向prototype 中加入两个方法
      console.log(Star.prototype)
      Star.prototype = {
        sing: function () {
          console.log('唱歌')
        },
        dance: function () {
          console.log('跳舞')
        },
        constructor: Star,  //重新指向Star ,如果不指向,会没有Star中的属性
      }
      console.log(Star.prototype)

1.6对象原型

对象都会有一个属性__proto__     
    指向构造函数的prototype原型对象,之所以我们对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有__proto__原型的存在。
      function Star() {}
      let ssx = new Star()
   console.log(ssx.__proto__ === Star.prototyp) //true

注意:

  1. __proto__是J S非标准属性
  2. [[prototype]__proto__意义相同
  3. 用来表明当前实例对象指向哪个原型对象prototype
  4. __proto__对象原型里面也有一个constructor属性,指向创建该实例对象的构造函数
  5. 对象原型里面也有一个constructor 指向 构造函数 Star

JavaScript笔记之面向对象

1.2 继承

继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,JavaScript 中大多是借助原型对象实现继承的特性。

龙生龙、凤生凤、老鼠的儿子会打洞描述的正是继承的含义,分别封装中国人和日本人的行为特征来理解编程中继承的含义,代码如下:

<script>
  // 封装中国人的行为特征
  function Chinese() {
    // 中国人的特征
    this.arms = 2;
    this.legs = 2;
    this.eyes = 2;

    this.skin = 'yellow';
    this.language = '中文';

    // 中国人的行为
    this.walk = function () {}
    this.sing = function () {}
    this.sleep = function () {}
  }

  // 封装日本人的行为特征
  function Japanese() {
    // 日本人的特征
    this.arms = 2;
    this.legs = 2;
    this.eyes = 2;

    this.skin = 'yellow';
    this.language = '日文';

    // 日本人的行为
    this.walk = function () {}
    this.sing = function () {}
    this.sleep = function () {}
  }
</script>

其实我们都知道无论是中国人、日本人还是其它民族,人们的大部分特征是一致的,然而体现在代码中时人的相同的行为特征被重复编写了多次,代码显得十分冗余,我们可以将重复的代码抽离出来:

原型继承

基于构造函数原型对象实现面向对象的继承特性。

<script>
     //原型继承
      const Person = {
        eyes: 2,
        head: 1,
      }
      //通过原型来继承Person
      Woman.prototype = Person
      //找回原来的构造函数
      Woman.prototype.constructor = Woman
      //女人
      function Woman() {}
      const woman = new Woman()
      console.log(woman)

      //通过原型来继承Person
      Man.prototype = Person
      //找回原来的构造函数
      Man.prototype.constructor = Man
      //男人
      function Man() {}
      const man = new Man()
      console.log(man)
</script>

原型继承不同的方法时出现问题

*   当为一个构造函数的原型对象上添加一个方法时,另一个对象也会改变
//问题解决
      //原型继承
      // const Person = {
      //   eyes: 2,
      //   head: 1,
      // }

      //构造函数  new 出来的对象,结构一样,但是对象不一样
      function Person() {
        this.eyes = 2
        this.head = 1
      }
--------------------------------------------------------------
      //通过原型来继承Person
      Woman.prototype = new Person()
      //找回原来的构造函数
      Woman.prototype.constructor = Woman
      //女人
      //独有的方法:
      Woman.prototype.baby = function () {
        console.log('生孩子')
      }
      function Woman() {}
      const woman = new Woman()
      console.log(woman)
--------------------------------------------------------------
      //通过原型来继承Person
      Man.prototype = new Person()
      //找回原来的构造函数
      Man.prototype.constructor = Man
      //男人
      function Man() {}
      const man = new Man()
      console.log(man)

JavaScript笔记之面向对象

如下图所示:

JavaScript笔记之面向对象

上述代码中是以命名空间的形式实现的继承,事实上 JavaScript 中继承更常见的是借助构造函数来实现:

如下图所示:

JavaScript笔记之面向对象

原型链

基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对象的链状结构关系称为原型链,如下图所示:

JavaScript笔记之面向对象

作用:用于查找成员提供机制

<script>
  // Person 构造函数
  function Person() {
    this.arms = 2;
    this.walk = function () {}
  }
	
  // Person 原型对象
  Person.prototype.legs = 2;
  Person.prototype.eyes = 2;
  Person.prototype.sing = function () {}
  Person.prototype.sleep = function () {}
	
  // Chinese 构造函数
  function Chinese() {
    this.skin = 'yellow';
    this.language = '中文';
  }
	
  // Chinese 原型对象
  Chinese.prototype = new Person();
  Chinese.prototype.constructor = Chinese;
	
  // 实例化
  let c1 = new Chinese();
  console.log(c1);
</script>

在 JavaScript 对象中包括了一个非标准备的属性 __proto__ 它指向了构造函数的原型对象,通过它可以清楚的查看原型对象的链状结构。

1.25原型链-查找规则(只要是对象就是 __proto__ ,只要是原型对象就有constructor指向构造函数)

  1. 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
  2. 如果没有就查找它的原型(也就是__proto__指向的prototype原型对象
  3. 如果还没有就查找原型对象的原型(Object的原型对象)
  4. 依此类推一直找到Object为止(null)
  5. __proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线
  6. 可以使用instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上

1.26 instanceof

object instanceof constructor
object   要检测的对象.
constructor   某个构造函数


1.3 写在最后

面向对象(OOP)是编程时的一种指导思想,需要通过不断的实践才能体会面向对象编程的优势,在 JavaScript 中面向对象编程的实现是以构造函数和原型对象为核心的,因此掌握构造函数和原型对象的语法是灵活运用面向对象的基础。

面向对象多态的特性在 JavaScript 中应用场景相对较少,本次课中暂不讲解。

二、异常处理

了解 JavaScript 中程序异常处理的方法,提升代码运行的健壮性。

2.1 throw

异常处理是指预估代码执行过程中可能发生的错误,然后最大程度的避免错误的发生导致整个程序无法继续运行。

<script>
  function counter(x, y) {

    if(!x || !y) {
      // throw '参数不能为空!';
      throw new Error('参数不能为空!');
    }

    return x + y;
  }

  counter();
</script>

总结:

  1. throw 抛出异常信息,程序也会终止执行
  2. throw 后面跟的是错误提示信息
  3. Error 对象配合 throw 使用,能够设置更详细的错误信息
2.2 try ... catch
<script>
  function foo() {

    try {
      // 查找 DOM 节点
      var p = docunent.querySelector('p');

    } catch(error) {
      // try 代码段中执行有错误时,会执行 catch 代码段

      // 查看错误信息
      console.log(error.message);

      // 终止代码继续执行
      return;
    }

    // 改变文本样式
    p.style.color = 'red';
  }

  foo();
</script>

总结:

  1. try...catch 用于捕获错误信息
  2. 将预估可能发生错误的代码写在 try 代码段中
  3. 如果 try 代码段中出现错误后,会执行 catch 代码段,并截获到错误信息